diff --git a/.github/workflows/WP_5_9.yaml b/.github/workflows/WP_6_6.yaml similarity index 65% rename from .github/workflows/WP_5_9.yaml rename to .github/workflows/WP_6_6.yaml index d39c56d..a3c67d6 100644 --- a/.github/workflows/WP_5_9.yaml +++ b/.github/workflows/WP_6_6.yaml @@ -1,4 +1,4 @@ -name: WP5.9 Test Suite [PHP7.2-8.1] +name: WP 6.6 [PHP8.0-8.4] Tests on: push: @@ -11,23 +11,22 @@ jobs: strategy: matrix: operating-system: [ubuntu-latest] - php-versions: [ '7.2', '7.3', '7.4', '8.0', '8.1'] + php-versions: ['8.0', '8.1', '8.2', '8.3', '8.4'] runs-on: ${{ matrix.operating-system }} services: - # Setup MYSQL mysql-service: - image: 'mysql:5.7' + image: mysql:8.4 env: MYSQL_ROOT_PASSWORD: 'crab' MYSQL_DATABASE: pc_core_tests ports: - 3306:3306 options: >- - --health-cmd="mysqladmin ping" + --health-cmd="healthcheck.sh --connect || mysqladmin ping -uroot -pcrab" --health-interval=10s - --health-timeout=5s - --health-retries=3 - + --health-timeout=10s + --health-retries=10 + steps: - name: Checkout uses: actions/checkout@v2 @@ -44,21 +43,21 @@ jobs: - name: Clear existing composer run: > - sudo rm -rf vendor - && rm -rf composer.lock + sudo rm -rf vendor + && rm -rf composer.lock - name: Validate composer.json and composer.lock run: composer validate - name: Install dependencies run: > - rm -rf composer.lock + rm -rf composer.lock && composer clearcache - && composer require php-stubs/wordpress-stubs:5.9.0 --dev --no-update - && composer require roots/wordpress:5.9.4 --dev --no-update - && composer require wp-phpunit/wp-phpunit:5.9.4 --dev --no-update + && composer require php-stubs/wordpress-stubs:6.6.* --dev --no-update + && composer require roots/wordpress:6.6.* --dev --no-update + && composer require wp-phpunit/wp-phpunit:6.6.* --dev --no-update && composer update --no-cache - - - name: Run Tests on WP5.9 + + - name: Run Tests on WP6.6 env: environment_github: true - run: composer all \ No newline at end of file + run: composer all diff --git a/.github/workflows/WP_6_1.yaml b/.github/workflows/WP_6_7.yaml similarity index 64% rename from .github/workflows/WP_6_1.yaml rename to .github/workflows/WP_6_7.yaml index 9d83072..3f6e96f 100644 --- a/.github/workflows/WP_6_1.yaml +++ b/.github/workflows/WP_6_7.yaml @@ -1,4 +1,4 @@ -name: WP6.1 [PHP7.2-8.1] Tests +name: WP 6.7 [PHP8.0-8.4] Tests on: push: @@ -11,23 +11,22 @@ jobs: strategy: matrix: operating-system: [ubuntu-latest] - php-versions: ['7.2', '7.3', '7.4', '8.0', '8.1'] + php-versions: ['8.0', '8.1', '8.2', '8.3', '8.4'] runs-on: ${{ matrix.operating-system }} services: - # Setup MYSQL mysql-service: - image: 'mysql:5.7' + image: mysql:8.4 env: MYSQL_ROOT_PASSWORD: 'crab' MYSQL_DATABASE: pc_core_tests ports: - 3306:3306 options: >- - --health-cmd="mysqladmin ping" + --health-cmd="healthcheck.sh --connect || mysqladmin ping -uroot -pcrab" --health-interval=10s - --health-timeout=5s - --health-retries=3 - + --health-timeout=10s + --health-retries=10 + steps: - name: Checkout uses: actions/checkout@v2 @@ -44,24 +43,21 @@ jobs: - name: Clear existing composer run: > - sudo rm -rf vendor - && rm -rf composer.lock + sudo rm -rf vendor + && rm -rf composer.lock - name: Validate composer.json and composer.lock run: composer validate - name: Install dependencies run: > - rm -rf composer.lock + rm -rf composer.lock && composer clearcache - && composer require php-stubs/wordpress-stubs:6.1.0 --dev --no-update - && composer require roots/wordpress:6.* --dev --no-update + && composer require php-stubs/wordpress-stubs:6.7.* --dev --no-update + && composer require roots/wordpress:6.7.* --dev --no-update + && composer require wp-phpunit/wp-phpunit:6.7.* --dev --no-update && composer update --no-cache - - name: Run Tests on Latest Version - WP6.1 + - name: Run Tests on WP6.7 env: environment_github: true run: composer all - - - name: Codecov - run: bash <(curl -s https://codecov.io/bash) -t ${{ secrets.CODECOV_TOKEN }} - \ No newline at end of file diff --git a/.github/workflows/WP_6_0.yaml b/.github/workflows/WP_6_8.yaml similarity index 62% rename from .github/workflows/WP_6_0.yaml rename to .github/workflows/WP_6_8.yaml index e46f8aa..b8c310a 100644 --- a/.github/workflows/WP_6_0.yaml +++ b/.github/workflows/WP_6_8.yaml @@ -1,4 +1,4 @@ -name: WP6.0 Test Suite [PHP7.2-8.1] +name: WP 6.8 [PHP8.0-8.4] Tests on: push: @@ -11,23 +11,22 @@ jobs: strategy: matrix: operating-system: [ubuntu-latest] - php-versions: ['7.2', '7.3', '7.4', '8.0', '8.1'] + php-versions: ['8.0', '8.1', '8.2', '8.3', '8.4'] runs-on: ${{ matrix.operating-system }} services: - # Setup MYSQL mysql-service: - image: 'mysql:5.7' + image: mysql:8.4 env: MYSQL_ROOT_PASSWORD: 'crab' MYSQL_DATABASE: pc_core_tests ports: - 3306:3306 options: >- - --health-cmd="mysqladmin ping" + --health-cmd="healthcheck.sh --connect || mysqladmin ping -uroot -pcrab" --health-interval=10s - --health-timeout=5s - --health-retries=3 - + --health-timeout=10s + --health-retries=10 + steps: - name: Checkout uses: actions/checkout@v2 @@ -44,25 +43,21 @@ jobs: - name: Clear existing composer run: > - sudo rm -rf vendor - && rm -rf composer.lock + sudo rm -rf vendor + && rm -rf composer.lock - name: Validate composer.json and composer.lock run: composer validate - name: Install dependencies run: > - rm -rf composer.lock + rm -rf composer.lock && composer clearcache - && composer require php-stubs/wordpress-stubs:6.0.0 --dev --no-update - && composer require roots/wordpress:6.0.0 --dev --no-update - && composer require wp-phpunit/wp-phpunit:6.0.0 --dev --no-update + && composer require php-stubs/wordpress-stubs:6.8.* --dev --no-update + && composer require roots/wordpress:6.8.* --dev --no-update + && composer require wp-phpunit/wp-phpunit:6.8.* --dev --no-update && composer update --no-cache - - name: Run Tests on Latest Version - WP6.0 + - name: Run Tests on WP6.8 env: environment_github: true run: composer all - - - name: Codecov - run: bash <(curl -s https://codecov.io/bash) -t ${{ secrets.CODECOV_TOKEN }} - \ No newline at end of file diff --git a/.github/workflows/WP_6_9.yaml b/.github/workflows/WP_6_9.yaml new file mode 100644 index 0000000..ef22bb4 --- /dev/null +++ b/.github/workflows/WP_6_9.yaml @@ -0,0 +1,70 @@ +name: WP 6.9 [PHP8.0-8.4] Tests + +on: + push: + branches: [ master ] + pull_request: + branches: [ master, develop ] + +jobs: + build: + strategy: + matrix: + operating-system: [ubuntu-latest] + php-versions: ['8.0', '8.1', '8.2', '8.3', '8.4'] + runs-on: ${{ matrix.operating-system }} + services: + mysql-service: + image: mysql:8.4 + env: + MYSQL_ROOT_PASSWORD: 'crab' + MYSQL_DATABASE: pc_core_tests + ports: + - 3306:3306 + options: >- + --health-cmd="healthcheck.sh --connect || mysqladmin ping -uroot -pcrab" + --health-interval=10s + --health-timeout=10s + --health-retries=10 + + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: mbstring, intl, pcov + ini-values: post_max_size=256M, log_errors=1 + tools: pecl + + - name: Check PHP Version + run: php -v + + - name: Clear existing composer + run: > + sudo rm -rf vendor + && rm -rf composer.lock + - name: Validate composer.json and composer.lock + run: composer validate + + - name: Install dependencies + run: > + rm -rf composer.lock + && composer clearcache + && composer require php-stubs/wordpress-stubs:6.9.* --dev --no-update + && composer require roots/wordpress:6.9.* --dev --no-update + && composer require wp-phpunit/wp-phpunit:6.9.* --dev --no-update + && composer update --no-cache + + - name: Run Tests on WP6.9 + env: + environment_github: true + run: composer all + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./clover.xml + fail_ci_if_error: false diff --git a/.gitignore b/.gitignore index 6801830..c16414e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,4 @@ coverage-report clover.xml workspace.code-workspace .phpunit.result.cache -tests/.env .vscode \ No newline at end of file diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..f45b34a --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,85 @@ +checks: + php: + code_rating: true + duplication: true + fix_php_opening_tag: false + remove_php_closing_tag: false + one_class_per_file: false + side_effects_or_types: false + no_mixed_inline_html: false + require_braces_around_control_structures: false + php5_style_constructor: false + no_global_keyword: false + avoid_usage_of_logical_operators: false + psr2_class_declaration: false + no_underscore_prefix_in_properties: false + no_underscore_prefix_in_methods: false + blank_line_after_namespace_declaration: false + single_namespace_per_use: false + psr2_switch_declaration: false + psr2_control_structure_declaration: false + avoid_superglobals: false + security_vulnerabilities: false + no_exit: false + +build: + dependencies: + override: + - 'composer install --no-interaction --prefer-dist' + nodes: + analysis: + project_setup: + override: + - 'true' + tests: + override: + - php-scrutinizer-run + +tools: + php_analyzer: + enabled: true + filter: + excluded_paths: ['tests/*', 'docs/*', 'template/*', 'node_modules/*', 'vendor/*'] + config: + checkstyle: + enabled: true + naming: + isser_method_name: ^.*$ + utility_class_name: ^.*$ + doc_comment_fixes: + enabled: false + reflection_fixes: + enabled: false + use_statement_fixes: + enabled: false + simplify_boolean_return: + enabled: true + php_changetracking: true + php_cpd: true + php_cs_fixer: false + php_mess_detector: true + php_pdepend: true + sensiolabs_security_checker: true + +filter: + paths: + - 'src/*' + excluded_paths: + - 'tests/*' + - 'docs/*' + - 'docs-gen/*' + - 'node_modules/*' + - 'vendor/*' + - 'template/*' + +coding_style: + php: + indentation: + general: + use_tabs: true + size: 4 + spaces: + before_parentheses: + closure_definition: true + around_operators: + concatenation: true diff --git a/README.md b/README.md index a40a34d..5565743 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,19 @@ # PinkCrab Enqueue # -[![Latest Stable Version](http://poser.pugx.org/pinkcrab/enqueue/v)](https://packagist.org/packages/pinkcrab/enqueue) -[![Total Downloads](http://poser.pugx.org/pinkcrab/enqueue/downloads)](https://packagist.org/packages/pinkcrab/enqueue) -[![License](http://poser.pugx.org/pinkcrab/enqueue/license)](https://packagist.org/packages/pinkcrab/enqueue) -[![PHP Version Require](http://poser.pugx.org/pinkcrab/enqueue/require/php)](https://packagist.org/packages/pinkcrab/enqueue) +[![Latest Stable Version](https://poser.pugx.org/pinkcrab/enqueue/v)](https://packagist.org/packages/pinkcrab/enqueue) +[![Total Downloads](https://poser.pugx.org/pinkcrab/enqueue/downloads)](https://packagist.org/packages/pinkcrab/enqueue) +[![License](https://poser.pugx.org/pinkcrab/enqueue/license)](https://packagist.org/packages/pinkcrab/enqueue) +[![PHP Version Require](https://poser.pugx.org/pinkcrab/enqueue/require/php)](https://packagist.org/packages/pinkcrab/enqueue) ![GitHub contributors](https://img.shields.io/github/contributors/Pink-Crab/Enqueue?label=Contributors) ![GitHub issues](https://img.shields.io/github/issues-raw/Pink-Crab/Enqueue) -[![WP5.9 Test Suite [PHP7.2-8.1]](https://github.com/Pink-Crab/Enqueue/actions/workflows/WP_5_9.yaml/badge.svg)](https://github.com/Pink-Crab/Enqueue/actions/workflows/WP_5_9.yaml)[![WP6.0 Test Suite [PHP7.2-8.1]](https://github.com/Pink-Crab/Enqueue/actions/workflows/WP_6_0.yaml/badge.svg)](https://github.com/Pink-Crab/Enqueue/actions/workflows/WP_6_0.yaml)[![WP6.1 [PHP7.2-8.1] Tests](https://github.com/Pink-Crab/Enqueue/actions/workflows/WP_6_1.yaml/badge.svg)](https://github.com/Pink-Crab/Enqueue/actions/workflows/WP_6_1.yaml) -[![codecov](https://codecov.io/gh/Pink-Crab/Enqueue/branch/master/graph/badge.svg?token=9O27LAKVWI)](https://codecov.io/gh/Pink-Crab/Enqueue) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/Pink-Crab/Enqueue/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/Pink-Crab/Enqueue/?branch=master) -[![Maintainability](https://api.codeclimate.com/v1/badges/cbf72c7619f39ea64d2b/maintainability)](https://codeclimate.com/github/Pink-Crab/Enqueue/maintainability) + +[![WP 6.6 [PHP8.0-8.4] Tests](https://github.com/Pink-Crab/Enqueue/actions/workflows/WP_6_6.yaml/badge.svg)](https://github.com/Pink-Crab/Enqueue/actions/workflows/WP_6_6.yaml) +[![WP 6.7 [PHP8.0-8.4] Tests](https://github.com/Pink-Crab/Enqueue/actions/workflows/WP_6_7.yaml/badge.svg)](https://github.com/Pink-Crab/Enqueue/actions/workflows/WP_6_7.yaml) +[![WP 6.8 [PHP8.0-8.4] Tests](https://github.com/Pink-Crab/Enqueue/actions/workflows/WP_6_8.yaml/badge.svg)](https://github.com/Pink-Crab/Enqueue/actions/workflows/WP_6_8.yaml) +[![WP 6.9 [PHP8.0-8.4] Tests](https://github.com/Pink-Crab/Enqueue/actions/workflows/WP_6_9.yaml/badge.svg)](https://github.com/Pink-Crab/Enqueue/actions/workflows/WP_6_9.yaml) + +[![codecov](https://codecov.io/gh/Pink-Crab/Enqueue/branch/master/graph/badge.svg?token=9O27LAKVWI)](https://codecov.io/gh/Pink-Crab/Enqueue) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/Pink-Crab/Enqueue/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/Pink-Crab/Enqueue/?branch=master) The PinkCrab Enqueue class allows for a clean and fluent alternative for enqueuing scripts and styles in WordPress. @@ -19,7 +24,7 @@ composer require pinkcrab/enqueue ``` ## Version ## -**Release 1.3.0** +**Release 1.5.0** @@ -209,28 +214,104 @@ Enqueue::script('my_style') // ``` ### Async & Defer ### -There is also some shortcuts for making any script or style be deferred or async tagged. + +Scripts can be marked as `async` or `defer`. As of **v1.5.0** these forward to +WordPress's `wp_script_add_data( $handle, 'strategy', … )` API (WP 6.3+), which +is dependency-chain aware — a deferred script waits for deferred deps to +evaluate in order, rather than racing them via an unordered HTML attribute. + ```php src('http://www.site.com/my-style.css') - ->async() +Enqueue::script('my_script') + ->src('http://www.site.com/my-scripts.js') + ->defer() ->register(); -// Rendered as -// +// Rendered as (WP decides the exact attribute form) +// ``` -or ```php src('http://www.site.com/my-scripts.js') - ->defer() + ->async() ->register(); +``` -// Rendered as -// +Calling `defer()` then `async()` (or vice versa) overwrites the strategy — a +script can only have one. `defer()` / `async()` on a **style** is a no-op +(style tags have no strategy concept). + +### Script Translations ### + +Register the handle for i18n via `wp_set_script_translations()` so strings +loaded with `wp-i18n` resolve from a `.json` file in the given path. + +```php +src(PLUGIN_BASE_URL . 'assets/js/admin-app.js') + ->translations('my-text-domain', PLUGIN_BASE_PATH . 'languages') + ->register(); +``` + +The path is optional — WP falls back to its language directory when omitted. +Only meaningful on scripts; silent no-op on styles. + +### Inline Snippets ### + +Append arbitrary inline JavaScript to the registered script handle via +`with_code()`, which wraps `wp_add_inline_script()`. Multiple calls stack, and +the optional second argument is the position (`'before'` or `'after'`, +defaulting to `'after'`). + +This is **different** from the existing `inline()` method, which replaces +the `src` with the whole-file contents and inlines them. `with_code()` keeps +the script loading normally from its URL and attaches short snippets around it. + +```php +src(PLUGIN_BASE_URL . 'assets/js/app.js') + ->with_code('window.PC_CONFIG = { apiKey: "...", nonce: "..." };', 'before') + ->with_code('window.PC_APP.boot();') // runs after the script has loaded + ->register(); +``` + +### Inline Styles ### + +Same idea for styles — `with_style()` attaches inline CSS to a registered +style handle via `wp_add_inline_style()`. + +```php +src(PLUGIN_BASE_URL . 'assets/css/theme.css') + ->with_style(':root { --brand: #f06; }') + ->with_style('.block-editor .is-root-container { padding: 1rem; }') + ->register(); +``` + +### Register Only ### + +Register the asset with WP without enqueueing it. Useful when another script +will reference this one via a dependency array, or when a block's `block.json` +names the handle — the asset is available but doesn't emit on every page load. + +```php +src(PLUGIN_BASE_URL . 'assets/js/lib.js') + ->register_only() + ->register(); + +// Later, another script can depend on it: +Enqueue::script('my-app') + ->src(PLUGIN_BASE_URL . 'assets/js/app.js') + ->deps('my-lib') + ->register(); ``` ### Registration ### @@ -276,19 +357,69 @@ class My_Thingy{ add_action('wp_loaded', [new My_Thingy, 'init']); ``` -## Gutenberg ## +## Gutenberg / Block Editor ## + +### for_block ### -When registering scripts and styles for use with Gutenberg blocks, it is necessary to only register the assets before `wp_enqueue_scripts` hook is called. To do this, all you need to is set `for_block()`. +When registering scripts and styles for use with Gutenberg blocks declared in +PHP, it is necessary to only register the assets — the block itself handles +the enqueue. `for_block()` flags the Enqueue as register-only. ```php add_action('init', function(){ - Enqueue::script('my_style') - ->src('http://www.site.com/my-scripts.js') - ->defer() + Enqueue::script('my_block_script') + ->src('http://www.site.com/block-script.js') ->for_block() ->register(); - // Register block as normal + // Register block as normal, naming 'my_block_script' in its script handles. +}); +``` + +### Block Editor Assets ### + +For scripts/styles that should only load inside the WordPress admin block +editor screens, use `for_block_editor()`. This defers the registration/enqueue +flow to the `enqueue_block_editor_assets` hook, so you don't need to register +the hook yourself. + +```php +src(PLUGIN_BASE_URL . 'assets/js/editor.js') + ->deps('wp-blocks', 'wp-element', 'wp-i18n') + ->translations('my-text-domain') + ->for_block_editor() + ->register(); +``` + +### Block Styles ### + +Attach a stylesheet to a specific block name via +`wp_enqueue_block_style()` so the CSS only loads on pages that render the +block. Use `for_block_style( $block_name )`. + +```php +src(PLUGIN_BASE_URL . 'assets/css/my-block.css') + ->ver('1.0.0') + ->for_block_style('my-plugin/my-block') + ->register(); +``` + +### Block JSON ### + +Register a block type directly from its `block.json` metadata directory. This +short-circuits the normal script/style registration — `block.json` is the +source of truth for the block's asset declarations, so the fluent API's +`src`/`deps`/`ver` aren't used. + +```php +add_action('init', function(){ + Enqueue::script('placeholder-handle') + ->block_json( __DIR__ . '/build/my-block' ) + ->register(); }); ``` @@ -353,10 +484,69 @@ add_action('init', function(){ /** * Sets the version as last modified file time. + * Performs a HEAD request to the src URL via wp_remote_head(). + * + * @return self + */ + public function latest_version(): self + +/** + * Register the script for i18n via wp_set_script_translations(). + * + * @param string $domain Text domain. + * @param string|null $path Optional path to JSON translation files. + * @return self + */ + public function translations( string $domain, ?string $path = null ): self + +/** + * Attach an inline JS snippet to the registered handle via wp_add_inline_script(). + * + * @param string $code JavaScript source. + * @param string $position 'before' or 'after'. Default 'after'. + * @return self + */ + public function with_code( string $code, string $position = 'after' ): self + +/** + * Attach an inline CSS snippet to the registered handle via wp_add_inline_style(). + * + * @param string $css CSS source. + * @return self + */ + public function with_style( string $css ): self + +/** + * Register the asset with WP without enqueueing it. + * + * @param bool $register_only Default true. + * @return self + */ + public function register_only( bool $register_only = true ): self + +/** + * Defer the registration to the enqueue_block_editor_assets hook. + * + * @param bool $for_block_editor Default true. + * @return self + */ + public function for_block_editor( bool $for_block_editor = true ): self + +/** + * Attach a style to a named block via wp_enqueue_block_style(). + * + * @param string $block_name e.g. 'core/paragraph' or 'my-plugin/my-block'. + * @return self + */ + public function for_block_style( string $block_name ): self + +/** + * Register a block type from a block.json directory. * + * @param string $path Absolute path to the directory containing block.json. * @return self */ - public function lastEditedVersion(): self + public function block_json( string $path ): self /** * Should the script be called in the footer. @@ -440,6 +630,8 @@ public function script_type( string $script_type ): self This obviously can be passed around between different classes/functions ### Changelog ### +* 1.5.0 - Drop PHP 7.x, require PHP 8.0+. Modernise the tooling chain (PHPStan 2.x at level max, PHPUnit 8|9, WPCS 3.x, phpunit-polyfills widened to include v4, symfony/var-dumper + css-selector + dom-crawler unpinned). Replace the WP 5.9/6.0/6.1 workflows with the WP 6.6-6.9 matrix (PHP 8.0-8.4, `mysql:8.4`) using `codecov/codecov-action@v4`. Suppress the WP 6.8 `wp_is_block_theme` early-call notice in `tests/wp-config.php`. Guard `latest_version()` against null src (avoids PHP 8 TypeError). Narrow `strtotime()` argument when the `Last-Modified` header is returned as a list. Swap the `trigger_error( …, E_USER_DEPRECATED )` in the legacy `lastest_version()` alias for `_deprecated_function()` so WP_UnitTestCase can track the deprecation properly. Drop the `Codeclimate Maintainability` badge from the README. **Swap direct curl for `wp_remote_head()`** in `does_file_exist()` / `latest_version()` so consumers can filter the HTTP layer and tests can mock via `pre_http_request`. **New features:** `translations()` wraps `wp_set_script_translations()`; `with_code()` attaches inline JS via `wp_add_inline_script()`; `with_style()` attaches inline CSS via `wp_add_inline_style()`; `for_block_editor()` defers registration to `enqueue_block_editor_assets`; `for_block_style( $block_name )` wraps `wp_enqueue_block_style()`; `register_only()` registers without enqueueing; `block_json()` wraps `register_block_type_from_metadata()`. **`defer()` / `async()` repurposed** to use the dep-aware `wp_script_add_data( $handle, 'strategy', … )` API (WP 6.3+) instead of splatting attributes through the `script_loader_tag` filter — runtime behaviour unchanged for consumers, rendering now handled by WP core. +* 1.4.0 - Dev dependency bumps to support WP 6.1. * 1.3.0 - Updated for php8, includes setting of custom script types, renamed lastest_version() to latest_version() and set deprecation notice. * 1.2.1 : Now supports block use. If defined for block, scripts and styles will only be registered, not enqueued. * 1.2.0 : Added in Attribute and Flag support with helpers for Aysnc and Defer @@ -449,4 +641,4 @@ If you would like to make any suggestions or contributions to this little class, ### WordPress Core Functions ### This package uses the following wp core functions. To use PHP Scoper, please add the following functions. -['wp_enqueue_style', 'wp_register_script', 'wp_add_inline_script', 'wp_localize_script', 'wp_enqueue_script'] +['wp_register_style', 'wp_enqueue_style', 'wp_register_script', 'wp_enqueue_script', 'wp_add_inline_script', 'wp_add_inline_style', 'wp_localize_script', 'wp_set_script_translations', 'wp_script_add_data', 'wp_enqueue_block_style', 'register_block_type_from_metadata', 'wp_remote_head', 'wp_remote_retrieve_response_code', 'wp_remote_retrieve_header', 'is_wp_error', '_deprecated_function'] diff --git a/composer.json b/composer.json index 2022b63..c5e6fe8 100644 --- a/composer.json +++ b/composer.json @@ -14,8 +14,7 @@ "autoload": { "psr-4": { "PinkCrab\\Enqueue\\": "src" - }, - "files": [] + } }, "autoload-dev": { "psr-4": { @@ -23,36 +22,38 @@ } }, "require-dev": { - "phpunit/phpunit": "^7.0 || ^8.0", - "phpstan/phpstan": "1.*", - "szepeviktor/phpstan-wordpress": "<=1.1.7", - "php-stubs/wordpress-stubs": "6.* || 5.9.*", - "roots/wordpress": "6.1.*", - "wp-phpunit/wp-phpunit": "6.1.*", - "dealerdirect/phpcodesniffer-composer-installer": "<=1.0.0", - "wp-coding-standards/wpcs": "<=2.3.0", - "yoast/phpunit-polyfills": "^0.2.0 || ^1.0.0", - "symfony/var-dumper": "<=6.2.7", - "gin0115/wpunit-helpers": "1.1.*", - "vlucas/phpdotenv": "<=5.5.0", - "symfony/css-selector": "<=6.2.7", - "symfony/dom-crawler": "<=6.2.7", - "pinkcrab/function-constructors": "0.2.*" + "phpunit/phpunit": "^8.0 || ^9.0", + "phpstan/phpstan": "^2.0", + "szepeviktor/phpstan-wordpress": "^2.0", + "php-stubs/wordpress-stubs": "6.9.*", + "roots/wordpress": "6.9.*", + "wp-phpunit/wp-phpunit": "6.9.*", + "dealerdirect/phpcodesniffer-composer-installer": "*", + "wp-coding-standards/wpcs": "*", + "yoast/phpunit-polyfills": "^1.0.0 || ^2.0.0 || ^4.0.0", + "symfony/var-dumper": "*", + "gin0115/wpunit-helpers": "~1", + "vlucas/phpdotenv": "^5.4", + "symfony/css-selector": "*", + "symfony/dom-crawler": "*", + "pinkcrab/function-constructors": "^0.2" }, "require": { - "php": ">=7.2.0" + "php": ">=8.0.0" }, "scripts": { - "test": "phpunit --coverage-clover clover.xml --testdox", - "coverage": "phpunit --coverage-html coverage-report --testdox", + "test": "vendor/bin/phpunit --colors=always --testdox --coverage-clover clover.xml", + "coverage": "vendor/bin/phpunit --colors=always --testdox --coverage-html coverage-report --coverage-clover clover.xml", "analyse": "vendor/bin/phpstan analyse src -l8", "sniff": "./vendor/bin/phpcs src/ -v", "all": "composer test && composer analyse && composer sniff" }, + "minimum-stability": "dev", + "prefer-stable": true, "config": { "allow-plugins": { "roots/wordpress-core-installer": true, "dealerdirect/phpcodesniffer-composer-installer": true } } -} \ No newline at end of file +} diff --git a/phpcs.xml b/phpcs.xml index dacf3ac..df50103 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -19,6 +19,7 @@ + diff --git a/phpstan.neon.dist b/phpstan.neon.dist index cccf1f4..ee65564 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,4 @@ includes: - - vendor/phpstan/phpstan/conf/bleedingEdge.neon - vendor/szepeviktor/phpstan-wordpress/extension.neon parameters: level: max @@ -8,7 +7,3 @@ parameters: - %currentWorkingDirectory%/src/ excludePaths: - %currentWorkingDirectory%/tests/* - bootstrapFiles: - - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php - ignoreErrors: - - '^function get_headers expects (?:bool|int), int\|true given^' diff --git a/src/Enqueue.php b/src/Enqueue.php index 84f147e..d6b06c0 100644 --- a/src/Enqueue.php +++ b/src/Enqueue.php @@ -119,6 +119,62 @@ class Enqueue { */ protected $script_type = 'text/javascript'; + /** + * Script translations domain + optional path for wp_set_script_translations(). + * + * @var array{domain: string, path: string|null}|null + */ + protected $translations = null; + + /** + * Inline script snippets to attach to the handle via wp_add_inline_script(). + * + * @var array + */ + protected $inline_scripts = array(); + + /** + * Inline CSS strings to attach to the handle via wp_add_inline_style(). + * + * @var array + */ + protected $inline_styles = array(); + + /** + * Defer/async strategy forwarded to wp_script_add_data( $handle, 'strategy', $strategy ). + * + * @var string|null + */ + protected $strategy = null; + + /** + * When true, register() attaches the registration/enqueue flow to enqueue_block_editor_assets. + * + * @var bool + */ + protected $for_block_editor = false; + + /** + * When set, register_style() calls wp_enqueue_block_style() against this block name. + * + * @var string|null + */ + protected $block_name = null; + + /** + * Register the asset with WP but do not enqueue it. Handle remains available for deps. + * + * @var bool + */ + protected $register_only = false; + + /** + * Path to a block.json directory for register_block_type_from_metadata(). + * + * @var string|null + */ + protected $block_json_path = null; + /** * Creates an Enqueue instance. * @@ -194,14 +250,28 @@ public function media( string $media ): self { return $this; } + /** + * Attaches an inline CSS snippet to the registered style handle. + * + * Calls stack — each call adds another entry. Only applies to styles; a + * silent no-op on a script Enqueue. + * + * @param string $css CSS source. + * @return self + */ + public function with_style( string $css ): self { + $this->inline_styles[] = $css; + return $this; + } + /** * DEPRECATED DUE TO TYPO * * see latest_version() * @deprecated 1.3.0 */ - public function lastest_version():self { - trigger_error( 'Method ' . __METHOD__ . ' is deprecated', E_USER_DEPRECATED ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error + public function lastest_version(): self { + _deprecated_function( __METHOD__, '1.3.0', __CLASS__ . '::latest_version()' ); return $this->latest_version(); } @@ -212,18 +282,21 @@ public function lastest_version():self { * @return self */ public function latest_version(): self { - if ( $this->does_file_exist( $this->src ) ) { - - // If php8 or above set as bool, else int - $associate = ( PHP_VERSION_ID >= 80000 ) ? true : 1; + if ( empty( $this->src ) ) { + return $this; + } - $headers = get_headers( $this->src, $associate ); + $response = \wp_remote_head( $this->src, array( 'timeout' => 1 ) ); + if ( \is_wp_error( $response ) || \wp_remote_retrieve_response_code( $response ) !== 200 ) { + return $this; + } - if ( is_array( $headers ) - && array_key_exists( 'Last-Modified', $headers ) - ) { - $this->ver = strtotime( $headers['Last-Modified'] ); - } + $last_modified = \wp_remote_retrieve_header( $response, 'last-modified' ); + if ( is_array( $last_modified ) ) { + $last_modified = end( $last_modified ); + } + if ( ! empty( $last_modified ) ) { + $this->ver = strtotime( (string) $last_modified ); } return $this; } @@ -231,20 +304,18 @@ public function latest_version(): self { /** * Checks to see if a file exist using URL (not path). * + * Uses wp_remote_head() so test mocks via the pre_http_request filter can + * fake the response without a real network round-trip. + * * @param string $url The URL of the file being checked. * @return boolean true if it does, false if it doesnt. */ private function does_file_exist( string $url ): bool { - $ch = curl_init( $url ); - if ( ! $ch ) { + $response = \wp_remote_head( $url, array( 'timeout' => 1 ) ); + if ( \is_wp_error( $response ) ) { return false; } - curl_setopt( $ch, CURLOPT_NOBODY, true ); - curl_setopt( $ch, CURLOPT_TIMEOUT_MS, 50 ); - curl_exec( $ch ); - $http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE ); - curl_close( $ch ); - return $http_code === 200; + return \wp_remote_retrieve_response_code( $response ) === 200; } /** @@ -274,7 +345,7 @@ public function header(): self { * @param boolean $inline * @return self */ - public function inline( bool $inline = true ):self { + public function inline( bool $inline = true ): self { $this->inline = $inline; return $this; } @@ -290,6 +361,43 @@ public function localize( array $args ): self { return $this; } + /** + * Registers the script for translation via wp_set_script_translations(). + * + * Only applies to scripts. On a style Enqueue this is a silent no-op so + * fluent chains don't need to branch. + * + * @param string $domain Text domain. + * @param string|null $path Optional path to the JSON translation files. + * @return self + */ + public function translations( string $domain, ?string $path = null ): self { + $this->translations = array( + 'domain' => $domain, + 'path' => $path, + ); + return $this; + } + + /** + * Attaches an inline JavaScript snippet to the registered handle. + * + * Calls stack — each call adds another entry. `$position` is `'before'` or + * `'after'` (matching wp_add_inline_script). Only applies to scripts; a + * silent no-op on a style Enqueue. + * + * @param string $code JavaScript source. + * @param string $position 'before' or 'after'. Default 'after'. + * @return self + */ + public function with_code( string $code, string $position = 'after' ): self { + $this->inline_scripts[] = array( + 'code' => $code, + 'position' => $position, + ); + return $this; + } + /** * Adds a Flag (attribute with no value) to a script/style tag * @@ -314,32 +422,28 @@ public function attribute( string $key, string $value ): self { } /** - * Marks the script or style as deferred loaded. + * Marks the script as deferred loaded. + * + * Uses WordPress 6.3+'s dep-aware script strategy via wp_script_add_data() + * instead of splatting the attribute through the script_loader_tag filter. * * @return self */ public function defer(): self { - // Remove ASYNC if set. - if ( \array_key_exists( 'async', $this->attributes ) ) { - unset( $this->attributes['async'] ); - } - - $this->attributes['defer'] = ''; + $this->strategy = 'defer'; return $this; } /** - * Marks the script or style as async loaded. + * Marks the script as async loaded. + * + * Uses WordPress 6.3+'s dep-aware script strategy via wp_script_add_data() + * instead of splatting the attribute through the script_loader_tag filter. * * @return self */ public function async(): self { - // Remove DEFER if set. - if ( \array_key_exists( 'defer', $this->attributes ) ) { - unset( $this->attributes['defer'] ); - } - - $this->attributes['async'] = ''; + $this->strategy = 'async'; return $this; } @@ -349,17 +453,108 @@ public function async(): self { * @param bool $for_block Denotes if being enqueued for a block. * @return self */ - public function for_block( bool $for_block = true ) : self { + public function for_block( bool $for_block = true ): self { $this->for_block = $for_block; return $this; } + /** + * Defer the registration/enqueue to the enqueue_block_editor_assets hook. + * + * When set, calling register() attaches the rest of the flow to the block + * editor hook instead of running immediately. Use for scripts/styles that + * should only load inside the admin block editor. + * + * @param bool $for_block_editor Default true. + * @return self + */ + public function for_block_editor( bool $for_block_editor = true ): self { + $this->for_block_editor = $for_block_editor; + return $this; + } + + /** + * Attach the style to a named block via wp_enqueue_block_style(). + * + * Only applies to styles. When set, register_style() calls + * wp_enqueue_block_style() against the block so the stylesheet loads only + * when the block is rendered on the front-end. + * + * @param string $block_name Block name (e.g. 'core/paragraph' or 'my-plugin/my-block'). + * @return self + */ + public function for_block_style( string $block_name ): self { + $this->block_name = $block_name; + return $this; + } + + /** + * Register the asset with WP without enqueueing it. + * + * Useful when another handle will reference this one via a dep array, or + * when a block's block.json names it. Keeps the handle available without + * emitting it on every page. + * + * @param bool $register_only Default true. + * @return self + */ + public function register_only( bool $register_only = true ): self { + $this->register_only = $register_only; + return $this; + } + + /** + * Register a block type from a block.json metadata directory. + * + * When set, register() short-circuits to register_block_type_from_metadata() + * instead of the normal script/style registration path. The handle / src / + * deps / ver passed via the fluent API are ignored — block.json is the + * source of truth for asset declarations. + * + * @param string $path Absolute path to the directory containing block.json. + * @return self + */ + public function block_json( string $path ): self { + $this->block_json_path = $path; + return $this; + } + /** * Registers the file as either enqueued or inline parsed. * * @return void */ public function register(): void { + // block.json registration takes over completely — the handle/src/deps + // passed via the fluent API are ignored; block.json drives the assets. + if ( null !== $this->block_json_path ) { + \register_block_type_from_metadata( $this->block_json_path ); + return; + } + + // Block-editor scope: defer the rest of the flow to the editor hook. + if ( $this->for_block_editor ) { + add_action( + 'enqueue_block_editor_assets', + function () { + $this->register_inner(); + } + ); + return; + } + + $this->register_inner(); + } + + /** + * Dispatches the registration to the script or style path. + * + * Extracted from register() so it can be re-entered from the + * enqueue_block_editor_assets callback. + * + * @return void + */ + private function register_inner(): void { if ( $this->type === 'script' ) { $this->register_script(); } @@ -375,20 +570,38 @@ public function register(): void { * @return void */ private function register_style() { + // Block-bound style: wp_enqueue_block_style() registers and attaches it + // to the named block, loads only when the block renders. + if ( null !== $this->block_name ) { + \wp_enqueue_block_style( + $this->block_name, + array( + 'handle' => $this->handle, + 'src' => $this->src, + 'deps' => $this->deps, + 'ver' => $this->ver, + 'media' => $this->media, + ) + ); + } else { + \wp_register_style( + $this->handle, + $this->src, + $this->deps, + $this->ver, + $this->media + ); + + if ( false === $this->for_block && false === $this->register_only ) { + wp_enqueue_style( $this->handle ); + } + } - \wp_register_style( - $this->handle, - $this->src, - $this->deps, - $this->ver, - $this->media - ); - if ( false === $this->for_block ) { - wp_enqueue_style( $this->handle ); + foreach ( $this->inline_styles as $css ) { + \wp_add_inline_style( $this->handle, $css ); } $this->add_style_attributes(); - } /** @@ -406,9 +619,29 @@ private function register_script() { $this->footer ); - // Maybe add as an inline script. + // Script strategy (defer/async) via WP's dep-aware API. + if ( null !== $this->strategy ) { + \wp_script_add_data( $this->handle, 'strategy', $this->strategy ); + } + + // Script translations (i18n JSON lookup). + if ( null !== $this->translations ) { + \wp_set_script_translations( + $this->handle, + $this->translations['domain'], + $this->translations['path'] ?? '' + ); + } + + // Maybe add as an inline script (whole-file inline — legacy behaviour). if ( $this->inline && $this->does_file_exist( $this->src ) ) { - \wp_add_inline_script( $this->handle, file_get_contents( $this->src ) ?: '' ); + $contents = file_get_contents( $this->src ); + \wp_add_inline_script( $this->handle, false !== $contents ? $contents : '' ); + } + + // Attach inline code snippets added via with_code(). + foreach ( $this->inline_scripts as $snippet ) { + \wp_add_inline_script( $this->handle, $snippet['code'], $snippet['position'] ); } // Localize all values if defined. @@ -416,8 +649,8 @@ private function register_script() { \wp_localize_script( $this->handle, $this->handle, $this->localize ); } - // Enqueue file if not used for a block. - if ( false === $this->for_block ) { + // Enqueue file if not used for a block / not register-only. + if ( false === $this->for_block && false === $this->register_only ) { \wp_enqueue_script( $this->handle ); } @@ -441,7 +674,7 @@ private function add_script_attributes(): void { // Add to any scripts. add_filter( 'script_loader_tag', - function( string $tag, string $handle, string $source ) use ( $attributes ): string { + function ( string $tag, string $handle, string $source ) use ( $attributes ): string { // Bail if not our script. if ( $this->handle !== $handle ) { return $tag; @@ -489,7 +722,7 @@ private function add_style_attributes(): void { // Add to any relevant styles. add_filter( 'style_loader_tag', - function( string $tag, string $handle, string $href, string $media ) use ( $attributes ): string { + function ( string $tag, string $handle, string $href, string $media ) use ( $attributes ): string { // Bail if not our script. if ( $this->handle !== $handle ) { return $tag; @@ -523,9 +756,9 @@ public function script_type( string $script_type ) { * * @return string[] */ - private function get_attributes():array { + private function get_attributes(): array { return array_map( - function( string $key, ?string $value ): string { + function ( string $key, ?string $value ): string { return null === $value ? "{$key}" : "{$key}='{$value}'"; @@ -534,5 +767,4 @@ function( string $key, ?string $value ): string { $this->attributes ); } - } diff --git a/tests/.env b/tests/.env new file mode 100644 index 0000000..7bb1e2d --- /dev/null +++ b/tests/.env @@ -0,0 +1,8 @@ +# WP Config Details + +# Please ensure the database exists before running the tests + +WP_DB_NAME=pc_core_tests +WP_DB_USER=root +WP_DB_PASS= +WP_DB_HOST=127.0.0.1 \ No newline at end of file diff --git a/tests/Fixtures/test-block/block.json b/tests/Fixtures/test-block/block.json new file mode 100644 index 0000000..ba9d93b --- /dev/null +++ b/tests/Fixtures/test-block/block.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "pinkcrab-enqueue/test-block", + "title": "PinkCrab Enqueue Test Block", + "category": "widgets", + "description": "Fixture block for PinkCrab Enqueue integration tests.", + "textdomain": "pinkcrab-enqueue-test", + "supports": { + "html": false + } +} diff --git a/tests/Test_Enqueue.php b/tests/Test_Enqueue.php index 7167377..aa58512 100644 --- a/tests/Test_Enqueue.php +++ b/tests/Test_Enqueue.php @@ -125,54 +125,46 @@ public function test_style_setters(): void { } - /** @testdox It should be possible to denote a scriptas async easily. */ + /** @testdox It should be possible to denote a script as async easily. */ public function test_can_set_async_on_script(): void { $script = self::create_script()->async(); - $attributes = Objects::get_property( $script, 'attributes' ); - $this->assertArrayHasKey( 'async', $attributes ); + $this->assertSame( 'async', Objects::get_property( $script, 'strategy' ) ); } /** @testdox It should be possible to denote a style as async easily. */ public function test_can_set_async_style(): void { $style = self::create_style()->async(); - $attributes = Objects::get_property( $style, 'attributes' ); - $this->assertArrayHasKey( 'async', $attributes ); + // async() sets the strategy flag regardless of type; wp_script_add_data + // on render is script-only, so on a style this is a harmless no-op. + $this->assertSame( 'async', Objects::get_property( $style, 'strategy' ) ); } - /** @testdox It should be possible to denote a scriptas defer easily. */ + /** @testdox It should be possible to denote a script as defer easily. */ public function test_can_set_defer_on_script(): void { $script = self::create_script()->defer(); - $attributes = Objects::get_property( $script, 'attributes' ); - $this->assertArrayHasKey( 'defer', $attributes ); + $this->assertSame( 'defer', Objects::get_property( $script, 'strategy' ) ); } /** @testdox It should be possible to denote a style as defer easily. */ public function test_can_set_defer_style(): void { $style = self::create_style()->defer(); - $attributes = Objects::get_property( $style, 'attributes' ); - $this->assertArrayHasKey( 'defer', $attributes ); + $this->assertSame( 'defer', Objects::get_property( $style, 'strategy' ) ); } /** @testdox It should not be possible to set both async and defer, either should unset the other */ public function test_can_only_be_async_or_defer(): void { - $script = self::create_script()->async(); - $attributes = Objects::get_property( $script, 'attributes' ); - $this->assertArrayHasKey( 'async', $attributes ); - $this->assertArrayNotHasKey( 'defer', $attributes ); + $script = self::create_script()->async(); + $this->assertSame( 'async', Objects::get_property( $script, 'strategy' ) ); $script->defer(); - $attributes = Objects::get_property( $script, 'attributes' ); - $this->assertArrayHasKey( 'defer', $attributes ); - $this->assertArrayNotHasKey( 'async', $attributes ); + $this->assertSame( 'defer', Objects::get_property( $script, 'strategy' ) ); $script->async(); - $attributes = Objects::get_property( $script, 'attributes' ); - $this->assertArrayHasKey( 'async', $attributes ); - $this->assertArrayNotHasKey( 'defer', $attributes ); + $this->assertSame( 'async', Objects::get_property( $script, 'strategy' ) ); } /** @testdox It should be possible to define if a script is added to the header */ @@ -226,12 +218,78 @@ public function test_can_register_script_with_custom_types(): void { /** @testdox Calling the old, mistyped lastest_version() should throw deprecation notice. */ public function test_calling_old_latest_version_should_throw_deprecation_notice(): void { - try { - Enqueue::script('ff')->lastest_version(); - } catch (\Throwable $th) { - $this->assertStringContainsString('Enqueue::lastest_version', $th->getMessage()); - $this->assertStringContainsString('is deprecated', $th->getMessage()); - } + $this->setExpectedDeprecated( 'PinkCrab\Enqueue\Enqueue::lastest_version' ); + Enqueue::script( 'ff' )->src( 'https://example.test/js.js' )->lastest_version(); + } + + /** @testdox It should be possible to set script translations via translations(). */ + public function test_can_set_translations(): void { + $script = self::create_script()->translations( 'my-domain', '/path/to/translations' ); + $translations = Objects::get_property( $script, 'translations' ); + $this->assertSame( 'my-domain', $translations['domain'] ); + $this->assertSame( '/path/to/translations', $translations['path'] ); + + // Path is optional. + $script = self::create_script()->translations( 'just-domain' ); + $translations = Objects::get_property( $script, 'translations' ); + $this->assertSame( 'just-domain', $translations['domain'] ); + $this->assertNull( $translations['path'] ); + } + + /** @testdox It should be possible to attach inline JS snippets via with_code() and have them accumulate. */ + public function test_can_add_inline_code(): void { + $script = self::create_script() + ->with_code( 'var foo = 1;' ) + ->with_code( 'var bar = 2;', 'before' ); + + $snippets = Objects::get_property( $script, 'inline_scripts' ); + $this->assertCount( 2, $snippets ); + $this->assertSame( 'var foo = 1;', $snippets[0]['code'] ); + $this->assertSame( 'after', $snippets[0]['position'] ); + $this->assertSame( 'var bar = 2;', $snippets[1]['code'] ); + $this->assertSame( 'before', $snippets[1]['position'] ); + } + + /** @testdox It should be possible to attach inline CSS snippets via with_style() and have them accumulate. */ + public function test_can_add_inline_style(): void { + $style = self::create_style() + ->with_style( '.foo { color: red; }' ) + ->with_style( '.bar { color: blue; }' ); + + $snippets = Objects::get_property( $style, 'inline_styles' ); + $this->assertCount( 2, $snippets ); + $this->assertSame( '.foo { color: red; }', $snippets[0] ); + $this->assertSame( '.bar { color: blue; }', $snippets[1] ); + } + + /** @testdox It should be possible to flag an asset for the block editor context. */ + public function test_can_set_for_block_editor(): void { + $script = self::create_script()->for_block_editor(); + $this->assertTrue( Objects::get_property( $script, 'for_block_editor' ) ); + + // Default arg is true; explicit false also works. + $script = self::create_script()->for_block_editor( false ); + $this->assertFalse( Objects::get_property( $script, 'for_block_editor' ) ); + } + + /** @testdox It should be possible to attach a style to a named block via for_block_style(). */ + public function test_can_set_for_block_style(): void { + $style = self::create_style()->for_block_style( 'my-plugin/my-block' ); + $this->assertSame( 'my-plugin/my-block', Objects::get_property( $style, 'block_name' ) ); + } + + /** @testdox It should be possible to register without enqueueing via register_only(). */ + public function test_can_set_register_only(): void { + $script = self::create_script()->register_only(); + $this->assertTrue( Objects::get_property( $script, 'register_only' ) ); + + $script = self::create_script()->register_only( false ); + $this->assertFalse( Objects::get_property( $script, 'register_only' ) ); + } + /** @testdox It should be possible to point at a block.json directory via block_json(). */ + public function test_can_set_block_json(): void { + $script = self::create_script()->block_json( '/path/to/block-dir' ); + $this->assertSame( '/path/to/block-dir', Objects::get_property( $script, 'block_json_path' ) ); } } diff --git a/tests/Test_Enqueue_Functional.php b/tests/Test_Enqueue_Functional.php index 635fdf6..94672fe 100644 --- a/tests/Test_Enqueue_Functional.php +++ b/tests/Test_Enqueue_Functional.php @@ -388,6 +388,351 @@ function() { $this->assertArrayNotHasKey( 'script_loader_tag', $GLOBALS['wp_filter'] ); } + /** @testdox Calling defer() should register a 'defer' strategy via wp_script_add_data. */ + public function test_defer_sets_strategy_data(): void { + Enqueue::script( 'script_defer' ) + ->src( 'https://url.com/Fixtures/defer.js' ) + ->ver( '1.0.0' ) + ->defer() + ->register(); + + $this->assertArrayHasKey( 'script_defer', $GLOBALS['wp_scripts']->registered ); + $this->assertSame( 'defer', $GLOBALS['wp_scripts']->registered['script_defer']->extra['strategy'] ); + } + + /** @testdox Calling async() should register an 'async' strategy via wp_script_add_data. */ + public function test_async_sets_strategy_data(): void { + Enqueue::script( 'script_async' ) + ->src( 'https://url.com/Fixtures/async.js' ) + ->ver( '1.0.0' ) + ->async() + ->register(); + + $this->assertArrayHasKey( 'script_async', $GLOBALS['wp_scripts']->registered ); + $this->assertSame( 'async', $GLOBALS['wp_scripts']->registered['script_async']->extra['strategy'] ); + } + + /** @testdox translations() should register the handle for wp_set_script_translations. */ + public function test_translations_registered_against_handle(): void { + Enqueue::script( 'script_i18n' ) + ->src( 'https://url.com/Fixtures/i18n.js' ) + ->ver( '1.0.0' ) + ->translations( 'pinkcrab-enqueue-test', '/tmp/translations' ) + ->register(); + + $this->assertArrayHasKey( 'script_i18n', $GLOBALS['wp_scripts']->registered ); + $script = $GLOBALS['wp_scripts']->registered['script_i18n']; + $this->assertSame( 'pinkcrab-enqueue-test', $script->textdomain ); + $this->assertSame( '/tmp/translations', $script->translations_path ); + } + + /** @testdox with_code() should attach inline snippets to the handle in the correct positions. */ + public function test_with_code_attaches_inline_snippets(): void { + Enqueue::script( 'script_with_code' ) + ->src( 'https://url.com/Fixtures/with_code.js' ) + ->with_code( 'window.A = 1;' ) + ->with_code( 'window.B = 2;', 'before' ) + ->with_code( 'window.C = 3;', 'after' ) + ->register(); + + $script = $GLOBALS['wp_scripts']->registered['script_with_code']; + $after = is_array( $script->extra['after'] ) ? implode( "\n", $script->extra['after'] ) : $script->extra['after']; + $before = is_array( $script->extra['before'] ) ? implode( "\n", $script->extra['before'] ) : $script->extra['before']; + $this->assertStringContainsString( 'window.A = 1;', $after ); + $this->assertStringContainsString( 'window.C = 3;', $after ); + $this->assertStringContainsString( 'window.B = 2;', $before ); + } + + /** @testdox with_style() should attach inline CSS to the style handle's 'after' slot. */ + public function test_with_style_attaches_inline_css(): void { + Enqueue::style( 'style_with_css' ) + ->src( 'https://url.com/Fixtures/with_css.css' ) + ->with_style( '.foo { color: red; }' ) + ->with_style( '.bar { color: blue; }' ) + ->register(); + + $style = $GLOBALS['wp_styles']->registered['style_with_css']; + $after_css = implode( "\n", $style->extra['after'] ); + $this->assertStringContainsString( '.foo { color: red; }', $after_css ); + $this->assertStringContainsString( '.bar { color: blue; }', $after_css ); + } + + /** @testdox register_only() should register the script without enqueueing it. */ + public function test_register_only_script_does_not_enqueue(): void { + Enqueue::script( 'script_register_only' ) + ->src( 'https://url.com/Fixtures/register_only.js' ) + ->register_only() + ->register(); + + $this->assertTrue( wp_script_is( 'script_register_only', 'registered' ) ); + $this->assertFalse( wp_script_is( 'script_register_only', 'enqueued' ) ); + } + + /** @testdox register_only() should register the style without enqueueing it. */ + public function test_register_only_style_does_not_enqueue(): void { + Enqueue::style( 'style_register_only' ) + ->src( 'https://url.com/Fixtures/register_only.css' ) + ->register_only() + ->register(); + + $this->assertTrue( wp_style_is( 'style_register_only', 'registered' ) ); + $this->assertFalse( wp_style_is( 'style_register_only', 'enqueued' ) ); + } + + /** @testdox for_block_editor() should defer the registration to the enqueue_block_editor_assets hook. */ + public function test_for_block_editor_defers_to_editor_hook(): void { + Enqueue::script( 'script_block_editor' ) + ->src( 'https://url.com/Fixtures/block_editor.js' ) + ->for_block_editor() + ->register(); + + // Before the hook fires, the script is NOT registered. + $this->assertFalse( wp_script_is( 'script_block_editor', 'registered' ) ); + + // Firing the editor hook should register + enqueue the script. + do_action( 'enqueue_block_editor_assets' ); + $this->assertTrue( wp_script_is( 'script_block_editor', 'registered' ) ); + $this->assertTrue( wp_script_is( 'script_block_editor', 'enqueued' ) ); + } + + /** @testdox for_block_style() should register the style and attach a render_block_{name} filter. */ + public function test_for_block_style_attaches_to_block(): void { + // Block must be registered first for wp_enqueue_block_style to attach a filter to it. + \register_block_type( + 'pinkcrab-enqueue/style-target', + array( + 'api_version' => 3, + 'title' => 'Style Target', + 'category' => 'widgets', + ) + ); + + Enqueue::style( 'block_bound_style' ) + ->src( 'https://url.com/Fixtures/block_bound.css' ) + ->ver( '1.0.0' ) + ->for_block_style( 'pinkcrab-enqueue/style-target' ) + ->register(); + + // wp_enqueue_block_style wires a deferred callback. Depending on the + // WP version and wp_should_load_block_assets_on_demand(), it lands on + // render_block, wp_enqueue_scripts/wp_footer, or enqueue_block_assets. + // Fire the full set to cover every branch. + apply_filters( + 'render_block', + '

test

', + array( + 'blockName' => 'pinkcrab-enqueue/style-target', + 'attrs' => array(), + 'innerBlocks' => array(), + 'innerHTML' => '', + 'innerContent' => array(), + ), + null + ); + do_action( 'wp_enqueue_scripts' ); + do_action( 'wp_footer' ); + do_action( 'enqueue_block_assets' ); + + $this->assertTrue( wp_style_is( 'block_bound_style', 'registered' ) ); + + \unregister_block_type( 'pinkcrab-enqueue/style-target' ); + } + + /** @testdox block_json() should register a block type from the block.json directory. */ + public function test_block_json_registers_block_type(): void { + $block_dir = __DIR__ . '/Fixtures/test-block'; + + Enqueue::script( 'ignored_handle' ) + ->block_json( $block_dir ) + ->register(); + + $registry = \WP_Block_Type_Registry::get_instance(); + $this->assertTrue( $registry->is_registered( 'pinkcrab-enqueue/test-block' ) ); + + $registry->unregister( 'pinkcrab-enqueue/test-block' ); + } + + /** @testdox latest_version() is a no-op when no src has been set. */ + public function test_latest_version_no_op_when_src_empty(): void { + $script = Enqueue::script( 'no_src' )->latest_version(); + $this->assertFalse( \Gin0115\WPUnit_Helpers\Objects::get_property( $script, 'ver' ) ); + } + + /** @testdox latest_version() bails when wp_remote_head returns a WP_Error. */ + public function test_latest_version_bails_on_http_error(): void { + add_filter( + 'pre_http_request', + static function () { + return new \WP_Error( 'http_request_failed', 'fake network fail' ); + }, + 10, + 3 + ); + + $script = Enqueue::script( 'errored' ) + ->src( 'https://fake.example/script.js' ) + ->latest_version(); + + // ver stays at the default false (no timestamp parsed). + $this->assertFalse( \Gin0115\WPUnit_Helpers\Objects::get_property( $script, 'ver' ) ); + + remove_all_filters( 'pre_http_request' ); + } + + /** @testdox latest_version() bails when the remote returns a non-200 status. */ + public function test_latest_version_bails_on_non_200(): void { + add_filter( + 'pre_http_request', + static function () { + return array( + 'headers' => array(), + 'body' => '', + 'response' => array( + 'code' => 404, + 'message' => 'Not Found', + ), + 'cookies' => array(), + 'filename' => null, + ); + }, + 10, + 3 + ); + + $script = Enqueue::script( 'missing' ) + ->src( 'https://fake.example/missing.js' ) + ->latest_version(); + + $this->assertFalse( \Gin0115\WPUnit_Helpers\Objects::get_property( $script, 'ver' ) ); + + remove_all_filters( 'pre_http_request' ); + } + + /** @testdox latest_version() parses a scalar Last-Modified header into a unix timestamp. */ + public function test_latest_version_sets_ver_from_last_modified(): void { + $modified = 'Mon, 20 Apr 2026 12:34:56 GMT'; + add_filter( + 'pre_http_request', + static function () use ( $modified ) { + return array( + 'headers' => array( 'last-modified' => $modified ), + 'body' => '', + 'response' => array( + 'code' => 200, + 'message' => 'OK', + ), + 'cookies' => array(), + 'filename' => null, + ); + }, + 10, + 3 + ); + + $script = Enqueue::script( 'latest' ) + ->src( 'https://fake.example/latest.js' ) + ->latest_version(); + + $this->assertSame( strtotime( $modified ), \Gin0115\WPUnit_Helpers\Objects::get_property( $script, 'ver' ) ); + + remove_all_filters( 'pre_http_request' ); + } + + /** @testdox latest_version() handles a Last-Modified header returned as an array (multi-valued). */ + public function test_latest_version_handles_array_last_modified(): void { + $oldest = 'Mon, 01 Jan 2026 00:00:00 GMT'; + $newest = 'Mon, 20 Apr 2026 12:34:56 GMT'; + add_filter( + 'pre_http_request', + static function () use ( $oldest, $newest ) { + return array( + 'headers' => array( 'last-modified' => array( $oldest, $newest ) ), + 'body' => '', + 'response' => array( + 'code' => 200, + 'message' => 'OK', + ), + 'cookies' => array(), + 'filename' => null, + ); + }, + 10, + 3 + ); + + $script = Enqueue::script( 'latest_array' ) + ->src( 'https://fake.example/latest.js' ) + ->latest_version(); + + // end() picks the last entry — the newer timestamp. + $this->assertSame( strtotime( $newest ), \Gin0115\WPUnit_Helpers\Objects::get_property( $script, 'ver' ) ); + + remove_all_filters( 'pre_http_request' ); + } + + /** @testdox inline() does not attach file contents when the remote HEAD request fails (WP_Error). */ + public function test_inline_skips_file_contents_on_http_error(): void { + add_filter( + 'pre_http_request', + static function () { + return new \WP_Error( 'http_request_failed', 'fake network fail' ); + }, + 10, + 3 + ); + + Enqueue::script( 'inline_errored' ) + ->src( 'https://fake.example/dead.js' ) + ->inline() + ->register(); + + $script = $GLOBALS['wp_scripts']->registered['inline_errored']; + // No inline contents should have been attached. + $this->assertFalse( isset( $script->extra['after'] ) && ! empty( $script->extra['after'] ) ); + + remove_all_filters( 'pre_http_request' ); + } + + /** @testdox inline() attaches the file contents to the registered script when does_file_exist returns true. */ + public function test_inline_attaches_file_contents_when_remote_exists(): void { + $body = 'console.log("inline-me");'; + add_filter( + 'pre_http_request', + static function () use ( $body ) { + return array( + 'headers' => array(), + 'body' => $body, + 'response' => array( + 'code' => 200, + 'message' => 'OK', + ), + 'cookies' => array(), + 'filename' => null, + ); + }, + 10, + 3 + ); + + // Create a temp file locally because the legacy inline() path still + // uses file_get_contents(); the wp_remote_head does_file_exist check + // is the HTTP-mocked part. + $tmp = tempnam( sys_get_temp_dir(), 'pcenq' ); + file_put_contents( $tmp, $body ); + + Enqueue::script( 'inline_file' ) + ->src( $tmp ) + ->inline() + ->register(); + + $script = $GLOBALS['wp_scripts']->registered['inline_file']; + $after = is_array( $script->extra['after'] ) ? implode( "\n", $script->extra['after'] ) : $script->extra['after']; + $this->assertStringContainsString( $body, $after ); + + unlink( $tmp ); + remove_all_filters( 'pre_http_request' ); + } + /** @testdox When registering a script, if the type is custom and manual id is added to attributes, it should not be auto generated. */ public function test_manual_id_added_to_attributes_if_custom_type_and_manual_id(): void { // Enqueue diff --git a/tests/wp-config.php b/tests/wp-config.php index f5764eb..3791f5d 100644 --- a/tests/wp-config.php +++ b/tests/wp-config.php @@ -71,3 +71,15 @@ define( 'WP_PHP_BINARY', 'php' ); define( 'WPLANG', '' ); + +// WP 6.8 emits an E_USER_NOTICE for wp_is_block_theme() being called too early +// during tests. https://core.trac.wordpress.org/ticket/63086 +set_error_handler( + function ( $errno, $errstr ) { + if ( $errno === E_USER_NOTICE && strpos( $errstr, 'wp_is_block_theme' ) !== false ) { + return true; + } + return false; + }, + E_USER_NOTICE +);