Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .distignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ phpstan-baseline.neon
AGENTS.md
.wp-env.json
.claude
skills

26 changes: 2 additions & 24 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down
55 changes: 52 additions & 3 deletions classes/Visualizer/Module/Setup.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) {
Expand All @@ -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 );
}

Expand Down Expand Up @@ -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;
}
}
56 changes: 50 additions & 6 deletions classes/Visualizer/Module/Upgrade.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down Expand Up @@ -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;
}
}
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
45 changes: 44 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions index.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
43 changes: 43 additions & 0 deletions skills/e2e.md
Original file line number Diff line number Diff line change
@@ -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.
25 changes: 25 additions & 0 deletions skills/unit.md
Original file line number Diff line number Diff line change
@@ -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-<name>.php`
- Single test method: `./vendor/bin/phpunit --filter testMethodName`
Loading