From 979556bb870f1e30bf3f3f265cb67e8a0333ac1f Mon Sep 17 00:00:00 2001 From: Joost de Valk Date: Tue, 15 Sep 2026 16:57:50 +0200 Subject: [PATCH] Preserve settings on deactivation and clean up on uninstall --- src/Plugin.php | 5 ---- src/WordPressHooks.php | 5 ---- tests/Browser/phpRegression.spec.ts | 16 ++++++++++++ tests/Regression/deactivation.php | 34 +++++++++++++++++++++++++ tests/Support/isolated-options.php | 39 +++++++++++++++++++++++++++++ uninstall.php | 29 +++++++++++++++++++++ 6 files changed, 118 insertions(+), 10 deletions(-) create mode 100644 tests/Browser/phpRegression.spec.ts create mode 100644 tests/Regression/deactivation.php create mode 100644 tests/Support/isolated-options.php create mode 100644 uninstall.php diff --git a/src/Plugin.php b/src/Plugin.php index e8ce1ab..73313e2 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -36,7 +36,6 @@ public function boot(): void { $this->hooks->addAction('init', \Closure::fromCallable([$this, 'onInit'])); $this->hooks->onActivation(\Closure::fromCallable([$this, 'onActivation'])); - $this->hooks->onDeactivation(\Closure::fromCallable([$this, 'onUninstall'])); if ($this->hooks->isAdmin()) { $this->adminPage->register(); @@ -81,8 +80,4 @@ public function onActivation(): void $this->settings->update(SettingName::CUSTOM_DOMAIN, $this->settings->get(SettingName::CUSTOM_DOMAIN), true); } - public function onUninstall(): void - { - foreach (SettingName::cases() as $key) $this->settings->delete($key); - } } diff --git a/src/WordPressHooks.php b/src/WordPressHooks.php index c72529d..cca957b 100644 --- a/src/WordPressHooks.php +++ b/src/WordPressHooks.php @@ -14,11 +14,6 @@ public function onActivation($callback): void register_activation_hook(ENTRYPOINT_FILE, $callback); } - public function onDeactivation($callback): void - { - register_deactivation_hook(ENTRYPOINT_FILE, $callback); - } - public function isAdmin(): bool { return is_admin(); diff --git a/tests/Browser/phpRegression.spec.ts b/tests/Browser/phpRegression.spec.ts new file mode 100644 index 0000000..4202937 --- /dev/null +++ b/tests/Browser/phpRegression.spec.ts @@ -0,0 +1,16 @@ +import { test, expect } from '@playwright/test'; +import { execFileSync } from 'node:child_process'; +import { readdirSync } from 'node:fs'; +import { basename, resolve } from 'node:path'; + +// Each script uses its own temporary options table, so these checks can run +// alongside browser tests without changing their WordPress settings. +for (const file of readdirSync(resolve('tests/Regression')).filter(name => name.endsWith('.php'))) { + test(`WordPress regression: ${file}`, () => { + const output = execFileSync(resolve('node_modules/.bin/wp-env'), [ + 'run', 'cli', 'wp', 'eval-file', + `wp-content/plugins/${basename(process.cwd())}/tests/Regression/${file}`, + ], { encoding: 'utf8', timeout: 45000 }); + expect(output).toContain('Regression checks passed'); + }); +} diff --git a/tests/Regression/deactivation.php b/tests/Regression/deactivation.php new file mode 100644 index 0000000..92a129d --- /dev/null +++ b/tests/Regression/deactivation.php @@ -0,0 +1,34 @@ + 'stats.example.test', + SimpleAnalytics\SettingName::EXCLUDED_IP_ADDRESSES => ['203.0.113.10'], + SimpleAnalytics\SettingName::EXCLUDED_ROLES => ['administrator'], + SimpleAnalytics\SettingName::MANUAL_COLLECT => '1', + ]; + foreach ($values as $key => $value) update_option($key, $value); + + deactivate_plugins($plugin, false, false); + sa_assert(! is_plugin_active($plugin), 'The plugin must be deactivated for this test.'); + foreach ($values as $key => $value) { + sa_assert(get_option($key) === $value, 'Deactivation deleted ' . $key); + } + + // The plugin is already loaded in this process; fire WordPress's activation + // hook directly to verify that activation retains the existing options. + do_action('activate_' . $plugin, false); + foreach ($values as $key => $value) { + sa_assert(get_option($key) === $value, 'Reactivation changed ' . $key); + } + + update_option('sa_unrelated_test_option', 'keep'); + uninstall_plugin($plugin); + foreach (SimpleAnalytics\SettingName::cases() as $key) { + sa_assert(get_option($key, 'missing') === 'missing', 'Uninstall did not remove ' . $key); + } + sa_assert(get_option('sa_unrelated_test_option') === 'keep', 'Uninstall must retain unrelated options.'); +}); diff --git a/tests/Support/isolated-options.php b/tests/Support/isolated-options.php new file mode 100644 index 0000000..f4f724e --- /dev/null +++ b/tests/Support/isolated-options.php @@ -0,0 +1,39 @@ +options; + $originalCache = $wp_object_cache; + $table = 'sa_regression_options'; + $guard = static function ($sql) use ($table) { + if (! preg_match('/^\s*(SELECT|SHOW|DESCRIBE|EXPLAIN)\b/i', $sql) && strpos($sql, $table) === false) { + throw new RuntimeException('Refusing a write outside the temporary test table.'); + } + return $sql; + }; + add_filter('query', $guard, 9999); + + try { + sa_assert($wpdb->query("CREATE TEMPORARY TABLE $table LIKE $originalTable") !== false, $wpdb->last_error); + sa_assert($wpdb->query("INSERT INTO $table SELECT * FROM $originalTable") !== false, $wpdb->last_error); + $wpdb->options = $table; + $wp_object_cache = new WP_Object_Cache(); + wp_cache_switch_to_blog(get_current_blog_id()); + $test(); + echo "Regression checks passed\n"; + } finally { + $wpdb->options = $originalTable; + $wp_object_cache = $originalCache; + remove_filter('query', $guard, 9999); + } + // The database connection automatically drops the temporary table on exit. +} diff --git a/uninstall.php b/uninstall.php new file mode 100644 index 0000000..de714f4 --- /dev/null +++ b/uninstall.php @@ -0,0 +1,29 @@ + 'ids', 'number' => 100, 'offset' => $offset, 'orderby' => 'id', 'order' => 'ASC']); + foreach ($siteIds as $siteId) { + switch_to_blog($siteId); + try { + $deleteOptions(); + } finally { + restore_current_blog(); + } + } + $offset += count($siteIds); + } while (count($siteIds) === 100); +} else { + $deleteOptions(); +}