Skip to content

Commit bc14d4b

Browse files
feat: Replace WP-Cron with Action Scheduler for reliable auto-sync (#1286)
* feat(dev): add action scheduler * chore: update skills * chore: phpstan fix
1 parent da64108 commit bc14d4b

9 files changed

Lines changed: 226 additions & 35 deletions

File tree

.distignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,5 @@ phpstan-baseline.neon
3232
AGENTS.md
3333
.wp-env.json
3434
.claude
35+
skills
3536
classes/Visualizer/Gutenberg/src

AGENTS.md

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -33,31 +33,9 @@ npm run build # Production build → build/block.js
3333
npm run dev # Watch mode for development
3434
```
3535

36-
### E2E Tests & Environment
36+
### E2E & PHPUnit Tests
3737

38-
Requires Docker to be running. Uses `docker-compose.ci.yml` (MariaDB + WordPress on port 8889).
39-
40-
```bash
41-
# 1. Install dependencies
42-
npm ci
43-
npx playwright install --with-deps chromium
44-
composer install --no-dev
45-
46-
# 2. Start the WordPress environment (boots Docker, installs WP, activates plugin)
47-
DOCKER_FILE=docker-compose.ci.yml bash bin/wp-init.sh
48-
49-
# 3. Run the full Playwright suite
50-
npm run test:e2e:playwright
51-
52-
# 4. Run a single spec file
53-
npx wp-scripts test-playwright --config tests/e2e/playwright.config.js tests/e2e/specs/gutenberg-editor.spec.js
54-
55-
# 5. Tear down
56-
DOCKER_FILE=docker-compose.ci.yml bash bin/wp-down.sh
57-
```
58-
59-
WordPress is installed at `http://localhost:8889` with credentials `admin` / `password`.
60-
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.
38+
> Skill files for running tests are in [`skills/`](skills/): use `skills/e2e.md` for E2E and `skills/unit.md` for PHPUnit.
6139
6240
---
6341

classes/Visualizer/Module/Setup.php

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,7 @@ public function activate( $network_wide ) {
209209
* Activates the plugin on a particular blog instance (supports multisite and single site).
210210
*/
211211
private function activate_on_site() {
212-
wp_clear_scheduled_hook( 'visualizer_schedule_refresh_db' );
213-
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' );
212+
$this->schedule_refresh_db_action();
214213
add_option( 'visualizer-activated', true );
215214
$is_fresh_install = get_option( 'visualizer_fresh_install', false );
216215
if ( ! defined( 'TI_E2E_TESTING' ) && false === $is_fresh_install ) {
@@ -237,7 +236,7 @@ public function deactivate( $network_wide ) {
237236
* Deactivates the plugin on a particular blog instance (supports multisite and single site).
238237
*/
239238
private function deactivate_on_site() {
240-
wp_clear_scheduled_hook( 'visualizer_schedule_refresh_db' );
239+
$this->unschedule_refresh_db_action();
241240
delete_option( 'visualizer-activated', true );
242241
}
243242

@@ -469,4 +468,54 @@ public function custom_cron_schedules( $schedules ) {
469468

470469
return $schedules;
471470
}
471+
472+
/**
473+
* Schedule the recurring DB refresh action.
474+
*/
475+
private function schedule_refresh_db_action(): void {
476+
$hook = 'visualizer_schedule_refresh_db';
477+
$group = 'visualizer';
478+
$interval_key = apply_filters( 'visualizer_chart_schedule_interval', 'visualizer_ten_minutes' );
479+
$interval = $this->get_schedule_interval_seconds( $interval_key );
480+
$timestamp = strtotime( 'midnight' ) - get_option( 'gmt_offset' ) * HOUR_IN_SECONDS;
481+
482+
if ( function_exists( 'as_next_scheduled_action' ) && function_exists( 'as_schedule_recurring_action' ) ) {
483+
$next = as_next_scheduled_action( $hook, array(), $group );
484+
if ( false === $next ) {
485+
as_schedule_recurring_action( $timestamp, $interval, $hook, array(), $group );
486+
}
487+
wp_clear_scheduled_hook( $hook );
488+
return;
489+
}
490+
491+
wp_clear_scheduled_hook( $hook );
492+
wp_schedule_event( $timestamp, $interval_key, $hook );
493+
}
494+
495+
/**
496+
* Unschedule the recurring DB refresh action.
497+
*/
498+
private function unschedule_refresh_db_action(): void {
499+
$hook = 'visualizer_schedule_refresh_db';
500+
$group = 'visualizer';
501+
if ( function_exists( 'as_unschedule_all_actions' ) ) {
502+
as_unschedule_all_actions( $hook, array(), $group );
503+
}
504+
wp_clear_scheduled_hook( $hook );
505+
}
506+
507+
/**
508+
* Resolve a cron schedule key to seconds.
509+
*
510+
* @param string $interval_key Cron schedule key.
511+
* @return int Interval in seconds.
512+
*/
513+
private function get_schedule_interval_seconds( $interval_key ) {
514+
$schedules = wp_get_schedules();
515+
if ( isset( $schedules[ $interval_key ]['interval'] ) ) {
516+
return (int) $schedules[ $interval_key ]['interval'];
517+
}
518+
519+
return 600;
520+
}
472521
}

classes/Visualizer/Module/Upgrade.php

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,20 @@ class Visualizer_Module_Upgrade extends Visualizer_Module {
1717
*/
1818
public static function upgrade() {
1919
$last_version = get_option( 'visualizer-upgraded', '0.0.0' );
20+
$upgraded = false;
2021

21-
switch ( $last_version ) {
22-
case '0.0.0':
23-
self::makeAllTableChartsTabular();
24-
break;
25-
default:
26-
return;
22+
if ( version_compare( $last_version, '3.4.3', '<' ) ) {
23+
self::makeAllTableChartsTabular();
24+
$upgraded = true;
25+
}
26+
27+
if ( wp_next_scheduled( 'visualizer_schedule_refresh_db' ) ) {
28+
self::migrate_action_scheduler();
29+
$upgraded = true;
30+
}
31+
32+
if ( ! $upgraded ) {
33+
return;
2734
}
2835

2936
update_option( 'visualizer-upgraded', Visualizer_Plugin::VERSION );
@@ -72,4 +79,41 @@ private static function makeAllTableChartsTabular() {
7279
);
7380
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
7481
}
82+
83+
/**
84+
* Migrate recurring WP-Cron jobs to Action Scheduler.
85+
*/
86+
private static function migrate_action_scheduler(): void {
87+
if ( ! function_exists( 'as_schedule_recurring_action' ) || ! function_exists( 'as_next_scheduled_action' ) ) {
88+
return;
89+
}
90+
91+
$hook = 'visualizer_schedule_refresh_db';
92+
$group = 'visualizer';
93+
$interval_key = apply_filters( 'visualizer_chart_schedule_interval', 'visualizer_ten_minutes' );
94+
$interval = self::get_schedule_interval_seconds( $interval_key );
95+
$timestamp = strtotime( 'midnight' ) - get_option( 'gmt_offset' ) * HOUR_IN_SECONDS;
96+
97+
$next = as_next_scheduled_action( $hook, array(), $group );
98+
if ( false === $next ) {
99+
as_schedule_recurring_action( $timestamp, $interval, $hook, array(), $group );
100+
}
101+
102+
wp_clear_scheduled_hook( $hook );
103+
}
104+
105+
/**
106+
* Resolve a cron schedule key to seconds.
107+
*
108+
* @param string $interval_key Cron schedule key.
109+
* @return int Interval in seconds.
110+
*/
111+
private static function get_schedule_interval_seconds( $interval_key ) {
112+
$schedules = wp_get_schedules();
113+
if ( isset( $schedules[ $interval_key ]['interval'] ) ) {
114+
return (int) $schedules[ $interval_key ]['interval'];
115+
}
116+
117+
return 600;
118+
}
75119
}

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
"require": {
2424
"codeinwp/themeisle-sdk": "^3.3",
2525
"neitanod/forceutf8": "~2.0",
26-
"openspout/openspout": "^3.7"
26+
"openspout/openspout": "^3.7",
27+
"woocommerce/action-scheduler": "^3.8"
2728
},
2829
"autoload": {
2930
"files": [

composer.lock

Lines changed: 44 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

index.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,13 @@ function () {
201201
if ( is_readable( $vendor_file ) ) {
202202
include_once $vendor_file;
203203
}
204+
205+
$action_scheduler_file = VISUALIZER_ABSPATH . '/vendor/woocommerce/action-scheduler/action-scheduler.php';
206+
207+
if ( is_readable( $action_scheduler_file ) ) {
208+
require_once $action_scheduler_file;
209+
}
210+
204211
add_filter( 'themeisle_sdk_products', 'visualizer_register_sdk', 10, 1 );
205212
add_filter( 'pirate_parrot_log', 'visualizer_register_parrot', 10, 1 );
206213
add_filter(

skills/e2e.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Run E2E Tests (Visualizer Free)
2+
3+
Run the Playwright end-to-end test suite for the Visualizer free plugin. The environment uses Docker (MariaDB + WordPress on port 8889).
4+
5+
## Pre-flight checks
6+
7+
1. Make sure Docker is running: `docker info`
8+
2. Check for port conflicts on 8889 and 3306 — if anything is using them, stop those services first (e.g. `brew services stop mariadb`).
9+
10+
## Commands
11+
12+
```bash
13+
# 1. Install dependencies (skip if already done)
14+
npm ci
15+
npx playwright install --with-deps chromium
16+
composer install --no-dev
17+
18+
# 2. Boot WordPress environment (Docker + WP install + plugin activation)
19+
DOCKER_FILE=docker-compose.ci.yml bash bin/wp-init.sh
20+
21+
# 3a. Run the full Playwright suite
22+
npm run test:e2e:playwright
23+
24+
# 3b. OR run a single spec file (replace the path as needed)
25+
# npx wp-scripts test-playwright --config tests/e2e/playwright.config.js tests/e2e/specs/gutenberg-editor.spec.js
26+
27+
# 4. Tear down when done
28+
DOCKER_FILE=docker-compose.ci.yml bash bin/wp-down.sh
29+
```
30+
31+
## Environment
32+
33+
- WordPress: http://localhost:8889
34+
- Credentials: `admin` / `password`
35+
- `TI_E2E_TESTING=true` is set in `wp-config.php` by the setup script
36+
37+
## Instructions
38+
39+
1. Run the pre-flight checks.
40+
2. If `wp-init.sh` fails due to a port conflict, identify and stop the conflicting service, then retry.
41+
3. Run the tests. Show output as it streams.
42+
4. After tests complete (pass or fail), always run the tear-down command.
43+
5. Report a summary: how many tests passed, failed, and any error messages from failures.

skills/unit.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Run Unit Tests (Visualizer Free)
2+
3+
Run the PHPUnit test suite for the Visualizer free plugin.
4+
5+
## Commands
6+
7+
```bash
8+
# Install PHP dependencies if not already done
9+
composer install
10+
11+
# Run the full PHPUnit suite
12+
./vendor/bin/phpunit
13+
14+
# Run a single test file (replace the path as needed)
15+
# ./vendor/bin/phpunit tests/test-export.php
16+
```
17+
18+
## Instructions
19+
20+
1. Check that `vendor/` exists. If not, run `composer install` first.
21+
2. Run the tests. Show output as it streams.
22+
3. Report a summary: how many tests passed, failed, and any error messages.
23+
4. If the user specified a particular test file or test name, run only that:
24+
- Single file: `./vendor/bin/phpunit tests/test-<name>.php`
25+
- Single test method: `./vendor/bin/phpunit --filter testMethodName`

0 commit comments

Comments
 (0)