diff --git a/.distignore b/.distignore index e76a78f8..11658d71 100755 --- a/.distignore +++ b/.distignore @@ -32,4 +32,5 @@ phpstan-baseline.neon AGENTS.md .wp-env.json .claude +skills diff --git a/AGENTS.md b/AGENTS.md index 9dbe23c1..dff1caaa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,31 +33,9 @@ npm run build # Production build → build/block.js npm run dev # Watch mode for development ``` -### E2E Tests & Environment +### E2E & PHPUnit Tests -Requires Docker to be running. Uses `docker-compose.ci.yml` (MariaDB + WordPress on port 8889). - -```bash -# 1. Install dependencies -npm ci -npx playwright install --with-deps chromium -composer install --no-dev - -# 2. Start the WordPress environment (boots Docker, installs WP, activates plugin) -DOCKER_FILE=docker-compose.ci.yml bash bin/wp-init.sh - -# 3. Run the full Playwright suite -npm run test:e2e:playwright - -# 4. Run a single spec file -npx wp-scripts test-playwright --config tests/e2e/playwright.config.js tests/e2e/specs/gutenberg-editor.spec.js - -# 5. Tear down -DOCKER_FILE=docker-compose.ci.yml bash bin/wp-down.sh -``` - -WordPress is installed at `http://localhost:8889` with credentials `admin` / `password`. -The `TI_E2E_TESTING` constant is set to `true` in `wp-config.php` by the setup script, which enables test-only code paths in the plugin. +> Skill files for running tests are in [`skills/`](skills/): use `skills/e2e.md` for E2E and `skills/unit.md` for PHPUnit. --- diff --git a/classes/Visualizer/Module/Setup.php b/classes/Visualizer/Module/Setup.php index 5c0968d1..8a56055e 100644 --- a/classes/Visualizer/Module/Setup.php +++ b/classes/Visualizer/Module/Setup.php @@ -209,8 +209,7 @@ public function activate( $network_wide ) { * Activates the plugin on a particular blog instance (supports multisite and single site). */ private function activate_on_site() { - wp_clear_scheduled_hook( 'visualizer_schedule_refresh_db' ); - wp_schedule_event( strtotime( 'midnight' ) - get_option( 'gmt_offset' ) * HOUR_IN_SECONDS, apply_filters( 'visualizer_chart_schedule_interval', 'visualizer_ten_minutes' ), 'visualizer_schedule_refresh_db' ); + $this->schedule_refresh_db_action(); add_option( 'visualizer-activated', true ); $is_fresh_install = get_option( 'visualizer_fresh_install', false ); if ( ! defined( 'TI_E2E_TESTING' ) && false === $is_fresh_install ) { @@ -237,7 +236,7 @@ public function deactivate( $network_wide ) { * Deactivates the plugin on a particular blog instance (supports multisite and single site). */ private function deactivate_on_site() { - wp_clear_scheduled_hook( 'visualizer_schedule_refresh_db' ); + $this->unschedule_refresh_db_action(); delete_option( 'visualizer-activated', true ); } @@ -469,4 +468,54 @@ public function custom_cron_schedules( $schedules ) { return $schedules; } + + /** + * Schedule the recurring DB refresh action. + */ + private function schedule_refresh_db_action(): void { + $hook = 'visualizer_schedule_refresh_db'; + $group = 'visualizer'; + $interval_key = apply_filters( 'visualizer_chart_schedule_interval', 'visualizer_ten_minutes' ); + $interval = $this->get_schedule_interval_seconds( $interval_key ); + $timestamp = strtotime( 'midnight' ) - get_option( 'gmt_offset' ) * HOUR_IN_SECONDS; + + if ( function_exists( 'as_next_scheduled_action' ) && function_exists( 'as_schedule_recurring_action' ) ) { + $next = as_next_scheduled_action( $hook, array(), $group ); + if ( false === $next ) { + as_schedule_recurring_action( $timestamp, $interval, $hook, array(), $group ); + } + wp_clear_scheduled_hook( $hook ); + return; + } + + wp_clear_scheduled_hook( $hook ); + wp_schedule_event( $timestamp, $interval_key, $hook ); + } + + /** + * Unschedule the recurring DB refresh action. + */ + private function unschedule_refresh_db_action(): void { + $hook = 'visualizer_schedule_refresh_db'; + $group = 'visualizer'; + if ( function_exists( 'as_unschedule_all_actions' ) ) { + as_unschedule_all_actions( $hook, array(), $group ); + } + wp_clear_scheduled_hook( $hook ); + } + + /** + * Resolve a cron schedule key to seconds. + * + * @param string $interval_key Cron schedule key. + * @return int Interval in seconds. + */ + private function get_schedule_interval_seconds( $interval_key ) { + $schedules = wp_get_schedules(); + if ( isset( $schedules[ $interval_key ]['interval'] ) ) { + return (int) $schedules[ $interval_key ]['interval']; + } + + return 600; + } } diff --git a/classes/Visualizer/Module/Upgrade.php b/classes/Visualizer/Module/Upgrade.php index 5bf975b6..1989d63b 100644 --- a/classes/Visualizer/Module/Upgrade.php +++ b/classes/Visualizer/Module/Upgrade.php @@ -17,13 +17,20 @@ class Visualizer_Module_Upgrade extends Visualizer_Module { */ public static function upgrade() { $last_version = get_option( 'visualizer-upgraded', '0.0.0' ); + $upgraded = false; - switch ( $last_version ) { - case '0.0.0': - self::makeAllTableChartsTabular(); - break; - default: - return; + if ( version_compare( $last_version, '3.4.3', '<' ) ) { + self::makeAllTableChartsTabular(); + $upgraded = true; + } + + if ( wp_next_scheduled( 'visualizer_schedule_refresh_db' ) ) { + self::migrate_action_scheduler(); + $upgraded = true; + } + + if ( ! $upgraded ) { + return; } update_option( 'visualizer-upgraded', Visualizer_Plugin::VERSION ); @@ -72,4 +79,41 @@ private static function makeAllTableChartsTabular() { ); // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared } + + /** + * Migrate recurring WP-Cron jobs to Action Scheduler. + */ + private static function migrate_action_scheduler(): void { + if ( ! function_exists( 'as_schedule_recurring_action' ) || ! function_exists( 'as_next_scheduled_action' ) ) { + return; + } + + $hook = 'visualizer_schedule_refresh_db'; + $group = 'visualizer'; + $interval_key = apply_filters( 'visualizer_chart_schedule_interval', 'visualizer_ten_minutes' ); + $interval = self::get_schedule_interval_seconds( $interval_key ); + $timestamp = strtotime( 'midnight' ) - get_option( 'gmt_offset' ) * HOUR_IN_SECONDS; + + $next = as_next_scheduled_action( $hook, array(), $group ); + if ( false === $next ) { + as_schedule_recurring_action( $timestamp, $interval, $hook, array(), $group ); + } + + wp_clear_scheduled_hook( $hook ); + } + + /** + * Resolve a cron schedule key to seconds. + * + * @param string $interval_key Cron schedule key. + * @return int Interval in seconds. + */ + private static function get_schedule_interval_seconds( $interval_key ) { + $schedules = wp_get_schedules(); + if ( isset( $schedules[ $interval_key ]['interval'] ) ) { + return (int) $schedules[ $interval_key ]['interval']; + } + + return 600; + } } diff --git a/composer.json b/composer.json index 31a5cfe6..1153b42d 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,8 @@ "require": { "codeinwp/themeisle-sdk": "^3.3", "neitanod/forceutf8": "~2.0", - "openspout/openspout": "^3.7" + "openspout/openspout": "^3.7", + "woocommerce/action-scheduler": "^3.8" }, "autoload": { "files": [ diff --git a/composer.lock b/composer.lock index 2becf81d..c60c74d8 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": "f939ee5350ac149f2ec1e5fca3a9ec30", + "content-hash": "881b5f99c0b72f47e79eb09bdac7fbea", "packages": [ { "name": "codeinwp/themeisle-sdk", @@ -176,6 +176,49 @@ } ], "time": "2022-03-31T06:15:15+00:00" + }, + { + "name": "woocommerce/action-scheduler", + "version": "3.9.3", + "source": { + "type": "git", + "url": "https://github.com/woocommerce/action-scheduler.git", + "reference": "c58cdbab17651303d406cd3b22cf9d75c71c986c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/woocommerce/action-scheduler/zipball/c58cdbab17651303d406cd3b22cf9d75c71c986c", + "reference": "c58cdbab17651303d406cd3b22cf9d75c71c986c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5", + "woocommerce/woocommerce-sniffs": "0.1.0", + "wp-cli/wp-cli": "~2.5.0", + "yoast/phpunit-polyfills": "^2.0" + }, + "type": "wordpress-plugin", + "extra": { + "scripts-description": { + "test": "Run unit tests", + "phpcs": "Analyze code against the WordPress coding standards with PHP_CodeSniffer", + "phpcbf": "Fix coding standards warnings/errors automatically with PHP Code Beautifier" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-or-later" + ], + "description": "Action Scheduler for WordPress and WooCommerce", + "homepage": "https://actionscheduler.org/", + "support": { + "issues": "https://github.com/woocommerce/action-scheduler/issues", + "source": "https://github.com/woocommerce/action-scheduler/tree/3.9.3" + }, + "time": "2025-07-15T09:32:30+00:00" } ], "packages-dev": [ diff --git a/index.php b/index.php index 41f50ffb..24f32319 100644 --- a/index.php +++ b/index.php @@ -135,6 +135,13 @@ function visualizer_launch() { if ( is_readable( $vendor_file ) ) { include_once $vendor_file; } + + $action_scheduler_file = VISUALIZER_ABSPATH . '/vendor/woocommerce/action-scheduler/action-scheduler.php'; + + if ( is_readable( $action_scheduler_file ) ) { + require_once $action_scheduler_file; + } + add_filter( 'themeisle_sdk_products', 'visualizer_register_sdk', 10, 1 ); add_filter( 'pirate_parrot_log', 'visualizer_register_parrot', 10, 1 ); add_filter( diff --git a/skills/e2e.md b/skills/e2e.md new file mode 100644 index 00000000..28bc1bdb --- /dev/null +++ b/skills/e2e.md @@ -0,0 +1,43 @@ +# Run E2E Tests (Visualizer Free) + +Run the Playwright end-to-end test suite for the Visualizer free plugin. The environment uses Docker (MariaDB + WordPress on port 8889). + +## Pre-flight checks + +1. Make sure Docker is running: `docker info` +2. Check for port conflicts on 8889 and 3306 — if anything is using them, stop those services first (e.g. `brew services stop mariadb`). + +## Commands + +```bash +# 1. Install dependencies (skip if already done) +npm ci +npx playwright install --with-deps chromium +composer install --no-dev + +# 2. Boot WordPress environment (Docker + WP install + plugin activation) +DOCKER_FILE=docker-compose.ci.yml bash bin/wp-init.sh + +# 3a. Run the full Playwright suite +npm run test:e2e:playwright + +# 3b. OR run a single spec file (replace the path as needed) +# npx wp-scripts test-playwright --config tests/e2e/playwright.config.js tests/e2e/specs/gutenberg-editor.spec.js + +# 4. Tear down when done +DOCKER_FILE=docker-compose.ci.yml bash bin/wp-down.sh +``` + +## Environment + +- WordPress: http://localhost:8889 +- Credentials: `admin` / `password` +- `TI_E2E_TESTING=true` is set in `wp-config.php` by the setup script + +## Instructions + +1. Run the pre-flight checks. +2. If `wp-init.sh` fails due to a port conflict, identify and stop the conflicting service, then retry. +3. Run the tests. Show output as it streams. +4. After tests complete (pass or fail), always run the tear-down command. +5. Report a summary: how many tests passed, failed, and any error messages from failures. diff --git a/skills/unit.md b/skills/unit.md new file mode 100644 index 00000000..ce8a79aa --- /dev/null +++ b/skills/unit.md @@ -0,0 +1,25 @@ +# Run Unit Tests (Visualizer Free) + +Run the PHPUnit test suite for the Visualizer free plugin. + +## Commands + +```bash +# Install PHP dependencies if not already done +composer install + +# Run the full PHPUnit suite +./vendor/bin/phpunit + +# Run a single test file (replace the path as needed) +# ./vendor/bin/phpunit tests/test-export.php +``` + +## Instructions + +1. Check that `vendor/` exists. If not, run `composer install` first. +2. Run the tests. Show output as it streams. +3. Report a summary: how many tests passed, failed, and any error messages. +4. If the user specified a particular test file or test name, run only that: + - Single file: `./vendor/bin/phpunit tests/test-.php` + - Single test method: `./vendor/bin/phpunit --filter testMethodName`