diff --git a/.github/scripts/fill-in-scaffold.sh b/.github/scripts/fill-in-scaffold.sh index 740352c..e3de119 100755 --- a/.github/scripts/fill-in-scaffold.sh +++ b/.github/scripts/fill-in-scaffold.sh @@ -22,18 +22,18 @@ done # A namespace that is a reserved word, such as List, parses from PHP 8.0 only, which the fill-in job's PHP 7.4 syntax # check refuses. [[ $NS =~ ^[A-Za-z][A-Za-z0-9]*$ ]] || fail 'php-namespace must match ^[A-Za-z][A-Za-z0-9]*$.' -# One segment after dws_ keeps every prefix in one plugin's family from starting another plugin's; past 23 characters +# One segment after dws_ keeps every prefix in one plugin's family from starting another plugin's; past 22 characters # the template's longest option constants fail PHPCS's array alignment, which Quality's rehearsal at the cap checks, # and the framework's admin notices refuse a network or hidden segment. -[[ $PREFIX =~ ^dws_[a-z0-9]{1,23}$ && $PREFIX != dws_network && $PREFIX != dws_hidden ]] || fail 'prefix must match ^dws_[a-z0-9]{1,23}$ and must not be dws_network or dws_hidden.' +[[ $PREFIX =~ ^dws_[a-z0-9]{1,22}$ && $PREFIX != dws_network && $PREFIX != dws_hidden ]] || fail 'prefix must match ^dws_[a-z0-9]{1,22}$ and must not be dws_network or dws_hidden.' # The ports land in JSON numbers, where a leading zero is invalid, and the tests and multisite environments take the # next two; none may be the template's own 8811, 8812 or 8813, which the leftover check below refuses. [[ $PORT =~ ^[1-9][0-9]{0,4}$ && $PORT -le 65533 && ( $PORT -lt 8809 || $PORT -gt 8813 ) ]] || fail 'wp-env-port must be 1-65533 with no leading zero, and neither it nor the next two ports may be 8811, 8812 or 8813.' -# WordPress skips a plugin whose name is blank, and the name lands in single-quoted PHP and JavaScript strings, and in -# JSX text. -name_breakers="['\\\$/<>{}]" -[[ $PLUGIN_NAME =~ [^[:space:]] && ! $PLUGIN_NAME =~ $name_breakers ]] || fail "plugin-name must not be blank or contain \\, \$, /, ', <, >, { or }." +# WordPress skips a plugin whose name is blank and strips tags from the name it shows, and the name lands in +# single-quoted PHP and JavaScript strings. +name_breakers="['\\\$/<>]" +[[ $PLUGIN_NAME =~ [^[:space:]] && ! $PLUGIN_NAME =~ $name_breakers ]] || fail "plugin-name must not be blank or contain \\, \$, /, ', < or >." # The description lands in the entry's header docblock, which */ would close. [[ $DESCRIPTION =~ [^[:space:]] && $DESCRIPTION != *'*/'* ]] || fail 'description must not be blank or contain */.' diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 953c8f3..39b465c 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -88,7 +88,7 @@ jobs: SLUG: acme-orders NS: AcmeOrders # The rehearsal fills in the longest prefix the script accepts, so its PHPCS run fails once the cap stops passing. - PREFIX: dws_acmeextraordinaryorders + PREFIX: dws_acmeextraordinarysales PORT: '8821' REPO: acme/acme-orders run: .github/scripts/fill-in-scaffold.sh @@ -98,7 +98,7 @@ jobs: run: | composer lint:php composer test:unit - # The display name lands in the block's JSX, which only a build and the script and type lints parse. + # The display name lands in the block's editor script, which only a build and the script and type lints parse. - name: Build and lint the filled-in plugin's scripts run: | npm run build diff --git a/AGENTS.md b/AGENTS.md index 580dbff..91337d1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,7 @@ src/ the composition root and its components only ├── Plugin.php SLUG, PREFIX (dws_tpl), COMPONENTS, the logger, the scheduler, the migration runner and the kernel ├── Settings.php the WooCommerce settings tab dws_tpl: the greeting's fields and the uninstall consent ├── Blocks.php the blocks from blocks/build/blocks-manifest.php and the block-editor script -└── Feature/ the greeting: a composite gated on DWS_TPL_OPTION_FEATURE and its account-dashboard child +└── Greeting/ the greeting: GreetingFeature, gated on DWS_TPL_OPTION_GREETING_ENABLED, and its AccountDashboardGreeting child includes/ hook-free code as dws_tpl_ functions and DWS_TPL_ constants, loaded in name order, _-prefixed files skipped ├── safeguards.php restrictions that hold whenever the plugin cannot boot; loaded by the entry before the floor check ├── options.php the option keys, their defaults, the typed readers, the translated text and the validator @@ -20,7 +20,7 @@ includes/ hook-free code as dws_tpl_ functions and DWS_TPL_ c ├── migrations.php dws_tpl_get_migration_steps() and one function per step, append-only ├── assets.php the *.asset.php reader and the script and style registrars └── metadata.php the slug, name and version getters, the plugin's surface for a peer plugin -blocks/src/, assets/src/ TypeScript and SCSS, built into blocks/build/ and assets/build/ (untracked) +blocks/src/, assets/src/ TypeScript, SCSS and each block's render.php, built into blocks/build/ and assets/build/ (untracked) models/, templates/ empty extension points .github/scripts/fill-in-scaffold.sh the fill-in's sweep, which fill-in-scaffold.yml runs once in a new plugin and Quality rehearses here ``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b2c0bc0..92a7b49 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,4 +18,4 @@ A release writes the fragments into a new version block of `CHANGELOG.md`, whose composer changelog:write ``` -The release commit also sets the entry file's `Version:` header to that version, and `readme.txt`'s `Stable tag:` where the plugin has one, because the release refuses a tag either differs from. Pushing the tag `v` on that commit starts the release. +The release commit also sets the entry file's `Version:` header to that version, and `readme.txt`'s `Stable tag:` where the plugin has one, because the release refuses a tag either differs from. It sets each `blocks/src/*/block.json`'s `version` too, which WordPress puts in the block's stylesheet URL and the Unit suite holds to the header. Pushing the tag `v` on that commit starts the release. diff --git a/README.md b/README.md index 4dc454e..949a2d7 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ The workflow: ## Naming - A WooCommerce plugin's display name reads `X for WooCommerce`, as WooCommerce's trademark guidelines ask, and its slug ends in `-for-woocommerce`. -- The prefix is `dws_` and one segment of up to 23 lowercase letters and digits, such as `dws_lpmwc`, so no plugin's prefix starts with another's and the template's longest constants still pass PHPCS; the framework's admin notices refuse `dws_network` and `dws_hidden`; a WooCommerce plugin's segment ends in `wc`. +- The prefix is `dws_` and one segment of up to 22 lowercase letters and digits, such as `dws_lpmwc`, so no plugin's prefix starts with another's and the template's longest constants still pass PHPCS; the framework's admin notices refuse `dws_network` and `dws_hidden`; a WooCommerce plugin's segment ends in `wc`. - The namespace is flat: `DeepWebSolutions\\`, with no segment in between. - The repository is `wc-` for a WooCommerce plugin and `wp-` otherwise, as the plugin mirrors are named, and the fill-in names the Composer package `/` and the npm package `@/` after it. - Each plugin takes its own three consecutive wp-env ports, for its development, tests and multisite sites, apart from 8888 and 8889, which wp-env uses by default, and the template's own 8811, 8812 and 8813. diff --git a/README.scaffold.md b/README.scaffold.md index ffec189..a0ddf4f 100644 --- a/README.scaffold.md +++ b/README.scaffold.md @@ -53,9 +53,9 @@ The fill-in renamed the plugin and kept the template's demo, which boots and pas | Part | Where it lives | Removing it | | --- | --- | --- | -| The greeting | `src/Feature/`, the Greeting section of `src/Settings.php`, and the `DWS_TPL_OPTION_FEATURE`, `DWS_TPL_OPTION_GREETING` and `DWS_TPL_OPTION_GREETING_ORDERS` keys in `includes/options.php` | Delete `src/Feature/` and its row in `Plugin::COMPONENTS`, the Greeting section of `src/Settings.php`, `assets/src/greeting.scss` and its `greeting=` entry in `package.json`'s `build:assets` and `start:assets`, the three keys with their defaults, text and validator, their lines in `dws_tpl_add_settings()`, and `tests/Integration/Feature/`, then the rows naming them in `OptionsTest`, `MigrationsTest` and `SettingsTest`, and `PluginBootTest`'s expectation that the greeting is skipped. `tests/EndToEnd/settings.spec.ts` drives the greeting's validated field, so point it at a validated field of the plugin's own. | -| The example block | `blocks/src/example-notice/`, registered by `src/Blocks.php` from `blocks/build/blocks-manifest.php` | Delete `blocks/`, `build:blocks` and `start:blocks` with their calls in `build` and `start`, `register_blocks()` and its `init` hook in `src/Blocks.php`, and `BlocksTest`'s block rows. | -| The editor script | `assets/src/editor.ts`, enqueued in the block editor by `src/Blocks.php` | Delete it, its `editor=` entry in `build:assets` and `start:assets`, `enqueue_editor_script()` and its hook in `src/Blocks.php`, and `BlocksTest`'s editor row. Once `assets/src/` holds nothing, delete `build:assets` and `start:assets` with their calls in `build` and `start`. Once the plugin registers no blocks either, delete `src/Blocks.php`, its row in `Plugin::COMPONENTS` and `BlocksTest`. | +| The greeting | `src/Greeting/`, the Greeting section of `src/Settings.php`, and the `DWS_TPL_OPTION_GREETING_ENABLED`, `DWS_TPL_OPTION_GREETING` and `DWS_TPL_OPTION_GREETING_ORDERS` keys in `includes/options.php` | Delete `src/Greeting/` and its row in `Plugin::COMPONENTS`, the Greeting section of `src/Settings.php`, `assets/src/greeting.scss` and its `greeting=` entry in `package.json`'s `build:assets` and `start:assets`, the three keys with their defaults, text and validator, their lines in `dws_tpl_add_settings()`, and `tests/Integration/Greeting/`, then the rows naming them in `OptionsTest`, `MigrationsTest` and `SettingsTest`, and `PluginBootTest`'s expectation that the greeting is skipped. `tests/EndToEnd/settings.spec.ts` drives the greeting's validated field, so point it at a validated field of the plugin's own. | +| The example block | `blocks/src/example-notice/`, registered by `src/Blocks.php` from `blocks/build/blocks-manifest.php` and rendered by its `render.php` | Delete `blocks/`, `build:blocks` and `start:blocks` with their calls in `build` and `start`, `register_blocks()` and its `init` hook in `src/Blocks.php`, `BlocksTest`'s block rows, and the editor script's command, which inserts the block, with `tests/EndToEnd/editor.spec.ts`. | +| The editor script | `assets/src/editor.ts`, a command in the editor's command palette that inserts the example block, enqueued in the block editor by `src/Blocks.php` | Delete it, its `editor=` entry in `build:assets` and `start:assets`, `@wordpress/commands` and `@wordpress/data` from `package.json`'s `devDependencies`, `enqueue_editor_script()` and its hook in `src/Blocks.php`, `BlocksTest`'s editor row and `tests/EndToEnd/editor.spec.ts`. Once `assets/src/` holds nothing, delete `build:assets` and `start:assets` with their calls in `build` and `start`. Once the plugin registers no blocks either, delete `src/Blocks.php`, its row in `Plugin::COMPONENTS` and `BlocksTest`. | | `models/` and `templates/` | Empty extension points, for classes on Composer's classmap and for template partials | Delete the folders the plugin never fills, and `models/` from `composer.json`'s `autoload.classmap` and `phpstan.dist.neon`'s `paths`, since Composer and PHPStan both fail on a missing folder. | | The fill-in | `.github/workflows/fill-in-scaffold.yml`, `.github/scripts/fill-in-scaffold.sh` and Quality's `scaffold` job | Delete all three: GitHub refuses the fill-in's own token for workflow changes, so the fill-in cannot remove them, and the job skips itself outside the template. | diff --git a/assets/src/editor.ts b/assets/src/editor.ts index b01fec1..07ca759 100644 --- a/assets/src/editor.ts +++ b/assets/src/editor.ts @@ -1,7 +1,16 @@ -import { registerBlockStyle } from '@wordpress/blocks'; +import { store as blockEditorStore } from '@wordpress/block-editor'; +import { createBlock } from '@wordpress/blocks'; +import { store as commandsStore } from '@wordpress/commands'; +import { dispatch } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; -registerBlockStyle( 'dws-plugin-template/example-notice', { - name: 'outlined', - label: __( 'Outlined', 'dws-plugin-template' ), +dispatch( commandsStore ).registerCommand( { + name: 'dws-plugin-template/insert-example-notice', + label: __( 'Insert an example notice', 'dws-plugin-template' ), + callback: ( { close } ) => { + dispatch( blockEditorStore ).insertBlocks( + createBlock( 'dws-plugin-template/example-notice' ) + ); + close(); + }, } ); diff --git a/blocks/src/declarations.d.ts b/blocks/src/declarations.d.ts index a12fe77..38c0da6 100644 --- a/blocks/src/declarations.d.ts +++ b/blocks/src/declarations.d.ts @@ -1,17 +1,26 @@ // wp-scripts bundles the styles a block imports, so TypeScript only needs to know the imports exist. declare module '*.scss'; -// @wordpress/block-editor ships no type declarations, so the hooks the blocks use are declared here. +// @wordpress/block-editor ships no type declarations, so the hooks the blocks use and the store the editor script +// dispatches to are declared here. declare module '@wordpress/block-editor' { + import type { BlockInstance } from '@wordpress/blocks'; + import type { ReduxStoreConfig, StoreDescriptor } from '@wordpress/data'; import type { HTMLAttributes } from 'react'; + export const store: StoreDescriptor< + ReduxStoreConfig< + unknown, + { + insertBlocks: ( + blocks: BlockInstance | BlockInstance[] + ) => object; + }, + unknown + > + >; + export function useBlockProps( props?: HTMLAttributes< HTMLElement > ): HTMLAttributes< HTMLElement >; - - export namespace useBlockProps { - function save( - props?: HTMLAttributes< HTMLElement > - ): HTMLAttributes< HTMLElement >; - } } diff --git a/blocks/src/example-notice/block.json b/blocks/src/example-notice/block.json index cbd478e..7c99149 100644 --- a/blocks/src/example-notice/block.json +++ b/blocks/src/example-notice/block.json @@ -2,17 +2,25 @@ "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 3, "name": "dws-plugin-template/example-notice", + "version": "1.0.0", "title": "Example Notice", "category": "widgets", "icon": "info", - "description": "Displays a static example notice.", + "description": "Displays an example notice.", "attributes": {}, "example": {}, "supports": { "html": false }, + "styles": [ + { + "name": "outlined", + "label": "Outlined" + } + ], "textdomain": "dws-plugin-template", "editorScript": "file:./index.js", "editorStyle": "file:./index.css", - "style": "file:./style-index.css" + "style": "file:./style-index.css", + "render": "file:./render.php" } diff --git a/blocks/src/example-notice/editor.scss b/blocks/src/example-notice/editor.scss index 1a20a8c..3cf86ff 100644 --- a/blocks/src/example-notice/editor.scss +++ b/blocks/src/example-notice/editor.scss @@ -1,3 +1,3 @@ -.wp-block-dws-plugin-template-example-notice { +.wp-block-dws-plugin-template-example-notice:not(.is-style-outlined) { outline: 1px dashed currentcolor; } diff --git a/blocks/src/example-notice/index.ts b/blocks/src/example-notice/index.ts index c8c36a8..b1f49ea 100644 --- a/blocks/src/example-notice/index.ts +++ b/blocks/src/example-notice/index.ts @@ -1,11 +1,9 @@ import { registerBlockType } from '@wordpress/blocks'; import Edit from './edit'; -import save from './save'; import metadata from './block.json'; import './style.scss'; registerBlockType( metadata, { edit: Edit, - save, } ); diff --git a/blocks/src/example-notice/render.php b/blocks/src/example-notice/render.php new file mode 100644 index 0000000..96c92d5 --- /dev/null +++ b/blocks/src/example-notice/render.php @@ -0,0 +1,14 @@ + +

>

diff --git a/blocks/src/example-notice/save.tsx b/blocks/src/example-notice/save.tsx deleted file mode 100644 index 9a1ba4f..0000000 --- a/blocks/src/example-notice/save.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { useBlockProps } from '@wordpress/block-editor'; - -// The editor validates saved markup against the post's content, so a translated string would break the block in another locale. -export default function save() { - return ( -

- An example notice from DWS Plugin Template. -

- ); -} diff --git a/composer.json b/composer.json index c8c089b..aa935cc 100644 --- a/composer.json +++ b/composer.json @@ -56,7 +56,7 @@ "automattic/jetpack-changelogger": "^7", "humbug/php-scoper": "^0.18", "johnpbloch/wordpress-core": "^7.1", - "laravel/serializable-closure": "^2.1", + "laravel/serializable-closure": "^2.2", "php-di/php-di": "^7.1.1", "php-stubs/woocommerce-stubs": "^11.2", "phpunit/phpunit": "^13", diff --git a/composer.lock b/composer.lock index f4ca067..2de0372 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d1e6261c6b90ef84ee46e78f483994ec", + "content-hash": "131557a0ae2dc46ebbd539b11c6d2992", "packages": [], "packages-dev": [ { @@ -1233,16 +1233,16 @@ }, { "name": "laravel/serializable-closure", - "version": "v2.1.0", + "version": "v2.2.0", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "2d5869a838bbcf37e0d8b0568fc41914e81374b5" + "reference": "2bfb44e8af009b553ed2c060c94b0bc3c67b4769" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/2d5869a838bbcf37e0d8b0568fc41914e81374b5", - "reference": "2d5869a838bbcf37e0d8b0568fc41914e81374b5", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/2bfb44e8af009b553ed2c060c94b0bc3c67b4769", + "reference": "2bfb44e8af009b553ed2c060c94b0bc3c67b4769", "shasum": "" }, "require": { @@ -1290,8 +1290,8 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2026-09-22T14:32:34+00:00", - "published-time": "2026-09-22T14:33:13+00:00" + "time": "2026-10-14T21:20:29+00:00", + "published-time": "2026-11-10T16:11:08+00:00" }, { "name": "myclabs/deep-copy", diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3649c56..8fc4560 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -29,7 +29,7 @@ The request that activates the plugin does not boot it, because WordPress includ - `src/` holds the composition root and its components: final classes that implement the framework's `ComponentInterface` and add their hooks in `register_hooks()`. A component takes ungated services in its constructor, such as the logger, the scheduler or `AdminNotices`, which `Plugin::get_definitions()` provides. - `includes/` holds code that registers no hooks and is not part of the tree, as prefixed functions and constants: the option keys, defaults and typed readers (`options.php`), the footprint (`footprint.php`), the migration steps (`migrations.php`), the asset registrars (`assets.php`) and the slug, name and version getters a peer plugin reads (`metadata.php`). Quality's syntax job parses the folder on PHP 7.4, because `safeguards.php` runs before the floor check, and `phpstan.safeguards.neon` analyzes it at PHP 7.4 and the oldest WordPress the bootstrap package runs on. -- `blocks/src/` and `assets/src/` hold the TypeScript and SCSS sources, which `npm run build` compiles into `blocks/build/` and `assets/build/`; `includes/assets.php` reads the `*.asset.php` file wp-scripts writes beside each build, and `src/Blocks.php` registers the blocks from `blocks/build/blocks-manifest.php`. +- `blocks/src/` and `assets/src/` hold the TypeScript and SCSS sources, which `npm run build` compiles into `blocks/build/` and `assets/build/`, and each block's `render.php`, which it copies beside the block's build and WordPress runs on each request, so the block's text follows the visitor's language; `includes/assets.php` reads the `*.asset.php` file wp-scripts writes beside each build, and `src/Blocks.php` registers the blocks from `blocks/build/blocks-manifest.php`. - `models/` and `templates/` are empty extension points, for classes on Composer's classmap and for template partials. ## Data diff --git a/docs/recipes.md b/docs/recipes.md index 8f54ecc..6961739 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -256,6 +256,8 @@ function dws_tpl_get_int_setting( string $field ): int { /** * Returns the limit as the settings page stores it, or an error when the submission is not a whole number of at least 1. * + * The number input sends a whole number in any form a browser accepts, such as 03 or 3.0, so the submission parses as a float. + * * @since 1.0.0 * @version 1.0.0 * @@ -264,9 +266,12 @@ function dws_tpl_get_int_setting( string $field ): int { * @return int|WP_Error */ function dws_tpl_validate_limit( $submitted ) { - $limit = is_string( $submitted ) ? filter_var( $submitted, FILTER_VALIDATE_INT, array( 'options' => array( 'min_range' => 1 ) ) ) : false; + $limit = is_string( $submitted ) ? filter_var( $submitted, FILTER_VALIDATE_FLOAT ) : false; + if ( false === $limit || $limit < 1 || $limit >= PHP_INT_MAX || floor( $limit ) !== $limit ) { + return new WP_Error( 'invalid_number', __( 'Enter a whole number of 1 or more, such as 3.', 'dws-plugin-template' ) ); + } - return false === $limit ? new WP_Error( 'invalid_number', __( 'Enter a whole number of 1 or more, such as 3.', 'dws-plugin-template' ) ) : $limit; + return (int) $limit; } ``` @@ -999,7 +1004,7 @@ use DeepWebSolutions\PluginTemplate\Scoped\DeepWebSolutions\Framework\Core\Migra public function __construct() { $this->logger = new ErrorLogLogger( self::SLUG ); $this->scheduler = new Scheduler( self::PREFIX ); - $this->boot = new PluginBoot( DWS_TPL_FILE, self::PREFIX, $this->logger ); + $this->boundary = new PluginBoot( DWS_TPL_FILE, self::PREFIX, $this->logger ); } // ... @@ -1014,7 +1019,7 @@ use DeepWebSolutions\PluginTemplate\Scoped\DeepWebSolutions\Framework\Core\Migra */ protected function compose(): bool { $this->scheduler->register_hooks(); - $this->migrations = new MigrationRunner( DWS_TPL_FILE, self::PREFIX . '_revision', dws_tpl_get_migration_steps(), $this->logger, $this->scheduler ); + $this->migrations = new MigrationRunner( DWS_TPL_FILE, DWS_TPL_OPTION_REVISION, dws_tpl_get_migration_steps(), $this->logger, $this->scheduler ); $this->migrations->register_hooks(); $this->kernel = new PluginKernel( new ContainerBuilder()->addDefinitions( $this->get_definitions( $this->logger, $this->scheduler ) )->build() ); @@ -1024,7 +1029,7 @@ use DeepWebSolutions\PluginTemplate\Scoped\DeepWebSolutions\Framework\Core\Migra } ``` -6. Delete the greeting: `src/Feature/`, `tests/Integration/Feature/` and `assets/src/greeting.scss`, then its entries in `package.json`: +6. Delete the greeting: `src/Greeting/`, `tests/Integration/Greeting/` and `assets/src/greeting.scss`, then its entries in `package.json`: ```json "build:assets": "wp-scripts build editor=./assets/src/editor.ts --output-path=assets/build", @@ -1097,8 +1102,8 @@ final class Settings implements ComponentInterface, OptionsPageDefinitionInterfa array( 'type' => 'checkbox', 'id' => DWS_TPL_SETTING_DELETE_DATA, - 'title' => __( 'Data on uninstall', 'dws-plugin-template' ), - 'desc' => __( 'Delete the plugin\'s data when it is uninstalled', 'dws-plugin-template' ), + 'title' => __( 'Remove all data on uninstallation', 'dws-plugin-template' ), + 'desc' => __( 'Delete the plugin\'s data when it is uninstalled, so it must be configured again if it is installed later', 'dws-plugin-template' ), ), ), ), @@ -1127,6 +1132,16 @@ use DeepWebSolutions\PluginTemplate\Plugin; defined( 'ABSPATH' ) || exit; +/** + * The option in which the migration runner records the revision the plugin's data has reached. + * + * @since 1.0.0 + * @version 1.0.0 + * + * @var string + */ +const DWS_TPL_OPTION_REVISION = 'dws_tpl_revision'; + /** * The option that holds the settings page's fields, keyed by field id. * @@ -1205,6 +1220,8 @@ function dws_tpl_get_int_setting( string $field ): int { /** * Returns the limit as the settings page stores it, or an error when the submission is not a whole number of at least 1. * + * The number input sends a whole number in any form a browser accepts, such as 03 or 3.0, so the submission parses as a float. + * * @since 1.0.0 * @version 1.0.0 * @@ -1213,9 +1230,12 @@ function dws_tpl_get_int_setting( string $field ): int { * @return int|WP_Error */ function dws_tpl_validate_limit( $submitted ) { - $limit = is_string( $submitted ) ? filter_var( $submitted, FILTER_VALIDATE_INT, array( 'options' => array( 'min_range' => 1 ) ) ) : false; + $limit = is_string( $submitted ) ? filter_var( $submitted, FILTER_VALIDATE_FLOAT ) : false; + if ( false === $limit || $limit < 1 || $limit >= PHP_INT_MAX || floor( $limit ) !== $limit ) { + return new WP_Error( 'invalid_number', __( 'Enter a whole number of 1 or more, such as 3.', 'dws-plugin-template' ) ); + } - return false === $limit ? new WP_Error( 'invalid_number', __( 'Enter a whole number of 1 or more, such as 3.', 'dws-plugin-template' ) ) : $limit; + return (int) $limit; } /** @@ -1270,21 +1290,22 @@ function dws_tpl_add_settings(): void { } ``` -10. In `uninstall.php`, delete the `MetaTypes` import, and take consent from the settings row's `delete_data` key: +10. In `uninstall.php`, delete the `MetaTypes` import and `$dws_tpl_meta_types`, and take consent from the settings row's `delete_data` key: ```php $dws_tpl_footprint = dws_tpl_get_footprint(); - $dws_tpl_consent = array( - 'option' => DWS_TPL_OPTION_SETTINGS, - 'key' => DWS_TPL_SETTING_DELETE_DATA, - ); // ... -Uninstaller::run( - $dws_tpl_file, - $dws_tpl_footprint, - array( 'consent' => $dws_tpl_consent ) -); + Uninstaller::run( + $dws_tpl_file, + $dws_tpl_footprint, + array( + 'consent' => array( + 'option' => DWS_TPL_OPTION_SETTINGS, + 'key' => DWS_TPL_SETTING_DELETE_DATA, + ), + ) + ); ``` 11. In `.wp-env.json` and `.wp-env.tests.json`, delete the WooCommerce mapping, and let `afterStart` only activate the plugin; `.wp-env.json`'s command takes no `--config`: diff --git a/dws-plugin-template.php b/dws-plugin-template.php index 567d84e..b325246 100644 --- a/dws-plugin-template.php +++ b/dws-plugin-template.php @@ -96,5 +96,5 @@ function dws_tpl_render_build_notice(): void { return; } - printf( '

%s

', esc_html__( 'DWS Plugin Template is missing some of its files, so its features are off. Reinstall the plugin to restore them.', 'dws-plugin-template' ) ); + printf( '

%s

', esc_html__( 'DWS Plugin Template is missing some of its files, so its features stay off until the plugin is reinstalled.', 'dws-plugin-template' ) ); } diff --git a/includes/assets.php b/includes/assets.php index 5dd0fb0..d17176a 100644 --- a/includes/assets.php +++ b/includes/assets.php @@ -28,7 +28,7 @@ function dws_tpl_get_asset( string $path ): array { * * @var array{dependencies: list, version: string, type?: string, handle?: string} $asset */ - $asset = require plugin_dir_path( DWS_TPL_FILE ) . dirname( $path ) . '/' . basename( $path, '.' . pathinfo( $path, PATHINFO_EXTENSION ) ) . '.asset.php'; + $asset = require plugin_dir_path( DWS_TPL_FILE ) . dirname( $path ) . '/' . pathinfo( $path, PATHINFO_FILENAME ) . '.asset.php'; return $asset; } @@ -39,9 +39,11 @@ function dws_tpl_get_asset( string $path ): array { * @since 1.0.0 * @version 1.0.0 * - * @param non-empty-string $handle The script's handle. - * @param non-empty-string $path The built script's path, relative to the plugin's directory. - * @param array{strategy?: 'async'|'defer', in_footer?: bool, fetchpriority?: 'auto'|'low'|'high'} $args Optional. How the script loads. Default none, which loads it in the head. + * @phpstan-param array{strategy?: 'async'|'defer', in_footer?: bool, fetchpriority?: 'auto'|'low'|'high'} $args + * + * @param non-empty-string $handle The script's handle. + * @param non-empty-string $path The built script's path, relative to the plugin's directory. + * @param array $args Optional. How the script loads. Default none, which loads it in the head. * * @return bool */ diff --git a/includes/footprint.php b/includes/footprint.php index dac6f3c..71700f5 100644 --- a/includes/footprint.php +++ b/includes/footprint.php @@ -26,7 +26,7 @@ function dws_tpl_get_footprint(): array { 'user_meta_site_prefixes' => array( Plugin::PREFIX . '_' ), 'user_meta_global_prefixes' => array( Plugin::PREFIX . '_' ), 'network_options' => array( Plugin::PREFIX . '_network_notices' ), - 'cron_hooks' => array( Plugin::PREFIX . '_revision/migration/run' ), + 'cron_hooks' => array( DWS_TPL_OPTION_REVISION . '/migration/run' ), 'as_groups' => array( Plugin::PREFIX ), ); } diff --git a/includes/migrations.php b/includes/migrations.php index 99b88e7..6c96580 100644 --- a/includes/migrations.php +++ b/includes/migrations.php @@ -36,9 +36,9 @@ function dws_tpl_get_migration_steps(): array { */ function dws_tpl_add_settings(): void { $defaults = array( - DWS_TPL_OPTION_FEATURE => wc_bool_to_string( DWS_TPL_OPTION_DEFAULTS[ DWS_TPL_OPTION_FEATURE ] ), - DWS_TPL_OPTION_GREETING_ORDERS => (string) DWS_TPL_OPTION_DEFAULTS[ DWS_TPL_OPTION_GREETING_ORDERS ], - DWS_TPL_OPTION_DELETE_DATA => wc_bool_to_string( DWS_TPL_OPTION_DEFAULTS[ DWS_TPL_OPTION_DELETE_DATA ] ), + DWS_TPL_OPTION_GREETING_ENABLED => wc_bool_to_string( DWS_TPL_OPTION_DEFAULTS[ DWS_TPL_OPTION_GREETING_ENABLED ] ), + DWS_TPL_OPTION_GREETING_ORDERS => (string) DWS_TPL_OPTION_DEFAULTS[ DWS_TPL_OPTION_GREETING_ORDERS ], + DWS_TPL_OPTION_DELETE_DATA => wc_bool_to_string( DWS_TPL_OPTION_DEFAULTS[ DWS_TPL_OPTION_DELETE_DATA ] ), ); foreach ( $defaults as $key => $value ) { diff --git a/includes/options.php b/includes/options.php index 0078f7b..782d7b8 100644 --- a/includes/options.php +++ b/includes/options.php @@ -23,24 +23,24 @@ const DWS_TPL_OPTION_DELETE_DATA = Plugin::PREFIX . '_delete_data'; /** - * The option that holds 'yes' where the site greets returning customers on their account dashboard. + * The option that holds the greeting. * * @since 1.0.0 * @version 1.0.0 * * @var string */ -const DWS_TPL_OPTION_FEATURE = Plugin::PREFIX . '_feature'; +const DWS_TPL_OPTION_GREETING = Plugin::PREFIX . '_greeting'; /** - * The option that holds the greeting. + * The option that holds 'yes' where the site greets returning customers on their account dashboard. * * @since 1.0.0 * @version 1.0.0 * * @var string */ -const DWS_TPL_OPTION_GREETING = Plugin::PREFIX . '_greeting'; +const DWS_TPL_OPTION_GREETING_ENABLED = 'dws_tpl_greeting_enabled'; /** * The option that holds the number of orders a customer places before the greeting shows. @@ -52,6 +52,16 @@ */ const DWS_TPL_OPTION_GREETING_ORDERS = Plugin::PREFIX . '_greeting_orders'; +/** + * The option in which the migration runner records the revision the plugin's data has reached. + * + * @since 1.0.0 + * @version 1.0.0 + * + * @var string + */ +const DWS_TPL_OPTION_REVISION = 'dws_tpl_revision'; + /** * The value each option reads as while nothing valid is stored. * @@ -61,9 +71,9 @@ * @var array */ const DWS_TPL_OPTION_DEFAULTS = array( - DWS_TPL_OPTION_DELETE_DATA => false, - DWS_TPL_OPTION_FEATURE => false, - DWS_TPL_OPTION_GREETING_ORDERS => 1, + DWS_TPL_OPTION_DELETE_DATA => false, + DWS_TPL_OPTION_GREETING_ENABLED => false, + DWS_TPL_OPTION_GREETING_ORDERS => 1, ); /** @@ -139,7 +149,9 @@ function dws_tpl_get_option_text( string $key ): string { } /** - * Returns the number of orders before the greeting shows in the form WooCommerce stores, or an error when the submission is not a whole number of at least 1. + * Returns the number of orders before the greeting shows as the digits WooCommerce stores, or an error when the submission is not a whole number of at least 1. + * + * The number input sends a whole number in any form a browser accepts, such as 03 or 3.0, so the submission parses as a float. * * @since 1.0.0 * @version 1.0.0 @@ -149,9 +161,12 @@ function dws_tpl_get_option_text( string $key ): string { * @return string|WP_Error */ function dws_tpl_validate_greeting_orders( $submitted ) { - $orders = is_string( $submitted ) ? filter_var( $submitted, FILTER_VALIDATE_INT, array( 'options' => array( 'min_range' => 1 ) ) ) : false; + $orders = is_string( $submitted ) ? filter_var( $submitted, FILTER_VALIDATE_FLOAT ) : false; + if ( false === $orders || $orders < 1 || $orders >= PHP_INT_MAX || floor( $orders ) !== $orders ) { + return new WP_Error( 'invalid_number', __( 'Enter a whole number of 1 or more, such as 3.', 'dws-plugin-template' ) ); + } - return false === $orders ? new WP_Error( 'invalid_number', __( 'Enter a whole number of 1 or more, such as 3.', 'dws-plugin-template' ) ) : (string) $orders; + return (string) (int) $orders; } /** diff --git a/package-lock.json b/package-lock.json index 2c386ca..0275cde 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,8 @@ "@types/react": "^19.3.0", "@wordpress/block-editor": "^19.0.0", "@wordpress/blocks": "^17.0.0", + "@wordpress/commands": "^2.0.0", + "@wordpress/data": "^11.0.0", "@wordpress/e2e-test-utils-playwright": "^3.1.0", "@wordpress/env": "^12.0.0", "@wordpress/i18n": "^7.0.0", diff --git a/package.json b/package.json index 1879b51..e5c485b 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "@types/react": "^19.3.0", "@wordpress/block-editor": "^19.0.0", "@wordpress/blocks": "^17.0.0", + "@wordpress/commands": "^2.0.0", + "@wordpress/data": "^11.0.0", "@wordpress/e2e-test-utils-playwright": "^3.1.0", "@wordpress/env": "^12.0.0", "@wordpress/i18n": "^7.0.0", diff --git a/phpstan.dist.neon b/phpstan.dist.neon index 2f49558..762f33d 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -7,6 +7,7 @@ includes: parameters: paths: + - blocks/src - functions.php - uninstall.php - includes diff --git a/src/Blocks.php b/src/Blocks.php index 5bdafd2..130a54b 100644 --- a/src/Blocks.php +++ b/src/Blocks.php @@ -7,7 +7,7 @@ \defined( 'ABSPATH' ) || exit; /** - * Registers the plugin's blocks from their build manifest, and the block-editor script that adds their styles. + * Registers the plugin's blocks from their build manifest, and the block-editor script. * * @since 1.0.0 * @version 1.0.0 @@ -52,8 +52,9 @@ public function register_blocks(): void { * @return void */ public function enqueue_editor_script(): void { - dws_tpl_register_script( Plugin::SLUG . '-editor', 'assets/build/editor.js' ); - wp_enqueue_script( Plugin::SLUG . '-editor' ); + if ( dws_tpl_register_script( Plugin::SLUG . '-editor', 'assets/build/editor.js' ) ) { + wp_enqueue_script( Plugin::SLUG . '-editor' ); + } } // endregion diff --git a/src/Feature/FeatureDetail.php b/src/Greeting/AccountDashboardGreeting.php similarity index 54% rename from src/Feature/FeatureDetail.php rename to src/Greeting/AccountDashboardGreeting.php index be3f1d4..685f6d0 100644 --- a/src/Feature/FeatureDetail.php +++ b/src/Greeting/AccountDashboardGreeting.php @@ -1,6 +1,6 @@ get_current_user_id(), + 'status' => wc_get_is_paid_statuses(), + 'limit' => $threshold, + 'return' => 'ids', + ) + ); + if ( ! \is_array( $paid ) || \count( $paid ) < $threshold ) { return; } @@ -50,7 +63,7 @@ public function print_greeting(): void { } /** - * Enqueues the greeting's stylesheet on the account dashboard. + * Enqueues the greeting's stylesheet on a logged-in customer's account dashboard. * * @since 1.0.0 * @version 1.0.0 @@ -58,12 +71,13 @@ public function print_greeting(): void { * @return void */ public function enqueue_style(): void { - if ( ! is_account_page() || is_wc_endpoint_url() ) { + if ( ! is_user_logged_in() || ! is_account_page() || is_wc_endpoint_url() ) { return; } - dws_tpl_register_style( Plugin::SLUG . '-greeting', 'assets/build/greeting.css' ); - wp_enqueue_style( Plugin::SLUG . '-greeting' ); + if ( dws_tpl_register_style( Plugin::SLUG . '-greeting', 'assets/build/greeting.css' ) ) { + wp_enqueue_style( Plugin::SLUG . '-greeting' ); + } } // endregion diff --git a/src/Feature/Feature.php b/src/Greeting/GreetingFeature.php similarity index 76% rename from src/Feature/Feature.php rename to src/Greeting/GreetingFeature.php index 37fc224..888a060 100644 --- a/src/Feature/Feature.php +++ b/src/Greeting/GreetingFeature.php @@ -1,6 +1,6 @@ logger = new WooCommerceLogger( self::SLUG, \defined( 'DWS_TPL_DEBUG' ) && true === \constant( 'DWS_TPL_DEBUG' ) ? 'debug' : null ); $this->scheduler = new Scheduler( self::PREFIX ); - $this->boot = new PluginBoot( DWS_TPL_FILE, self::PREFIX, $this->logger ); + $this->boundary = new PluginBoot( DWS_TPL_FILE, self::PREFIX, $this->logger ); } // endregion @@ -139,7 +139,7 @@ public function __construct() { * @return void */ public function boot(): void { - $this->boot->run( $this->compose( ... ) ); + $this->boundary->run( $this->compose( ... ) ); } /** @@ -186,7 +186,7 @@ protected function compose(): bool { return false; } - $this->migrations = new MigrationRunner( DWS_TPL_FILE, self::PREFIX . '_revision', dws_tpl_get_migration_steps(), $this->logger, $this->scheduler ); + $this->migrations = new MigrationRunner( DWS_TPL_FILE, DWS_TPL_OPTION_REVISION, dws_tpl_get_migration_steps(), $this->logger, $this->scheduler ); $this->migrations->register_hooks(); $this->kernel = new PluginKernel( new ContainerBuilder()->addDefinitions( $this->get_definitions( $this->logger, $this->scheduler ) )->build() ); diff --git a/src/Settings.php b/src/Settings.php index 60ba651..f610fc7 100644 --- a/src/Settings.php +++ b/src/Settings.php @@ -45,15 +45,16 @@ public function get_section_fields( string $tab_id, string $section_id ): array ), array( 'type' => 'checkbox', - 'id' => DWS_TPL_OPTION_FEATURE, - 'title' => __( 'Greeting', 'dws-plugin-template' ), + 'id' => DWS_TPL_OPTION_GREETING_ENABLED, + 'title' => __( 'Show the greeting', 'dws-plugin-template' ), 'desc' => __( 'Greet returning customers', 'dws-plugin-template' ), - 'default' => wc_bool_to_string( DWS_TPL_OPTION_DEFAULTS[ DWS_TPL_OPTION_FEATURE ] ), + 'default' => wc_bool_to_string( DWS_TPL_OPTION_DEFAULTS[ DWS_TPL_OPTION_GREETING_ENABLED ] ), ), array( 'type' => 'text', 'id' => DWS_TPL_OPTION_GREETING, 'title' => __( 'Message', 'dws-plugin-template' ), + 'desc' => __( 'Leave empty to show the default greeting in each customer\'s language.', 'dws-plugin-template' ), 'default' => '', 'placeholder' => dws_tpl_get_option_text( DWS_TPL_OPTION_GREETING ), // A default would be saved in the admin's language with the first save of the tab. ), @@ -61,7 +62,7 @@ public function get_section_fields( string $tab_id, string $section_id ): array 'type' => 'number', 'id' => DWS_TPL_OPTION_GREETING_ORDERS, 'title' => __( 'Orders before the greeting', 'dws-plugin-template' ), - 'desc' => __( 'Customers see the greeting once they have placed this many orders.', 'dws-plugin-template' ), + 'desc' => __( 'Customers see the greeting once this many of their orders are paid.', 'dws-plugin-template' ), 'default' => (string) DWS_TPL_OPTION_DEFAULTS[ DWS_TPL_OPTION_GREETING_ORDERS ], 'custom_attributes' => array( 'min' => '1', @@ -81,8 +82,8 @@ public function get_section_fields( string $tab_id, string $section_id ): array array( 'type' => 'checkbox', 'id' => DWS_TPL_OPTION_DELETE_DATA, - 'title' => __( 'Data on uninstall', 'dws-plugin-template' ), - 'desc' => __( 'Delete the plugin\'s data when it is uninstalled', 'dws-plugin-template' ), + 'title' => __( 'Remove all data on uninstallation', 'dws-plugin-template' ), + 'desc' => __( 'Delete the plugin\'s data when it is uninstalled, so it must be configured again if it is installed later', 'dws-plugin-template' ), 'default' => wc_bool_to_string( DWS_TPL_OPTION_DEFAULTS[ DWS_TPL_OPTION_DELETE_DATA ] ), ), array( diff --git a/tests/EndToEnd/editor.spec.ts b/tests/EndToEnd/editor.spec.ts new file mode 100644 index 0000000..f47d9a8 --- /dev/null +++ b/tests/EndToEnd/editor.spec.ts @@ -0,0 +1,30 @@ +import { expect, test } from '@wordpress/e2e-test-utils-playwright'; + +test.describe( 'Block editor', () => { + test( 'inserts the example notice from the command palette', async ( { + admin, + editor, + page, + } ) => { + await admin.createNewPost(); + await editor.canvas + .getByRole( 'textbox', { name: 'Add title' } ) + .fill( 'Example' ); + await page.keyboard.press( 'ControlOrMeta+k' ); + await page + .getByRole( 'combobox', { name: 'Search commands and settings' } ) + .fill( 'Insert an example notice' ); + await page.keyboard.press( 'Enter' ); + + await expect + .poll( editor.getBlocks ) + .toMatchObject( [ + { name: 'dws-plugin-template/example-notice' }, + ] ); + await expect( + editor.canvas.getByText( + 'An example notice from DWS Plugin Template.' + ) + ).toBeVisible(); + } ); +} ); diff --git a/tests/EndToEnd/without-woocommerce.spec.ts b/tests/EndToEnd/without-woocommerce.spec.ts index bbdb120..b557705 100644 --- a/tests/EndToEnd/without-woocommerce.spec.ts +++ b/tests/EndToEnd/without-woocommerce.spec.ts @@ -17,7 +17,7 @@ const bootStatus = (): string => 'cli', 'wp', 'eval', - 'echo dws_tpl_plugin()->boot->status;', + 'echo dws_tpl_plugin()->boundary->status;', ], { encoding: 'utf8', stdio: [ 'ignore', 'pipe', 'ignore' ] } ) diff --git a/tests/Integration/BlocksTest.php b/tests/Integration/BlocksTest.php index a513809..baaf851 100644 --- a/tests/Integration/BlocksTest.php +++ b/tests/Integration/BlocksTest.php @@ -23,6 +23,31 @@ public function test_the_example_block_registers_from_the_build_with_its_scripts self::assertSame( array( 'dws-plugin-template-example-notice-editor-script' ), $block->editor_script_handles ); self::assertSame( array( 'dws-plugin-template-example-notice-editor-style' ), $block->editor_style_handles ); self::assertSame( array( 'dws-plugin-template-example-notice-style' ), $block->style_handles ); + self::assertSame( + array( + array( + 'name' => 'outlined', + 'label' => 'Outlined', + ), + ), + $block->styles + ); + } + + public function test_the_example_block_renders_its_text_in_the_visitors_language_with_its_style(): void { + $translate = static fn ( string $translation, string $text, string $domain ): string => 'dws-plugin-template' === $domain && 'An example notice from DWS Plugin Template.' === $text ? 'Ein Beispielhinweis.' : $translation; + add_filter( 'gettext', $translate, 10, 3 ); + try { + $html = do_blocks( '' ); + } finally { + remove_filter( 'gettext', $translate ); + } + + $notice = \Dom\HTMLDocument::createFromString( '' . $html . '', \LIBXML_NOERROR )->querySelector( 'p' ); + + self::assertNotNull( $notice ); + self::assertEqualsCanonicalizing( array( 'wp-block-dws-plugin-template-example-notice', 'is-style-outlined' ), \explode( ' ', (string) $notice->getAttribute( 'class' ) ) ); + self::assertSame( 'Ein Beispielhinweis.', \trim( (string) $notice->textContent ) ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- A DOM property. } public function test_the_block_editor_gets_the_editor_script_with_its_generated_dependencies(): void { @@ -39,4 +64,12 @@ public function test_the_block_editor_gets_the_editor_script_with_its_generated_ self::assertSame( $asset['dependencies'], wp_scripts()->registered[ Plugin::SLUG . '-editor' ]->deps ); self::assertSame( $asset['version'], wp_scripts()->registered[ Plugin::SLUG . '-editor' ]->ver ); } + + public function test_the_block_editor_gets_only_an_editor_script_the_plugin_registered(): void { + wp_register_script( Plugin::SLUG . '-editor', 'https://example.org/other.js', array(), '1', true ); + + new Blocks()->enqueue_editor_script(); + + self::assertFalse( wp_script_is( Plugin::SLUG . '-editor', 'enqueued' ) ); + } } diff --git a/tests/Integration/Feature/FeatureDetailTest.php b/tests/Integration/Greeting/AccountDashboardGreetingTest.php similarity index 67% rename from tests/Integration/Feature/FeatureDetailTest.php rename to tests/Integration/Greeting/AccountDashboardGreetingTest.php index 00934fd..af1f38e 100644 --- a/tests/Integration/Feature/FeatureDetailTest.php +++ b/tests/Integration/Greeting/AccountDashboardGreetingTest.php @@ -1,14 +1,15 @@ array( 'pending' ), + 'failed' => array( 'failed' ), + 'cancelled' => array( 'cancelled' ), + 'checkout-draft' => array( 'checkout-draft' ), + ); + } + #[\Override] protected function setUp(): void { foreach ( self::KEYS as $key ) { @@ -52,10 +62,12 @@ protected function clean_up(): void { wp_set_current_user( 0 ); wp_delete_user( $this->customer ); + wp_dequeue_style( 'dws-plugin-template-greeting' ); + wp_deregister_style( 'dws-plugin-template-greeting' ); } public function test_the_greeting_prints_on_the_account_dashboard(): void { - $detail = new FeatureDetail(); + $detail = new AccountDashboardGreeting(); $detail->register_hooks(); self::assertSame( 10, has_action( 'woocommerce_account_dashboard', array( $detail, 'print_greeting' ) ) ); @@ -65,20 +77,18 @@ public function test_the_greeting_prints_on_the_account_dashboard(): void { } public function test_the_greeting_style_loads_on_the_account_dashboard_only(): void { - ( new FeatureDetail() )->enqueue_style(); + ( new AccountDashboardGreeting() )->enqueue_style(); self::assertFalse( wp_style_is( 'dws-plugin-template-greeting' ) ); add_filter( 'woocommerce_is_account_page', '__return_true' ); try { - ( new FeatureDetail() )->enqueue_style(); + ( new AccountDashboardGreeting() )->enqueue_style(); } finally { remove_filter( 'woocommerce_is_account_page', '__return_true' ); } self::assertTrue( wp_style_is( 'dws-plugin-template-greeting' ) ); self::assertSame( plugins_url( 'assets/build/greeting.css', DWS_TPL_FILE ), wp_styles()->registered['dws-plugin-template-greeting']->src ); - wp_dequeue_style( 'dws-plugin-template-greeting' ); - wp_deregister_style( 'dws-plugin-template-greeting' ); } public function test_the_greeting_style_skips_the_account_page_endpoints(): void { @@ -87,7 +97,7 @@ public function test_the_greeting_style_skips_the_account_page_endpoints(): void add_filter( 'woocommerce_is_account_page', '__return_true' ); $wp->query_vars['orders'] = ''; try { - new FeatureDetail()->enqueue_style(); + new AccountDashboardGreeting()->enqueue_style(); } finally { unset( $wp->query_vars['orders'] ); remove_filter( 'woocommerce_is_account_page', '__return_true' ); @@ -96,6 +106,30 @@ public function test_the_greeting_style_skips_the_account_page_endpoints(): void self::assertFalse( wp_style_is( 'dws-plugin-template-greeting' ) ); } + public function test_the_greeting_style_skips_logged_out_visitors(): void { + wp_set_current_user( 0 ); + add_filter( 'woocommerce_is_account_page', '__return_true' ); + try { + new AccountDashboardGreeting()->enqueue_style(); + } finally { + remove_filter( 'woocommerce_is_account_page', '__return_true' ); + } + + self::assertFalse( wp_style_is( 'dws-plugin-template-greeting' ) ); + } + + public function test_the_greeting_style_enqueues_only_a_stylesheet_it_registered(): void { + wp_register_style( 'dws-plugin-template-greeting', 'https://example.org/other.css', array(), '1' ); + add_filter( 'woocommerce_is_account_page', '__return_true' ); + try { + new AccountDashboardGreeting()->enqueue_style(); + } finally { + remove_filter( 'woocommerce_is_account_page', '__return_true' ); + } + + self::assertFalse( wp_style_is( 'dws-plugin-template-greeting' ) ); + } + public function test_a_customer_whose_orders_reach_the_set_number_is_greeted_as_text(): void { update_option( DWS_TPL_OPTION_GREETING, 'Hi & welcome' ); update_option( DWS_TPL_OPTION_GREETING_ORDERS, '2' ); @@ -115,6 +149,15 @@ public function test_a_customer_below_the_set_number_is_not_greeted(): void { self::assertSame( '', $this->print() ); } + #[DataProvider( 'unpaid_statuses' )] + public function test_orders_that_are_not_paid_do_not_count_towards_the_set_number( string $status ): void { + update_option( DWS_TPL_OPTION_GREETING_ORDERS, '2' ); + $this->place_orders( 1 ); + $this->place_orders( 2, $status ); + + self::assertSame( '', $this->print() ); + } + public function test_an_unsaved_greeting_prints_its_translated_default(): void { delete_option( DWS_TPL_OPTION_GREETING ); delete_option( DWS_TPL_OPTION_GREETING_ORDERS ); @@ -130,15 +173,20 @@ public function test_an_empty_greeting_prints_its_translated_default(): void { self::assertStringContainsString( esc_html( dws_tpl_get_option_text( DWS_TPL_OPTION_GREETING ) ), $this->print() ); } - protected function place_orders( int $count ): void { + protected function place_orders( int $count, string $status = 'completed' ): void { for ( $placed = 0; $placed < $count; $placed++ ) { - $this->orders[] = wc_create_order( array( 'customer_id' => $this->customer ) ); + $this->orders[] = wc_create_order( + array( + 'customer_id' => $this->customer, + 'status' => $status, + ) + ); } } protected function print(): string { \ob_start(); - ( new FeatureDetail() )->print_greeting(); + ( new AccountDashboardGreeting() )->print_greeting(); return (string) \ob_get_clean(); } diff --git a/tests/Integration/Feature/FeatureTest.php b/tests/Integration/Greeting/GreetingFeatureTest.php similarity index 52% rename from tests/Integration/Feature/FeatureTest.php rename to tests/Integration/Greeting/GreetingFeatureTest.php index e0036b7..424e6c0 100644 --- a/tests/Integration/Feature/FeatureTest.php +++ b/tests/Integration/Greeting/GreetingFeatureTest.php @@ -1,8 +1,8 @@ stored = get_option( DWS_TPL_OPTION_FEATURE ); + $this->stored = get_option( DWS_TPL_OPTION_GREETING_ENABLED ); } #[After] protected function restore_row(): void { - false === $this->stored ? delete_option( DWS_TPL_OPTION_FEATURE ) : update_option( DWS_TPL_OPTION_FEATURE, $this->stored ); + false === $this->stored ? delete_option( DWS_TPL_OPTION_GREETING_ENABLED ) : update_option( DWS_TPL_OPTION_GREETING_ENABLED, $this->stored ); } #[DataProvider( 'stored_toggles' )] - public function test_the_feature_loads_only_where_the_greeting_is_switched_on( ?string $stored, bool $expected ): void { - \is_null( $stored ) ? delete_option( DWS_TPL_OPTION_FEATURE ) : update_option( DWS_TPL_OPTION_FEATURE, $stored ); + public function test_the_greeting_loads_only_where_a_site_switches_it_on( ?string $stored, bool $expected ): void { + \is_null( $stored ) ? delete_option( DWS_TPL_OPTION_GREETING_ENABLED ) : update_option( DWS_TPL_OPTION_GREETING_ENABLED, $stored ); - self::assertSame( $expected, Feature::should_load() ); + self::assertSame( $expected, GreetingFeature::should_load() ); } #[RunInSeparateProcess] public function test_with_the_greeting_switched_on_every_declared_component_boots(): void { - update_option( DWS_TPL_OPTION_FEATURE, 'yes' ); + update_option( DWS_TPL_OPTION_GREETING_ENABLED, 'yes' ); $plugin = new Plugin(); $plugin->boot(); - self::assertSame( 'booted', $plugin->boot->status ); + self::assertSame( 'booted', $plugin->boundary->status ); self::assertSame( array(), $plugin->kernel?->skipped ); } } diff --git a/tests/Integration/MigrationsTest.php b/tests/Integration/MigrationsTest.php index 82a5590..384cd32 100644 --- a/tests/Integration/MigrationsTest.php +++ b/tests/Integration/MigrationsTest.php @@ -2,7 +2,6 @@ namespace DeepWebSolutions\PluginTemplate\Tests\Integration; -use DeepWebSolutions\PluginTemplate\Plugin; use DeepWebSolutions\PluginTemplate\Scoped\DeepWebSolutions\Framework\Core\Migration\MigrationRunner; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\CoversNothing; @@ -11,7 +10,7 @@ #[CoversNothing] final class MigrationsTest extends TestCase { - protected const array KEYS = array( DWS_TPL_OPTION_FEATURE, DWS_TPL_OPTION_GREETING, DWS_TPL_OPTION_GREETING_ORDERS, DWS_TPL_OPTION_DELETE_DATA ); + protected const array KEYS = array( DWS_TPL_OPTION_GREETING_ENABLED, DWS_TPL_OPTION_GREETING, DWS_TPL_OPTION_GREETING_ORDERS, DWS_TPL_OPTION_DELETE_DATA ); protected array $stored = array(); @@ -28,7 +27,7 @@ protected function restore_rows(): void { false === $value ? delete_option( $key ) : update_option( $key, $value ); } - update_option( Plugin::PREFIX . '_revision', \count( dws_tpl_get_migration_steps() ) ); + update_option( DWS_TPL_OPTION_REVISION, \count( dws_tpl_get_migration_steps() ) ); } // The runner's time budget lasts for its request, and this process's bootstrap spends it when it migrates a new site, so the row runs as a request of its own. @@ -38,27 +37,27 @@ public function test_the_runner_brings_a_new_site_to_revision_one_with_the_untra foreach ( self::KEYS as $key ) { delete_option( $key ); } - update_option( Plugin::PREFIX . '_revision', 0 ); + update_option( DWS_TPL_OPTION_REVISION, 0 ); dws_tpl_plugin()->migrations?->run(); - self::assertSame( 1, MigrationRunner::get_revision( Plugin::PREFIX . '_revision' ) ); - self::assertSame( 'no', get_option( DWS_TPL_OPTION_FEATURE ) ); + self::assertSame( 1, MigrationRunner::get_revision( DWS_TPL_OPTION_REVISION ) ); + self::assertSame( 'no', get_option( DWS_TPL_OPTION_GREETING_ENABLED ) ); self::assertSame( '1', get_option( DWS_TPL_OPTION_GREETING_ORDERS ) ); self::assertSame( 'no', get_option( DWS_TPL_OPTION_DELETE_DATA ) ); self::assertFalse( get_option( DWS_TPL_OPTION_GREETING ) ); - foreach ( array( DWS_TPL_OPTION_FEATURE, DWS_TPL_OPTION_GREETING_ORDERS, DWS_TPL_OPTION_DELETE_DATA ) as $key ) { + foreach ( array( DWS_TPL_OPTION_GREETING_ENABLED, DWS_TPL_OPTION_GREETING_ORDERS, DWS_TPL_OPTION_DELETE_DATA ) as $key ) { self::assertSame( 'on', $wpdb->get_var( $wpdb->prepare( "SELECT autoload FROM {$wpdb->options} WHERE option_name = %s", $key ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Reads the stored autoload flag. } } public function test_the_first_step_keeps_the_values_a_site_stored(): void { - update_option( DWS_TPL_OPTION_FEATURE, 'yes' ); + update_option( DWS_TPL_OPTION_GREETING_ENABLED, 'yes' ); update_option( DWS_TPL_OPTION_GREETING_ORDERS, '5' ); dws_tpl_add_settings(); - self::assertSame( 'yes', get_option( DWS_TPL_OPTION_FEATURE ) ); + self::assertSame( 'yes', get_option( DWS_TPL_OPTION_GREETING_ENABLED ) ); self::assertSame( '5', get_option( DWS_TPL_OPTION_GREETING_ORDERS ) ); } diff --git a/tests/Integration/OptionsTest.php b/tests/Integration/OptionsTest.php index 438a245..28ea271 100644 --- a/tests/Integration/OptionsTest.php +++ b/tests/Integration/OptionsTest.php @@ -133,7 +133,7 @@ public function test_a_string_that_is_not_a_string_reads_its_translated_default( } public function test_an_option_without_a_translated_default_has_no_text(): void { - self::assertSame( '', dws_tpl_get_option_text( DWS_TPL_OPTION_FEATURE ) ); + self::assertSame( '', dws_tpl_get_option_text( DWS_TPL_OPTION_GREETING_ENABLED ) ); } #[DataProvider( 'submitted_order_counts' )] diff --git a/tests/Integration/PartialDeployTest.php b/tests/Integration/PartialDeployTest.php index 7659cfd..7a84809 100644 --- a/tests/Integration/PartialDeployTest.php +++ b/tests/Integration/PartialDeployTest.php @@ -13,6 +13,21 @@ final class PartialDeployTest extends TestCase { use RunsRequestsTrait; + public function test_the_build_notice_says_the_features_stay_off_until_the_plugin_is_reinstalled(): void { + wp_set_current_user( 1 ); + \ob_start(); + try { + dws_tpl_render_build_notice(); + } finally { + $notice = (string) \ob_get_clean(); + wp_set_current_user( 0 ); + } + + $sentence = \Dom\HTMLDocument::createFromString( '' . $notice . '', \LIBXML_NOERROR )->querySelector( '.notice-error p' ); + + self::assertSame( 'DWS Plugin Template is missing some of its files, so its features stay off until the plugin is reinstalled.', $sentence?->textContent ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- A DOM property. + } + public function test_a_missing_blocks_manifest_shows_the_build_notice_and_loads_nothing_built(): void { $manifest = \dirname( DWS_TPL_FILE ) . '/blocks/build/blocks-manifest.php'; \rename( $manifest, "$manifest.hidden" ); diff --git a/tests/Integration/PluginBootTest.php b/tests/Integration/PluginBootTest.php index a272955..36283b8 100644 --- a/tests/Integration/PluginBootTest.php +++ b/tests/Integration/PluginBootTest.php @@ -2,7 +2,7 @@ namespace DeepWebSolutions\PluginTemplate\Tests\Integration; -use DeepWebSolutions\PluginTemplate\Feature\Feature; +use DeepWebSolutions\PluginTemplate\Greeting\GreetingFeature; use DeepWebSolutions\PluginTemplate\Plugin; use DeepWebSolutions\PluginTemplate\Scoped\DeepWebSolutions\Framework\Core\Migration\MigrationRunner; use DeepWebSolutions\PluginTemplate\Scoped\DeepWebSolutions\Framework\WooCommerce\Logging\WooCommerceLogger; @@ -18,8 +18,8 @@ final class PluginBootTest extends TestCase { public function test_the_plugin_boots_with_the_greeting_gated_off_until_a_site_switches_it_on(): void { $plugin = dws_tpl_plugin(); - self::assertSame( 'booted', $plugin->boot->status ); - self::assertSame( array( Feature::class ), $plugin->kernel?->skipped ); + self::assertSame( 'booted', $plugin->boundary->status ); + self::assertSame( array( GreetingFeature::class ), $plugin->kernel?->skipped ); } public function test_the_entry_loads_every_include_but_those_whose_names_start_with_an_underscore(): void { @@ -71,7 +71,7 @@ public function test_the_debug_constant_logs_every_level(): void { public function test_the_stored_data_is_at_the_revision_of_the_code(): void { self::assertInstanceOf( MigrationRunner::class, dws_tpl_plugin()->migrations ); - self::assertSame( \count( dws_tpl_get_migration_steps() ), MigrationRunner::get_revision( Plugin::PREFIX . '_revision' ) ); + self::assertSame( \count( dws_tpl_get_migration_steps() ), MigrationRunner::get_revision( DWS_TPL_OPTION_REVISION ) ); } protected static function get_threshold( WooCommerceLogger $logger ): mixed { diff --git a/tests/Integration/SettingsTest.php b/tests/Integration/SettingsTest.php index 9125696..e7739c3 100644 --- a/tests/Integration/SettingsTest.php +++ b/tests/Integration/SettingsTest.php @@ -12,7 +12,7 @@ #[CoversNothing] final class SettingsTest extends TestCase { - protected const array KEYS = array( DWS_TPL_OPTION_FEATURE, DWS_TPL_OPTION_GREETING, DWS_TPL_OPTION_GREETING_ORDERS, DWS_TPL_OPTION_DELETE_DATA ); + protected const array KEYS = array( DWS_TPL_OPTION_GREETING_ENABLED, DWS_TPL_OPTION_GREETING, DWS_TPL_OPTION_GREETING_ORDERS, DWS_TPL_OPTION_DELETE_DATA ); protected static SettingsTab $tab; @@ -38,6 +38,25 @@ public static function invalid_order_counts(): array { 'decimal' => array( '2.5' ), 'word' => array( 'three' ), 'empty' => array( '' ), + 'too-big' => array( '1e20' ), + ); + } + + public static function field_texts(): array { + return array( + 'toggle' => array( DWS_TPL_OPTION_GREETING_ENABLED, 'Show the greeting', 'Greet returning customers' ), + 'message' => array( DWS_TPL_OPTION_GREETING, 'Message', 'Leave empty to show the default greeting in each customer\'s language.' ), + 'threshold' => array( DWS_TPL_OPTION_GREETING_ORDERS, 'Orders before the greeting', 'Customers see the greeting once this many of their orders are paid.' ), + 'consent' => array( DWS_TPL_OPTION_DELETE_DATA, 'Remove all data on uninstallation', 'Delete the plugin\'s data when it is uninstalled, so it must be configured again if it is installed later' ), + ); + } + + public static function whole_order_counts(): array { + return array( + 'padded' => array( ' 3 ' ), + 'leading-zero' => array( '03' ), + 'zero-decimals' => array( '3.0' ), + 'exponent' => array( '0.3e1' ), ); } @@ -65,7 +84,7 @@ public function test_the_tab_shows_the_greeting_and_data_fields_with_their_defau $form = $this->render(); self::assertSame( 'DWS Plugin Template', self::$tab->get_label() ); - self::assertFalse( $form->getElementById( DWS_TPL_OPTION_FEATURE )?->hasAttribute( 'checked' ) ); + self::assertFalse( $form->getElementById( DWS_TPL_OPTION_GREETING_ENABLED )?->hasAttribute( 'checked' ) ); self::assertSame( '', $form->getElementById( DWS_TPL_OPTION_GREETING )?->getAttribute( 'value' ) ); self::assertSame( dws_tpl_get_option_text( DWS_TPL_OPTION_GREETING ), $form->getElementById( DWS_TPL_OPTION_GREETING )?->getAttribute( 'placeholder' ) ); self::assertSame( '1', $form->getElementById( DWS_TPL_OPTION_GREETING_ORDERS )?->getAttribute( 'value' ) ); @@ -73,6 +92,14 @@ public function test_the_tab_shows_the_greeting_and_data_fields_with_their_defau self::assertFalse( $form->getElementById( DWS_TPL_OPTION_DELETE_DATA )?->hasAttribute( 'checked' ) ); } + #[DataProvider( 'field_texts' )] + public function test_the_tab_words_each_field_by_what_it_does( string $key, string $title, string $description ): void { + $row = (string) $this->render()->getElementById( $key )?->closest( 'tr' )?->textContent; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- A DOM property. + + self::assertStringContainsString( $title, $row ); + self::assertStringContainsString( $description, $row ); + } + public function test_a_save_that_leaves_the_message_untouched_stores_no_translated_text(): void { delete_option( DWS_TPL_OPTION_GREETING ); @@ -105,8 +132,9 @@ public function test_an_order_count_that_is_not_a_whole_number_of_at_least_one_k self::assertSame( array( 'Orders before the greeting was not saved and keeps its previous value. The other changes were saved. Enter a whole number of 1 or more, such as 3.' ), ( new \ReflectionProperty( \WC_Admin_Settings::class, 'errors' ) )->getValue() ); } - public function test_a_whole_order_count_of_at_least_one_is_stored(): void { - $this->save( array( DWS_TPL_OPTION_GREETING_ORDERS => ' 3 ' ) ); + #[DataProvider( 'whole_order_counts' )] + public function test_a_whole_order_count_of_at_least_one_is_stored_as_its_digits( string $posted ): void { + $this->save( array( DWS_TPL_OPTION_GREETING_ORDERS => $posted ) ); self::assertSame( '3', get_option( DWS_TPL_OPTION_GREETING_ORDERS ) ); self::assertSame( 3, dws_tpl_get_int_option( DWS_TPL_OPTION_GREETING_ORDERS ) ); diff --git a/tests/Integration/UninstallTest.php b/tests/Integration/UninstallTest.php index f7ab8e2..d00be34 100644 --- a/tests/Integration/UninstallTest.php +++ b/tests/Integration/UninstallTest.php @@ -79,7 +79,7 @@ public function test_a_web_uninstall_whose_engine_fails_ends_with_its_message_an \define( 'WP_UNINSTALL_PLUGIN', plugin_basename( DWS_TPL_FILE ) ); update_option( DWS_TPL_OPTION_DELETE_DATA, 'yes' ); - $failing = "DELETE FROM `$wpdb->options` WHERE `option_name` = '" . Plugin::PREFIX . "_revision'"; + $failing = "DELETE FROM `$wpdb->options` WHERE `option_name` = '" . DWS_TPL_OPTION_REVISION . "'"; add_filter( 'query', static fn ( string $query ): string => \str_contains( $query, $failing ) ? 'SELECT * FROM dws_tpl_missing_table' : $query ); add_filter( 'wp_die_handler', static fn (): \Closure => static fn ( string $message, string $title, array $args ): never => throw new \LogicException( $args['response'] . ' ' . $message ) ); $wpdb->suppress_errors(); @@ -88,12 +88,12 @@ public function test_a_web_uninstall_whose_engine_fails_ends_with_its_message_an include \dirname( DWS_TPL_FILE ) . '/uninstall.php'; self::fail( 'The uninstall finished.' ); } catch ( \LogicException $died ) { - self::assertStringStartsWith( '500 Uninstall could not remove everything: option ' . Plugin::PREFIX . '_revision', $died->getMessage() ); + self::assertStringStartsWith( '500 Uninstall could not remove everything: option ' . DWS_TPL_OPTION_REVISION, $died->getMessage() ); self::assertSame( 'yes', get_option( DWS_TPL_OPTION_DELETE_DATA ) ); } finally { remove_all_filters( 'query' ); delete_option( DWS_TPL_OPTION_DELETE_DATA ); - delete_option( Plugin::PREFIX . '_revision' ); + delete_option( DWS_TPL_OPTION_REVISION ); dws_tpl_plugin()->migrations?->run(); } } diff --git a/tests/README.md b/tests/README.md index 3a5db9e..582e94d 100644 --- a/tests/README.md +++ b/tests/README.md @@ -8,7 +8,7 @@ This document describes the plugin's three test suites, the environments they ru | --- | --- | --- | --- | | Unit | `tests/Unit/` | PHP alone, without WordPress | the component tree, the footprint's shape and coverage, and the entry's `WC tested up to` against the locked WooCommerce | | Integration | `tests/Integration/` | the tests site, with WordPress, WooCommerce and order tables loaded, and the multisite site for its `multisite` group | the boot, every component, the options and their readers, the migrations, deactivation, uninstall, the assets and blocks, the peer getters, a partial deploy: a missing build file or engine, or an include that throws, and the boot and settings on a second site of a network | -| End to end | `tests/EndToEnd/` | the tests site, in a browser through Playwright | the settings tab by keyboard, a rejected and a corrected value, accessibility through axe, and the plugin with WooCommerce deactivated | +| End to end | `tests/EndToEnd/` | the tests site, in a browser through Playwright | the settings tab by keyboard, a rejected and a corrected value, accessibility through axe, the example block inserted from the editor's command palette, and the plugin with WooCommerce deactivated | PHPUnit runs both PHP suites in random order, so each row sets up what it needs and restores what it changes; its cache lives in `tests/.cache/phpunit/`, and Playwright's output in `tests/.cache/artifacts/`. diff --git a/tests/Unit/EntryHeadersTest.php b/tests/Unit/EntryHeadersTest.php index c1d41f0..86d762a 100644 --- a/tests/Unit/EntryHeadersTest.php +++ b/tests/Unit/EntryHeadersTest.php @@ -17,4 +17,15 @@ public function test_wc_tested_up_to_names_the_locked_woocommerce(): void { self::assertSame( 1, \preg_match( '/^\s*\*\s*WC tested up to:\s*(\S+)\s*$/m', $entry, $header ) ); self::assertSame( \implode( '.', \array_slice( \explode( '.', $woocommerce['version'] ), 0, 2 ) ), $header[1] ); } + + public function test_every_block_carries_the_entry_version(): void { + $entry = (string) \file_get_contents( \dirname( __DIR__, 2 ) . '/' . Plugin::SLUG . '.php' ); + $blocks = \glob( \dirname( __DIR__, 2 ) . '/blocks/src/*/block.json' ); + + self::assertSame( 1, \preg_match( '/^\s*\*\s*Version:\s*(\S+)\s*$/m', $entry, $header ) ); + self::assertIsArray( $blocks ); + foreach ( $blocks as $block ) { + self::assertSame( $header[1], \json_decode( (string) \file_get_contents( $block ), true, 512, \JSON_THROW_ON_ERROR )['version'] ?? null, $block ); + } + } } diff --git a/tests/Unit/FootprintTest.php b/tests/Unit/FootprintTest.php index 2b8434d..12b1f9b 100644 --- a/tests/Unit/FootprintTest.php +++ b/tests/Unit/FootprintTest.php @@ -52,7 +52,7 @@ public function test_it_covers_what_the_framework_roots_store(): void { self::assertContains( $prefix, dws_tpl_get_footprint()['user_meta_site_prefixes'] ); self::assertContains( $prefix, dws_tpl_get_footprint()['user_meta_global_prefixes'] ); self::assertContains( $prefix . 'network_notices', dws_tpl_get_footprint()['network_options'] ); - self::assertContains( $prefix . 'revision/migration/run', dws_tpl_get_footprint()['cron_hooks'] ); + self::assertContains( DWS_TPL_OPTION_REVISION . '/migration/run', dws_tpl_get_footprint()['cron_hooks'] ); self::assertContains( Plugin::PREFIX, dws_tpl_get_footprint()['as_groups'] ); } diff --git a/uninstall.php b/uninstall.php index a470ee4..a537cdb 100644 --- a/uninstall.php +++ b/uninstall.php @@ -30,7 +30,6 @@ require_once __DIR__ . '/includes/footprint.php'; require_once __DIR__ . '/includes/options.php'; $dws_tpl_footprint = dws_tpl_get_footprint(); - $dws_tpl_consent = DWS_TPL_OPTION_DELETE_DATA; $dws_tpl_meta_types = MetaTypes::DEFINITIONS; if ( ! class_exists( Uninstaller::class ) ) { throw new RuntimeException( 'its uninstall engine is missing.' ); @@ -50,7 +49,7 @@ $dws_tpl_file, $dws_tpl_footprint, array( - 'consent' => array( 'option' => $dws_tpl_consent ), + 'consent' => array( 'option' => DWS_TPL_OPTION_DELETE_DATA ), 'meta_types' => $dws_tpl_meta_types, ) );