Skip to content

Commit e565502

Browse files
committed
test: add PHPUnit coverage for PHP runtime seams
1 parent dcef1b3 commit e565502

22 files changed

Lines changed: 1037 additions & 2 deletions

‎.github/workflows/php-unit.yml‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: PHP Unit
2+
3+
on:
4+
push:
5+
branches: [ master, develop ]
6+
paths:
7+
- '**.php'
8+
- 'composer.json'
9+
- 'phpunit.xml.dist'
10+
- 'tests/phpunit/**'
11+
- '.github/workflows/php-unit.yml'
12+
pull_request:
13+
branches: [ master, develop ]
14+
paths:
15+
- '**.php'
16+
- 'composer.json'
17+
- 'phpunit.xml.dist'
18+
- 'tests/phpunit/**'
19+
- '.github/workflows/php-unit.yml'
20+
21+
concurrency:
22+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
23+
cancel-in-progress: true
24+
25+
jobs:
26+
phpunit:
27+
name: PHPUnit PHP ${{ matrix.php_version }}
28+
runs-on: ubuntu-latest
29+
strategy:
30+
fail-fast: false
31+
matrix:
32+
php_version: [ '7.4', '8.5' ]
33+
steps:
34+
- uses: actions/checkout@v4
35+
- uses: shivammathur/setup-php@v2
36+
with:
37+
php-version: ${{ matrix.php_version }}
38+
coverage: none
39+
tools: composer:v2
40+
- name: Install Composer dependencies
41+
run: composer update --no-interaction --prefer-dist --no-progress
42+
- name: Run PHPUnit
43+
run: composer test

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ test-results/**/trace.zip
1717

1818
composer.phar
1919
composer.lock
20+
.phpunit.result.cache
2021
/vendor
2122
/wordpress
2223
/test-results/

‎AGENTS.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ Do not apply GitHub changes (close, comment, relabel) during issue triage withou
6161

6262
- Unit / Jest: `npm run test:unit` (wp-scripts / `@wordpress/jest-preset-default`). Watch: `npm run test:unit:watch`.
6363
Premium: `cd pro__premium_only && npm run test:unit` when `pro__premium_only/` is present.
64+
- PHP unit: `composer test` (PHPUnit 9 + Brain Monkey, no Docker / wp-env). Premium: `composer test:php:premium` when `pro__premium_only/` is present.
6465
- PHP compatibility: `composer phpcompat` (PHPCompatibilityWP; floor from `readme.txt` `Requires PHP`).
6566
Premium PHP: `composer phpcompat:premium` when `pro__premium_only/` is present.
6667
- Playwright e2e (WordPress Playground, no Docker): see [`e2e/readme.md`](./e2e/readme.md)

‎CONTRIBUTING.MD‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,27 @@ This is to prevent errors when upgrading Stackable.
198198
📝 Writing Tests
199199
=============
200200

201+
### PHP unit tests
202+
203+
PHP runtime tests live in `tests/phpunit/` (PHPUnit 9 + Brain Monkey). They do
204+
not boot WordPress, MySQL, or Freemius. Each test `require_once`s the PHP file
205+
under test after stubbing WordPress helpers.
206+
207+
```bash
208+
composer test
209+
```
210+
211+
Premium PHP tests live in `pro__premium_only/tests/phpunit/` and reuse the free
212+
Composer `vendor/` tree:
213+
214+
```bash
215+
composer test:php:premium
216+
```
217+
218+
Keep PHPUnit files under `tests/` so they are not packaged in the plugin zip.
219+
220+
CI runs PHP 7.4 and 8.5 (same corners as Playwright).
221+
201222
### Block Testing
202223

203224
Block testing is done in 5 steps, and covers the entirety of a block's codebase.

‎composer.json‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
"require-dev": {
77
"wp-cli/i18n-command": "^2.2",
88
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
9-
"phpcompatibility/phpcompatibility-wp": "^2.1"
9+
"phpcompatibility/phpcompatibility-wp": "^2.1",
10+
"phpunit/phpunit": "^9.6",
11+
"yoast/phpunit-polyfills": "^2.0",
12+
"brain/monkey": "^2.6"
1013
},
1114
"config": {
1215
"allow-plugins": {
@@ -15,6 +18,9 @@
1518
},
1619
"scripts": {
1720
"phpcompat": "phpcs",
18-
"phpcompat:premium": "phpcs --standard=pro__premium_only/phpcs.xml.dist pro__premium_only"
21+
"phpcompat:premium": "phpcs --standard=pro__premium_only/phpcs.xml.dist pro__premium_only",
22+
"test": "@test:php",
23+
"test:php": "phpunit",
24+
"test:php:premium": "php tests/phpunit/run-premium.php"
1925
}
2026
}

‎phpcs.xml.dist‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<exclude-pattern>*/dist/*</exclude-pattern>
1111
<exclude-pattern>*/build/*</exclude-pattern>
1212
<exclude-pattern>*/pro__premium_only/*</exclude-pattern>
13+
<exclude-pattern>*/tests/*</exclude-pattern>
1314

1415
<arg name="extensions" value="php"/>
1516
<arg name="basepath" value="."/>

‎phpunit.xml.dist‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0"?>
2+
<phpunit
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.6/phpunit.xsd"
5+
bootstrap="tests/phpunit/bootstrap.php"
6+
colors="true"
7+
beStrictAboutTestsThatDoNotTestAnything="true"
8+
convertDeprecationsToExceptions="false"
9+
>
10+
<testsuites>
11+
<testsuite name="free">
12+
<directory suffix="Test.php">tests/phpunit</directory>
13+
</testsuite>
14+
</testsuites>
15+
</phpunit>

‎plugin.php‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,11 @@ function is_frontend() {
251251
}
252252
}
253253

254+
// PHPUnit loads this file only for deactivation cleanup + is_frontend().
255+
if ( defined( 'STACKABLE_PHPUNIT' ) && STACKABLE_PHPUNIT ) {
256+
return;
257+
}
258+
254259
/**
255260
* Freemius.
256261
* This needs to be first.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
/**
3+
* Stored block-style sanitizers.
4+
*
5+
* @package Stackable
6+
*/
7+
8+
use Brain\Monkey\Functions;
9+
10+
class BlockDefaultsSanitizeTest extends Stackable_TestCase {
11+
12+
protected function set_up() {
13+
parent::set_up();
14+
Functions\when( 'wp_check_invalid_utf8' )->returnArg( 1 );
15+
Functions\when( 'parse_blocks' )->justReturn( array() );
16+
Functions\when( 'serialize_blocks' )->justReturn( '' );
17+
$this->require_plugin_file( 'src/deprecated/block-defaults/custom-block-styles.php' );
18+
}
19+
20+
private function styles() {
21+
return new Stackable_Custom_Block_Styles();
22+
}
23+
24+
public function test_sanitize_block_name_rejects_core_blocks() {
25+
$styles = $this->styles();
26+
$this->assertSame( '', $styles->sanitize_block_name( 'core/paragraph' ) );
27+
$this->assertSame( 'stackable/heading', $styles->sanitize_block_name( 'stackable/heading' ) );
28+
}
29+
30+
public function test_sanitize_style_slug_uses_sanitize_title() {
31+
$this->assertSame( 'my-style', $this->styles()->sanitize_style_slug( 'My Style' ) );
32+
}
33+
34+
public function test_sanitize_array_setting_rejects_non_array() {
35+
$this->assertSame( array(), $this->styles()->sanitize_array_setting( 'nope' ) );
36+
}
37+
38+
public function test_sanitize_stored_block_styles_drops_invalid_blocks() {
39+
$styles = $this->styles();
40+
$stored = array(
41+
array(
42+
'block' => 'core/paragraph',
43+
'styles' => array(
44+
array(
45+
'slug' => 'plain',
46+
'name' => 'Plain',
47+
'data' => '{"attributes":{"text":"<script>x</script>"},"innerBlocks":[]}',
48+
'save' => '',
49+
),
50+
),
51+
),
52+
array(
53+
'block' => 'stackable/heading',
54+
'styles' => array(
55+
array(
56+
'slug' => 'hero',
57+
'name' => 'Hero',
58+
'data' => '{"attributes":{"text":"Hello <script>x</script>"},"innerBlocks":[]}',
59+
'save' => '',
60+
),
61+
),
62+
),
63+
);
64+
$out = $styles->sanitize_stored_block_styles( $stored );
65+
$this->assertCount( 1, $out );
66+
$this->assertSame( 'stackable/heading', $out[0]->block );
67+
$this->assertSame( 'hero', $out[0]->styles[0]->slug );
68+
$this->assertStringNotContainsString( '<script>', $out[0]->styles[0]->data );
69+
}
70+
}

‎tests/phpunit/CssOptimizeTest.php‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
/**
3+
* Inline CSS optimizer: parse, skip dynamic, combine selectors.
4+
*
5+
* @package Stackable
6+
*/
7+
8+
class CssOptimizeTest extends Stackable_TestCase {
9+
10+
protected function set_up() {
11+
parent::set_up();
12+
$this->require_plugin_file( 'src/css-optimize.php' );
13+
}
14+
15+
public function test_parse_block_style_collects_css_by_unique_id() {
16+
$styles = array();
17+
Stackable_CSS_Optimize::parse_block_style(
18+
array(
19+
'innerHTML' => '<div><style>.stk-aaaaaaa{color:red}</style></div>',
20+
'attrs' => array( 'uniqueId' => 'aaaaaaa' ),
21+
),
22+
$styles
23+
);
24+
$this->assertArrayHasKey( 'aaaaaaa', $styles );
25+
$this->assertSame( '.stk-aaaaaaa{color:red}', $styles['aaaaaaa'][0][1] );
26+
}
27+
28+
public function test_dynamic_style_is_not_collected() {
29+
$styles = array();
30+
Stackable_CSS_Optimize::parse_block_style(
31+
array(
32+
'innerHTML' => '<div><style>.stk-aaaaaaa{background:url(!#stk_dynamic/current-page/featured-image-data!#)}</style></div>',
33+
'attrs' => array( 'uniqueId' => 'aaaaaaa' ),
34+
),
35+
$styles
36+
);
37+
$this->assertSame( array(), $styles );
38+
}
39+
40+
public function test_generate_css_combines_matching_rules() {
41+
$css = Stackable_CSS_Optimize::generate_css( array(
42+
'.stk-aaaaaaa .child{color:red}',
43+
'.stk-bbbbbbb .child{color:red}',
44+
) );
45+
$this->assertStringContainsString( ':is(.stk-aaaaaaa, .stk-bbbbbbb)', $css );
46+
$this->assertStringContainsString( 'color:red', $css );
47+
}
48+
49+
public function test_zero_px_is_left_in_generated_css() {
50+
$css = Stackable_CSS_Optimize::generate_css( array(
51+
'.stk-aaaaaaa{margin:0px}',
52+
) );
53+
$this->assertStringContainsString( '0px', $css );
54+
}
55+
}

0 commit comments

Comments
 (0)