From e175338f50a445b11f92cd688ab4bc838d63cb23 Mon Sep 17 00:00:00 2001 From: Rami Yushuvaev Date: Tue, 15 Sep 2026 12:35:25 +0300 Subject: [PATCH 01/19] feat: add new tests --- tests/e2e/code-snippets-list.spec.ts | 126 ++++++++++++++++++++++++ tests/e2e/helpers/SnippetsTestHelper.ts | 6 +- tests/unit/Core/Upgrader_Test.php | 78 +++++++++++++++ 3 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 tests/unit/Core/Upgrader_Test.php diff --git a/tests/e2e/code-snippets-list.spec.ts b/tests/e2e/code-snippets-list.spec.ts index e8b4b1204..4c5537379 100644 --- a/tests/e2e/code-snippets-list.spec.ts +++ b/tests/e2e/code-snippets-list.spec.ts @@ -63,6 +63,95 @@ test.describe('Code Snippets List Page Actions', () => { await expect(search.getByRole('button', { name: 'Search' })).toHaveCount(0) }) + test('Searches snippets by name, description, and code', async ({ page }) => { + const nameQuery = SnippetsTestHelper.makeUniqueSnippetName('Search name') + const descriptionQuery = SnippetsTestHelper.makeUniqueSnippetName('Search-description-query') + const codeQuery = SnippetsTestHelper.makeUniqueSnippetName('Search-code-query') + const fixtures = [ + { + name: nameQuery, + query: nameQuery + }, + { + name: SnippetsTestHelper.makeUniqueSnippetName('Search description'), + description: descriptionQuery, + query: descriptionQuery + }, + { + name: SnippetsTestHelper.makeUniqueSnippetName('Search code'), + code: `// ${codeQuery}`, + query: codeQuery + } + ] + + try { + for (const fixture of fixtures) { + await SnippetsTestHelper.createSnippetViaCli({ ...fixture, active: false }) + } + + await helper.navigateToSnippetsAdmin() + const searchInput = page.getByRole('searchbox', { name: 'Search Snippets:' }) + + for (const fixture of fixtures) { + await searchInput.fill(fixture.query) + await expect(snippetRowByName(page, fixture.name)).toBeVisible() + + for (const otherFixture of fixtures.filter(other => other !== fixture)) { + await expect(snippetRowByName(page, otherFixture.name)).toBeHidden() + } + } + } finally { + for (const fixture of fixtures) { + await helper.cleanupSnippet(fixture.name) + } + } + }) + + test('Lists active and inactive snippets in the table', async ({ page }) => { + const inactiveSnippetName = SnippetsTestHelper.makeUniqueSnippetName('Inactive snippet') + + try { + await SnippetsTestHelper.createSnippetViaCli({ name: inactiveSnippetName, active: false }) + await helper.navigateToSnippetsAdmin() + + await expect(snippetRowByName(page, snippetName)).toBeVisible() + await expect(snippetRowByName(page, inactiveSnippetName)).toBeVisible() + } finally { + await helper.cleanupSnippet(inactiveSnippetName) + } + }) + + test('Filters snippets by each status link', async ({ page }) => { + const inactiveSnippetName = SnippetsTestHelper.makeUniqueSnippetName('Inactive snippet') + + try { + await SnippetsTestHelper.createSnippetViaCli({ name: inactiveSnippetName, active: false }) + await helper.navigateToSnippetsAdmin() + + const activeRow = snippetRowByName(page, snippetName) + const inactiveRow = snippetRowByName(page, inactiveSnippetName) + await expect(activeRow).toBeVisible() + await expect(inactiveRow).toBeVisible() + + await page.locator('.subsubsub .active a').click() + await expect(activeRow).toBeVisible() + await expect(inactiveRow).toBeHidden() + + await page.locator('.subsubsub .inactive a').click() + await expect(activeRow).toBeHidden() + await expect(inactiveRow).toBeVisible() + + await helper.navigateToSnippetsAdmin() + await activeRow.getByRole('switch').click({ force: true }) + await expect(activeRow.getByRole('switch')).not.toBeChecked() + await page.locator('.subsubsub .recently_active a').click() + await expect(activeRow).toBeVisible() + await expect(inactiveRow).toBeHidden() + } finally { + await helper.cleanupSnippet(inactiveSnippetName) + } + }) + test('Card action popovers let keyboard focus continue through the document', async ({ page }) => { await switchSnippetView(page, 'Card view') @@ -245,6 +334,43 @@ test.describe('Code Snippets List Page Actions', () => { await expect(trashedRow).toContainText(/Restore/i) }) + test('Can restore a trashed snippet from list page', async ({ page }) => { + const snippetRow = snippetRowByName(page, snippetName) + await clickRowAction(snippetRow, SELECTORS.DELETE_ACTION) + + const confirmDialog = page.locator('[role="dialog"]').filter({ hasText: /Are you sure\?/i }) + if (await confirmDialog.isVisible()) { + await confirmDialog.getByRole('button', { name: 'Trash' }).click() + } + + await page.locator('.subsubsub .trashed a').click() + await expect(snippetRow).toBeVisible() + await clickRowAction(snippetRow, 'button:has-text("Restore")') + + await expect(snippetRow).toBeVisible() + await expect(snippetRow).not.toHaveClass(/trashed-snippet/) + await expect(snippetRow.getByRole('switch')).not.toBeChecked() + }) + + test('Confirms permanent deletion from the Trash view', async ({ page }) => { + const snippetRow = snippetRowByName(page, snippetName) + await clickRowAction(snippetRow, SELECTORS.DELETE_ACTION) + + const trashDialog = page.locator('[role="dialog"]').filter({ hasText: /Are you sure\?/i }) + if (await trashDialog.isVisible()) { + await trashDialog.getByRole('button', { name: 'Trash' }).click() + } + + await page.locator('.subsubsub .trashed a').click() + await expect(snippetRow).toBeVisible() + await clickRowAction(snippetRow, 'button:has-text("Delete Permanently")') + + const deleteDialog = page.locator('[role="dialog"]').filter({ hasText: /Are you sure\?/i }) + await expect(deleteDialog).toBeVisible() + await deleteDialog.getByRole('button', { name: 'Delete' }).click() + await expect(snippetRow).toHaveCount(0) + }) + test('Can export snippet from list page', async ({ page }) => { test.setTimeout(EXPORT_TEST_TIMEOUT_MS) const snippetRow = snippetRowByName(page, snippetName) diff --git a/tests/e2e/helpers/SnippetsTestHelper.ts b/tests/e2e/helpers/SnippetsTestHelper.ts index ad43fab5e..0669c751f 100644 --- a/tests/e2e/helpers/SnippetsTestHelper.ts +++ b/tests/e2e/helpers/SnippetsTestHelper.ts @@ -32,6 +32,8 @@ export interface SnippetFormOptions { export interface CreateSnippetCliOptions { name: string; active: boolean; + description?: string; + code?: string; conditionId?: number; tags?: readonly string[]; type?: 'php' | 'html' | 'css' | 'js' | 'cond'; @@ -97,12 +99,12 @@ export class SnippetsTestHelper { break } - const code = 'html' === type ? `

${options.name}

\n` : `// ${options.name}\n` + const code = options.code ?? ('html' === type ? `

${options.name}

\n` : `// ${options.name}\n`) const php = ` $snippet = new \\Code_Snippets\\Model\\Snippet([ 'name' => ${JSON.stringify(options.name)}, - 'desc' => '', + 'desc' => ${JSON.stringify(options.description ?? '')}, 'code' => ${JSON.stringify(code)}, 'scope' => ${JSON.stringify(scope)}, 'active' => ${options.active ? 'true' : 'false'}, diff --git a/tests/unit/Core/Upgrader_Test.php b/tests/unit/Core/Upgrader_Test.php new file mode 100644 index 000000000..e2619a74f --- /dev/null +++ b/tests/unit/Core/Upgrader_Test.php @@ -0,0 +1,78 @@ +db->create_or_upgrade_tables(); + + parent::tear_down(); + } + + /** + * A first install creates the snippets table and stores the plugin version. + * + * @return void + */ + public function test_fresh_install_creates_the_snippets_table_and_records_the_current_version(): void { + global $wpdb; + + $db = code_snippets()->db; + + $wpdb->query( "DROP TABLE IF EXISTS $db->table" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange -- A fresh-install fixture has no snippets table. + delete_option( 'code_snippets_version' ); + + $upgrader = new Upgrader( PLUGIN_VERSION, $db ); + $upgrader->run(); + remove_action( 'init', [ $upgrader, 'create_sample_content' ] ); + + $this->assertTrue( DB::table_exists( $db->table, true ) ); + $this->assertSame( PLUGIN_VERSION, get_option( 'code_snippets_version' ) ); + } + + /** + * An upgrade keeps existing snippets and settings while recording the new version. + * + * @return void + */ + public function test_upgrade_from_an_older_version_preserves_snippets_and_settings(): void { + $settings = [ 'general' => [ 'enable_admin_bar' => false ] ]; + $snippet = save_snippet( + new Snippet( + [ + 'name' => 'Existing installation snippet', + 'code' => 'add_action( \'init\', \'__return_null\' );', + ] + ) + ); + + $this->assertInstanceOf( Snippet::class, $snippet ); + + update_option( 'code_snippets_settings', $settings ); + update_option( 'code_snippets_version', '3.9.9' ); + + ( new Upgrader( PLUGIN_VERSION, code_snippets()->db ) )->run(); + + $preserved_snippet = get_snippet( $snippet->id ); + + $this->assertSame( 'Existing installation snippet', $preserved_snippet->name ); + $this->assertSame( $settings, get_option( 'code_snippets_settings' ) ); + $this->assertSame( PLUGIN_VERSION, get_option( 'code_snippets_version' ) ); + } +} From d838469433cbf5dc442d37e346ee4bca33c011ae Mon Sep 17 00:00:00 2001 From: Rami Yushuvaev Date: Tue, 15 Sep 2026 12:53:15 +0300 Subject: [PATCH 02/19] feat: add new tests --- tests/e2e/code-snippets-edit.spec.ts | 57 +++++++++++ tests/e2e/code-snippets-evaluation.spec.ts | 33 +++++++ tests/e2e/code-snippets-list.spec.ts | 108 +++++++++++++++++++++ tests/e2e/helpers/SnippetsTestHelper.ts | 16 ++- 4 files changed, 210 insertions(+), 4 deletions(-) diff --git a/tests/e2e/code-snippets-edit.spec.ts b/tests/e2e/code-snippets-edit.spec.ts index 2257461bb..41608b85a 100644 --- a/tests/e2e/code-snippets-edit.spec.ts +++ b/tests/e2e/code-snippets-edit.spec.ts @@ -64,6 +64,38 @@ test.describe('Code Snippets Admin', () => { await helper.cleanupSnippet(snippetName) }) + test('Locks a snippet until it is unlocked from the editor sidebar', async ({ page }) => { + const snippetName = SnippetsTestHelper.makeUniqueSnippetName('Locked snippet') + const updatedSnippetName = `${snippetName} unlocked` + let cleanupName = snippetName + + try { + await helper.createSnippet({ + name: snippetName, + code: "add_filter('show_admin_bar', '__return_false');" + }) + await helper.openSnippet(snippetName) + + const lockButton = page.locator('button.snippet-lock-button') + await lockButton.click() + await expect(page.getByRole('heading', { name: /View Snippet/ })).toBeVisible() + await expect(page.locator('#title')).toBeDisabled() + await expect(page.locator('button.delete-button')).toBeDisabled() + + await lockButton.click() + await expect(page.getByRole('heading', { name: /Edit Snippet/ })).toBeVisible() + await expect(page.locator('#title')).toBeEnabled() + await expect(page.locator('button.delete-button')).toBeEnabled() + + await page.locator('#title').fill(updatedSnippetName) + await helper.saveSnippet() + await helper.expectSuccessMessage(/Snippet updated/i) + cleanupName = updatedSnippetName + } finally { + await helper.cleanupSnippet(cleanupName) + } + }) + test('Back navigation confirms before discarding unsaved changes', async ({ page }) => { const snippetName = SnippetsTestHelper.makeUniqueSnippetName() await helper.clickAddNewSnippet() @@ -121,6 +153,31 @@ test.describe('Code Snippets Admin', () => { await helper.cleanupSnippet(snippetName) }) + test('Shows a clear error and leaves a snippet inactive when its PHP has a syntax error', async ({ page }) => { + const snippetName = SnippetsTestHelper.makeUniqueSnippetName('Syntax error snippet') + + try { + await helper.clickAddNewSnippet() + await helper.fillSnippetForm({ + name: snippetName, + code: 'function invalid_syntax( {' + }) + await helper.saveSnippet('save_and_activate') + + const errorNotice = page.locator('.wrap > .notice.error').first() + await expect(errorNotice).toBeVisible({ timeout: TIMEOUTS.DEFAULT }) + await expect(errorNotice).toContainText(/syntax error/i) + await expect(errorNotice).toContainText(/remains inactive/i) + + await helper.navigateToSnippetsAdmin() + await helper.filterSnippetsByName(snippetName) + const snippetRow = page.locator(SELECTORS.SNIPPET_ROW).filter({ hasText: snippetName }).first() + await expect(snippetRow.getByRole('switch')).not.toBeChecked() + } finally { + await helper.cleanupSnippet(snippetName) + } + }) + test('Shows an error notice when activation fails after saving', async ({ page }) => { const snippetName = SnippetsTestHelper.makeUniqueSnippetName() await helper.clickAddNewSnippet() diff --git a/tests/e2e/code-snippets-evaluation.spec.ts b/tests/e2e/code-snippets-evaluation.spec.ts index be5deb5a0..155b415be 100644 --- a/tests/e2e/code-snippets-evaluation.spec.ts +++ b/tests/e2e/code-snippets-evaluation.spec.ts @@ -148,6 +148,39 @@ test.describe('Code Snippets Evaluation', () => { await expect(page.locator('body')).toHaveClass(/custom-frontend-class/) }) + test('PHP snippets execute in priority order', async ({ page }) => { + const outputPrefix = `snippet-priority-${Date.now()}` + const highPriorityId = `${outputPrefix}-high` + const lowPriorityId = `${outputPrefix}-low` + const highPriorityName = SnippetsTestHelper.makeUniqueSnippetName('High priority snippet') + const lowPriorityName = SnippetsTestHelper.makeUniqueSnippetName('Low priority snippet') + + try { + await SnippetsTestHelper.createSnippetViaCli({ + name: highPriorityName, + active: true, + priority: 20, + code: `add_action('wp_footer', function() { echo ''; });` + }) + await SnippetsTestHelper.createSnippetViaCli({ + name: lowPriorityName, + active: true, + priority: 5, + code: `add_action('wp_footer', function() { echo ''; });` + }) + + await helper.navigateToFrontend() + await expect(page.locator(`#${lowPriorityId}`)).toBeAttached() + await expect(page.locator(`#${highPriorityId}`)).toBeAttached() + expect(await page.locator(`span[id^="${outputPrefix}"]`).evaluateAll(elements => + elements.map(({ id }) => id) + )).toEqual([lowPriorityId, highPriorityId]) + } finally { + await helper.cleanupSnippet(highPriorityName) + await helper.cleanupSnippet(lowPriorityName) + } + }) + test('HTML snippet is evaluating correctly in footer', async () => { await helper.createAndActivateSnippet({ name: snippetName, diff --git a/tests/e2e/code-snippets-list.spec.ts b/tests/e2e/code-snippets-list.spec.ts index 4c5537379..f5e13c40d 100644 --- a/tests/e2e/code-snippets-list.spec.ts +++ b/tests/e2e/code-snippets-list.spec.ts @@ -220,6 +220,37 @@ test.describe('Code Snippets List Page Actions', () => { await expect(toggleSwitch).toHaveAccessibleName(initialChecked ? /Deactivate/i : /Activate/i) }) + test('Runs and stops a snippet when its row toggle changes', async ({ page }) => { + const executionSnippetName = SnippetsTestHelper.makeUniqueSnippetName('Row toggle execution') + const bodyClass = `row-toggle-${Date.now()}` + + try { + await SnippetsTestHelper.createSnippetViaCli({ + name: executionSnippetName, + active: false, + code: `add_filter('body_class', function() { return ['${bodyClass}']; });` + }) + await helper.navigateToSnippetsAdmin() + + let executionRow = snippetRowByName(page, executionSnippetName) + await executionRow.getByRole('switch').click({ force: true }) + await expect(executionRow.getByRole('switch')).toBeChecked() + + await helper.navigateToFrontend() + await expect(page.locator('body')).toHaveClass(new RegExp(bodyClass)) + + await helper.navigateToSnippetsAdmin() + executionRow = snippetRowByName(page, executionSnippetName) + await executionRow.getByRole('switch').click({ force: true }) + await expect(executionRow.getByRole('switch')).not.toBeChecked() + + await helper.navigateToFrontend() + await expect(page.locator('body')).not.toHaveClass(new RegExp(bodyClass)) + } finally { + await helper.cleanupSnippet(executionSnippetName) + } + }) + test('Can access edit from list page', async ({ page }) => { const snippetRow = snippetRowByName(page, snippetName) @@ -230,6 +261,30 @@ test.describe('Code Snippets List Page Actions', () => { await expect(page.locator('#title')).toHaveValue(snippetName) }) + test('Shows View and omits Trash for a locked snippet', async ({ page }) => { + const lockedSnippetName = SnippetsTestHelper.makeUniqueSnippetName('Locked snippet') + + try { + await SnippetsTestHelper.createSnippetViaCli({ + name: lockedSnippetName, + active: false, + locked: true + }) + await helper.navigateToSnippetsAdmin() + + const lockedRow = snippetRowByName(page, lockedSnippetName) + await expect(lockedRow).toBeVisible() + await lockedRow.hover() + const actions = lockedRow.locator('.row-actions') + + await expect(actions.getByRole('link', { name: 'View' })).toBeVisible() + await expect(actions.getByRole('link', { name: 'Edit' })).toHaveCount(0) + await expect(actions.getByRole('button', { name: 'Trash' })).toHaveCount(0) + } finally { + await helper.cleanupSnippet(lockedSnippetName) + } + }) + test('Can clone snippet from list page', async ({ page }) => { const snippetRow = snippetRowByName(page, snippetName) @@ -386,6 +441,59 @@ test.describe('Code Snippets List Page Actions', () => { expect(download.suggestedFilename()).toMatch(/\.json$/) }) + test('Can activate, deactivate, trash, and permanently delete snippets from bulk actions', async ({ page }) => { + const bulkSnippetNames = [ + SnippetsTestHelper.makeUniqueSnippetName('Bulk action snippet'), + SnippetsTestHelper.makeUniqueSnippetName('Bulk action snippet') + ] + const rows = () => bulkSnippetNames.map(name => snippetRowByName(page, name)) + const selectRows = async () => { + for (const row of rows()) { + await row.locator('input[name="checked[]"]').check({ force: true }) + } + } + const applyAction = async (action: string) => { + await page.locator('select[name="action"]').first().selectOption({ label: action }) + await page.locator('#doaction').click() + } + + try { + for (const name of bulkSnippetNames) { + await SnippetsTestHelper.createSnippetViaCli({ name, active: false }) + } + await helper.navigateToSnippetsAdmin() + await selectRows() + await applyAction('Activate') + + for (const row of rows()) { + await expect(row.getByRole('switch')).toBeChecked() + } + + await selectRows() + await applyAction('Deactivate') + for (const row of rows()) { + await expect(row.getByRole('switch')).not.toBeChecked() + } + + await selectRows() + await applyAction('Trash') + for (const row of rows()) { + await expect(row).toHaveCount(0) + } + + await page.locator('.subsubsub .trashed a').click() + await selectRows() + await applyAction('Delete Permanently') + for (const row of rows()) { + await expect(row).toHaveCount(0) + } + } finally { + for (const name of bulkSnippetNames) { + await helper.cleanupSnippet(name) + } + } + }) + test('Can export multiple snippets from bulk actions', async ({ page }) => { test.setTimeout(EXPORT_TEST_TIMEOUT_MS) const secondSnippetName = SnippetsTestHelper.makeUniqueSnippetName() diff --git a/tests/e2e/helpers/SnippetsTestHelper.ts b/tests/e2e/helpers/SnippetsTestHelper.ts index 0669c751f..7fc4e0de1 100644 --- a/tests/e2e/helpers/SnippetsTestHelper.ts +++ b/tests/e2e/helpers/SnippetsTestHelper.ts @@ -35,6 +35,8 @@ export interface CreateSnippetCliOptions { description?: string; code?: string; conditionId?: number; + locked?: boolean; + priority?: number; tags?: readonly string[]; type?: 'php' | 'html' | 'css' | 'js' | 'cond'; } @@ -107,11 +109,15 @@ export class SnippetsTestHelper { 'desc' => ${JSON.stringify(options.description ?? '')}, 'code' => ${JSON.stringify(code)}, 'scope' => ${JSON.stringify(scope)}, - 'active' => ${options.active ? 'true' : 'false'}, - 'condition_id' => ${options.conditionId ?? 0}, - 'tags' => ${JSON.stringify(options.tags ?? [])}, + 'active' => ${options.active ? 'true' : 'false'}, + 'condition_id' => ${options.conditionId ?? 0}, + 'priority' => ${options.priority ?? 10}, + 'tags' => ${JSON.stringify(options.tags ?? [])}, ]); $snippet = \\Code_Snippets\\save_snippet($snippet); + if (${options.locked ? 'true' : 'false'}) { + \\Code_Snippets\\set_snippet_locked($snippet->id, true); + } echo $snippet->id; ` @@ -133,7 +139,9 @@ export class SnippetsTestHelper { [ $network, $table ] = $target; $ids = $wpdb->get_col( $wpdb->prepare( "SELECT id FROM {$table} WHERE name LIKE %s", $like ) ); foreach ( $ids as $id ) { - \\Code_Snippets\\delete_snippet( intval( $id ), (bool) $network ); + $id = intval( $id ); + \\Code_Snippets\\set_snippet_locked( $id, false, (bool) $network ); + \\Code_Snippets\\delete_snippet( $id, (bool) $network ); } } ` From cb748b5e242f32ebe0f774a9c3d58b9a93a8de56 Mon Sep 17 00:00:00 2001 From: Rami Yushuvaev Date: Tue, 15 Sep 2026 14:23:22 +0300 Subject: [PATCH 03/19] feat: add new tests --- tests/e2e/code-snippets-evaluation.spec.ts | 65 ++++++++ tests/e2e/code-snippets-import.spec.ts | 140 ++++++++++++++++++ tests/e2e/code-snippets-list.spec.ts | 41 +++++ tests/e2e/helpers/SnippetsTestHelper.ts | 2 + tests/e2e/rest-api-auth.spec.ts | 30 ++++ tests/e2e/settings-permissions.spec.ts | 40 +++++ tests/e2e/settings-tabs.spec.ts | 137 +++++++++++++++++ tests/unit/Core/Uninstaller_Test.php | 50 +++++++ .../unit/REST_API/REST_API_Snippets_Test.php | 61 ++++++++ 9 files changed, 566 insertions(+) create mode 100644 tests/e2e/code-snippets-import.spec.ts create mode 100644 tests/e2e/rest-api-auth.spec.ts create mode 100644 tests/e2e/settings-permissions.spec.ts diff --git a/tests/e2e/code-snippets-evaluation.spec.ts b/tests/e2e/code-snippets-evaluation.spec.ts index 155b415be..6df82b20a 100644 --- a/tests/e2e/code-snippets-evaluation.spec.ts +++ b/tests/e2e/code-snippets-evaluation.spec.ts @@ -148,6 +148,51 @@ test.describe('Code Snippets Evaluation', () => { await expect(page.locator('body')).toHaveClass(/custom-frontend-class/) }) + test('Safe mode query disables front-end snippet execution', async ({ page }) => { + const safeModeClass = `safe-mode-${Date.now()}` + + await helper.createAndActivateSnippet({ + name: snippetName, + code: `add_filter('body_class', function($classes) { $classes[] = '${safeModeClass}'; return $classes; });` + }) + + await page.goto(URLS.FRONTEND) + await expect(page.locator('body')).toHaveClass(new RegExp(safeModeClass)) + + await page.goto(`${URLS.FRONTEND}?snippets-safe-mode=1`) + await expect(page.locator('body')).not.toHaveClass(new RegExp(safeModeClass)) + + await page.goto(`${URLS.SNIPPETS_ADMIN}&snippets-safe-mode=1`) + await page.getByRole('link', { name: 'Add New' }).click() + await expect(page).toHaveURL(/snippets-safe-mode=1/, { timeout: 5000 }) + }) + + test('Single-use PHP snippets run once from the list', async ({ page }) => { + const markerKey = `code_snippets_e2e_single_use_${Date.now()}` + + try { + await SnippetsTestHelper.createSnippetViaCli({ + name: snippetName, + active: false, + scope: 'single-use', + code: `update_option('${markerKey}', 'ran once');` + }) + await helper.navigateToSnippetsAdmin() + + const row = page.locator(SELECTORS.SNIPPET_ROW).filter({ hasText: snippetName }).first() + const runOnce = row.getByRole('link', { name: 'Run Once' }) + await expect(runOnce).toBeVisible() + await Promise.all([ + page.waitForURL(/result=executed/), + runOnce.click() + ]) + + expect((await wpCli(['option', 'get', markerKey])).trim()).toBe('ran once') + } finally { + await wpCli(['eval', `delete_option('${markerKey}');`]) + } + }) + test('PHP snippets execute in priority order', async ({ page }) => { const outputPrefix = `snippet-priority-${Date.now()}` const highPriorityId = `${outputPrefix}-high` @@ -181,6 +226,26 @@ test.describe('Code Snippets Evaluation', () => { } }) + test('CSS snippets load on the front end', async ({ page }) => { + if (!await SnippetsTestHelper.isProLicensed()) { + test.skip(true, 'CSS snippets require an active Pro license.') + } + + const property = `--e2e-css-${Date.now()}` + + await SnippetsTestHelper.createSnippetViaCli({ + name: snippetName, + active: true, + type: 'css', + code: `body { ${property}: loaded; }` + }) + + await page.goto(URLS.FRONTEND) + await expect.poll(() => page.locator('body').evaluate((body, propertyName) => + getComputedStyle(body).getPropertyValue(propertyName).trim(), property) + ).toBe('loaded') + }) + test('HTML snippet is evaluating correctly in footer', async () => { await helper.createAndActivateSnippet({ name: snippetName, diff --git a/tests/e2e/code-snippets-import.spec.ts b/tests/e2e/code-snippets-import.spec.ts new file mode 100644 index 000000000..0c340f7fe --- /dev/null +++ b/tests/e2e/code-snippets-import.spec.ts @@ -0,0 +1,140 @@ +import { readFileSync } from 'fs' +import { expect, test } from '@playwright/test' +import { DEFAULT_E2E_SNIPPET_BASE_NAME, SnippetsTestHelper } from './helpers/SnippetsTestHelper' +import { SELECTORS, URLS } from './helpers/constants' + +const importFile = (snippet: Record) => ({ + name: 'code-snippets-export.json', + mimeType: 'application/json', + buffer: Buffer.from(JSON.stringify({ snippets: [snippet] })) +}) + +test.describe('Code Snippets Import', () => { + test.beforeEach(async () => { + await SnippetsTestHelper.cleanupSnippetsByPrefix(DEFAULT_E2E_SNIPPET_BASE_NAME) + }) + + test.afterEach(async () => { + await SnippetsTestHelper.cleanupSnippetsByPrefix(DEFAULT_E2E_SNIPPET_BASE_NAME) + }) + + test('imports a selected JSON snippet as inactive', async ({ page }) => { + const snippetName = SnippetsTestHelper.makeUniqueSnippetName('Imported JSON') + + await page.goto(URLS.IMPORT_ADMIN) + await page.getByRole('radio', { name: /Do not import any duplicate snippets/ }).check() + await page.getByLabel('Select files to import').setInputFiles(importFile({ + id: 1, + name: snippetName, + desc: 'Imported from an E2E JSON export.', + code: `// ${snippetName}`, + tags: ['imported'], + scope: 'global', + priority: 10 + })) + await page.getByRole('button', { name: 'Upload files' }).click() + + await expect(page.getByRole('heading', { name: 'Available snippets (1)' })).toBeVisible() + await expect(page.getByRole('row', { name: new RegExp(snippetName) })).toBeVisible() + await page.getByRole('checkbox', { name: 'Select all snippets' }).check() + await page.getByRole('button', { name: 'Import Selected (1)' }).first().click() + + await expect(page.getByRole('heading', { name: 'Import successful' })).toBeVisible() + await expect(page.locator('.import-result-message')).toContainText('Successfully imported 1 snippet.') + await expect(page.locator('.import-result-display-card').getByRole('link', { name: 'All Snippets' })).toBeVisible() + + await page.goto(URLS.SNIPPETS_ADMIN) + const importedRow = page.locator(SELECTORS.SNIPPET_ROW).filter({ hasText: snippetName }).first() + await expect(importedRow).toBeVisible() + await expect(importedRow.getByRole('switch')).not.toBeChecked() + }) + + test('re-imports an exported snippet from its downloaded JSON file', async ({ page }) => { + test.setTimeout(60000) + const snippetName = SnippetsTestHelper.makeUniqueSnippetName('Export round trip') + + await SnippetsTestHelper.createSnippetViaCli({ + name: snippetName, + description: 'A snippet restored from its exported file.', + active: false, + priority: 7, + scope: 'front-end', + tags: ['round-trip'] + }) + await page.goto(URLS.SNIPPETS_ADMIN) + + const sourceRow = page.locator(SELECTORS.SNIPPET_ROW).filter({ hasText: snippetName }).first() + await sourceRow.hover() + const download = await Promise.all([ + page.waitForEvent('download'), + sourceRow.locator(SELECTORS.EXPORT_ACTION).first().click() + ]).then(([downloadEvent]) => downloadEvent) + const downloadPath = await download.path() + + if (!downloadPath) { + throw new Error('Export did not produce a local file path') + } + + await SnippetsTestHelper.cleanupSnippetsByPrefix(snippetName) + await page.goto(URLS.IMPORT_ADMIN) + await page.getByLabel('Select files to import').setInputFiles({ + name: download.suggestedFilename(), + mimeType: 'application/json', + buffer: readFileSync(downloadPath) + }) + await page.getByRole('button', { name: 'Upload files' }).click() + await page.getByRole('checkbox', { name: 'Select all snippets' }).check() + await page.getByRole('button', { name: 'Import Selected (1)' }).first().click() + + await expect(page.getByRole('heading', { name: 'Import successful' })).toBeVisible() + await page.goto(URLS.SNIPPETS_ADMIN) + const importedRow = page.locator(SELECTORS.SNIPPET_ROW).filter({ hasText: snippetName }).first() + await expect(importedRow).toBeVisible() + await expect(importedRow.getByRole('switch')).not.toBeChecked() + }) + + test('rejects an invalid upload with a clear error', async ({ page }) => { + await page.goto(URLS.IMPORT_ADMIN) + await page.getByLabel('Select files to import').setInputFiles({ + name: 'not-a-snippet.txt', + mimeType: 'text/plain', + buffer: Buffer.from('not a Code Snippets export') + }) + await page.getByRole('button', { name: 'Upload files' }).click() + + await expect(page.getByRole('heading', { name: 'File upload error' })).toBeVisible() + await expect(page.locator('.import-result-message')).toContainText('No valid snippets found') + }) + + test('skips and replaces duplicate snippets according to the selected policy', async ({ page }) => { + const snippetName = SnippetsTestHelper.makeUniqueSnippetName('Imported duplicate') + const duplicate = { + id: 1, + name: snippetName, + code: `// replacement for ${snippetName}`, + scope: 'global' + } + + await SnippetsTestHelper.createSnippetViaCli({ + name: snippetName, + code: `// original ${snippetName}`, + active: false + }) + + await page.goto(URLS.IMPORT_ADMIN) + await page.getByRole('radio', { name: /Do not import any duplicate snippets/ }).check() + await page.getByLabel('Select files to import').setInputFiles(importFile(duplicate)) + await page.getByRole('button', { name: 'Upload files' }).click() + await page.getByRole('checkbox', { name: 'Select all snippets' }).check() + await page.getByRole('button', { name: 'Import Selected (1)' }).first().click() + await expect(page.locator('.import-result-message')).toContainText('Successfully imported 0 snippets.') + + await page.goto(URLS.IMPORT_ADMIN) + await page.getByRole('radio', { name: /Replace any existing snippets/ }).check() + await page.getByLabel('Select files to import').setInputFiles(importFile(duplicate)) + await page.getByRole('button', { name: 'Upload files' }).click() + await page.getByRole('checkbox', { name: 'Select all snippets' }).check() + await page.getByRole('button', { name: 'Import Selected (1)' }).first().click() + await expect(page.locator('.import-result-message')).toContainText('Successfully imported 1 snippet.') + }) +}) diff --git a/tests/e2e/code-snippets-list.spec.ts b/tests/e2e/code-snippets-list.spec.ts index f5e13c40d..472cf9d58 100644 --- a/tests/e2e/code-snippets-list.spec.ts +++ b/tests/e2e/code-snippets-list.spec.ts @@ -296,6 +296,7 @@ test.describe('Code Snippets List Page Actions', () => { // Verify that a cloned snippet exists in the table (use table-scoped check to avoid admin bar matches) const clonedRow = snippetRowByName(page, `${snippetName} [CLONE]`) await expect(clonedRow).toBeVisible() + await expect(clonedRow.getByRole('switch')).not.toBeChecked() // Clean up the clone by trashing it await clickRowAction(clonedRow, SELECTORS.DELETE_ACTION) @@ -441,6 +442,46 @@ test.describe('Code Snippets List Page Actions', () => { expect(download.suggestedFilename()).toMatch(/\.json$/) }) + test('Exports the metadata needed to re-import a snippet', async ({ page }) => { + test.setTimeout(EXPORT_TEST_TIMEOUT_MS) + const exportName = SnippetsTestHelper.makeUniqueSnippetName('Export metadata') + + try { + await SnippetsTestHelper.createSnippetViaCli({ + name: exportName, + description: 'An E2E export description.', + priority: 7, + scope: 'front-end', + tags: ['e2e-export'], + active: false + }) + await helper.navigateToSnippetsAdmin() + + const exportRow = snippetRowByName(page, exportName) + await exportRow.hover() + const download = await Promise.all([ + page.waitForEvent('download'), + exportRow.locator(SELECTORS.EXPORT_ACTION).first().click() + ]).then(([downloadEvent]) => downloadEvent) + const downloadPath = await download.path() + + if (!downloadPath) { + throw new Error('Export did not produce a local file path') + } + + const exported = <{ snippets: Record[] }>JSON.parse(readFileSync(downloadPath, 'utf-8')) + expect(exported.snippets).toEqual([expect.objectContaining({ + name: exportName, + desc: 'An E2E export description.', + priority: 7, + scope: 'front-end', + tags: ['e2e-export'] + })]) + } finally { + await helper.cleanupSnippet(exportName) + } + }) + test('Can activate, deactivate, trash, and permanently delete snippets from bulk actions', async ({ page }) => { const bulkSnippetNames = [ SnippetsTestHelper.makeUniqueSnippetName('Bulk action snippet'), diff --git a/tests/e2e/helpers/SnippetsTestHelper.ts b/tests/e2e/helpers/SnippetsTestHelper.ts index 7fc4e0de1..b82827aea 100644 --- a/tests/e2e/helpers/SnippetsTestHelper.ts +++ b/tests/e2e/helpers/SnippetsTestHelper.ts @@ -37,6 +37,7 @@ export interface CreateSnippetCliOptions { conditionId?: number; locked?: boolean; priority?: number; + scope?: string; tags?: readonly string[]; type?: 'php' | 'html' | 'css' | 'js' | 'cond'; } @@ -102,6 +103,7 @@ export class SnippetsTestHelper { } const code = options.code ?? ('html' === type ? `

${options.name}

\n` : `// ${options.name}\n`) + scope = options.scope ?? scope const php = ` $snippet = new \\Code_Snippets\\Model\\Snippet([ diff --git a/tests/e2e/rest-api-auth.spec.ts b/tests/e2e/rest-api-auth.spec.ts new file mode 100644 index 000000000..14f7e430d --- /dev/null +++ b/tests/e2e/rest-api-auth.spec.ts @@ -0,0 +1,30 @@ +import { expect, test } from '@playwright/test' + +test.describe('Snippets REST API authentication', () => { + test('rejects creating a snippet without a REST nonce', async ({ page }) => { + await page.goto('/wp-admin/') + + const response = await page.evaluate(async () => { + const request = await fetch('/?rest_route=/code-snippets/v1/snippets', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: 'Unauthorised E2E snippet', + code: '// This must not be saved.', + scope: 'global', + network: false + }) + }) + + const body: unknown = JSON.parse(await request.text()) + + return { + status: request.status, + body + } + }) + + expect(response.status).toBe(401) + expect(response.body).toMatchObject({ code: 'rest_forbidden' }) + }) +}) diff --git a/tests/e2e/settings-permissions.spec.ts b/tests/e2e/settings-permissions.spec.ts new file mode 100644 index 000000000..89f81f943 --- /dev/null +++ b/tests/e2e/settings-permissions.spec.ts @@ -0,0 +1,40 @@ +import { expect, test } from '@playwright/test' +import { URLS } from './helpers/constants' +import { wpCli } from './helpers/wpCli' + +test.describe('Settings permissions', () => { + test('does not allow an Editor to access Settings', async ({ browser, page }) => { + const username = `cs-e2e-editor-${Date.now()}` + const password = 'e2e-editor-password' + let userCreated = false + + await page.goto(URLS.WP_ADMIN) + const baseUrl = new URL(page.url()).origin + const editorContext = await browser.newContext() + const editorPage = await editorContext.newPage() + + try { + await wpCli([ + 'user', + 'create', + username, + `${username}@example.test`, + '--role=editor', + `--user_pass=${password}` + ]) + userCreated = true + await editorPage.goto(`${baseUrl}${URLS.WP_LOGIN}`) + await editorPage.getByLabel('Username or Email Address').fill(username) + await editorPage.getByRole('textbox', { name: 'Password' }).fill(password) + await editorPage.getByRole('button', { name: 'Log In' }).click() + + await editorPage.goto(`${baseUrl}${URLS.SETTINGS_ADMIN}`) + await expect(editorPage.getByText('Sorry, you are not allowed to access this page.')).toBeVisible() + } finally { + await editorContext.close() + if (userCreated) { + await wpCli(['user', 'delete', username, '--yes']) + } + } + }) +}) diff --git a/tests/e2e/settings-tabs.spec.ts b/tests/e2e/settings-tabs.spec.ts index 142294c1d..1d2b6fc2a 100644 --- a/tests/e2e/settings-tabs.spec.ts +++ b/tests/e2e/settings-tabs.spec.ts @@ -1,9 +1,29 @@ import { expect, test } from '@playwright/test' +import { wpCli } from './helpers/wpCli' import { URLS } from './helpers/constants' const TABS = '#settings-sections-tabs' test.describe('Settings tabs', () => { + test('shows a success notice after saving the current tab', async ({ page }) => { + await page.goto(`${URLS.SETTINGS_ADMIN}§ion=editing`) + const editorRows = page.getByRole('spinbutton').first() + const originalRows = await editorRows.inputValue() + + try { + await editorRows.fill(String(Number(originalRows) + 1)) + await page.getByRole('button', { name: 'Save Changes' }).click() + + await expect(page).toHaveURL(/section=editing/) + await expect(page.locator('#setting-error-settings-saved')).toContainText('Settings saved.') + } finally { + await wpCli([ + 'eval', + `\\Code_Snippets\\Settings\\update_setting('general', 'visual_editor_rows', ${originalRows});` + ]) + } + }) + test('switch between rendered sections in place', async ({ page }) => { await page.goto(`${URLS.SETTINGS_ADMIN}§ion=editing`) @@ -32,4 +52,121 @@ test.describe('Settings tabs', () => { await expect(wrap).toHaveAttribute('data-active-tab', 'editing') await expect(page.locator(`${TABS} [data-section="editing"]`)).toHaveClass(/active-type/) }) + + test('confirms cache reset from Advanced settings', async ({ page }) => { + await page.goto(`${URLS.SETTINGS_ADMIN}§ion=advanced`) + await page.getByRole('button', { name: 'Reset Caches' }).click() + + await expect(page.locator('#setting-error-snippet_caches_reset')).toContainText('Successfully reset snippets caches.') + }) + + test('confirms database table upgrade from Advanced settings', async ({ page }) => { + await page.goto(`${URLS.SETTINGS_ADMIN}§ion=advanced`) + await page.getByRole('button', { name: 'Upgrade Database Table' }).click() + + await expect(page.locator('#setting-error-database_update_done')).toContainText('Successfully performed database table upgrade.') + }) + + test('changes the default snippet save action when Activate by Default is toggled', async ({ page }) => { + await page.goto(`${URLS.SETTINGS_ADMIN}§ion=running`) + const activateByDefault = page.getByRole('checkbox', { name: /Make the 'Save and Activate' button/ }) + const originalValue = await activateByDefault.isChecked() + + try { + await activateByDefault.uncheck() + await page.getByRole('button', { name: 'Save Changes' }).click() + await page.goto(URLS.ADD_SNIPPET_ADMIN) + await expect(page.getByRole('button', { name: 'Save Snippet' })).toHaveClass(/button-primary/) + + await page.goto(`${URLS.SETTINGS_ADMIN}§ion=running`) + await page.getByRole('checkbox', { name: /Make the 'Save and Activate' button/ }).check() + await page.getByRole('button', { name: 'Save Changes' }).click() + await page.goto(URLS.ADD_SNIPPET_ADMIN) + await expect(page.getByRole('button', { name: 'Save and Activate' })).toHaveClass(/button-primary/) + } finally { + await wpCli([ + 'eval', + `\\Code_Snippets\\Settings\\update_setting('general', 'activate_by_default', ${originalValue ? 'true' : 'false'});` + ]) + } + }) + + test('shows Insights controls for performance tracking and security scanning', async ({ page }) => { + await page.goto(`${URLS.SETTINGS_ADMIN}§ion=insights`) + + await expect(page.getByRole('checkbox', { name: /Track Snippet Performance/ })).toBeVisible({ timeout: 5000 }) + await expect(page.getByRole('checkbox', { name: /Scan Snippets for Security Issues/ })).toBeVisible({ timeout: 5000 }) + }) + + test('resets settings to their defaults', async ({ page }) => { + const originalSettings = (await wpCli(['eval', "echo wp_json_encode(get_option('code_snippets_settings')); "])).trim() + + try { + await page.goto(`${URLS.SETTINGS_ADMIN}§ion=running`) + await page.getByRole('checkbox', { name: /Make the 'Save and Activate' button/ }).uncheck() + await page.getByRole('button', { name: 'Save Changes' }).click() + await page.locator('input[name="code_snippets_settings[reset_settings]"]').click() + + await expect(page.locator('#setting-error-settings_reset')).toContainText( + 'All settings have been reset to their defaults.', + { timeout: 5000 } + ) + await page.goto(URLS.ADD_SNIPPET_ADMIN) + await expect(page.getByRole('button', { name: 'Save and Activate' })).toHaveClass(/button-primary/) + } finally { + await wpCli([ + 'eval', + `update_option('code_snippets_settings', json_decode(${JSON.stringify(originalSettings)}, true));` + ]) + } + }) + + test('stores the Complete Uninstall setting', async ({ page }) => { + await page.goto(`${URLS.SETTINGS_ADMIN}§ion=advanced`) + const completeUninstall = page.getByRole('checkbox', { name: /also delete all snippets and plugin settings/ }) + const originalValue = await completeUninstall.isChecked() + + try { + await completeUninstall.uncheck() + await page.getByRole('button', { name: 'Save Changes' }).click() + await page.reload() + await expect(page.getByRole('checkbox', { name: /also delete all snippets and plugin settings/ })).not.toBeChecked() + + await page.getByRole('checkbox', { name: /also delete all snippets and plugin settings/ }).check() + await page.getByRole('button', { name: 'Save Changes' }).click() + await page.reload() + await expect( + page.getByRole('checkbox', { name: /also delete all snippets and plugin settings/ }) + ).toBeChecked({ timeout: 5000 }) + } finally { + await wpCli([ + 'eval', + `\\Code_Snippets\\Settings\\update_setting('general', 'complete_uninstall', ${originalValue ? 'true' : 'false'});` + ]) + } + }) + + test('shows and hides the admin bar menu when its setting changes', async ({ page }) => { + await page.goto(`${URLS.SETTINGS_ADMIN}§ion=interface`) + const enableAdminBar = page.getByRole('checkbox', { name: /Show a Snippets menu in the admin bar/ }) + const originalValue = await enableAdminBar.isChecked() + + try { + await enableAdminBar.uncheck() + await page.getByRole('button', { name: 'Save Changes' }).click() + await page.goto(URLS.SNIPPETS_ADMIN) + await expect(page.locator('#wp-admin-bar-code-snippets')).toHaveCount(0) + + await page.goto(`${URLS.SETTINGS_ADMIN}§ion=interface`) + await page.getByRole('checkbox', { name: /Show a Snippets menu in the admin bar/ }).check() + await page.getByRole('button', { name: 'Save Changes' }).click() + await page.goto(URLS.SNIPPETS_ADMIN) + await expect(page.locator('#wp-admin-bar-code-snippets')).toBeVisible() + } finally { + await wpCli([ + 'eval', + `\\Code_Snippets\\Settings\\update_setting('general', 'enable_admin_bar', ${originalValue ? 'true' : 'false'});` + ]) + } + }) }) diff --git a/tests/unit/Core/Uninstaller_Test.php b/tests/unit/Core/Uninstaller_Test.php index ed0f2e3ea..58fab5e9b 100644 --- a/tests/unit/Core/Uninstaller_Test.php +++ b/tests/unit/Core/Uninstaller_Test.php @@ -2,10 +2,14 @@ namespace Code_Snippets\Core; +use Code_Snippets\Model\Snippet; use Code_Snippets\REST_API\Preferences\Insights_View_Rest_Controller; use Code_Snippets\REST_API\Preferences\Snippet_View_REST_Controller; use Code_Snippets\UnitTestCase; use function Code_Snippets\code_snippets; +use function Code_Snippets\delete_snippet; +use function Code_Snippets\get_snippet; +use function Code_Snippets\save_snippet; /** * Tests for complete plugin uninstallation. @@ -52,4 +56,50 @@ public function test_complete_uninstall_removes_insights_chart_view_preferences( $this->assertFalse( get_option( Insights_View_Rest_Controller::OPTION_NAME ) ); } + + /** + * A standard uninstall leaves snippets and settings ready for a reinstall. + * + * @return void + */ + public function test_incomplete_uninstall_preserves_snippets_and_settings(): void { + $settings = [ 'general' => [ 'complete_uninstall' => false ] ]; + $snippet = save_snippet( + new Snippet( + [ + 'name' => 'Preserved uninstall snippet', + 'code' => 'add_action( \'init\', \'__return_null\' );', + ] + ) + ); + + update_option( 'code_snippets_settings', $settings ); + ( new Uninstaller() )->uninstall_plugin(); + + $this->assertSame( $settings, get_option( 'code_snippets_settings' ) ); + $this->assertSame( 'Preserved uninstall snippet', get_snippet( $snippet->id )->name ); + + delete_snippet( $snippet->id ); + } + + /** + * A complete uninstall removes the snippets table and plugin settings. + * + * @return void + */ + public function test_complete_uninstall_removes_the_snippets_table_and_settings(): void { + $db = code_snippets()->db; + + update_option( + 'code_snippets_settings', + [ + 'general' => [ 'complete_uninstall' => true ], + ] + ); + + ( new Uninstaller() )->uninstall_plugin(); + + $this->assertFalse( DB::table_exists( $db->table, true ) ); + $this->assertFalse( get_option( 'code_snippets_settings' ) ); + } } diff --git a/tests/unit/REST_API/REST_API_Snippets_Test.php b/tests/unit/REST_API/REST_API_Snippets_Test.php index 8ecf21ed5..902d03293 100644 --- a/tests/unit/REST_API/REST_API_Snippets_Test.php +++ b/tests/unit/REST_API/REST_API_Snippets_Test.php @@ -125,6 +125,67 @@ public function test_get_all_snippets_without_pagination() { $this->assertArrayHasKey( 'code', $response[0] ); } + /** + * A missing snippet is reported as a 404 instead of an internal error. + * + * @return void + */ + public function test_getting_a_missing_snippet_returns_404(): void { + $request = new WP_REST_Request( 'GET', "/$this->namespace/$this->base_route/999999" ); + $request->set_param( 'network', false ); + $response = rest_do_request( $request ); + $data = $response->get_data(); + + $this->assertSame( 404, $response->get_status() ); + $this->assertSame( 'rest_cannot_get', $data['code'] ); + $this->assertSame( 'The snippet could not be found.', $data['message'] ); + } + + /** + * Snippets can be created, read, updated, trashed, and permanently deleted. + * + * @return void + */ + public function test_snippet_crud_lifecycle(): void { + $endpoint = "/$this->namespace/$this->base_route"; + $created = $this->make_mutating_request( + 'POST', + $endpoint, + [ + 'name' => 'REST CRUD fixture', + 'code' => '// REST CRUD fixture', + 'scope' => 'global', + 'active' => false, + 'network' => false, + ] + ); + $snippet_id = $created['id']; + + $this->assertGreaterThan( 0, $snippet_id ); + $this->assertSame( 'REST CRUD fixture', $this->make_request( "$endpoint/$snippet_id", [ 'network' => false ] )['name'] ); + + $updated = $this->make_mutating_request( + 'PUT', + "$endpoint/$snippet_id", + [ + 'name' => 'Updated REST CRUD fixture', + 'network' => false, + ] + ); + + $this->assertSame( 'Updated REST CRUD fixture', $updated['name'] ); + + $trashed = $this->make_mutating_request( 'DELETE', "$endpoint/$snippet_id", [ 'network' => false ] ); + $this->assertTrue( $trashed['trashed'] ); + + $request = new WP_REST_Request( 'DELETE', "$endpoint/$snippet_id" ); + $request->set_param( 'network', false ); + $response = rest_do_request( $request ); + + $this->assertSame( 204, $response->get_status() ); + $this->assertSame( 0, get_snippet( $snippet_id )->id ); + } + /** * Test pagination with per_page parameter only (first page). */ From bdf27e06078e30a6ff85e05293de01cf99656f99 Mon Sep 17 00:00:00 2001 From: Rami Yushuvaev Date: Tue, 15 Sep 2026 18:24:17 +0300 Subject: [PATCH 04/19] feat: add new tests --- tests/e2e/code-snippets-edit.spec.ts | 86 ++++++++++++++ tests/e2e/code-snippets-evaluation.spec.ts | 55 +++++++++ tests/e2e/code-snippets-insights.spec.ts | 56 ++++++++- tests/e2e/code-snippets-list.spec.ts | 129 +++++++++++++++++++++ tests/e2e/code-snippets-preview.spec.ts | 14 +++ tests/e2e/contextual-help.spec.ts | 28 +++++ tests/e2e/helpers/SnippetsTestHelper.ts | 8 +- tests/e2e/settings-tabs.spec.ts | 9 ++ tests/e2e/welcome.spec.ts | 11 ++ tests/unit/Core/Uninstaller_Test.php | 24 ++++ 10 files changed, 410 insertions(+), 10 deletions(-) create mode 100644 tests/e2e/contextual-help.spec.ts create mode 100644 tests/e2e/welcome.spec.ts diff --git a/tests/e2e/code-snippets-edit.spec.ts b/tests/e2e/code-snippets-edit.spec.ts index 41608b85a..5e9d465cd 100644 --- a/tests/e2e/code-snippets-edit.spec.ts +++ b/tests/e2e/code-snippets-edit.spec.ts @@ -23,6 +23,28 @@ test.describe('Code Snippets Admin', () => { }) }) + test('Expands and collapses the code editor', async ({ page }) => { + await helper.clickAddNewSnippet() + const form = page.locator('form.snippet-form') + + await expect(form).toHaveClass(/snippet-form-collapsed/) + await page.getByRole('button', { name: 'Expand' }).click() + await expect(form).toHaveClass(/snippet-form-expanded/) + await expect(page.getByRole('button', { name: 'Minimize' })).toBeVisible() + + await page.getByRole('button', { name: 'Minimize' }).click() + await expect(form).toHaveClass(/snippet-form-collapsed/) + }) + + test('Shows the code editor keyboard shortcut reference', async ({ page }) => { + await helper.clickAddNewSnippet() + const shortcuts = page.locator('.snippet-editor-help') + + await expect(shortcuts).toBeVisible() + await shortcuts.hover() + await expect(shortcuts.locator('.tooltip-content')).toContainText('Save changes') + }) + test('Can activate and deactivate a snippet', async () => { const snippetName = SnippetsTestHelper.makeUniqueSnippetName() await helper.createSnippet({ @@ -64,6 +86,70 @@ test.describe('Code Snippets Admin', () => { await helper.cleanupSnippet(snippetName) }) + test('Saves a snippet priority from the editor sidebar', async ({ page }) => { + const snippetName = SnippetsTestHelper.makeUniqueSnippetName('Priority snippet') + + try { + await helper.createSnippet({ + name: snippetName, + code: "add_filter('show_admin_bar', '__return_false');" + }) + await helper.openSnippet(snippetName) + + const priority = page.getByRole('spinbutton', { name: 'Priority' }) + await priority.fill('7') + await helper.saveSnippet() + await helper.expectSuccessMessage(/Snippet updated/i) + + await page.reload() + await expect(page.getByRole('spinbutton', { name: 'Priority' })).toHaveValue('7') + } finally { + await helper.cleanupSnippet(snippetName) + } + }) + + test('Exports a saved snippet as JSON from the editor sidebar', async ({ page }) => { + const snippetName = SnippetsTestHelper.makeUniqueSnippetName('Sidebar export') + + try { + await helper.createSnippet({ + name: snippetName, + code: "add_filter('show_admin_bar', '__return_false');" + }) + await helper.openSnippet(snippetName) + + const download = await Promise.all([ + page.waitForEvent('download'), + page.getByRole('button', { name: 'Export', exact: true }).click() + ]).then(([event]) => event) + + expect(download.suggestedFilename()).toMatch(/\.json$/) + } finally { + await helper.cleanupSnippet(snippetName) + } + }) + + test('Downloads a PHP code file from the editor sidebar', async ({ page }) => { + const snippetName = SnippetsTestHelper.makeUniqueSnippetName('Sidebar code download') + + try { + await helper.createSnippet({ + name: snippetName, + code: "add_filter('show_admin_bar', '__return_false');" + }) + await helper.openSnippet(snippetName) + + const download = await Promise.all([ + page.waitForEvent('download'), + page.getByRole('button', { name: 'Download Code' }).click() + ]).then(([event]) => event) + + expect(download.suggestedFilename()).toMatch(/\.php$/) + } finally { + await helper.cleanupSnippet(snippetName) + } + }) + test('Locks a snippet until it is unlocked from the editor sidebar', async ({ page }) => { const snippetName = SnippetsTestHelper.makeUniqueSnippetName('Locked snippet') const updatedSnippetName = `${snippetName} unlocked` diff --git a/tests/e2e/code-snippets-evaluation.spec.ts b/tests/e2e/code-snippets-evaluation.spec.ts index 6df82b20a..620fa5eac 100644 --- a/tests/e2e/code-snippets-evaluation.spec.ts +++ b/tests/e2e/code-snippets-evaluation.spec.ts @@ -167,6 +167,47 @@ test.describe('Code Snippets Evaluation', () => { await expect(page).toHaveURL(/snippets-safe-mode=1/, { timeout: 5000 }) }) + test('Safe mode constant disables snippets while keeping the editor accessible', async ({ page }) => { + const safeModeClass = `safe-mode-constant-${Date.now()}` + const safeModeMuPluginPath = 'wp-content/mu-plugins/code-snippets-e2e-safe-mode-execution.php' + const removeMuPlugin = () => + wpCli(['eval', `@unlink( ABSPATH . ${JSON.stringify(safeModeMuPluginPath)} );`]) + const enableSafeMode = ` + $path = ABSPATH . ${JSON.stringify(safeModeMuPluginPath)}; + wp_mkdir_p( dirname( $path ) ); + file_put_contents( $path, " { const markerKey = `code_snippets_e2e_single_use_${Date.now()}` @@ -193,6 +234,20 @@ test.describe('Code Snippets Evaluation', () => { } }) + test('asks for confirmation before running a single-use snippet', async ({ page }) => { + await SnippetsTestHelper.createSnippetViaCli({ + name: snippetName, + active: false, + scope: 'single-use', + code: '// A harmless Run Once confirmation fixture.' + }) + await helper.navigateToSnippetsAdmin() + + const row = page.locator(SELECTORS.SNIPPET_ROW).filter({ hasText: snippetName }).first() + await row.getByRole('link', { name: 'Run Once' }).click() + await expect(page.getByRole('dialog', { name: /Run Once/ })).toBeVisible({ timeout: 5000 }) + }) + test('PHP snippets execute in priority order', async ({ page }) => { const outputPrefix = `snippet-priority-${Date.now()}` const highPriorityId = `${outputPrefix}-high` diff --git a/tests/e2e/code-snippets-insights.spec.ts b/tests/e2e/code-snippets-insights.spec.ts index 9071fcb78..e5c9aa057 100644 --- a/tests/e2e/code-snippets-insights.spec.ts +++ b/tests/e2e/code-snippets-insights.spec.ts @@ -88,7 +88,7 @@ test.describe('Insights screen', () => { active: false, type: 'js' }) - await page.goto(URLS.SNIPPETS_ADMIN.replace('page=snippets', 'page=code-snippets-insights')) + await page.goto(URLS.INSIGHTS_ADMIN) const activationPie = page.locator('[data-insights-chart="activation"] .insights-pie-chart') const conditionsChart = page.locator('[data-insights-chart="conditions"]') @@ -116,6 +116,35 @@ test.describe('Insights screen', () => { await expect(withoutConditions.locator('strong')).toHaveText('4') }) + test('shows snippet scope counts in the location chart', async ({ page }) => { + await SnippetsTestHelper.createSnippetViaCli({ name: 'Insights Global One', active: true }) + await SnippetsTestHelper.createSnippetViaCli({ name: 'Insights Global Two', active: true }) + await SnippetsTestHelper.createSnippetViaCli({ + name: 'Insights Admin Scope', + active: true, + scope: 'admin' + }) + await SnippetsTestHelper.createSnippetViaCli({ + name: 'Insights Front-end Scope', + active: true, + scope: 'front-end' + }) + + await page.goto(URLS.INSIGHTS_ADMIN) + const locationChart = page.locator('[data-insights-chart="location"]') + + for (const [label, count] of [ + ['Run everywhere', '2'], + ['Only run in administration area', '1'], + ['Only run on site front-end', '1'] + ]) { + const entry = locationChart.locator('li').filter({ hasText: label }) + + await expect(entry).toHaveCount(1) + await expect(entry.locator('strong')).toHaveText(count) + } + }) + test('switches used tags between bar and cloud views', async ({ page }) => { await SnippetsTestHelper.createSnippetViaCli({ name: 'Insights Shared and Alpha Tags', @@ -128,7 +157,7 @@ test.describe('Insights screen', () => { tags: ['Shared'] }) - await page.goto(URLS.SNIPPETS_ADMIN.replace('page=snippets', 'page=code-snippets-insights')) + await page.goto(URLS.INSIGHTS_ADMIN) const tagsChart = page.locator('[data-insights-chart="tags"]') await expect(page.getByRole('heading', { name: 'Tags' })).toBeVisible() @@ -164,7 +193,7 @@ test.describe('Insights screen', () => { tags: ['sample'] }) - await page.goto(URLS.SNIPPETS_ADMIN.replace('page=snippets', 'page=code-snippets-insights')) + await page.goto(URLS.INSIGHTS_ADMIN) const manageUrl = (query: string) => new URL(`${URLS.SNIPPETS_ADMIN}${query}`, baseURL).toString() @@ -202,8 +231,23 @@ test.describe('Insights screen', () => { await expect(tagLink).toHaveCSS('text-decoration-line', 'none') }) + test('opens the matching filtered list from a chart entry', async ({ page }) => { + const name = 'Insights Chart Link Snippet' + await SnippetsTestHelper.createSnippetViaCli({ + name, + active: true, + tags: ['chart-link'] + }) + + await page.goto(URLS.INSIGHTS_ADMIN) + await page.locator('[data-insights-chart="tags"]').getByRole('link', { name: 'chart-link' }).click() + + await expect(page).toHaveURL(/page=snippets.*tag=chart-link/) + await expect(page.locator('.wp-list-table tbody tr').filter({ hasText: name })).toBeVisible() + }) + test('switches and restores each Insights chart view', async ({ page }) => { - await page.goto(URLS.SNIPPETS_ADMIN.replace('page=snippets', 'page=code-snippets-insights')) + await page.goto(URLS.INSIGHTS_ADMIN) const typeChart = page.locator('[data-insights-chart="type"]') const activationChart = page.locator('[data-insights-chart="activation"]') @@ -254,7 +298,7 @@ test.describe('Insights screen', () => { }) test('restores a chart view when saving the preference fails', async ({ page }) => { - await page.goto(URLS.SNIPPETS_ADMIN.replace('page=snippets', 'page=code-snippets-insights')) + await page.goto(URLS.INSIGHTS_ADMIN) const conditionsChart = page.locator('[data-insights-chart="conditions"]') await expect(conditionsChart).toHaveAttribute('data-view', 'pie') @@ -269,7 +313,7 @@ test.describe('Insights screen', () => { }) test('keeps the latest chart views when an earlier save fails', async ({ page }) => { - await page.goto(URLS.SNIPPETS_ADMIN.replace('page=snippets', 'page=code-snippets-insights')) + await page.goto(URLS.INSIGHTS_ADMIN) const typeChart = page.locator('[data-insights-chart="type"]') const activationChart = page.locator('[data-insights-chart="activation"]') let rejectFirstRequest: (() => void) | undefined diff --git a/tests/e2e/code-snippets-list.spec.ts b/tests/e2e/code-snippets-list.spec.ts index 472cf9d58..ca371b5d9 100644 --- a/tests/e2e/code-snippets-list.spec.ts +++ b/tests/e2e/code-snippets-list.spec.ts @@ -63,6 +63,65 @@ test.describe('Code Snippets List Page Actions', () => { await expect(search.getByRole('button', { name: 'Search' })).toHaveCount(0) }) + test('Shows an empty state when no snippets match the search', async ({ page }) => { + const searchInput = page.getByRole('searchbox', { name: 'Search Snippets:' }) + await searchInput.fill(`${snippetName}-does-not-exist`) + + await expect(page.getByText('No snippets were found matching the current search query.')).toBeVisible() + }) + + test('Filters snippets by the selected tag', async ({ page }) => { + const taggedSnippetName = SnippetsTestHelper.makeUniqueSnippetName('Tag filter') + const tag = `e2e-tag-${Date.now()}` + + try { + await SnippetsTestHelper.createSnippetViaCli({ + name: taggedSnippetName, + active: false, + tags: [tag] + }) + await helper.navigateToSnippetsAdmin() + + await page.getByRole('combobox', { name: 'Filter snippets by tag' }).selectOption({ label: tag }) + await expect(snippetRowByName(page, taggedSnippetName)).toBeVisible() + await expect(snippetRowByName(page, snippetName)).toBeHidden() + } finally { + await helper.cleanupSnippet(taggedSnippetName) + } + }) + + test('Clears search, tag, and status filters together', async ({ page }) => { + const taggedSnippetName = SnippetsTestHelper.makeUniqueSnippetName('Clear filters') + const tag = `e2e-clear-${Date.now()}` + + try { + await SnippetsTestHelper.createSnippetViaCli({ + name: taggedSnippetName, + active: false, + tags: [tag] + }) + await helper.navigateToSnippetsAdmin() + + await page.locator('.subsubsub .inactive a').click() + await page.getByRole('combobox', { name: 'Filter snippets by tag' }).selectOption({ label: tag }) + await page.getByRole('searchbox', { name: 'Search Snippets:' }).fill(taggedSnippetName) + await expect(snippetRowByName(page, taggedSnippetName)).toBeVisible() + + await page.getByRole('button', { name: 'Clear Filters' }).click() + await expect.poll(async () => ({ + search: await page.getByRole('searchbox', { name: 'Search Snippets:' }).inputValue(), + tag: await page.getByRole('combobox', { name: 'Filter snippets by tag' }).inputValue(), + activeSnippetVisible: await snippetRowByName(page, snippetName).isVisible() + }), { timeout: 5000 }).toEqual({ + search: '', + tag: '', + activeSnippetVisible: true + }) + } finally { + await helper.cleanupSnippet(taggedSnippetName) + } + }) + test('Searches snippets by name, description, and code', async ({ page }) => { const nameQuery = SnippetsTestHelper.makeUniqueSnippetName('Search name') const descriptionQuery = SnippetsTestHelper.makeUniqueSnippetName('Search-description-query') @@ -340,6 +399,47 @@ test.describe('Code Snippets List Page Actions', () => { await helper.cleanupSnippet(`${snippetName} [CLONE]`) }) + test('Selects rows with the header checkbox and individual checkboxes', async ({ page }) => { + const row = snippetRowByName(page, snippetName) + const rowCheckbox = row.locator('input[name="checked[]"]') + const selectAll = page.locator(SELECTORS.SNIPPETS_TABLE) + .locator('thead') + .getByRole('checkbox', { name: 'Select All', exact: true }) + + await selectAll.check() + await expect(rowCheckbox).toBeChecked() + + await rowCheckbox.uncheck() + await expect(selectAll).not.toBeChecked() + }) + + test('labels table controls and row actions for assistive technology', async ({ page }) => { + const tableHead = page.locator(SELECTORS.SNIPPETS_TABLE).locator('thead') + const row = snippetRowByName(page, snippetName) + + await expect(tableHead.getByRole('checkbox', { name: 'Select All', exact: true })) + .toHaveAccessibleName('Select All') + + for (const column of ['Name', 'Type', 'Modified', 'Priority']) { + await expect(tableHead.getByRole('button', { name: new RegExp(`^${column}`) })) + .toHaveAccessibleName(new RegExp(column)) + } + + await row.hover() + await expect(row.getByRole('link', { name: 'Edit' })).toHaveAccessibleName('Edit') + for (const action of ['Preview', 'Clone', 'Export', 'Trash']) { + await expect(row.getByRole('button', { name: action })).toHaveAccessibleName(action) + } + }) + + test('reduces switch animation when the user prefers less motion', async ({ page }) => { + await page.emulateMedia({ reducedMotion: 'reduce' }) + const toggleSwitch = snippetRowByName(page, snippetName).getByRole('switch') + + expect(await toggleSwitch.evaluate(element => + getComputedStyle(element, '::before').transitionDuration)).toBe('0.01s') + }) + test('Can trash a snippet from the preview modal', async ({ page }) => { const snippetRow = snippetRowByName(page, snippetName) await clickRowAction(snippetRow, SELECTORS.PREVIEW_ACTION) @@ -535,6 +635,35 @@ test.describe('Code Snippets List Page Actions', () => { } }) + test('Does not apply a bulk action when no rows are selected', async ({ page }) => { + const row = snippetRowByName(page, snippetName) + + await page.locator('select[name="action"]').first().selectOption({ label: 'Trash' }) + await page.locator('#doaction').click() + + await expect(row).toBeVisible() + await expect(row).not.toHaveClass(/trashed-snippet/) + }) + + test('Restores selected snippets from the Trash with a bulk action', async ({ page }) => { + const row = snippetRowByName(page, snippetName) + + await row.locator('input[name="checked[]"]').check({ force: true }) + await page.locator('select[name="action"]').first().selectOption({ label: 'Trash' }) + await page.locator('#doaction').click() + await expect(row).toHaveCount(0) + + await page.locator('.subsubsub .trashed a').click() + const trashedRow = snippetRowByName(page, snippetName) + await trashedRow.locator('input[name="checked[]"]').check({ force: true }) + await page.locator('select[name="action"]').first().selectOption({ label: 'Restore' }) + await page.locator('#doaction').click() + await expect(trashedRow).toHaveCount(0) + + await page.locator('.subsubsub .all a').click() + await expect(snippetRowByName(page, snippetName)).toBeVisible() + }) + test('Can export multiple snippets from bulk actions', async ({ page }) => { test.setTimeout(EXPORT_TEST_TIMEOUT_MS) const secondSnippetName = SnippetsTestHelper.makeUniqueSnippetName() diff --git a/tests/e2e/code-snippets-preview.spec.ts b/tests/e2e/code-snippets-preview.spec.ts index 68b6fe3d4..327d1e620 100644 --- a/tests/e2e/code-snippets-preview.spec.ts +++ b/tests/e2e/code-snippets-preview.spec.ts @@ -174,6 +174,20 @@ test.describe('Code Snippets Preview Modal', () => { .toBeCloseTo(CONTROL_HEIGHT, 0) }) + test('keeps keyboard focus inside the preview dialog', async ({ page }) => { + await openPreviewEditor(page) + const modal = page.locator('.code-snippets-preview-modal') + const containsFocusedElement = () => modal.evaluate(element => element.contains(document.activeElement)) + + await modal.getByRole('button', { name: 'Close' }).focus() + await page.keyboard.press('Shift+Tab') + await expect.poll(containsFocusedElement).toBe(true) + + await modal.getByRole('button').last().focus() + await page.keyboard.press('Tab') + await expect.poll(containsFocusedElement).toBe(true) + }) + for (const keypress of ['Tab', 'Shift+Tab']) { test(`${keypress} leaves the preview editor`, async ({ page }) => { const editor = await openPreviewEditor(page) diff --git a/tests/e2e/contextual-help.spec.ts b/tests/e2e/contextual-help.spec.ts new file mode 100644 index 000000000..f769173d6 --- /dev/null +++ b/tests/e2e/contextual-help.spec.ts @@ -0,0 +1,28 @@ +import { expect, test } from '@playwright/test' +import { URLS } from './helpers/constants' + +const SCREENS_WITH_HELP = [ + { name: 'Add Snippet', url: URLS.ADD_SNIPPET_ADMIN }, + { name: 'Manage Snippets', url: URLS.SNIPPETS_ADMIN }, + // { name: 'Cloud Community', url: URLS.CLOUD_COMMUNITY_ADMIN }, + { name: 'Cloud Library', url: URLS.CLOUD_LIBRARY_ADMIN }, + { name: 'Blueprints', url: URLS.BLUEPRINTS_ADMIN }, + { name: 'AI Agent', url: URLS.AI_AGENT_ADMIN }, + { name: 'Insights', url: URLS.INSIGHTS_ADMIN }, + { name: 'Import', url: URLS.IMPORT_ADMIN }, + { name: 'Settings', url: URLS.SETTINGS_ADMIN }, + { name: 'Welcome', url: URLS.WELCOME_ADMIN } +] + +test.describe('Contextual Help', () => { + for (const screen of SCREENS_WITH_HELP) { + test(`${screen.name} exposes its Help tabs`, async ({ page }) => { + await page.goto(screen.url) + + await page.locator('#contextual-help-link').click() + const help = page.locator('#contextual-help-wrap') + await expect(help).toBeVisible() + await expect(help.locator('.contextual-help-tabs li')).not.toHaveCount(0) + }) + } +}) diff --git a/tests/e2e/helpers/SnippetsTestHelper.ts b/tests/e2e/helpers/SnippetsTestHelper.ts index b82827aea..f5af800a3 100644 --- a/tests/e2e/helpers/SnippetsTestHelper.ts +++ b/tests/e2e/helpers/SnippetsTestHelper.ts @@ -111,10 +111,10 @@ export class SnippetsTestHelper { 'desc' => ${JSON.stringify(options.description ?? '')}, 'code' => ${JSON.stringify(code)}, 'scope' => ${JSON.stringify(scope)}, - 'active' => ${options.active ? 'true' : 'false'}, - 'condition_id' => ${options.conditionId ?? 0}, - 'priority' => ${options.priority ?? 10}, - 'tags' => ${JSON.stringify(options.tags ?? [])}, + 'active' => ${options.active ? 'true' : 'false'}, + 'condition_id' => ${options.conditionId ?? 0}, + 'priority' => ${options.priority ?? 10}, + 'tags' => ${JSON.stringify(options.tags ?? [])}, ]); $snippet = \\Code_Snippets\\save_snippet($snippet); if (${options.locked ? 'true' : 'false'}) { diff --git a/tests/e2e/settings-tabs.spec.ts b/tests/e2e/settings-tabs.spec.ts index 1d2b6fc2a..d51a9e60a 100644 --- a/tests/e2e/settings-tabs.spec.ts +++ b/tests/e2e/settings-tabs.spec.ts @@ -3,6 +3,7 @@ import { wpCli } from './helpers/wpCli' import { URLS } from './helpers/constants' const TABS = '#settings-sections-tabs' +const SETTINGS_SECTIONS = ['editing', 'running', 'insights', 'library', 'interface', 'advanced'] test.describe('Settings tabs', () => { test('shows a success notice after saving the current tab', async ({ page }) => { @@ -53,6 +54,14 @@ test.describe('Settings tabs', () => { await expect(page.locator(`${TABS} [data-section="editing"]`)).toHaveClass(/active-type/) }) + test('renders every mandatory Settings tab', async ({ page }) => { + await page.goto(`${URLS.SETTINGS_ADMIN}§ion=editing`) + + for (const section of SETTINGS_SECTIONS) { + await expect(page.locator(`${TABS} [data-section="${section}"]`)).toBeVisible({ timeout: 5000 }) + } + }) + test('confirms cache reset from Advanced settings', async ({ page }) => { await page.goto(`${URLS.SETTINGS_ADMIN}§ion=advanced`) await page.getByRole('button', { name: 'Reset Caches' }).click() diff --git a/tests/e2e/welcome.spec.ts b/tests/e2e/welcome.spec.ts new file mode 100644 index 000000000..ae9c4931d --- /dev/null +++ b/tests/e2e/welcome.spec.ts @@ -0,0 +1,11 @@ +import { expect, test } from '@playwright/test' +import { URLS } from './helpers/constants' + +test.describe('What’s New screen', () => { + test('renders the Resources and Updates screen', async ({ page }) => { + await page.goto(URLS.WELCOME_ADMIN) + + await expect(page.getByRole('heading', { level: 1, name: 'Resources and Updates' })).toBeVisible() + await expect(page.locator('.code-snippets-welcome')).toBeVisible() + }) +}) diff --git a/tests/unit/Core/Uninstaller_Test.php b/tests/unit/Core/Uninstaller_Test.php index 58fab5e9b..389c5422e 100644 --- a/tests/unit/Core/Uninstaller_Test.php +++ b/tests/unit/Core/Uninstaller_Test.php @@ -82,6 +82,30 @@ public function test_incomplete_uninstall_preserves_snippets_and_settings(): voi delete_snippet( $snippet->id ); } + /** + * A reinstall after a preserving uninstall keeps existing snippets available. + * + * @return void + */ + public function test_reinstall_after_incomplete_uninstall_keeps_existing_snippets(): void { + $snippet = save_snippet( + new Snippet( + [ + 'name' => 'Reinstalled preserved snippet', + 'code' => 'add_action( \'init\', \'__return_null\' );', + ] + ) + ); + + update_option( 'code_snippets_settings', [ 'general' => [ 'complete_uninstall' => false ] ] ); + ( new Uninstaller() )->uninstall_plugin(); + code_snippets()->db->create_or_upgrade_tables(); + + $this->assertSame( 'Reinstalled preserved snippet', get_snippet( $snippet->id )->name ); + + delete_snippet( $snippet->id ); + } + /** * A complete uninstall removes the snippets table and plugin settings. * From 0f17bd1f3b0070a1b5c268f70911dfb28eea9f03 Mon Sep 17 00:00:00 2001 From: Rami Yushuvaev Date: Tue, 15 Sep 2026 18:25:33 +0300 Subject: [PATCH 05/19] refactor: standardize timeout values across tests using TIMEOUTS constants --- tests/e2e/auth.setup.ts | 9 +++-- tests/e2e/code-snippets-evaluation.spec.ts | 6 ++-- tests/e2e/code-snippets-import.spec.ts | 4 +-- tests/e2e/code-snippets-list.spec.ts | 35 ++++++++++--------- tests/e2e/code-snippets-paste-tags.spec.ts | 3 +- .../code-snippets-quicknav-admin-bar.spec.ts | 16 ++++----- tests/e2e/flat-files.setup.ts | 5 +-- tests/e2e/helpers/SnippetsTestHelper.ts | 3 +- tests/e2e/helpers/constants.ts | 7 +++- tests/e2e/rtl.setup.ts | 8 ++--- tests/e2e/settings-tabs.spec.ts | 12 +++---- 11 files changed, 55 insertions(+), 53 deletions(-) diff --git a/tests/e2e/auth.setup.ts b/tests/e2e/auth.setup.ts index 76cbcf471..e96f0a28c 100644 --- a/tests/e2e/auth.setup.ts +++ b/tests/e2e/auth.setup.ts @@ -1,13 +1,12 @@ import { join } from 'path' import { expect, test as setup } from '@playwright/test' import { wpCli } from './helpers/wpCli' -import { URLS } from './helpers/constants' +import { TIMEOUTS, URLS } from './helpers/constants' const authFile = join(__dirname, '.auth/user.json') -const AUTH_SETUP_TIMEOUT_MS = 120000 setup('authenticate', async ({ page }) => { - setup.setTimeout(AUTH_SETUP_TIMEOUT_MS) + setup.setTimeout(TIMEOUTS.VERY_LONG) // Ensure a clean environment across local runs / retries. // If Safe Mode is enabled via `wp-config.php` it disables snippet execution and can @@ -60,10 +59,10 @@ setup('authenticate', async ({ page }) => { await updateBtn.first().click() } // Give the upgrade process more time to complete and the admin UI to load. - await page.waitForSelector('#wpbody-content, #adminmenu', { timeout: 120000 }) + await page.waitForSelector('#wpbody-content, #adminmenu', { timeout: TIMEOUTS.VERY_LONG }) } else { // Normal path: wait for admin UI. - await page.waitForSelector('#wpbody-content, #adminmenu', { timeout: 60000 }) + await page.waitForSelector('#wpbody-content, #adminmenu', { timeout: TIMEOUTS.LONG }) } await expect(page.locator('#adminmenu')).toBeVisible() diff --git a/tests/e2e/code-snippets-evaluation.spec.ts b/tests/e2e/code-snippets-evaluation.spec.ts index 620fa5eac..58cc47654 100644 --- a/tests/e2e/code-snippets-evaluation.spec.ts +++ b/tests/e2e/code-snippets-evaluation.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test' import { DEFAULT_E2E_SNIPPET_BASE_NAME, SnippetsTestHelper } from './helpers/SnippetsTestHelper' -import { SELECTORS, URLS } from './helpers/constants' +import { SELECTORS, TIMEOUTS, URLS } from './helpers/constants' import { wpCli } from './helpers/wpCli' import type { Page } from '@playwright/test' @@ -164,7 +164,7 @@ test.describe('Code Snippets Evaluation', () => { await page.goto(`${URLS.SNIPPETS_ADMIN}&snippets-safe-mode=1`) await page.getByRole('link', { name: 'Add New' }).click() - await expect(page).toHaveURL(/snippets-safe-mode=1/, { timeout: 5000 }) + await expect(page).toHaveURL(/snippets-safe-mode=1/, { timeout: TIMEOUTS.SHORT }) }) test('Safe mode constant disables snippets while keeping the editor accessible', async ({ page }) => { @@ -245,7 +245,7 @@ test.describe('Code Snippets Evaluation', () => { const row = page.locator(SELECTORS.SNIPPET_ROW).filter({ hasText: snippetName }).first() await row.getByRole('link', { name: 'Run Once' }).click() - await expect(page.getByRole('dialog', { name: /Run Once/ })).toBeVisible({ timeout: 5000 }) + await expect(page.getByRole('dialog', { name: /Run Once/ })).toBeVisible({ timeout: TIMEOUTS.SHORT }) }) test('PHP snippets execute in priority order', async ({ page }) => { diff --git a/tests/e2e/code-snippets-import.spec.ts b/tests/e2e/code-snippets-import.spec.ts index 0c340f7fe..2403158aa 100644 --- a/tests/e2e/code-snippets-import.spec.ts +++ b/tests/e2e/code-snippets-import.spec.ts @@ -1,7 +1,7 @@ import { readFileSync } from 'fs' import { expect, test } from '@playwright/test' import { DEFAULT_E2E_SNIPPET_BASE_NAME, SnippetsTestHelper } from './helpers/SnippetsTestHelper' -import { SELECTORS, URLS } from './helpers/constants' +import { SELECTORS, TIMEOUTS, URLS } from './helpers/constants' const importFile = (snippet: Record) => ({ name: 'code-snippets-export.json', @@ -50,7 +50,7 @@ test.describe('Code Snippets Import', () => { }) test('re-imports an exported snippet from its downloaded JSON file', async ({ page }) => { - test.setTimeout(60000) + test.setTimeout(TIMEOUTS.LONG) const snippetName = SnippetsTestHelper.makeUniqueSnippetName('Export round trip') await SnippetsTestHelper.createSnippetViaCli({ diff --git a/tests/e2e/code-snippets-list.spec.ts b/tests/e2e/code-snippets-list.spec.ts index ca371b5d9..aaa5a4636 100644 --- a/tests/e2e/code-snippets-list.spec.ts +++ b/tests/e2e/code-snippets-list.spec.ts @@ -1,14 +1,17 @@ import { readFileSync } from 'fs' import { expect, test } from '@playwright/test' import { DEFAULT_E2E_SNIPPET_BASE_NAME, SnippetsTestHelper } from './helpers/SnippetsTestHelper' -import { SELECTORS } from './helpers/constants' +import { SELECTORS, TIMEOUTS } from './helpers/constants' import type { Page, Route } from '@playwright/test' // The view preference saves through an optimistic background request, so wait // for it to persist before navigating or ending the test. const switchSnippetView = async (page: Page, view: 'Card view' | 'Table view') => { const saved = page - .waitForResponse(response => response.url().includes('/snippet-view') && 'GET' !== response.request().method(), { timeout: 5000 }) + .waitForResponse( + response => response.url().includes('/snippet-view') && 'GET' !== response.request().method(), + { timeout: TIMEOUTS.SHORT } + ) .catch(() => undefined) await page.getByRole('button', { name: view }).click() await saved @@ -31,8 +34,6 @@ const clickRowAction = async (row: ReturnType, selector test.describe('Code Snippets List Page Actions', () => { let helper: SnippetsTestHelper let snippetName: string - const EXPORT_TEST_TIMEOUT_MS = 60000 - test.beforeEach(async ({ page }) => { helper = new SnippetsTestHelper(page) snippetName = SnippetsTestHelper.makeUniqueSnippetName() @@ -112,7 +113,7 @@ test.describe('Code Snippets List Page Actions', () => { search: await page.getByRole('searchbox', { name: 'Search Snippets:' }).inputValue(), tag: await page.getByRole('combobox', { name: 'Filter snippets by tag' }).inputValue(), activeSnippetVisible: await snippetRowByName(page, snippetName).isVisible() - }), { timeout: 5000 }).toEqual({ + }), { timeout: TIMEOUTS.SHORT }).toEqual({ search: '', tag: '', activeSnippetVisible: true @@ -376,7 +377,7 @@ test.describe('Code Snippets List Page Actions', () => { if (isCreateRequest) { createRequests += 1 - await new Promise(resolve => setTimeout(resolve, 500)) + await new Promise(resolve => setTimeout(resolve, TIMEOUTS.VERY_SHORT)) } await route.continue() @@ -465,13 +466,13 @@ test.describe('Code Snippets List Page Actions', () => { // Some implementations show a confirmation modal that must be dismissed. const confirmDialog = page.locator('[role="dialog"]').filter({ hasText: /Are you sure\\?/i }) const dialogVisible = await confirmDialog - .waitFor({ state: 'visible', timeout: 2000 }) + .waitFor({ state: 'visible', timeout: TIMEOUTS.VERY_SHORT }) .then(() => true) .catch(() => false) if (dialogVisible) { await confirmDialog.locator('button:has-text("Trash"), button:has-text("Delete")').first().click() - await confirmDialog.waitFor({ state: 'hidden', timeout: 30000 }).catch(() => undefined) + await confirmDialog.waitFor({ state: 'hidden', timeout: TIMEOUTS.DEFAULT }).catch(() => undefined) } await expect(page).toHaveURL(/page=snippets/) @@ -482,11 +483,11 @@ test.describe('Code Snippets List Page Actions', () => { await expect(trashedLink).toBeVisible() await trashedLink.click() - await expect(page).toHaveURL(/status=trashed/, { timeout: 30000 }) + await expect(page).toHaveURL(/status=trashed/, { timeout: TIMEOUTS.DEFAULT }) await expect(page.locator(SELECTORS.SNIPPETS_TABLE)).toBeVisible() const trashedRow = page.locator(`${SELECTORS.SNIPPET_ROW}:has-text("${snippetName}")`).first() - await expect(trashedRow).toBeVisible({ timeout: 30000 }) + await expect(trashedRow).toBeVisible({ timeout: TIMEOUTS.DEFAULT }) await expect(trashedRow).toContainText(/Restore/i) }) @@ -528,7 +529,7 @@ test.describe('Code Snippets List Page Actions', () => { }) test('Can export snippet from list page', async ({ page }) => { - test.setTimeout(EXPORT_TEST_TIMEOUT_MS) + test.setTimeout(TIMEOUTS.LONG) const snippetRow = snippetRowByName(page, snippetName) await expect(snippetRow).toBeVisible() await snippetRow.hover() @@ -543,7 +544,7 @@ test.describe('Code Snippets List Page Actions', () => { }) test('Exports the metadata needed to re-import a snippet', async ({ page }) => { - test.setTimeout(EXPORT_TEST_TIMEOUT_MS) + test.setTimeout(TIMEOUTS.LONG) const exportName = SnippetsTestHelper.makeUniqueSnippetName('Export metadata') try { @@ -665,7 +666,7 @@ test.describe('Code Snippets List Page Actions', () => { }) test('Can export multiple snippets from bulk actions', async ({ page }) => { - test.setTimeout(EXPORT_TEST_TIMEOUT_MS) + test.setTimeout(TIMEOUTS.LONG) const secondSnippetName = SnippetsTestHelper.makeUniqueSnippetName() await helper.createAndActivateSnippet({ @@ -695,7 +696,7 @@ test.describe('Code Snippets List Page Actions', () => { }) test('Can download a single snippet from bulk actions', async ({ page }) => { - test.setTimeout(EXPORT_TEST_TIMEOUT_MS) + test.setTimeout(TIMEOUTS.LONG) await helper.filterSnippetsByName(snippetName) const snippetRow = snippetRowByName(page, snippetName) await expect(snippetRow).toBeVisible() @@ -712,7 +713,7 @@ test.describe('Code Snippets List Page Actions', () => { }) test('Can download multiple snippets from bulk actions as a zip archive', async ({ page }) => { - test.setTimeout(EXPORT_TEST_TIMEOUT_MS) + test.setTimeout(TIMEOUTS.LONG) const secondSnippetName = SnippetsTestHelper.makeUniqueSnippetName('E2E Download CSS') await SnippetsTestHelper.createSnippetViaCli({ @@ -743,7 +744,7 @@ test.describe('Code Snippets List Page Actions', () => { }) test('Bulk download stays scoped to the current page selection', async ({ page }) => { - test.setTimeout(EXPORT_TEST_TIMEOUT_MS) + test.setTimeout(TIMEOUTS.LONG) const bulkScopeBaseName = 'E2E Bulk Scope' const firstScopedSnippetName = SnippetsTestHelper.makeUniqueSnippetName(bulkScopeBaseName) const secondScopedSnippetName = SnippetsTestHelper.makeUniqueSnippetName(bulkScopeBaseName) @@ -788,7 +789,7 @@ test.describe('Code Snippets List Page Actions', () => { }) test('Bulk export stays scoped to the current page selection', async ({ page }) => { - test.setTimeout(EXPORT_TEST_TIMEOUT_MS) + test.setTimeout(TIMEOUTS.LONG) const bulkScopeBaseName = 'E2E Bulk Scope Export' const firstScopedSnippetName = SnippetsTestHelper.makeUniqueSnippetName(bulkScopeBaseName) const secondScopedSnippetName = SnippetsTestHelper.makeUniqueSnippetName(bulkScopeBaseName) diff --git a/tests/e2e/code-snippets-paste-tags.spec.ts b/tests/e2e/code-snippets-paste-tags.spec.ts index 7fa4d869d..eed3abcc5 100644 --- a/tests/e2e/code-snippets-paste-tags.spec.ts +++ b/tests/e2e/code-snippets-paste-tags.spec.ts @@ -1,5 +1,6 @@ import { expect, test } from '@playwright/test' import { SnippetsTestHelper } from './helpers/SnippetsTestHelper' +import { TIMEOUTS } from './helpers/constants' import type { Page } from '@playwright/test' interface CodeMirrorHost { @@ -28,7 +29,7 @@ const enterCode = async (page: Page, code: string, origin: 'paste' | '+input'): cm.replaceRange(text, { line: 0, ch: 0 }, { line: 0, ch: 0 }, changeOrigin) }, [code, origin]) - await page.waitForTimeout(400) + await page.waitForTimeout(TIMEOUTS.VERY_SHORT) } const editorValue = (page: Page): Promise => diff --git a/tests/e2e/code-snippets-quicknav-admin-bar.spec.ts b/tests/e2e/code-snippets-quicknav-admin-bar.spec.ts index ff6dc39db..e1454b88b 100644 --- a/tests/e2e/code-snippets-quicknav-admin-bar.spec.ts +++ b/tests/e2e/code-snippets-quicknav-admin-bar.spec.ts @@ -1,12 +1,10 @@ import { expect, test } from '@playwright/test' import { SnippetsTestHelper } from './helpers/SnippetsTestHelper' import { wpCli } from './helpers/wpCli' -import { URLS } from './helpers/constants' +import { TIMEOUTS, URLS } from './helpers/constants' const QUICKNAV_PREFIX = 'E2E QuickNav' const QUICKNAV_PER_PAGE = 2 -const QUICKNAV_TEST_TIMEOUT_MS = 180000 - test.describe('Admin Bar Snippets QuickNav', () => { let activeA: string let activeB: string @@ -16,7 +14,7 @@ test.describe('Admin Bar Snippets QuickNav', () => { let inactiveA: string test.beforeAll(async () => { - test.setTimeout(QUICKNAV_TEST_TIMEOUT_MS) + test.setTimeout(TIMEOUTS.EXTRA_LONG) await SnippetsTestHelper.setAdminBarQuickNavSettings({ enabled: true, perPage: QUICKNAV_PER_PAGE }) await SnippetsTestHelper.cleanupSnippetsByPrefix(QUICKNAV_PREFIX) @@ -41,7 +39,7 @@ test.describe('Admin Bar Snippets QuickNav', () => { }) test('Menu structure and pagination works', async ({ page }) => { - test.setTimeout(QUICKNAV_TEST_TIMEOUT_MS) + test.setTimeout(TIMEOUTS.EXTRA_LONG) const helper = new SnippetsTestHelper(page) await helper.navigateToSnippetsAdmin() @@ -100,7 +98,7 @@ test.describe('Admin Bar Snippets QuickNav', () => { }) test('Manage submenu contains status quick links', async ({ page }) => { - test.setTimeout(QUICKNAV_TEST_TIMEOUT_MS) + test.setTimeout(TIMEOUTS.EXTRA_LONG) const helper = new SnippetsTestHelper(page) await helper.navigateToSnippetsAdmin() @@ -118,7 +116,7 @@ test.describe('Admin Bar Snippets QuickNav', () => { }) test('QuickNav menu can be disabled via setting', async ({ page }) => { - test.setTimeout(QUICKNAV_TEST_TIMEOUT_MS) + test.setTimeout(TIMEOUTS.EXTRA_LONG) await SnippetsTestHelper.setAdminBarQuickNavSettings({ enabled: false, perPage: QUICKNAV_PER_PAGE }) @@ -133,7 +131,7 @@ test.describe('Admin Bar Snippets QuickNav', () => { }) test('Safe Mode indicator appears only when Safe Mode is active', async ({ page }) => { - test.setTimeout(QUICKNAV_TEST_TIMEOUT_MS) + test.setTimeout(TIMEOUTS.EXTRA_LONG) const safeModeMuPluginPath = 'wp-content/mu-plugins/code-snippets-e2e-safe-mode.php' const removeMuPlugin = async () => { @@ -161,7 +159,7 @@ test.describe('Admin Bar Snippets QuickNav', () => { await page.goto(URLS.SNIPPETS_ADMIN) const safeModeNode = page.locator('#wp-admin-bar-code-snippets-safe-mode') - await expect(safeModeNode).toBeVisible({ timeout: 30000 }) + await expect(safeModeNode).toBeVisible({ timeout: TIMEOUTS.DEFAULT }) const safeModeLink = safeModeNode.locator('a').first() await expect(safeModeLink).toHaveAttribute('href', 'https://snipco.de/safe-mode') diff --git a/tests/e2e/flat-files.setup.ts b/tests/e2e/flat-files.setup.ts index 39de2119b..fcc6548de 100644 --- a/tests/e2e/flat-files.setup.ts +++ b/tests/e2e/flat-files.setup.ts @@ -1,4 +1,5 @@ import { expect, test as setup } from '@playwright/test' +import { TIMEOUTS } from './helpers/constants' setup('enable flat files', async ({ page }) => { const isMultisite = 'true' === process.env.WP_E2E_MULTISITE_MODE || '1' === process.env.WP_E2E_MULTISITE_MODE @@ -22,12 +23,12 @@ setup('enable flat files', async ({ page }) => { // Await page.click('input[type="submit"][name="submit"]') - // await page.waitForSelector('.notice-success', { timeout: 10000 }) + // await page.waitForSelector('.notice-success', { timeout: TIMEOUTS.MEDIUM }) // await expect(page.locator('.notice-success')).toContainText('Settings saved') const saveButton = page.getByRole('button', { name: 'Save Changes' }) await Promise.all([ - page.waitForURL(/settings-updated=true/, { timeout: 10000 }), + page.waitForURL(/settings-updated=true/, { timeout: TIMEOUTS.MEDIUM }), saveButton.click() ]) diff --git a/tests/e2e/helpers/SnippetsTestHelper.ts b/tests/e2e/helpers/SnippetsTestHelper.ts index f5af800a3..8028d90e6 100644 --- a/tests/e2e/helpers/SnippetsTestHelper.ts +++ b/tests/e2e/helpers/SnippetsTestHelper.ts @@ -13,7 +13,6 @@ const RANDOM_SLICE_END = 7 const CLICK_RETRIES = 3 const SAVE_CONFIRM_RETRIES = 3 const AT_LEAST_ONE = 1 -const SAVE_SETTLE_TIMEOUT_MS = 10000 const getErrorMessage = (error: unknown): string => { if (error instanceof Error) { @@ -359,7 +358,7 @@ export class SnippetsTestHelper { await this.clickButton(name) const settled = await this.page.locator(SELECTORS.SAVE_SETTLED_NOTICE).first() - .waitFor({ state: 'visible', timeout: SAVE_SETTLE_TIMEOUT_MS }) + .waitFor({ state: 'visible', timeout: TIMEOUTS.MEDIUM }) .then(() => true) .catch(() => false) diff --git a/tests/e2e/helpers/constants.ts b/tests/e2e/helpers/constants.ts index b52bd1ced..457a4fbb3 100644 --- a/tests/e2e/helpers/constants.ts +++ b/tests/e2e/helpers/constants.ts @@ -23,8 +23,13 @@ export const SELECTORS = { } export const TIMEOUTS = { + VERY_SHORT: 2000, + SHORT: 5000, + MEDIUM: 10000, DEFAULT: 30000, - SHORT: 5000 + LONG: 60000, + VERY_LONG: 120000, + EXTRA_LONG: 180000 } export const URLS = { diff --git a/tests/e2e/rtl.setup.ts b/tests/e2e/rtl.setup.ts index 25095d636..f64c7bfe3 100644 --- a/tests/e2e/rtl.setup.ts +++ b/tests/e2e/rtl.setup.ts @@ -2,17 +2,15 @@ import { writeFileSync } from 'fs' import { expect, test as setup } from '@playwright/test' import { RTL_LOCALE, RTL_USER, rtlAuthFile, rtlCreatedMarker } from './helpers/rtlUser' import { wpCli } from './helpers/wpCli' -import { URLS } from './helpers/constants' +import { TIMEOUTS, URLS } from './helpers/constants' // The RTL specs sign in as a user of their own whose locale is right-to-left, // so the rest of the suite, which signs in as the usual admin, never sees the // site mirrored, whatever order the projects run in. The language pack is // fetched from wordpress.org when missing; if that is impossible (offline), // the specs notice the page is still left-to-right and skip themselves. -const SETUP_TIMEOUT_MS = 180000 - setup('sign in as a right-to-left user', async ({ page }) => { - setup.setTimeout(SETUP_TIMEOUT_MS) + setup.setTimeout(TIMEOUTS.EXTRA_LONG) try { await wpCli(['language', 'core', 'install', RTL_LOCALE]) @@ -41,7 +39,7 @@ setup('sign in as a right-to-left user', async ({ page }) => { page.waitForLoadState('domcontentloaded'), page.click('#wp-submit') ]) - await page.waitForSelector('#wpbody-content, #adminmenu', { timeout: 60000 }) + await page.waitForSelector('#wpbody-content, #adminmenu', { timeout: TIMEOUTS.LONG }) await expect(page.locator('#adminmenu')).toBeVisible() const dir = await page.evaluate(() => document.documentElement.getAttribute('dir')) diff --git a/tests/e2e/settings-tabs.spec.ts b/tests/e2e/settings-tabs.spec.ts index d51a9e60a..85af641fd 100644 --- a/tests/e2e/settings-tabs.spec.ts +++ b/tests/e2e/settings-tabs.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test' import { wpCli } from './helpers/wpCli' -import { URLS } from './helpers/constants' +import { TIMEOUTS, URLS } from './helpers/constants' const TABS = '#settings-sections-tabs' const SETTINGS_SECTIONS = ['editing', 'running', 'insights', 'library', 'interface', 'advanced'] @@ -58,7 +58,7 @@ test.describe('Settings tabs', () => { await page.goto(`${URLS.SETTINGS_ADMIN}§ion=editing`) for (const section of SETTINGS_SECTIONS) { - await expect(page.locator(`${TABS} [data-section="${section}"]`)).toBeVisible({ timeout: 5000 }) + await expect(page.locator(`${TABS} [data-section="${section}"]`)).toBeVisible({ timeout: TIMEOUTS.SHORT }) } }) @@ -103,8 +103,8 @@ test.describe('Settings tabs', () => { test('shows Insights controls for performance tracking and security scanning', async ({ page }) => { await page.goto(`${URLS.SETTINGS_ADMIN}§ion=insights`) - await expect(page.getByRole('checkbox', { name: /Track Snippet Performance/ })).toBeVisible({ timeout: 5000 }) - await expect(page.getByRole('checkbox', { name: /Scan Snippets for Security Issues/ })).toBeVisible({ timeout: 5000 }) + await expect(page.getByRole('checkbox', { name: /Track Snippet Performance/ })).toBeVisible({ timeout: TIMEOUTS.SHORT }) + await expect(page.getByRole('checkbox', { name: /Scan Snippets for Security Issues/ })).toBeVisible({ timeout: TIMEOUTS.SHORT }) }) test('resets settings to their defaults', async ({ page }) => { @@ -118,7 +118,7 @@ test.describe('Settings tabs', () => { await expect(page.locator('#setting-error-settings_reset')).toContainText( 'All settings have been reset to their defaults.', - { timeout: 5000 } + { timeout: TIMEOUTS.SHORT } ) await page.goto(URLS.ADD_SNIPPET_ADMIN) await expect(page.getByRole('button', { name: 'Save and Activate' })).toHaveClass(/button-primary/) @@ -146,7 +146,7 @@ test.describe('Settings tabs', () => { await page.reload() await expect( page.getByRole('checkbox', { name: /also delete all snippets and plugin settings/ }) - ).toBeChecked({ timeout: 5000 }) + ).toBeChecked({ timeout: TIMEOUTS.SHORT }) } finally { await wpCli([ 'eval', From f04c4eaf6a840dfe87542b4266a97b2bc9ba0f35 Mon Sep 17 00:00:00 2001 From: Rami Yushuvaev Date: Wed, 16 Sep 2026 15:06:12 +0300 Subject: [PATCH 06/19] feat: add new tests --- .../code-snippets-community-featured.spec.ts | 70 +++++++++ tests/e2e/code-snippets-edit.spec.ts | 113 +++++++++++++ tests/e2e/code-snippets-evaluation.spec.ts | 65 ++++++++ tests/e2e/code-snippets-import.spec.ts | 63 ++++++++ tests/e2e/code-snippets-list.spec.ts | 21 +++ tests/e2e/code-snippets-migration.spec.ts | 148 ++++++++++++++++++ tests/e2e/contextual-help.spec.ts | 17 +- tests/e2e/rest-api-auth.spec.ts | 3 +- tests/e2e/rtl-layout.spec.ts | 17 ++ tests/e2e/welcome.spec.ts | 42 +++++ 10 files changed, 556 insertions(+), 3 deletions(-) create mode 100644 tests/e2e/code-snippets-migration.spec.ts diff --git a/tests/e2e/code-snippets-community-featured.spec.ts b/tests/e2e/code-snippets-community-featured.spec.ts index 4282ed5db..849b88c31 100644 --- a/tests/e2e/code-snippets-community-featured.spec.ts +++ b/tests/e2e/code-snippets-community-featured.spec.ts @@ -37,6 +37,11 @@ const isSnippetDownloadRequest = (url: URL): boolean => url.pathname.includes('/cloud/snippets/501/download') || true === url.searchParams.get('rest_route')?.includes('/cloud/snippets/501/download') +const isSearchRequest = (url: URL): boolean => + (url.pathname.includes('/cloud/snippets') || true === url.searchParams.get('rest_route')?.includes('/cloud/snippets')) && + !isFeaturedRequest(url) && !url.pathname.includes('/download') && + !url.searchParams.get('rest_route')?.includes('/download') + const makeCloudSnippet = (id: number, name: string, localId: number | null = null) => ({ id, slug: `mock-cloud-snippet-${id}`, @@ -169,6 +174,71 @@ test.describe('Community Cloud Featured Snippets', () => { .toContainText('An error occurred while fetching search results. Please try again.') }) + test('shows a clear empty state when a keyword search has no results', async ({ page }) => { + await page.route(isSearchRequest, route => route.fulfill({ + contentType: 'application/json', + body: JSON.stringify(makeFeaturedResponse([])) + })) + await openCommunityCloud(page) + + await page.getByRole('searchbox', { name: 'Search query' }).fill('no matching snippet') + await page.getByRole('button', { name: 'Search Cloud Library' }).click() + await expect(page.locator('.no-results')) + .toHaveText('No snippets could be found with that search term. Please try again.') + }) + + test('shows a library-specific empty state for a codevault search', async ({ page }) => { + await page.route(isSearchRequest, route => route.fulfill({ + contentType: 'application/json', + body: JSON.stringify(makeFeaturedResponse([])) + })) + await openCommunityCloud(page) + + await page.getByRole('combobox', { name: 'Search method' }).selectOption('codevault') + await page.getByRole('searchbox', { name: 'Search query' }).fill('missing library') + await page.getByRole('button', { name: 'Search Cloud Library' }).click() + await expect(page.locator('.no-results')) + .toHaveText('Could not find a codevault with that name. Please try again.') + }) + + test('shows every available cloud filter', async ({ page }) => { + await page.route(isFeaturedRequest, route => route.fulfill({ + contentType: 'application/json', + body: JSON.stringify({ + ...makeFeaturedResponse(), + available_filters: { + categories: [{ id: 12, name: 'Utilities' }], + types: [{ id: 1, name: 'PHP' }], + statuses: [{ id: 4, name: 'Verified' }] + } + }) + })) + await openCommunityCloud(page) + + await expect(page.getByRole('combobox', { name: 'Snippet Category' })).toContainText('Utilities') + await expect(page.getByRole('combobox', { name: 'Snippet Type' })).toContainText('PHP') + await expect(page.getByRole('combobox', { name: 'Snippet Status' })).toContainText('Verified') + }) + + test('includes a chosen category in the next cloud request', async ({ page }) => { + await page.route(isFeaturedRequest, route => route.fulfill({ + contentType: 'application/json', + body: JSON.stringify({ + ...makeFeaturedResponse(), + available_filters: { categories: [{ id: 12, name: 'Utilities' }] } + }) + })) + await openCommunityCloud(page) + await expect(page.getByRole('combobox', { name: 'Snippet Category' })).toBeVisible() + + const filteredRequest = page.waitForRequest(request => { + const url = new URL(request.url()) + return isFeaturedRequest(url) && '12' === url.searchParams.get('category') + }) + await page.getByRole('combobox', { name: 'Snippet Category' }).selectOption('12') + await filteredRequest + }) + test('Shares download state between the card and its preview', async ({ page }) => { let releaseDownload: VoidFunction = () => undefined const downloadPending = new Promise(resolve => { diff --git a/tests/e2e/code-snippets-edit.spec.ts b/tests/e2e/code-snippets-edit.spec.ts index 5e9d465cd..c682c2934 100644 --- a/tests/e2e/code-snippets-edit.spec.ts +++ b/tests/e2e/code-snippets-edit.spec.ts @@ -23,6 +23,119 @@ test.describe('Code Snippets Admin', () => { }) }) + test('Saves a description entered in the visual editor', async ({ page }) => { + const snippetName = SnippetsTestHelper.makeUniqueSnippetName('Visual description') + const description = 'Saved from the visual description editor.' + + try { + await helper.clickAddNewSnippet() + await helper.fillSnippetForm({ + name: snippetName, + code: "add_filter('show_admin_bar', '__return_false');" + }) + + const visualEditor = page.frameLocator('#snippet_description_ifr').locator('body') + await expect(visualEditor).toBeVisible() + await visualEditor.fill(description) + await helper.saveSnippet() + await helper.expectSuccessMessage(MESSAGES.SNIPPET_CREATED) + + await page.reload() + await expect(page.frameLocator('#snippet_description_ifr').locator('body')).toHaveText(description) + } finally { + await helper.cleanupSnippet(snippetName) + } + }) + + test('Preserves a description when switching between visual and text tabs', async ({ page }) => { + const description = 'Description preserved between editor tabs.' + + await helper.clickAddNewSnippet() + const visualEditor = page.frameLocator('#snippet_description_ifr').locator('body') + await expect(visualEditor).toBeVisible() + await visualEditor.fill(description) + + await page.getByRole('button', { name: 'Code', exact: true }).click() + await expect(page.locator('#snippet_description')).toHaveValue(new RegExp(description)) + + await page.getByRole('button', { name: 'Visual', exact: true }).click() + await expect(visualEditor).toHaveText(description) + }) + + test('Adds and removes tags before saving a snippet', async ({ page }) => { + const snippetName = SnippetsTestHelper.makeUniqueSnippetName('Tagged snippet') + const removedTag = 'remove-me' + const savedTag = 'saved-tag' + + try { + await helper.clickAddNewSnippet() + await helper.fillSnippetForm({ + name: snippetName, + code: "add_filter('show_admin_bar', '__return_false');" + }) + + const tags = page.getByRole('combobox', { name: 'Snippet Tags' }) + for (const tag of [removedTag, savedTag]) { + await tags.fill(tag) + await tags.press('Enter') + await expect(page.locator('.components-form-token-field__token').filter({ hasText: tag })).toBeVisible() + } + + await page.locator('.components-form-token-field__token') + .filter({ hasText: removedTag }) + .locator('.components-form-token-field__remove-token') + .click() + await expect(page.locator('.components-form-token-field__token').filter({ hasText: removedTag })).toHaveCount(0) + + await helper.saveSnippet() + await helper.expectSuccessMessage(MESSAGES.SNIPPET_CREATED) + await page.reload() + await expect(page.locator('.components-form-token-field__token').filter({ hasText: savedTag })).toBeVisible() + } finally { + await helper.cleanupSnippet(snippetName) + } + }) + + test('Splits comma-separated tags into individual tokens', async ({ page }) => { + const tags = ['first-pasted-tag', 'second-pasted-tag'] + + await helper.clickAddNewSnippet() + const tagInput = page.getByRole('combobox', { name: 'Snippet Tags' }) + await tagInput.fill(tags.join(',')) + await tagInput.press('Enter') + + for (const tag of tags) { + await expect(page.locator('.components-form-token-field__token').filter({ hasText: tag })).toBeVisible() + } + }) + + test('Copies a content snippet shortcode from the sidebar', async ({ page, context }) => { + const snippetName = SnippetsTestHelper.makeUniqueSnippetName('Shortcode snippet') + + try { + const snippetId = await SnippetsTestHelper.createSnippetViaCli({ + name: snippetName, + active: false, + type: 'html', + scope: 'content' + }) + const shortcode = `[code_snippet id=${snippetId} format name="${snippetName}"]` + + await helper.navigateToSnippetsAdmin() + await helper.openSnippet(snippetName) + await context.grantPermissions(['clipboard-read', 'clipboard-write'], { origin: new URL(page.url()).origin }) + + await page.getByRole('button', { name: 'See options' }).click() + const dialog = page.getByRole('dialog', { name: 'Embed Snippet with Shortcode' }) + await expect(dialog.locator('.shortcode-tag')).toHaveText(shortcode) + await dialog.getByRole('button', { name: 'Copy' }).click() + await expect(dialog.getByRole('status')).toHaveText('Copied to clipboard.') + expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(shortcode) + } finally { + await helper.cleanupSnippet(snippetName) + } + }) + test('Expands and collapses the code editor', async ({ page }) => { await helper.clickAddNewSnippet() const form = page.locator('form.snippet-form') diff --git a/tests/e2e/code-snippets-evaluation.spec.ts b/tests/e2e/code-snippets-evaluation.spec.ts index 58cc47654..4d40af257 100644 --- a/tests/e2e/code-snippets-evaluation.spec.ts +++ b/tests/e2e/code-snippets-evaluation.spec.ts @@ -208,6 +208,27 @@ test.describe('Code Snippets Evaluation', () => { } }) + test('Safe mode constant warns that snippets will not execute', async ({ page }) => { + const safeModeMuPluginPath = 'wp-content/mu-plugins/code-snippets-e2e-safe-mode-notice.php' + const removeMuPlugin = () => + wpCli(['eval', `@unlink( ABSPATH . ${JSON.stringify(safeModeMuPluginPath)} );`]) + + await removeMuPlugin() + + try { + await wpCli(['eval', ` + $path = ABSPATH . ${JSON.stringify(safeModeMuPluginPath)}; + wp_mkdir_p( dirname( $path ) ); + file_put_contents( $path, " { const markerKey = `code_snippets_e2e_single_use_${Date.now()}` @@ -301,6 +322,50 @@ test.describe('Code Snippets Evaluation', () => { ).toBe('loaded') }) + test('JavaScript snippets load in the site header', async ({ page }) => { + if (!await SnippetsTestHelper.isProLicensed()) { + test.skip(true, 'JavaScript snippets require an active Pro license.') + } + + const marker = `e2e-header-script-${Date.now()}` + + await SnippetsTestHelper.createSnippetViaCli({ + name: snippetName, + active: true, + type: 'js', + scope: 'site-head-js', + code: `document.documentElement.dataset.e2eHeaderScript = '${marker}';` + }) + + await helper.navigateToFrontend() + await expect(page.locator('html')).toHaveAttribute('data-e2e-header-script', marker) + expect(await page.locator('head script').evaluateAll((scripts, scriptMarker) => + scripts.some(script => script.textContent?.includes(scriptMarker)), marker + )).toBe(true) + }) + + test('JavaScript snippets load in the site footer', async ({ page }) => { + if (!await SnippetsTestHelper.isProLicensed()) { + test.skip(true, 'JavaScript snippets require an active Pro license.') + } + + const marker = `e2e-footer-script-${Date.now()}` + + await SnippetsTestHelper.createSnippetViaCli({ + name: snippetName, + active: true, + type: 'js', + scope: 'site-footer-js', + code: `document.body.dataset.e2eFooterScript = '${marker}';` + }) + + await helper.navigateToFrontend() + await expect(page.locator('body')).toHaveAttribute('data-e2e-footer-script', marker) + expect(await page.locator('body script').evaluateAll((scripts, scriptMarker) => + scripts.some(script => script.textContent?.includes(scriptMarker)), marker + )).toBe(true) + }) + test('HTML snippet is evaluating correctly in footer', async () => { await helper.createAndActivateSnippet({ name: snippetName, diff --git a/tests/e2e/code-snippets-import.spec.ts b/tests/e2e/code-snippets-import.spec.ts index 2403158aa..27990b5ea 100644 --- a/tests/e2e/code-snippets-import.spec.ts +++ b/tests/e2e/code-snippets-import.spec.ts @@ -106,6 +106,69 @@ test.describe('Code Snippets Import', () => { await expect(page.locator('.import-result-message')).toContainText('No valid snippets found') }) + test('removes a selected file before uploading the remaining file', async ({ page }) => { + const keptName = SnippetsTestHelper.makeUniqueSnippetName('Kept import') + const removedName = SnippetsTestHelper.makeUniqueSnippetName('Removed import') + + await page.goto(URLS.IMPORT_ADMIN) + await page.getByLabel('Select files to import').setInputFiles([ + { ...importFile({ id: 1, name: keptName, code: '// kept', scope: 'global' }), name: 'keep.json' }, + { ...importFile({ id: 2, name: removedName, code: '// removed', scope: 'global' }), name: 'remove.json' } + ]) + await expect(page.getByRole('heading', { name: 'Selected files: (2)' })).toBeVisible() + + await page.getByRole('button', { name: 'Remove file remove.json' }).click() + await expect(page.getByRole('heading', { name: 'Selected files: (1)' })).toBeVisible() + await page.getByRole('button', { name: 'Upload files' }).click() + + await expect(page.getByRole('heading', { name: 'Available snippets (1)' })).toBeVisible() + await expect(page.getByRole('row', { name: new RegExp(keptName) })).toBeVisible() + await expect(page.getByRole('row', { name: new RegExp(removedName) })).toHaveCount(0) + }) + + test('selects and deselects every parsed snippet before importing', async ({ page }) => { + const snippets = [ + { id: 1, name: SnippetsTestHelper.makeUniqueSnippetName('Select import'), code: '// first', scope: 'global' }, + { id: 2, name: SnippetsTestHelper.makeUniqueSnippetName('Select import'), code: '// second', scope: 'global' } + ] + + await page.goto(URLS.IMPORT_ADMIN) + await page.getByLabel('Select files to import').setInputFiles({ + name: 'multiple-snippets.json', + mimeType: 'application/json', + buffer: Buffer.from(JSON.stringify({ snippets })) + }) + await page.getByRole('button', { name: 'Upload files' }).click() + + const selectAll = page.getByRole('button', { name: 'Select All' }).first() + await selectAll.click() + await expect(page.getByRole('checkbox', { name: 'Select all snippets' })).toBeChecked() + await expect(page.getByRole('button', { name: 'Deselect All' }).first()).toBeVisible() + await page.getByRole('button', { name: 'Deselect All' }).first().click() + await expect(page.getByRole('checkbox', { name: 'Select all snippets' })).not.toBeChecked() + await expect(page.getByRole('button', { name: 'Import Selected (0)' }).first()).toBeDisabled() + }) + + test('imports snippets from multiple selected files', async ({ page }) => { + const firstName = SnippetsTestHelper.makeUniqueSnippetName('First file import') + const secondName = SnippetsTestHelper.makeUniqueSnippetName('Second file import') + + await page.goto(URLS.IMPORT_ADMIN) + await page.getByLabel('Select files to import').setInputFiles([ + { ...importFile({ id: 1, name: firstName, code: '// first', scope: 'global' }), name: 'first.json' }, + { ...importFile({ id: 2, name: secondName, code: '// second', scope: 'global' }), name: 'second.json' } + ]) + await page.getByRole('button', { name: 'Upload files' }).click() + await expect(page.getByRole('heading', { name: 'Available snippets (2)' })).toBeVisible() + await page.getByRole('button', { name: 'Select All' }).first().click() + await page.getByRole('button', { name: 'Import Selected (2)' }).first().click() + + await expect(page.locator('.import-result-message')).toContainText('Successfully imported 2 snippets.') + await page.goto(URLS.SNIPPETS_ADMIN) + await expect(page.locator(SELECTORS.SNIPPET_ROW).filter({ hasText: firstName })).toBeVisible() + await expect(page.locator(SELECTORS.SNIPPET_ROW).filter({ hasText: secondName })).toBeVisible() + }) + test('skips and replaces duplicate snippets according to the selected policy', async ({ page }) => { const snippetName = SnippetsTestHelper.makeUniqueSnippetName('Imported duplicate') const duplicate = { diff --git a/tests/e2e/code-snippets-list.spec.ts b/tests/e2e/code-snippets-list.spec.ts index aaa5a4636..f2f9625df 100644 --- a/tests/e2e/code-snippets-list.spec.ts +++ b/tests/e2e/code-snippets-list.spec.ts @@ -212,6 +212,27 @@ test.describe('Code Snippets List Page Actions', () => { } }) + test('Clears the Recently Active list without deleting snippets', async ({ page }) => { + const row = snippetRowByName(page, snippetName) + + await row.getByRole('switch').click({ force: true }) + await expect(row.getByRole('switch')).not.toBeChecked() + await page.locator('.subsubsub .recently_active a').click() + await expect(row).toBeVisible() + + const clearList = page.waitForResponse(response => + 'POST' === response.request().method() && + 'DELETE' === response.request().headers()['x-http-method-override'] && + response.url().includes('/recently-active') + ) + await page.getByRole('button', { name: 'Clear List' }).click() + expect((await clearList).status()).toBe(200) + await expect(row).toHaveCount(0) + + await page.locator('.subsubsub .all a').click() + await expect(snippetRowByName(page, snippetName)).toBeVisible() + }) + test('Card action popovers let keyboard focus continue through the document', async ({ page }) => { await switchSnippetView(page, 'Card view') diff --git a/tests/e2e/code-snippets-migration.spec.ts b/tests/e2e/code-snippets-migration.spec.ts new file mode 100644 index 000000000..0a22dde39 --- /dev/null +++ b/tests/e2e/code-snippets-migration.spec.ts @@ -0,0 +1,148 @@ +import { expect, test } from '@playwright/test' +import { URLS } from './helpers/constants' + +const IMPORTER = { + name: 'header-footer-code-manager', + title: 'Header Footer Code Manager', + is_active: true +} + +const SNIPPETS = [ + { id: 7, title: 'Legacy Header Code', table_data: { id: 7, title: 'Legacy Header Code' } }, + { id: 8, title: 'Legacy Footer Code', table_data: { id: 8, title: 'Legacy Footer Code' } } +] + +const isMigrationRequest = (url: URL): boolean => + url.pathname.includes('/import/plugins') || true === url.searchParams.get('rest_route')?.includes('/import/plugins') + +const migrationRoute = (url: URL) => url.searchParams.get('rest_route') ?? url.pathname + +const isSelectedImporterRoute = (url: URL) => migrationRoute(url).endsWith(`/import/plugins/${IMPORTER.name}`) + +test.describe('Code Snippets Migration', () => { + test('opens the migration tab from its URL state', async ({ page }) => { + await page.route(isMigrationRequest, route => route.fulfill({ json: {} })) + await page.goto(`${URLS.IMPORT_ADMIN}&tab=migrate`) + + await expect(page.getByRole('heading', { level: 1, name: 'Import: Migrate from other plugins' })).toBeVisible() + await expect(page.getByRole('combobox', { name: 'Select plugin' })).toBeVisible() + }) + + test('lists available importers', async ({ page }) => { + await page.route(isMigrationRequest, route => route.fulfill({ json: { [IMPORTER.name]: IMPORTER } })) + await page.goto(`${URLS.IMPORT_ADMIN}&tab=migrate`) + + await expect(page.getByRole('option', { name: IMPORTER.title })).toBeEnabled() + }) + + test('identifies inactive importers without allowing their selection', async ({ page }) => { + const inactiveImporter = { ...IMPORTER, name: 'insert-headers-footers', title: 'Insert Headers and Footers', is_active: false } + await page.route(isMigrationRequest, route => route.fulfill({ json: { [inactiveImporter.name]: inactiveImporter } })) + await page.goto(`${URLS.IMPORT_ADMIN}&tab=migrate`) + + await expect(page.getByRole('option', { name: `${inactiveImporter.title} (Inactive)` })).toBeDisabled() + }) + + test('loads an importer and records it in the URL', async ({ page }) => { + await page.route(isMigrationRequest, route => { + const url = new URL(route.request().url()) + return route.fulfill({ json: isSelectedImporterRoute(url) ? SNIPPETS : { [IMPORTER.name]: IMPORTER } }) + }) + await page.goto(`${URLS.IMPORT_ADMIN}&tab=migrate`) + + await page.getByRole('combobox', { name: 'Select plugin' }).selectOption(IMPORTER.name) + await expect(page.getByRole('heading', { name: 'Available snippets (2)' })).toBeVisible() + expect(new URL(page.url()).searchParams.get('from')).toBe(IMPORTER.name) + }) + + test('shows an empty state when an importer has no snippets', async ({ page }) => { + await page.route(isMigrationRequest, route => { + const url = new URL(route.request().url()) + return route.fulfill({ json: isSelectedImporterRoute(url) ? [] : { [IMPORTER.name]: IMPORTER } }) + }) + await page.goto(`${URLS.IMPORT_ADMIN}&tab=migrate`) + + await page.getByRole('combobox', { name: 'Select plugin' }).selectOption(IMPORTER.name) + await expect(page.getByRole('heading', { name: 'No snippets found' })).toBeVisible() + }) + + test('reports an importer fetch error', async ({ page }) => { + await page.route(isMigrationRequest, route => { + const url = new URL(route.request().url()) + return route.fulfill(isSelectedImporterRoute(url) + ? { status: 500, json: { message: 'Importer unavailable' } } + : { json: { [IMPORTER.name]: IMPORTER } }) + }) + await page.goto(`${URLS.IMPORT_ADMIN}&tab=migrate`) + + await page.getByRole('combobox', { name: 'Select plugin' }).selectOption(IMPORTER.name) + await expect(page.getByRole('heading', { name: 'Error loading snippets' })).toBeVisible() + }) + + test('shows automatic tag controls for migrated snippets', async ({ page }) => { + await page.route(isMigrationRequest, route => { + const url = new URL(route.request().url()) + return route.fulfill({ json: isSelectedImporterRoute(url) ? SNIPPETS : { [IMPORTER.name]: IMPORTER } }) + }) + await page.goto(`${URLS.IMPORT_ADMIN}&tab=migrate`) + + await page.getByRole('combobox', { name: 'Select plugin' }).selectOption(IMPORTER.name) + await page.getByRole('checkbox', { name: 'Add tag automatically' }).check() + await expect(page.getByRole('textbox', { name: 'Tag to add to imported snippets' })).toHaveValue(`imported-${IMPORTER.name}`) + }) + + test('selects all migrated snippets before importing', async ({ page }) => { + await page.route(isMigrationRequest, route => { + const url = new URL(route.request().url()) + return route.fulfill({ json: isSelectedImporterRoute(url) ? SNIPPETS : { [IMPORTER.name]: IMPORTER } }) + }) + await page.goto(`${URLS.IMPORT_ADMIN}&tab=migrate`) + + await page.getByRole('combobox', { name: 'Select plugin' }).selectOption(IMPORTER.name) + await page.getByRole('button', { name: 'Select All' }).first().click() + await expect(page.getByRole('button', { name: 'Import Selected (2)' }).first()).toBeEnabled() + await page.getByRole('button', { name: 'Deselect All' }).first().click() + await expect(page.getByRole('button', { name: 'Import Selected (0)' }).first()).toBeDisabled() + }) + + test('reports an unsuccessful migration', async ({ page }) => { + await page.route(isMigrationRequest, route => { + const url = new URL(route.request().url()) + const path = migrationRoute(url) + return route.fulfill({ + json: path.endsWith('/import') ? { imported: [] } : isSelectedImporterRoute(url) ? SNIPPETS : { [IMPORTER.name]: IMPORTER } + }) + }) + await page.goto(`${URLS.IMPORT_ADMIN}&tab=migrate`) + + await page.getByRole('combobox', { name: 'Select plugin' }).selectOption(IMPORTER.name) + await page.getByRole('checkbox', { name: 'Select Legacy Header Code' }).check() + await page.getByRole('button', { name: 'Import Selected (1)' }).first().click() + await expect(page.getByRole('heading', { name: 'Error importing snippets' })).toBeVisible() + }) + + test('imports selected snippets with an automatic tag', async ({ page }) => { + let importRequest: unknown + await page.route(isMigrationRequest, route => { + const url = new URL(route.request().url()) + const path = migrationRoute(url) + + if (path.endsWith('/import')) { + importRequest = route.request().postDataJSON() + return route.fulfill({ json: { imported: [7] } }) + } + + return route.fulfill({ json: isSelectedImporterRoute(url) ? SNIPPETS : { [IMPORTER.name]: IMPORTER } }) + }) + await page.goto(`${URLS.IMPORT_ADMIN}&tab=migrate`) + + await page.getByRole('combobox', { name: 'Select plugin' }).selectOption(IMPORTER.name) + await page.getByRole('checkbox', { name: 'Add tag automatically' }).check() + await page.getByRole('textbox', { name: 'Tag to add to imported snippets' }).fill('migrated') + await page.getByRole('checkbox', { name: 'Select Legacy Header Code' }).check() + await page.getByRole('button', { name: 'Import Selected (1)' }).first().click() + + await expect(page.getByRole('heading', { name: '1 snippets imported!' })).toBeVisible() + expect(importRequest).toMatchObject({ ids: [7], auto_add_tags: true, tag_value: 'migrated' }) + }) +}) diff --git a/tests/e2e/contextual-help.spec.ts b/tests/e2e/contextual-help.spec.ts index f769173d6..0ffc64e59 100644 --- a/tests/e2e/contextual-help.spec.ts +++ b/tests/e2e/contextual-help.spec.ts @@ -4,7 +4,7 @@ import { URLS } from './helpers/constants' const SCREENS_WITH_HELP = [ { name: 'Add Snippet', url: URLS.ADD_SNIPPET_ADMIN }, { name: 'Manage Snippets', url: URLS.SNIPPETS_ADMIN }, - // { name: 'Cloud Community', url: URLS.CLOUD_COMMUNITY_ADMIN }, + { name: 'Cloud Community', url: URLS.CLOUD_COMMUNITY_ADMIN }, { name: 'Cloud Library', url: URLS.CLOUD_LIBRARY_ADMIN }, { name: 'Blueprints', url: URLS.BLUEPRINTS_ADMIN }, { name: 'AI Agent', url: URLS.AI_AGENT_ADMIN }, @@ -16,7 +16,7 @@ const SCREENS_WITH_HELP = [ test.describe('Contextual Help', () => { for (const screen of SCREENS_WITH_HELP) { - test(`${screen.name} exposes its Help tabs`, async ({ page }) => { + test(`${screen.name} screen exposes its Help tabs`, async ({ page }) => { await page.goto(screen.url) await page.locator('#contextual-help-link').click() @@ -24,5 +24,18 @@ test.describe('Contextual Help', () => { await expect(help).toBeVisible() await expect(help.locator('.contextual-help-tabs li')).not.toHaveCount(0) }) + + test(`${screen.name} screen opens the first Help topic`, async ({ page }) => { + await page.goto(screen.url) + + await page.locator('#contextual-help-link').click() + const help = page.locator('#contextual-help-wrap') + const firstTopic = help.locator('.contextual-help-tabs a').first() + const panelId = await firstTopic.getAttribute('href') + + expect(panelId).toMatch(/^#.+/) + await firstTopic.click() + await expect(help.locator(panelId ?? '')).toBeVisible() + }) } }) diff --git a/tests/e2e/rest-api-auth.spec.ts b/tests/e2e/rest-api-auth.spec.ts index 14f7e430d..7e127b46f 100644 --- a/tests/e2e/rest-api-auth.spec.ts +++ b/tests/e2e/rest-api-auth.spec.ts @@ -1,8 +1,9 @@ import { expect, test } from '@playwright/test' +import { URLS } from './helpers/constants' test.describe('Snippets REST API authentication', () => { test('rejects creating a snippet without a REST nonce', async ({ page }) => { - await page.goto('/wp-admin/') + await page.goto(URLS.WP_ADMIN) const response = await page.evaluate(async () => { const request = await fetch('/?rest_route=/code-snippets/v1/snippets', { diff --git a/tests/e2e/rtl-layout.spec.ts b/tests/e2e/rtl-layout.spec.ts index e9f4d3231..078a547b9 100644 --- a/tests/e2e/rtl-layout.spec.ts +++ b/tests/e2e/rtl-layout.spec.ts @@ -45,6 +45,23 @@ const inspect = (): LayoutReport => { } test.describe('Right-to-left layout', () => { + test('switches the code editor direction', async ({ page }) => { + await page.goto(URLS.ADD_SNIPPET_ADMIN) + await page.waitForSelector('.CodeMirror') + const isRtl = 'rtl' === await page.locator('html').getAttribute('dir') + test.skip(!isRtl, 'The RTL locale is not available on this site, so there is nothing to check.') + + const direction = page.getByRole('combobox', { name: 'Code Direction' }) + const codeMirrorDirection = () => page.locator('.CodeMirror').evaluate(editor => + (<{ CodeMirror: { getOption: (option: string) => string } }>editor).CodeMirror.getOption('direction')) + + await direction.selectOption('rtl') + await expect.poll(codeMirrorDirection).toBe('rtl') + + await direction.selectOption('ltr') + await expect.poll(codeMirrorDirection).toBe('ltr') + }) + for (const { name, url, ready } of SCREENS) { test(`${name} mirrors without spilling off the page`, async ({ page }) => { await page.setViewportSize({ width: 1360, height: 900 }) diff --git a/tests/e2e/welcome.spec.ts b/tests/e2e/welcome.spec.ts index ae9c4931d..37097804f 100644 --- a/tests/e2e/welcome.spec.ts +++ b/tests/e2e/welcome.spec.ts @@ -8,4 +8,46 @@ test.describe('What’s New screen', () => { await expect(page.getByRole('heading', { level: 1, name: 'Resources and Updates' })).toBeVisible() await expect(page.locator('.code-snippets-welcome')).toBeVisible() }) + + test('links the Latest changes section to the full changelog in a new tab', async ({ page }) => { + await page.goto(URLS.WELCOME_ADMIN) + + const changelog = page.locator('.code-snippets-changelog') + const link = changelog.getByRole('link', { name: 'View changelog' }) + await expect(changelog.getByRole('heading', { name: 'Latest changes' })).toBeVisible() + await expect(link).toHaveAttribute('href', 'https://wordpress.org/plugins/code-snippets/changelog') + await expect(link).toHaveAttribute('target', '_blank') + }) + + test('protects the changelog link from sending referrer information', async ({ page }) => { + await page.goto(URLS.WELCOME_ADMIN) + + await expect(page.locator('.code-snippets-changelog').getByRole('link', { name: 'View changelog' })) + .toHaveAttribute('rel', 'noreferrer') + }) + + test('provides toolbar links to Insights and Import', async ({ page }) => { + await page.goto(URLS.WELCOME_ADMIN) + + const mainLinks = page.getByRole('navigation', { name: 'Main links' }) + const insights = new URL(await mainLinks.getByRole('link', { name: 'Insights' }).getAttribute('href') ?? '') + const importSnippets = new URL(await mainLinks.getByRole('link', { name: 'Import' }).getAttribute('href') ?? '') + + expect(`${insights.pathname}${insights.search}`).toBe(URLS.INSIGHTS_ADMIN) + expect(`${importSnippets.pathname}${importSnippets.search}`).toBe(URLS.IMPORT_ADMIN) + }) + + test('gives the hero image a meaningful text alternative', async ({ page }) => { + await page.goto(URLS.WELCOME_ADMIN) + + await expect(page.locator('.code-snippets-hero').getByRole('img', { name: 'Latest news image' })).toBeVisible() + }) + + test('opens the hero article in a protected new tab', async ({ page }) => { + await page.goto(URLS.WELCOME_ADMIN) + + const heroLink = page.locator('.code-snippets-hero').getByRole('link', { name: /Read more/ }) + await expect(heroLink).toHaveAttribute('target', '_blank') + await expect(heroLink).toHaveAttribute('rel', 'noopener noreferrer') + }) }) From f7fa6219e88fdeca35cfd1a88c601e48f6587007 Mon Sep 17 00:00:00 2001 From: Rami Yushuvaev Date: Wed, 16 Sep 2026 19:10:25 +0300 Subject: [PATCH 07/19] Merge changes from core-beta branch --- .gitignore | 7 + CHANGELOG.md | 296 ++++++---- config/webpack/webpack-js.ts | 2 +- package-lock.json | 532 +++++++++++++----- package.json | 5 +- scripts/test-setup-playwright.ts | 20 +- src/code-snippets.php | 6 +- src/css/admin-bar.scss | 14 + src/css/common/_badges.scss | 1 + src/css/common/_buttons.scss | 32 ++ src/css/common/_list-table.scss | 27 + src/css/common/_modal.scss | 13 + src/css/common/_page-subtitle.scss | 13 + src/css/common/_subnav.scss | 5 +- src/css/common/_theme.scss | 34 ++ src/css/common/_toolbar.scss | 70 ++- src/css/common/_tooltips.scss | 1 + src/css/common/_wp-admin.scss | 2 + src/css/common/list-table/_layout.scss | 23 +- src/css/common/list-table/_responsive.scss | 5 +- src/css/edit.scss | 3 +- src/css/edit/_conditions.scss | 5 +- src/css/edit/_form.scss | 3 +- src/css/edit/_gpt.scss | 28 - src/css/edit/_sidebar.scss | 145 +++-- src/css/import.scss | 1 + src/css/insights.scss | 1 + src/css/manage.scss | 12 +- src/css/manage/_ai-agent.scss | 46 +- src/css/manage/_blueprints.scss | 3 + src/css/manage/_cloud-community.scss | 8 +- src/css/manage/_snippets-table.scss | 109 +++- src/css/manage/blueprints/_detail.scss | 38 +- src/css/manage/blueprints/_form-layout.scss | 2 +- src/css/settings.scss | 4 +- .../ConditionModal/ConditionModalButton.tsx | 6 +- .../EditMenu/EditorSidebar/EditorSidebar.tsx | 11 +- .../EditorSidebar/actions/ExportButtons.tsx | 12 +- .../EditorSidebar/controls/LockControl.tsx | 1 + .../ManageMenu/CommunityCloud/CloudSearch.tsx | 85 +-- .../CommunityCloud/WithCloudSearchContext.tsx | 15 +- .../SnippetsTable/ManageSnippetCard.tsx | 38 +- .../ManageMenu/SnippetsTable/RowActions.tsx | 7 +- .../SnippetsTable/SnippetsTable.tsx | 6 +- .../ManageMenu/SnippetsTable/TableColumns.tsx | 42 +- .../WithFilteredSnippetsContext.tsx | 15 +- .../common/ListTable/TableNavigation.tsx | 2 +- .../common/LoadingStatusNotices.tsx | 5 +- src/js/components/common/SubnavTabs.tsx | 78 ++- src/js/components/common/Toolbar.tsx | 2 +- .../cloud/CloudSnippetCard.tsx} | 37 +- .../cloud/CloudSnippetDownloadButton.tsx | 4 +- .../common/icons/CloudUpdateIcon.tsx | 13 - .../common/snippets/ConfirmDeleteDialog.tsx | 17 +- .../common/snippets/SnippetPreviewModal.tsx | 29 +- src/js/hooks/useSnippetsAPI.tsx | 6 +- src/js/services/settings/tabs.ts | 12 +- src/js/types/Snippet.ts | 8 + src/js/types/schema/SnippetSchema.ts | 11 +- src/js/utils/errors.ts | 10 +- src/js/utils/restAPI.ts | 11 + src/js/utils/screen.ts | 3 + src/js/utils/snippets/objects.ts | 19 +- src/js/utils/snippets/snippets.ts | 2 +- src/js/utils/urls.ts | 12 +- src/php/Admin/Feedback_Panel.php | 2 +- src/php/Admin/Menus/Admin_Menu.php | 17 +- src/php/Admin/Menus/Edit_Menu.php | 4 +- .../Admin/Menus/Insights/Insights_Summary.php | 2 +- src/php/Admin/Menus/Manage/Manage_Menu.php | 9 +- .../Admin/Menus/Manage/Manage_Menu_Assets.php | 4 + .../Manage/Manage_Menu_Screen_Options.php | 17 +- src/php/Admin/Menus/Settings_Menu.php | 32 +- src/php/Client/Feedback_Client.php | 6 +- src/php/Core/DB.php | 39 +- src/php/Core/Uninstaller.php | 3 +- src/php/Core/load.php | 13 +- .../Handlers/Functions_Snippet_Handler.php | 10 +- src/php/Flat_Files/Snippet_Files.php | 10 +- src/php/Integration/Admin_Bar.php | 48 +- src/php/Integration/Evaluate_Content.php | 72 ++- src/php/Integration/Evaluate_Functions.php | 42 +- src/php/Integration/Shortcodes.php | 58 +- src/php/Model/Basic_Cloud_Connection.php | 3 +- src/php/Model/Cloud_Snippets.php | 21 +- src/php/Model/Feedback_Connection.php | 2 + src/php/Model/Model.php | 2 +- src/php/Model/Snippet.php | 7 + src/php/Plugin.php | 26 +- .../Cloud/Cloud_Snippets_REST_Controller.php | 37 +- .../Feedback/Feedback_REST_Controller.php | 2 +- .../Demos_Seen_REST_Controller.php | 2 +- .../Snippet_View_REST_Controller.php | 2 +- .../Snippets/Snippets_REST_Controller.php | 45 +- src/php/Settings/Setting_Field.php | 15 +- src/php/Settings/Settings_Fields.php | 42 +- src/php/Settings/Settings_Layout.php | 17 +- src/php/Settings/settings.php | 17 +- src/php/Utils/Validator.php | 4 +- src/php/Utils/editor.php | 2 +- src/php/Utils/options.php | 26 +- src/php/snippet-ops.php | 74 ++- src/readme.txt | 9 - tests/e2e/code-snippets-evaluation.spec.ts | 144 +++++ tests/e2e/code-snippets-list.spec.ts | 62 ++ tests/e2e/helpers/SnippetsTestHelper.ts | 117 ++++ tests/e2e/helpers/constants.ts | 6 +- tests/unit/Admin/Admin_Bar_Test.php | 14 +- tests/unit/Admin/Feedback_Panel_Test.php | 2 +- .../Menus/Manage/Manage_Menu_Assets_Test.php | 2 +- .../Manage/Manage_Menu_Demo_Reset_Test.php | 9 +- tests/unit/Admin/Notice_Filter_Test.php | 2 + tests/unit/Authorship_Test.php | 144 +++++ tests/unit/Core/Uninstaller_Test.php | 14 +- tests/unit/Model/Cloud_Snippets_Test.php | 53 ++ tests/unit/Plugin_Test.php | 6 +- .../Cloud_Snippets_REST_Controller_Test.php} | 85 ++- ...PI_Snippets_Shared_Network_Toggle_Test.php | 2 +- .../{ => Snippets}/REST_API_Snippets_Test.php | 2 +- 119 files changed, 2587 insertions(+), 855 deletions(-) create mode 100644 src/css/common/_buttons.scss create mode 100644 src/css/common/_page-subtitle.scss create mode 100644 src/css/manage/_blueprints.scss rename src/js/components/{ManageMenu/CommunityCloud/SearchResult.tsx => common/cloud/CloudSnippetCard.tsx} (75%) delete mode 100644 src/js/components/common/icons/CloudUpdateIcon.tsx create mode 100644 tests/unit/Authorship_Test.php rename tests/unit/REST_API/{REST_API_Cloud_Test.php => Cloud/Cloud_Snippets_REST_Controller_Test.php} (80%) rename tests/unit/REST_API/{ => Snippets}/REST_API_Snippets_Shared_Network_Toggle_Test.php (99%) rename tests/unit/REST_API/{ => Snippets}/REST_API_Snippets_Test.php (99%) diff --git a/.gitignore b/.gitignore index 453a38177..d52c8fce4 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,10 @@ tmp /.env .superpowers/ docs/superpowers/ +/docs/ + +# Local dev only (wp-env mu-plugin mappings, staging access) +/dev/ + +# Local wp-env overrides — may contain credentials, never commit +.wp-env.override.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 73d4e8faa..e94b17f42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,22 +3,83 @@ ## [4.0.0] (UPCOMING) ### Added -* AI Agent demo: a guided, scripted walkthrough of the Pro AI Agent that plans, builds, and refines a welcome banner snippet named after your site. Runs entirely inside the plugin — no data leaves your site and no snippets are added to your library. -* Blueprints demo: a guided, scripted walkthrough of Pro Blueprints that steps through the "Create a Shortcode" blueprint and confirms the snippet it would generate. Runs entirely inside the plugin — no code is generated and nothing is saved. -* Cloud Library demo: a guided, scripted walkthrough of the Pro Cloud Library, showing how a cloud snippet is previewed, downloaded inactive, and then kept in sync. Runs entirely inside the plugin — the snippets shown are examples and nothing is downloaded. -* "New" badges on the AI Agent, Blueprints, and Cloud Library toolbar tabs, which soften once each demo walkthrough has been watched. +* AI Agent for building snippets from a description: it proposes a plan you can refine or approve before any code is + written, and can revise the snippets it created. (PRO) +* Snippet revisions, with a history of past versions, a side-by-side diff against the current code, and one-click + restore. (PRO) +* Display conditions for controlling where and when a snippet runs, without writing the checks by hand. (PRO) +* Per-role snippet permissions, so you can decide which roles may view, edit, activate, or delete snippets. (PRO) +* Blueprints for setting up a site from a saved collection of snippets and settings. (PRO) +* Flat file storage, for keeping snippets as files so they can be version-controlled alongside the rest of a site. (PRO) +* "Ran on this page" tracking, showing which snippets actually executed on the page you are viewing. (PRO) +* Natural-language search in Community Cloud, so you can describe what you need instead of guessing keywords. (PRO) +* Snippet deployment from Code Snippets Cloud to connected sites, including deploying several snippets under one shared + display condition. (PRO) +* Installing a cloud bundle onto a connected site as a standalone plugin. (PRO) +* Updating Code Snippets Pro on a connected site from the cloud dashboard. (PRO) +* Drift detection, which reports when a snippet on the site no longer matches the copy stored in the cloud. (PRO) +* New admin interface for managing snippets, with a cleaner layout, faster interactions, and a more consistent + experience across plugin screens. (PRO) +* Card view for browsing snippets, with a view switcher on the snippets table and Community Cloud. (PRO) +* Snippet preview modal for viewing snippet code from the snippets table without opening the editor. (PRO) +* Automatic hiding of unrelated admin notices from other plugins on Code Snippets screens. (PRO) +* Admin bar snippet drawer, based on the Deckerweb Snippets workflow, for quick access to snippets and Safe Mode from + the WordPress admin bar. (PRO) +* Snippet locking to help prevent accidental edits or deletion of important snippets. Props + to https://github.com/mgiannopoulos24. (PRO) +* Improved screen options on the main snippets table, including controls for visible columns and truncating long snippet + names or descriptions. (PRO) +* Bulk actions and bulk code download support in the redesigned snippets table. (PRO) +* Featured snippets and improved browsing in Community Cloud. (PRO) +* WordPress modern theme admin styling compatibility. (PRO) +* Clearer accessibility labels, headings, tab markup, table checkboxes, sort buttons, copy buttons, and drag-and-drop + upload controls. (PRO) +* Feedback reporter for sending bug reports, feature requests and general feedback from the plugin screens, with an + optional summary of the site environment so the team can reproduce the problem. (PRO) + +### Changed +* Redesigned the main snippets table with improved search, filtering, sorting, pagination, row actions, and bulk + selection. (PRO) +* Improved the snippet import and migration experience, including clearer file upload handling and third-party plugin + migration flows. (PRO) +* Improved snippet error handling so activation failures, validation errors, and stack traces are easier to understand. + (PRO) +* Improved Community Cloud search and filtering, including server-side filters, better result loading, and clearer empty + states. (PRO) +* Updated the welcome screen, toolbar, import screen, and cloud screens to match the new admin experience. (PRO) +* Updated internal plugin architecture to a cleaner PSR-4 structure for better long-term maintainability. (PRO) +* Improved accessibility across the snippets table, import screen, migration flow, Community Cloud, welcome screen, + toolbar, dialogs, tooltips, and code editor. (PRO) +* Improved colour contrast and reduced-motion support across admin screens. (PRO) +* Faster loading of large code vaults, which are now fetched a page at a time instead of all at once. (PRO) + +### Fixed +* Fixed AI conversations from one connected site being visible from another. (PRO) +* Fixed a snippet list stored in Code Snippets Cloud being emptied when the site was temporarily unable to read its own + snippets. (PRO) +* Fixed REST API server error responses on missing snippets. (PRO) +* Fixed redundant frontend logic, improving overall performance. (PRO) +* Fixed Community Cloud search results and pagination to respect WordPress screen options. (PRO) +* Fixed snippet saving and activation feedback to improve validation and runtime error display. (PRO) +* Fixed downloaded Community Cloud snippets appearing as not downloaded after a page reload. (PRO) +* Fixed network snippet lookups using the wrong database table on multisite. (PRO) +* Fixed the inactive snippets count including trashed snippets. (PRO) +* Fixed featured Community Cloud snippets failing to load with some cloud API responses. (PRO) +* Fixed bulk actions in Community Cloud running against an empty selection, so selected snippets were never downloaded. + (PRO) ## [3.10.2] (2026-09-01) ### Added -* Added a confirmation flow for run-once snippet execution and hardened the handler to prevent failed or confusing actions. +* Added a confirmation flow for run-once snippet execution and hardened the handler to prevent failed or confusing + actions. ### Changed * Snippet names now respect the row truncation Screen Option in the admin list for better readability. * Version switching AJAX requests now validate the correct nonce, improving reliability when updating snippet versions. ### Fixed -* Fixed safe mode fatal errors caused by an undefined wp_get_current_user() call. +* Fixed safe mode fatal errors caused by an undefined wp_get_current_user () call. * Fixed PHP validation being triggered incorrectly when activating snippets in bulk. * Fixed saving issues after a user session expires. * Fixed warnings caused by aliased field names when reading modified snippet fields. @@ -33,7 +94,8 @@ ### Fixed * Fixed a fatal error affecting snippets that use a `namespace` or `declare` statement. * Fixed the snippets page rendering blank when another plugin's screen settings filter returned an invalid value. -* Fixed snippet saving on hosts that block REST API `PUT` and `PATCH` requests, by sending writes as `POST` with a method override. +* Fixed snippet saving on hosts that block REST API `PUT` and `PATCH` requests, by sending writes as `POST` with a + method override. * Fixed the Snippets List Order setting not being applied to the snippets list. * Fixed admin bar snippet scripts failing to load on the free version, including on the site front end. * Fixed snippet modified dates being sent without the correct UTC offset. @@ -44,26 +106,35 @@ ## [3.10.0] (2026-08-24) ### Added -* New admin interface for managing snippets, with a cleaner layout, faster interactions, and a more consistent experience across plugin screens. +* New admin interface for managing snippets, with a cleaner layout, faster interactions, and a more consistent + experience across plugin screens. * Card view for browsing snippets, with a view switcher on the snippets table and Community Cloud. * Snippet preview modal for viewing snippet code from the snippets table without opening the editor. * Automatic hiding of unrelated admin notices from other plugins on Code Snippets screens. -* Admin bar snippet drawer, based on the Deckerweb Snippets workflow, for quick access to snippets and Safe Mode from the WordPress admin bar. -* Snippet locking to help prevent accidental edits or deletion of important snippets. Props to https://github.com/mgiannopoulos24. -* Improved screen options on the main snippets table, including controls for visible columns and truncating long snippet names or descriptions. +* Admin bar snippet drawer, based on the Deckerweb Snippets workflow, for quick access to snippets and Safe Mode from + the WordPress admin bar. +* Snippet locking to help prevent accidental edits or deletion of important snippets. Props + to https://github.com/mgiannopoulos24. +* Improved screen options on the main snippets table, including controls for visible columns and truncating long snippet + names or descriptions. * Bulk actions and bulk code download support in the redesigned snippets table. * Featured snippets and improved browsing in Community Cloud. * WordPress modern theme admin styling compatibility. -* Clearer accessibility labels, headings, tab markup, table checkboxes, sort buttons, copy buttons, and drag-and-drop upload controls. +* Clearer accessibility labels, headings, tab markup, table checkboxes, sort buttons, copy buttons, and drag-and-drop + upload controls. ### Changed -* Redesigned the main snippets table with improved search, filtering, sorting, pagination, row actions, and bulk selection. -* Improved the snippet import and migration experience, including clearer file upload handling and third-party plugin migration flows. +* Redesigned the main snippets table with improved search, filtering, sorting, pagination, row actions, and bulk + selection. +* Improved the snippet import and migration experience, including clearer file upload handling and third-party plugin + migration flows. * Improved snippet error handling so activation failures, validation errors, and stack traces are easier to understand. -* Improved Community Cloud search and filtering, including server-side filters, better result loading, and clearer empty states. +* Improved Community Cloud search and filtering, including server-side filters, better result loading, and clearer empty + states. * Updated the welcome screen, toolbar, import screen, and cloud screens to match the new admin experience. * Updated internal plugin architecture to a cleaner PSR-4 structure for better long-term maintainability. -* Improved accessibility across the snippets table, import screen, migration flow, Community Cloud, welcome screen, toolbar, dialogs, tooltips, and code editor. +* Improved accessibility across the snippets table, import screen, migration flow, Community Cloud, welcome screen, + toolbar, dialogs, tooltips, and code editor. * Improved colour contrast and reduced-motion support across admin screens. ### Fixed @@ -91,7 +162,8 @@ ### Added * New import functionality to migrate snippets from file uploads with drag-and-drop interface. -* Support for importing snippets from other popular plugins (Header Footer Code Manager, Insert Headers and Footers, Insert PHP Code Snippet). +* Support for importing snippets from other popular plugins (Header Footer Code Manager, Insert Headers and Footers, + Insert PHP Code Snippet). * Enhanced file based execution support with improved multisite mode compatibility. ### Fixed @@ -102,7 +174,8 @@ ## [3.9.3] (2025-12-03) ### Added -* End-to-end tests to verify the toggle visual state in the snippets list page, improving UI verification and test reliability. +* End-to-end tests to verify the toggle visual state in the snippets list page, improving UI verification and test + reliability. ### Fixed * Restored missing styles styling and direction-aware layout from Manage menu. @@ -139,7 +212,8 @@ * Expanded Multisite Sharing settings for clearer control over network-wide snippet sharing ### Changed -* Modernized browser support targets and polished admin UI (clearer row-action badges, improved Pro badge hover, refined active snippet name styling) +* Modernized browser support targets and polished admin UI (clearer row-action badges, improved Pro badge hover, refined + active snippet name styling) ### Fixed * Fixed REST API pagination to return correct results and page counts @@ -251,14 +325,16 @@ * Improved UX of snippet activation toggle. ### Fixed -* Fetching active snippets on a multisite network now respects the 'priority' field above all else when ordering snippets. +* Fetching active snippets on a multisite network now respects the 'priority' field above all else when ordering + snippets. * Cloud search appears correctly and allows downloading snippets in the free version of Code Snippets. * Improved performance of loading admin menu icon. ## [3.6.9] (2025-02-17) ### Changed -* Updated `Cloud_API::get_bundles()` to properly check bundle data and return an empty array if no valid bundles are present. +* Updated `Cloud_API::get_bundles()` to properly check bundle data and return an empty array if no valid bundles are + present. * Refactored `Cloud_List_Table::fetch_snippets()` to always return a valid `Cloud_Snippets` instance. * Cleaned up bundle iteration code and improved translation handling in the bundles view. @@ -274,7 +350,8 @@ * Updated Freemius SDK to the latest version. (PRO) ### Removed -* Functionality allowing `[code_snippet]` shortcodes to be embedded recursively – it will be re-added in a future version. +* Functionality allowing `[code_snippet]` shortcodes to be embedded recursively – it will be re-added in a future + version. ### Fixed * Shortcodes embedded within `[code_snippet]` shortcodes not evaluating correctly. @@ -287,15 +364,19 @@ ### Added * Generated snippet shortcode tags will include the snippet name, for easier identification. -* Admin notices will dismiss automatically after five seconds. ([#208](https://github.com/codesnippetspro/code-snippets/issues/208)) +* Admin notices will dismiss automatically after five seconds. + ([#208](https://github.com/codesnippetspro/code-snippets/issues/208)) ### Changed * Updated CSS to use latest Sass features. -* Moved theme selector to just above editor preview on settings page (thanks to [brandonjp]). ([#206](https://github.com/codesnippetspro/code-snippets/issues/206)) -* `[code_snippet]` shortcodes can now be nested within each other. ([#198](https://github.com/codesnippetspro/code-snippets/issues/198)) +* Moved theme selector to just above editor preview on settings page (thanks to [brandonjp]). + ([#206](https://github.com/codesnippetspro/code-snippets/issues/206)) +* `[code_snippet]` shortcodes can now be nested within each other. + ([#198](https://github.com/codesnippetspro/code-snippets/issues/198)) ### Fixed -* Save buttons above editor did not follow usual validation process in Pro. (PRO) ([#197](https://github.com/codesnippetspro/code-snippets/issues/197)) +* Save buttons above editor did not follow usual validation process in Pro. (PRO) + ([#197](https://github.com/codesnippetspro/code-snippets/issues/197)) * Minor inconsistencies in consistent UI elements between Core and Pro. * Tags input not allowing input. ([#211](https://github.com/codesnippetspro/code-snippets/issues/211)) * Issue with Elementor source code widget. (PRO) ([#205](https://github.com/codesnippetspro/code-snippets/issues/205)) @@ -421,13 +502,16 @@ * Scroll new notices into view on edit menu. ### Fixed -* Error when attempting to update network shared snippets after saving. [[#](https://wordpress.org/support/topic/activating-snippets-breaks-on-wordpress-6-3/)] +* Error when attempting to update network shared snippets after + saving. [[#](https://wordpress.org/support/topic/activating-snippets-breaks-on-wordpress-6-3/)] ## [3.4.2] (2023-07-05) ### Fixed -* Issue causing export process to fail with fatal error. [[#](https://wordpress.org/support/topic/critical-error-on-exporting-snippets/)] -* Type issue on `the_posts` filter when no posts available. [[#](https://wordpress.org/support/topic/collision-with-plugin-xml-sitemap-google-news/)] +* Issue causing export process to fail with fatal + error. [[#](https://wordpress.org/support/topic/critical-error-on-exporting-snippets/)] +* Type issue on `the_posts` filter when no posts + available. [[#](https://wordpress.org/support/topic/collision-with-plugin-xml-sitemap-google-news/)] ## [3.4.1] (2023-06-29) @@ -435,14 +519,18 @@ * Added better debugging when calling REST API methods from the edit menu. ### Changed -* Escape special characters when sending snippet code through AJAX to avoid false-positives from security modules. [[#](https://wordpress.org/support/topic/latest-3-4-0-ajax-bug-cannot-save-snippets-403-error/)] +* Escape special characters when sending snippet code through AJAX to avoid false-positives from security + modules. [[#](https://wordpress.org/support/topic/latest-3-4-0-ajax-bug-cannot-save-snippets-403-error/)] * Only display the latest update or error notice on the edit page, instead of allowing them to stack. ### Fixed -* Undefined array key error. [[#](https://wordpress.org/support/topic/after-updating-occasionally-getting-undefined-array-key-query/)] -* Potential type issue when loading Prism. [[#](https://wordpress.org/support/topic/code-snippets-fatal-error-breaking-xml-sitemaps/)] +* Undefined array key + error. [[#](https://wordpress.org/support/topic/after-updating-occasionally-getting-undefined-array-key-query/)] +* Potential type issue when loading + Prism. [[#](https://wordpress.org/support/topic/code-snippets-fatal-error-breaking-xml-sitemaps/)] * Potential type issue when sorting snippets. [[#](https://github.com/codesnippetspro/code-snippets/issues/166)] -* Issue preventing asset revision numbers from updating correctly. (PRO) [[#](https://github.com/codesnippetspro/code-snippets/issues/166)] +* Issue preventing asset revision numbers from updating correctly. + (PRO) [[#](https://github.com/codesnippetspro/code-snippets/issues/166)] ## [3.4.0] (2023-05-17) @@ -453,10 +541,10 @@ ### Changed * Better compatibility with modern versions of PHP (7.0+). * Converted Edit/Add New Snippet page to use React: - - Converted action buttons to asynchronously use REST API endpoints through AJAX. - - Load page components dynamically through React. - - Added action notice queue system - - Replaced native alert dialog with proper React modal. + - Converted action buttons to asynchronously use REST API endpoints through AJAX. + - Load page components dynamically through React. + - Added action notice queue system + - Replaced native alert dialog with proper React modal. * Catch snippet execution errors to prevent site from crashing. * Display recent snippet errors in admin dashboard instead. * Updated editor block to use new REST API endpoints. (PRO) @@ -476,7 +564,8 @@ ### Added * Added additional editor shortcuts to list in tooltip. -* Filter for changing Snippets admin menu position. [See this help article for more information.](https://codesnippets.pro/doc/snippets-menu-location/) +* Filter for changing Snippets admin menu + position. [See this help article for more information.](https://codesnippets.pro/doc/snippets-menu-location/) * Ability to filter shortcode output. Thanks to contributions from [Jack Szwergold](https://github.com/JackSzwergold). ### Fixed @@ -493,10 +582,12 @@ ### Added * `Ctrl`+`/` or `Cmd`+`/` as shortcut for commenting out code in the snippet editor. -* Additional hooks to various snippet actions, thanks to contributions made by [ancient-spirit](https://github.com/ancient-spirit). +* Additional hooks to various snippet actions, thanks to contributions made + by [ancient-spirit](https://github.com/ancient-spirit). * Fold markers, additional keyboard shortcuts and keymap options to snippet editor, thanks to contributions made by [Amaral Krichman](https://github.com/karmaral). -* WP-CLI commands for retrieving, activating, deactivating, deleting, creating, updating, exporting and importing snippets. +* WP-CLI commands for retrieving, activating, deactivating, deleting, creating, updating, exporting and importing + snippets. ### Changed * Removed duplicate tables exist query. ([#](https://wordpress.org/support/topic/duplicate-queries-21)). @@ -515,7 +606,8 @@ * Support for multiple code styles in the source code Gutenberg editor block. (PRO) * Admin notice announcing release of Code Snippets Pro. * Button for copying shortcode text to clipboard. -* Option to choose from 44 different themes for the Prism code highlighter in the source editor block and Elementor widget. (PRO) +* Option to choose from 44 different themes for the Prism code highlighter in the source editor block and Elementor + widget. (PRO) ### Changed * Include Code Snippets CSS and JS source code in distributed package. @@ -634,15 +726,16 @@ ## [2.14.3] (2021-12-10) ### Fixed -* Potential security issue outputting snippets-safe-mode query variable value as-is. Thanks to Krzysztof Zając for reporting. +* Potential security issue outputting snippets-safe-mode query variable value as-is. Thanks to Krzysztof Zając for + reporting. ## [2.14.2] (2021-09-09) ### Added * Added translations: - - Spanish by [Ibidem Group](https://www.ibidemgroup.com) - - Urdu by [Samuel Badree](https://mobilemall.pk/) - - Greek by [Toni Bishop from Jrop](https://www.jrop.com/) + - Spanish by [Ibidem Group](https://www.ibidemgroup.com) + - Urdu by [Samuel Badree](https://mobilemall.pk/) + - Greek by [Toni Bishop from Jrop](https://www.jrop.com/) * Support for `:class` syntax to the code validator. * PHP8 support to the code linter. * Color picker feature to the code editor. @@ -665,7 +758,8 @@ * Code validator now supports `function_exists` and `class_exists` checks. * Code validator now supports anonymous functions. * Issue with saving the hidden columns setting. -* Replaced the outdated tag-it library with [tagger](https://github.com/jcubic/tagger) for powering the snippet tags editor. +* Replaced the outdated tag-it library with [tagger](https://github.com/jcubic/tagger) for powering the snippet tags + editor. ## [2.14.0] (2020-01-26) @@ -686,7 +780,8 @@ * Fixed a bug preventing the editor theme from being set to default. * Ensure that imported snippets are always inactive. * Check the referer on the import menu to prevent CSRF attacks. - Thanks to [Chloe with the Wordfence Threat Intelligence team](https://www.wordfence.com/blog/author/wfchloe/) for reporting. + Thanks to [Chloe with the Wordfence Threat Intelligence team](https://www.wordfence.com/blog/author/wfchloe/) for + reporting. * Ensure that individual snippet action links use proper verification. ## [2.13.3] (2019-03-13) @@ -733,7 +828,8 @@ ## [2.13.0] (2018-12-17) ### Added -* Search/replace functionality to the snippet editor. [See here for a list of keyboard shortcuts.](https://codemirror.net/demo/search.html) [[#](https://wordpress.org/support/topic/feature-request-codemirror-search-and-replace/)] +* Search/replace functionality to the snippet + editor. [See here for a list of keyboard shortcuts.](https://codemirror.net/demo/search.html) [[#](https://wordpress.org/support/topic/feature-request-codemirror-search-and-replace/)] * Option to make admin menu more compact. * Added additional styles to editor settings preview. * PHP linter to code editor. @@ -753,7 +849,8 @@ * CodeMirror updated to version 5.41.0. * Attempt to create database columns that might be missing after a table upgrade. * Streamlined upgrade process. -* Made search box appear at top of page on mobile. [[#](https://wordpress.org/support/topic/small-modification-for-mobile-ux/)] +* Made search box appear at top of page on + mobile. [[#](https://wordpress.org/support/topic/small-modification-for-mobile-ux/)] * Updated screenshots. ### Fixed @@ -810,7 +907,7 @@ ### Fixed * Prevent errors when trying to export no snippets. -* Use wp_json_encode() to encode export data. +* Use wp_json_encode () to encode export data. * Check both the file extension and MIME type of uploaded import files. ## [2.10.0] (2018-01-18) @@ -882,7 +979,7 @@ ### Changed * Moved code to disable snippet execution into a filter hook. -* execute_active_snippets() function updated with improved efficiency. +* execute_active_snippets () function updated with improved efficiency. * Renamed Snippet class to avoid name collisions with other plugins. * Don't hide output when executing a snippet. @@ -900,8 +997,10 @@ ## [2.8.6] (2017-05-14) ### Fixed -* Fixed snippet description field alias not mapping correctly, causing snippet descriptions to not be displayed in the table or when editing a snippet. -* Ensured that get_snippets() function retrieves snippets with the correct 'network' setting. Fixes snippet edit links in network admin. +* Fixed snippet description field alias not mapping correctly, causing snippet descriptions to not be displayed in the + table or when editing a snippet. +* Ensured that get_snippets () function retrieves snippets with the correct 'network' setting. Fixes snippet edit links + in network admin. ## [2.8.5] (2017-05-13) @@ -945,7 +1044,8 @@ ### Fixed * Fixed admin menu items not translating. * Corrected editor alignment on RTL sites. ([#](https://wordpress.org/support/topic/suggestion-css-fix-for-rtl-sites/)) -* Fixed bulk actions running when Filter button is clicked. ([#](https://wordpress.org/support/topic/bug-with-filtering-action-buttons/)) +* Fixed bulk actions running when Filter button is clicked. + ([#](https://wordpress.org/support/topic/bug-with-filtering-action-buttons/)) ## [2.8.0] (2016-12-14) @@ -984,7 +1084,8 @@ * Updated CodeMirror to version 5.19.0. ### Security -* Ensured that the editor theme setting is properly validated. Thanks to [Netsparker](https://www.netsparker.com) for reporting. +* Ensured that the editor theme setting is properly validated. Thanks to [Netsparker](https://www.netsparker.com) for + reporting. * Ensured that snippet tags are properly escaped. Thanks to [Netsparker](https://www.netsparker.com) for reporting. ## [2.7.0] (2016-07-23) @@ -1000,9 +1101,11 @@ ### Fixed * Fixed plugin translations being loaded. * Fixed description field not being imported. -* Fixed issue with CodeMirror rubyblue theme. [[#](https://wordpress.org/support/topic/a-problem-with-the-cursor-color-and-the-fix-that-worked-for-me)] +* Fixed issue with CodeMirror rubyblue + theme. [[#](https://wordpress.org/support/topic/a-problem-with-the-cursor-color-and-the-fix-that-worked-for-me)] * Fixed snippet fields not importing. -* Fixed a minor XSS vulnerability discovered by Burak Kelebek. [[#](https://wordpress.org/support/topic/security-vulnerability-20)] +* Fixed a minor XSS vulnerability discovered by Burak + Kelebek. [[#](https://wordpress.org/support/topic/security-vulnerability-20)] ## [2.6.1] (2016-02-10) @@ -1064,7 +1167,8 @@ ## [2.4.1] (2015-09-17) ### Fixed -* Fixed CodeMirror themes not being detected on settings page [[#](https://wordpress.org/support/topic/updated-to-240-now-i-cant-switch-theme)] +* Fixed CodeMirror themes not being detected on settings + page [[#](https://wordpress.org/support/topic/updated-to-240-now-i-cant-switch-theme)] ## [2.4.0] (2015-09-17) @@ -1090,7 +1194,8 @@ ### Added * Added icons for admin and front-end snippets to manage table. -* Added filter switch to prevent a snippet from executing. ([#25](https://github.com/codesnippetspro/code-snippets/issues/25)) +* Added filter switch to prevent a snippet from executing. + ([#25](https://github.com/codesnippetspro/code-snippets/issues/25)) ### Changed * Improved settings retrieval by caching settings. @@ -1126,7 +1231,7 @@ ### Fixed * Resolved JavaScript error on edit snippet pages. -* Added polyfill for array_replace_recursive() function for PHP 5.2. +* Added polyfill for array_replace_recursive () function for PHP 5.2. ## [2.2.1] (2015-05-10) @@ -1199,7 +1304,8 @@ * Added Russian translation by Alexander Samsonov. * Added Slovak translation by [Ján Fajčák] from [WordPress Slovakia](https://wp.sk). * Added setting to always save and activate snippets by default. -* Added braces to single-line conditionals in line with [new coding standards](https://make.wordpress.org/core/2013/11/13/proposed-coding-standards-change-always-require-braces/). +* Added braces to single-line conditionals in line + with [new coding standards](https://make.wordpress.org/core/2013/11/13/proposed-coding-standards-change-always-require-braces/). ### Changed * Improved plugin file structure. @@ -1241,7 +1347,8 @@ ### Added * Added French translation thanks to translator [oWEB](http://office-web.net). -* Added 'Save & Deactivate' button to the edit snippet page. ([#](https://wordpress.org/support/topic/deactivate-button-in-edit-snippet-page)) +* Added 'Save & Deactivate' button to the edit snippet page. + ([#](https://wordpress.org/support/topic/deactivate-button-in-edit-snippet-page)) * Added nonce to edit snippet page. * Added a fallback MP6 icon. @@ -1251,8 +1358,10 @@ * Updated CodeMirror to version 3.19. * Updated WordPress.org plugin banner. * Add and remove network capabilities as super admins are added and removed. -* Replaced buggy trim `` functionality with a much more reliable regex method. ([#](https://wordpress.org/support/topic/character-gets-cut)) -* Make the title of each snippet on the manage page a clickable link to edit the snippet ([#](https://wordpress.org/support/topic/deactivate-button-in-edit-snippet-page?replies=9#post-4682757)) +* Replaced buggy trim `` functionality with a much more reliable regex method. + ([#](https://wordpress.org/support/topic/character-gets-cut)) +* Make the title of each snippet on the manage page a clickable link to edit the snippet + ([#](https://wordpress.org/support/topic/deactivate-button-in-edit-snippet-page?replies=9#post-4682757)) * Hide row actions on manage snippet page by default. * Use the proper WordPress database APIs consistently. * Rewritten export functionality. @@ -1263,7 +1372,9 @@ * Removed CodeMirror bundled with plugin. ### Fixed -* Fixed snippet failing to save when code contains `%` character, props to [nikan06](https://wordpress.org/support/profile/nikan06). ([#](https://wordpress.org/support/topic/percent-sign-bug)) +* Fixed snippet failing to save when code contains `%` character, props + to [nikan06](https://wordpress.org/support/profile/nikan06). + ([#](https://wordpress.org/support/topic/percent-sign-bug)) * Fixed HTML breaking in export files. ([#](https://wordpress.org/support/topic/import-problem-7)) * Fixed incorrect export filename. * Fixed CodeMirror incompatibility with the WP Editor plugin. @@ -1289,7 +1400,9 @@ * Added error message handling for import snippets page. ### Changed -* Improved database table creation method: on a single-site install, the snippets table will always be created. On a multisite install, the network snippets table will always be created; the site-specific table will always be created for the main site; for sub-sites the snippets table will only be created on a visit to a snippets admin page. +* Improved database table creation method: on a single-site install, the snippets table will always be created. On a + multisite install, the network snippets table will always be created; the site-specific table will always be created + for the main site; for sub-sites the snippets table will only be created on a visit to a snippets admin page. * Updated to CodeMirror 3.14. * Allow no snippet name or code to be set. * Prevented an error on fresh multisite installations. @@ -1315,7 +1428,8 @@ ### Added * Added German translation thanks to [David Decker](https://deckerweb.de) -* Allow or deny site administrators access to snippet admin menus. Set your preference in the **Enable Administration Menus** setting under the *Settings > Network Settings* network admin menu. +* Allow or deny site administrators access to snippet admin menus. Set your preference in the **Enable Administration + Menus** setting under the *Settings > Network Settings* network admin menu. ### Changed * Updated PHP Documentation completely. [[View online](https://bungeshea.github.io/code-snippets/api)] @@ -1330,7 +1444,8 @@ ### Added * Added icon for the new MP6 admin UI ([#](https://wordpress.org/support/topic/icon-disappears-with-mp6)) -* Allow plugin to be activated on individual sites on multisite ([#](https://wordpress.org/support/topic/dont-work-at-multisite)) +* Allow plugin to be activated on individual sites on multisite + ([#](https://wordpress.org/support/topic/dont-work-at-multisite)) * Strip PHP tags from the beginning and end of a snippet on save ([#](https://wordpress.org/support/topic/php-tags)) * Change label in admin menu when editing a snippet. @@ -1346,12 +1461,14 @@ * Removed HTML, CSS and JavaScript CodeMirror modes that were messing things up. ### Fixed -* Fixed a bug with saving snippets per page option ([#](https://wordpress.org/support/topic/plugin-code-snippets-snippets-per-page-does-not-work#post-3710991)) +* Fixed a bug with saving snippets per page option + ([#](https://wordpress.org/support/topic/plugin-code-snippets-snippets-per-page-does-not-work#post-3710991)) ## [1.6.1] (2012-12-29) ### Fixed -* Fixed a bug with permissions not being applied on install ([#](https://wordpress.org/support/topic/permissions-problem-after-install)) +* Fixed a bug with permissions not being applied on install + ([#](https://wordpress.org/support/topic/permissions-problem-after-install)) * Fixed a bug in the uninstall method ([#](https://wordpress.org/support/topic/bug-in-delete-script)) ## [1.6.0] (2012-12-22) @@ -1378,19 +1495,22 @@ ### Added * Added custom capabilities. -* Added 'Export to PHP' feature. ([#](https://wordpress.org/support/topic/plugin-code-snippets-suggestion-bulk-export-to-php)) +* Added 'Export to PHP' feature. + ([#](https://wordpress.org/support/topic/plugin-code-snippets-suggestion-bulk-export-to-php)) * Added i18n. ### Changed * Updated CodeMirror to version 2.33. * Updated the 'Manage Snippets' page to use the WP_List_Table class: - - Added 'Screen Options' tab to 'Manage Snippets' page. - - Added search capability to 'Manage Snippets' page. - - Added views to easily filter activated, deactivated and recently activated snippets. - - Added ID column to 'Manage Snippets' page. - - Added sortable name and ID column on 'Manage Snippets' page ([#](https://wordpress.org/support/topic/plugin-code-snippets-suggestion-sort-by-snippet-name)) + - Added 'Screen Options' tab to 'Manage Snippets' page. + - Added search capability to 'Manage Snippets' page. + - Added views to easily filter activated, deactivated and recently activated snippets. + - Added ID column to 'Manage Snippets' page. + - Added sortable name and ID column on 'Manage Snippets' page + ([#](https://wordpress.org/support/topic/plugin-code-snippets-suggestion-sort-by-snippet-name)) * Improved API. -* Lengthened snippet name field to 64 characters. ([#](https://wordpress.org/support/topic/plugin-code-snippets-snippet-title-limited-to-36-characters)) +* Lengthened snippet name field to 64 characters. + ([#](https://wordpress.org/support/topic/plugin-code-snippets-snippet-title-limited-to-36-characters)) ## [1.4.0] (2012-08-20) @@ -1434,9 +1554,11 @@ ## [1.1.0] (2012-06-24) ### Fixed -* Fixed a permissions bug with `DISALLOW_FILE_EDIT` being set to true. ([#](https://wordpress.org/support/topic/plugin-code-snippets-cant-add-new)) +* Fixed a permissions bug with `DISALLOW_FILE_EDIT` being set to true. + ([#](https://wordpress.org/support/topic/plugin-code-snippets-cant-add-new)) * Fixed a bug with the page title reading 'Add New Snippet' on the 'Edit Snippets' page. -* Fixed a bug not allowing the plugin to be Network Activated. ([#](https://wordpress.org/support/topic/plugin-code-snippets-network-activate-does-not-create-snippets-tables)) +* Fixed a bug not allowing the plugin to be Network Activated. + ([#](https://wordpress.org/support/topic/plugin-code-snippets-network-activate-does-not-create-snippets-tables)) ## [1.0.0] (2012-06-13) @@ -1446,22 +1568,6 @@ [brandonjp]: https://github.com/brandonjp [unreleased]: https://github.com/codesnippetspro/code-snippets/tree/core -[3.10.0]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.10.0 -[3.9.6]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.9.6 -[3.9.5]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.9.5 -[3.9.4]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.9.4 -[3.9.3]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.9.3 -[3.9.2]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.9.2 -[3.9.1]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.9.1 -[3.9.0]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.9.0 -[3.9.0-beta.2]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.9.0-beta.2 -[3.9.0-beta.1]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.9.0-beta.1 -[3.8.2]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.8.2 -[3.8.1]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.8.1 -[3.8.0]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.8.0 -[3.7.1-beta.3]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.7.1-beta.3 -[3.7.1-beta.2]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.7.1-beta.2 -[3.7.1-beta.1]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.7.1-beta.1 [3.7.0]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.7.0 [3.6.7]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.6.7 [3.6.6.1]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.6.6.1 diff --git a/config/webpack/webpack-js.ts b/config/webpack/webpack-js.ts index 62981acea..6c51a3ce5 100644 --- a/config/webpack/webpack-js.ts +++ b/config/webpack/webpack-js.ts @@ -24,7 +24,6 @@ const babelConfig = { export const jsWebpackConfig: Configuration = { entry: { - 'admin-bar': `${SOURCE_DIR}/admin-bar.ts`, 'edit': { import: `${SOURCE_DIR}/edit.ts`, dependOn: 'editor' }, 'editor': `${SOURCE_DIR}/editor.ts`, 'feedback': `${SOURCE_DIR}/feedback.ts`, @@ -35,6 +34,7 @@ export const jsWebpackConfig: Configuration = { 'mce': `${SOURCE_DIR}/mce.ts`, 'prism': `${SOURCE_DIR}/prism.ts`, 'settings': { import: `${SOURCE_DIR}/settings.ts`, dependOn: 'editor' }, + 'admin-bar': `${SOURCE_DIR}/admin-bar.ts`, 'welcome': `${SOURCE_DIR}/welcome.ts` }, output: { diff --git a/package-lock.json b/package-lock.json index 9f98a02e3..8e437da30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "code-snippets", - "version": "4.0.0-beta.1", + "version": "4.0.0-beta.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "code-snippets", - "version": "4.0.0-beta.1", + "version": "4.0.0-beta.12", "license": "GPL-2.0-or-later", "dependencies": { "@codemirror/fold": "^0.19.4", @@ -23,7 +23,8 @@ "prismjs": "^1.29.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-select": "^5.10.0" + "react-select": "^5.10.0", + "uuid": "^11.1.0" }, "devDependencies": { "@axe-core/playwright": "^4.11.2", @@ -99,15 +100,23 @@ "node": ">=6.0.0" } }, - "node_modules/@ariakit/core": { - "version": "0.4.14", - "license": "MIT" + "node_modules/@ariakit/components": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@ariakit/components/-/components-0.1.9.tgz", + "integrity": "sha512-Rmj5gcdfNQ4r4z5FzHHeC9OFRu9HSCVXDCDz8ak/vNHBrGmjeZ6Q3LOup61Eh+/GsT6cc2TDIQ6i8X1aB6y0BA==", + "license": "MIT", + "dependencies": { + "@ariakit/store": "0.1.7", + "@ariakit/utils": "0.1.5" + } }, "node_modules/@ariakit/react": { - "version": "0.4.15", + "version": "0.4.36", + "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.36.tgz", + "integrity": "sha512-QKxSc6KvTHObB9khmaQlr6XOvjKyZSHr4JWnZTDwRFDgUAzz79a+p2vApCHFlqadqV1QeGezap5cxHrOysoNOg==", "license": "MIT", "dependencies": { - "@ariakit/react-core": "0.4.15" + "@ariakit/react-components": "0.4.0" }, "funding": { "type": "opencollective", @@ -118,19 +127,67 @@ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/@ariakit/react-core": { - "version": "0.4.15", + "node_modules/@ariakit/react-components": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@ariakit/react-components/-/react-components-0.4.0.tgz", + "integrity": "sha512-dYymiYvAbyu6a/ehqYN2ZeieiMSYt0CMAj2BnDRUI/dZ9vLF32cv9PQMcgXXyqn0ILpqu79PQs53vVEZ+Z+0rQ==", "license": "MIT", "dependencies": { - "@ariakit/core": "0.4.14", - "@floating-ui/dom": "^1.0.0", - "use-sync-external-store": "^1.2.0" + "@ariakit/components": "0.1.9", + "@ariakit/react-store": "0.1.8", + "@ariakit/react-utils": "0.2.3", + "@ariakit/store": "0.1.7", + "@ariakit/utils": "0.1.5", + "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@ariakit/react-store": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@ariakit/react-store/-/react-store-0.1.8.tgz", + "integrity": "sha512-VYZ1LTUVMrNUi4jP37Npvhe3mcAzKdznqnBSVeh1Jjsbcgw0JlN88oy6UpdoJPvLKWOZHVYr62Sqn7xE0GrsqQ==", + "license": "MIT", + "dependencies": { + "@ariakit/react-utils": "0.2.3", + "@ariakit/store": "0.1.7", + "@ariakit/utils": "0.1.5", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@ariakit/react-utils": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@ariakit/react-utils/-/react-utils-0.2.3.tgz", + "integrity": "sha512-fDaheb/7QEusanZb2oRT7mO55GTpQUyBOdjvQF5RPh3/CM15lm0TejLKN5bl1obmU5HRuByvckQmQvSyr8n/dw==", + "license": "MIT", + "dependencies": { + "@ariakit/store": "0.1.7", + "@ariakit/utils": "0.1.5" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@ariakit/store": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@ariakit/store/-/store-0.1.7.tgz", + "integrity": "sha512-/GcxscA9QTo2F+IFbFPvoyj1N8hzXBnaYsQt9UxRiJgCFPQ2jIe4i6QgPXdOZEZUuqYdyuvjcQrg7MDm9vpvCA==", + "license": "MIT", + "dependencies": { + "@ariakit/utils": "0.1.5" + } + }, + "node_modules/@ariakit/utils": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@ariakit/utils/-/utils-0.1.5.tgz", + "integrity": "sha512-BQebYH9nV1VZttZwoq/fsxcxIJjc8oW2bNV6yJDHLZ8OF8UtM15drFN0JOL8Jwn7jeeD7Ev+tIC8pUijJEditQ==", + "license": "MIT" + }, "node_modules/@axe-core/playwright": { "version": "4.11.3", "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.3.tgz", @@ -1994,14 +2051,16 @@ } }, "node_modules/@emotion/css": { - "version": "11.11.2", + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.13.5.tgz", + "integrity": "sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w==", "license": "MIT", "dependencies": { - "@emotion/babel-plugin": "^11.11.0", - "@emotion/cache": "^11.11.0", - "@emotion/serialize": "^1.1.2", - "@emotion/sheet": "^1.2.2", - "@emotion/utils": "^1.2.1" + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2" } }, "node_modules/@emotion/hash": { @@ -2057,7 +2116,9 @@ "license": "MIT" }, "node_modules/@emotion/styled": { - "version": "11.14.0", + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.3", @@ -2263,25 +2324,31 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.5.0", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.1.3" + "@floating-ui/utils": "^0.2.12" } }, "node_modules/@floating-ui/dom": { - "version": "1.5.3", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.4.2", - "@floating-ui/utils": "^0.1.3" + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.1", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.0.0" + "@floating-ui/dom": "^1.8.0" }, "peerDependencies": { "react": ">=16.8.0", @@ -2289,7 +2356,9 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.1.6", + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", "license": "MIT" }, "node_modules/@humanfs/core": { @@ -4146,13 +4215,13 @@ } }, "node_modules/@wordpress/a11y": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@wordpress/a11y/-/a11y-4.40.0.tgz", - "integrity": "sha512-WhBuBgJTvanbBMNeflgCvwQLOU9ToITdYSzOvWg0kzz1i/e138NlCxrVpcXGUc6MQulduKhOWOtjizSdotaQRA==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@wordpress/a11y/-/a11y-4.52.0.tgz", + "integrity": "sha512-0KlDa/vSASriu84+z+a/XA2teaau6t6rCH6PEqMXoW5a6EbP2e/REpZRumbu0VzV5MRO5kpxfD+fVYNYz9BdlA==", "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/dom-ready": "^4.40.0", - "@wordpress/i18n": "^6.13.0" + "@wordpress/dom-ready": "^4.52.0", + "@wordpress/i18n": "^6.25.0" }, "engines": { "node": ">=18.12.0", @@ -4160,15 +4229,14 @@ } }, "node_modules/@wordpress/a11y/node_modules/@wordpress/i18n": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.13.0.tgz", - "integrity": "sha512-Yx882uFxcg6QpB13fv8UhvM6k5NwMQGfNXKB9SVSNL/APvDWn2m/n4n+5GZYi+wOV+KJLojQZbdRpHWCnX/jFg==", + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.25.0.tgz", + "integrity": "sha512-UcyUT8CJgEkhjzEGTVV6kw0Qi4OraF0I9J9FBLmTda/1Wi1o6rLAXZBm2aJrP740ezFxPneHH92aQF4Z5xZqcQ==", "license": "GPL-2.0-or-later", "dependencies": { "@tannin/sprintf": "^1.3.2", - "@wordpress/hooks": "^4.40.0", + "@wordpress/hooks": "^4.52.0", "gettext-parser": "^1.3.1", - "memize": "^2.1.0", "tannin": "^1.2.0" }, "bin": { @@ -4275,6 +4343,20 @@ "react-dom": "^18.0.0" } }, + "node_modules/@wordpress/components/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@wordpress/compose": { "version": "7.40.0", "resolved": "https://registry.npmjs.org/@wordpress/compose/-/compose-7.40.0.tgz", @@ -4303,19 +4385,19 @@ } }, "node_modules/@wordpress/data": { - "version": "10.40.0", - "resolved": "https://registry.npmjs.org/@wordpress/data/-/data-10.40.0.tgz", - "integrity": "sha512-wwqkMc9iLteRO1zNxL/R3COWnijsdC5TIjenmd2JivReUmdA4ulAN3Tq7QiHkhwOV4jzZkuWW7DgR2ynxf55lw==", + "version": "10.52.0", + "resolved": "https://registry.npmjs.org/@wordpress/data/-/data-10.52.0.tgz", + "integrity": "sha512-7Nx66TfUkYZDdXL4wn2/DjIJOYRsKdi8Gvv24Warv9rZ1pwTNoFXFDagvc3UoCOjn0pM8+pFNbd7a9TJH8oQzg==", "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/compose": "^7.40.0", - "@wordpress/deprecated": "^4.40.0", - "@wordpress/element": "^6.40.0", - "@wordpress/is-shallow-equal": "^5.40.0", - "@wordpress/priority-queue": "^3.40.0", - "@wordpress/private-apis": "^1.40.0", - "@wordpress/redux-routine": "^5.40.0", - "deepmerge": "^4.3.0", + "@wordpress/compose": "^8.5.0", + "@wordpress/deprecated": "^4.52.0", + "@wordpress/element": "^8.4.0", + "@wordpress/is-shallow-equal": "^5.52.0", + "@wordpress/priority-queue": "^3.52.0", + "@wordpress/private-apis": "^1.52.0", + "@wordpress/redux-routine": "^5.52.0", + "deepmerge": "^4.3.1", "equivalent-key-map": "^0.2.2", "is-plain-object": "^5.0.0", "is-promise": "^4.0.0", @@ -4328,16 +4410,75 @@ "npm": ">=8.19.2" }, "peerDependencies": { - "react": "^18.0.0" + "@types/react": "^18 || ^19", + "react": "^18 || ^19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@wordpress/data/node_modules/@wordpress/compose": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@wordpress/compose/-/compose-8.5.0.tgz", + "integrity": "sha512-giYS22Tbhtr3Lj4lK6lxzGqV6EkM2QeVBiP7+c9r2F9rnQN3F6A8cN4gQM5sVGdgIOlps9tw+dsn9OHzIFVwIg==", + "license": "GPL-2.0-or-later", + "dependencies": { + "@types/mousetrap": "^1.6.8", + "@wordpress/deprecated": "^4.52.0", + "@wordpress/dom": "^4.52.0", + "@wordpress/element": "^8.4.0", + "@wordpress/is-shallow-equal": "^5.52.0", + "@wordpress/keycodes": "^4.52.0", + "@wordpress/priority-queue": "^3.52.0", + "@wordpress/private-apis": "^1.52.0", + "@wordpress/undo-manager": "^1.52.0", + "change-case": "^4.1.2", + "mousetrap": "^1.6.5", + "use-memo-one": "^1.1.1" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "@types/react": "^18 || ^19", + "react": "^18 || ^19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@wordpress/data/node_modules/@wordpress/element": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-8.4.0.tgz", + "integrity": "sha512-/LzW+0MpSmKfYiESoXUQtjBPqOPyvqUm17GigQR5c0Z8Wj2NiklP72x4XaF02uSkau1ru1Z9lP8NWEGjwBwLsA==", + "license": "GPL-2.0-or-later", + "dependencies": { + "@types/react": "^18.3.27", + "@types/react-dom": "^18.3.1", + "@wordpress/deprecated": "^4.52.0", + "@wordpress/escape-html": "^3.52.0", + "change-case": "^4.1.2", + "is-plain-object": "^5.0.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" } }, "node_modules/@wordpress/date": { - "version": "5.43.0", - "resolved": "https://registry.npmjs.org/@wordpress/date/-/date-5.43.0.tgz", - "integrity": "sha512-8DiFlE7YzP7F/P59Hr6h5fWJxJlvt6eZgU1C7huM9XhANh8Y3dZfepsySL6K7h1yE66SQDSq07cEefFQgJW31g==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@wordpress/date/-/date-5.52.0.tgz", + "integrity": "sha512-JRAXv2CQwUiJq9k2n/iUqblo9rX3LUFJ03lA4zRy4+bxcSiMdpXvYrpZfUTp3W4I1Oc/fPZIYQY0HB3XNyjUWw==", "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/deprecated": "^4.43.0", + "@wordpress/deprecated": "^4.52.0", "moment": "^2.29.4", "moment-timezone": "^0.5.40" }, @@ -4347,12 +4488,12 @@ } }, "node_modules/@wordpress/deprecated": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@wordpress/deprecated/-/deprecated-4.43.0.tgz", - "integrity": "sha512-Pxn+nUmCVAaKBiZun2tEVweVdevMvWFWyCRqIqsAKdWCLsD8Uk6o27EwXc1u8BlO65VmK8D2zF9uWKGKfdZbCw==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@wordpress/deprecated/-/deprecated-4.52.0.tgz", + "integrity": "sha512-94oHBKPty4pp6L5510LWDwJwNqFhikxLfwN6VUcDOQzj3itkc0MQ4ZQhmsfBy3Y6IRxSGx+p2bjSQvA4D+M96A==", "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/hooks": "^4.43.0" + "@wordpress/hooks": "^4.52.0" }, "engines": { "node": ">=18.12.0", @@ -4360,12 +4501,12 @@ } }, "node_modules/@wordpress/dom": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@wordpress/dom/-/dom-4.40.0.tgz", - "integrity": "sha512-JBF1sRjJMFgLn0pet0tmPzO1kNaa35/DwAAtG81zzjikctR1PzE3EK8o6ZGPtUY1sTa9l7aB1Lxfcum/eroyRg==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@wordpress/dom/-/dom-4.52.0.tgz", + "integrity": "sha512-LUY9nI9h6blk4xBuG83KgyfTnx7PMs/g6ZVzY/SOaCcOc2P3zOfzacADq/vxQydbZpcwtLM6juzgnNta6AX95w==", "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/deprecated": "^4.40.0" + "@wordpress/deprecated": "^4.52.0" }, "engines": { "node": ">=18.12.0", @@ -4373,9 +4514,9 @@ } }, "node_modules/@wordpress/dom-ready": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@wordpress/dom-ready/-/dom-ready-4.40.0.tgz", - "integrity": "sha512-mHVy4P6yc0XLmGgnccxptMKg83TwcbYKfYrQH8pTcIu43P24zONTd44eZFjkfz7c/b+RLJg1Kj+d5mKh1xqH1A==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@wordpress/dom-ready/-/dom-ready-4.52.0.tgz", + "integrity": "sha512-dAsch1oeyRV4kwoQuqdr6dXisGAs09OtvGOk09Ke+QC0fTTPzxdKUw+c4M1nk0AmCeyZfqAqYYGCMm4n241QeQ==", "license": "GPL-2.0-or-later", "engines": { "node": ">=18.12.0", @@ -4486,9 +4627,9 @@ } }, "node_modules/@wordpress/escape-html": { - "version": "3.40.0", - "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-3.40.0.tgz", - "integrity": "sha512-DD6xWVbnw4fGGgO6DFDTJiLj52om0OG4cYHLz7ZhuipmOlEUGljPYOcrj8uxtlh5EFrqHCIPkOya+qQXUHUSBw==", + "version": "3.52.0", + "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-3.52.0.tgz", + "integrity": "sha512-sW8X839Xu8aiJlCGF48MM3iYrcXspuiY0By8iTpXKpnUdd2u0SL9HRR0ALSAsOf2ujOeWm/x58ODMSchovRWGw==", "license": "GPL-2.0-or-later", "engines": { "node": ">=18.12.0", @@ -4496,9 +4637,9 @@ } }, "node_modules/@wordpress/hooks": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-4.43.0.tgz", - "integrity": "sha512-BY7GPjEwhOlgkavVak40E3RtA8Z9ehydqTZckRoesMRjXYfxKSzr1C1FT4wAPS5uXM1pNlWivfofMaJjVNQu5w==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-4.52.0.tgz", + "integrity": "sha512-EbV/nJTerhqwNW3DLvvGutJfNyXcmBHXuWyJpv1NypzT80k21jPGP79HBE5Z0A2oAI2kIBp6Klaa4O8uEjq/sw==", "license": "GPL-2.0-or-later", "engines": { "node": ">=18.12.0", @@ -4506,9 +4647,9 @@ } }, "node_modules/@wordpress/html-entities": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@wordpress/html-entities/-/html-entities-4.40.0.tgz", - "integrity": "sha512-bsJrwZk22On8gNhUd84yyWKt/nrNZtACNZpXmkpyue/oTlFqNenLfhqRkvTKJzjbLxrrcUPsXlskbPcS7mxwTQ==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@wordpress/html-entities/-/html-entities-4.52.0.tgz", + "integrity": "sha512-SM0XH+EgCi20Ptqv5WGkM8d2CAlThWRO/BVMeXzu6RLlcHKLMXNQZvW2waXCZtUpto8hlVZT9Dzg8z37sj4OiQ==", "license": "GPL-2.0-or-later", "engines": { "node": ">=18.12.0", @@ -4552,9 +4693,9 @@ } }, "node_modules/@wordpress/is-shallow-equal": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@wordpress/is-shallow-equal/-/is-shallow-equal-5.40.0.tgz", - "integrity": "sha512-IU11xOcHIGqDLxx9X+8RIk4WFo0qqba0bpeLqrVKsQXNGjP7tXSo2ufylxE9K9CEYXFMF0C65k83XpRZtEkA8g==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@wordpress/is-shallow-equal/-/is-shallow-equal-5.52.0.tgz", + "integrity": "sha512-LMIBLLTZ+vvq0DlYebL4d9XaCu16PNnhPGSn8QqsMPmBzwBYOjO0/btMLpYbs9rRc8k1QKBDWvw8eddvRVsYxA==", "license": "GPL-2.0-or-later", "engines": { "node": ">=18.12.0", @@ -4562,28 +4703,35 @@ } }, "node_modules/@wordpress/keycodes": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-4.40.0.tgz", - "integrity": "sha512-laLkfjwkhMdreCl/KQdHucBIQAYwSjkyk3BToq/PCrcxFJBwWK2NgEtSl/t1CEw2HJwe0H2ne3FEWtipY4iDrA==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-4.52.0.tgz", + "integrity": "sha512-KRvwViTTdxUDgikYaCm+qnXSH2UlFuCDjAIvtjjcen8oRviSkeMv6bPn1T2vqFRKcVgt88xTtKlvI8l5U99o2Q==", "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/i18n": "^6.13.0" + "@wordpress/i18n": "^6.25.0" }, "engines": { "node": ">=18.12.0", "npm": ">=8.19.2" + }, + "peerDependencies": { + "@types/react": "^18 || ^19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, "node_modules/@wordpress/keycodes/node_modules/@wordpress/i18n": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.13.0.tgz", - "integrity": "sha512-Yx882uFxcg6QpB13fv8UhvM6k5NwMQGfNXKB9SVSNL/APvDWn2m/n4n+5GZYi+wOV+KJLojQZbdRpHWCnX/jFg==", + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.25.0.tgz", + "integrity": "sha512-UcyUT8CJgEkhjzEGTVV6kw0Qi4OraF0I9J9FBLmTda/1Wi1o6rLAXZBm2aJrP740ezFxPneHH92aQF4Z5xZqcQ==", "license": "GPL-2.0-or-later", "dependencies": { "@tannin/sprintf": "^1.3.2", - "@wordpress/hooks": "^4.40.0", + "@wordpress/hooks": "^4.52.0", "gettext-parser": "^1.3.1", - "memize": "^2.1.0", "tannin": "^1.2.0" }, "bin": { @@ -4595,12 +4743,12 @@ } }, "node_modules/@wordpress/primitives": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@wordpress/primitives/-/primitives-4.40.0.tgz", - "integrity": "sha512-0gOw3n3kSUsAPo91xNDS9J4GGTrNXU90XmuWn7mNfXAl5uRAMRnxgkfL+pwd0ng0rmdPtjPqrJpljnP2oy3K2w==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@wordpress/primitives/-/primitives-4.52.0.tgz", + "integrity": "sha512-rgB+Q1i97AY8AECLAEhvV/bGw9sOV6CKtUaY90t7nuv6M+ZgXqwQLRRWVaXWFUsthcZ2y4sTIbKfxwVJV+JQ6g==", "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/element": "^6.40.0", + "@wordpress/element": "^8.4.0", "clsx": "^2.1.1" }, "engines": { @@ -4608,13 +4756,39 @@ "npm": ">=8.19.2" }, "peerDependencies": { - "react": "^18.0.0" + "@types/react": "^18 || ^19", + "react": "^18 || ^19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@wordpress/primitives/node_modules/@wordpress/element": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-8.4.0.tgz", + "integrity": "sha512-/LzW+0MpSmKfYiESoXUQtjBPqOPyvqUm17GigQR5c0Z8Wj2NiklP72x4XaF02uSkau1ru1Z9lP8NWEGjwBwLsA==", + "license": "GPL-2.0-or-later", + "dependencies": { + "@types/react": "^18.3.27", + "@types/react-dom": "^18.3.1", + "@wordpress/deprecated": "^4.52.0", + "@wordpress/escape-html": "^3.52.0", + "change-case": "^4.1.2", + "is-plain-object": "^5.0.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" } }, "node_modules/@wordpress/priority-queue": { - "version": "3.40.0", - "resolved": "https://registry.npmjs.org/@wordpress/priority-queue/-/priority-queue-3.40.0.tgz", - "integrity": "sha512-85km9+I7RWi7P73BU/yom41gpdu0watdQ1GscQhQBel6BjHOXO5qWG6P9i3sEH47bz7EyO248l4LC/h8oHqpfQ==", + "version": "3.52.0", + "resolved": "https://registry.npmjs.org/@wordpress/priority-queue/-/priority-queue-3.52.0.tgz", + "integrity": "sha512-aYnVXxV5j4D73tRld2n1D3G6cKLfSwKWcTnktJ7XlmiswW7xlaAqvn4FyBjwmIe20pZw0A7xLH5xCuQcqOM7nw==", "license": "GPL-2.0-or-later", "dependencies": { "requestidlecallback": "^0.3.0" @@ -4625,9 +4799,9 @@ } }, "node_modules/@wordpress/private-apis": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/@wordpress/private-apis/-/private-apis-1.40.0.tgz", - "integrity": "sha512-68cwZKVq8Xy8GBzKoDRuV4b3pQ4nJFItY689HXp+poc0XXrnAeC4ZhjeSgS1qGRpFo6RVvLjjcaZsN2OrSSMvQ==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@wordpress/private-apis/-/private-apis-1.52.0.tgz", + "integrity": "sha512-gFcmBXSti73Y4uEx++5qUNDaH/o0/2ijrHEJkY5e/df4RtmxVSQOIVxftRiI4m9thCWfJMoehMMreJxhwx6Qtg==", "license": "GPL-2.0-or-later", "engines": { "node": ">=18.12.0", @@ -4635,9 +4809,9 @@ } }, "node_modules/@wordpress/redux-routine": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@wordpress/redux-routine/-/redux-routine-5.40.0.tgz", - "integrity": "sha512-V+c1yCBl4i7qvRsWtQpGevbFCGtrRlzDe++4bwnrYJUiu79wbSXWRrmiSFr/EQie2KNM680t2MeFcfO7nsDVoA==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@wordpress/redux-routine/-/redux-routine-5.52.0.tgz", + "integrity": "sha512-MpyNAKpfQAk8IWJ3Cwzc/p1dVEUl/iYxL6GCTX/y7K3c8qYd7csU4o5kuK9lfeqgVVI+d0y6rzBZiTaL2Z6Ojw==", "license": "GPL-2.0-or-later", "dependencies": { "is-plain-object": "^5.0.0", @@ -4653,42 +4827,99 @@ } }, "node_modules/@wordpress/rich-text": { - "version": "7.40.0", - "resolved": "https://registry.npmjs.org/@wordpress/rich-text/-/rich-text-7.40.0.tgz", - "integrity": "sha512-eHImTvzPEg4GWAuzcagyc2tArc6neA2sbqvybpd5JzhEpgv/Q0zcKwLfUKI05kYaaPI/Rg5WXgeXDxjGYpq5hA==", + "version": "7.52.0", + "resolved": "https://registry.npmjs.org/@wordpress/rich-text/-/rich-text-7.52.0.tgz", + "integrity": "sha512-zyd7w4hs8TV+nWrC/hbULg4lKJQyxpd5AolFfOHAJmKM2LrU/Lvlobz9oyGuTrZ/HXdkFs3wzPMwS/VGydSjHw==", "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/a11y": "^4.40.0", - "@wordpress/compose": "^7.40.0", - "@wordpress/data": "^10.40.0", - "@wordpress/deprecated": "^4.40.0", - "@wordpress/dom": "^4.40.0", - "@wordpress/element": "^6.40.0", - "@wordpress/escape-html": "^3.40.0", - "@wordpress/i18n": "^6.13.0", - "@wordpress/keycodes": "^4.40.0", - "@wordpress/private-apis": "^1.40.0", - "colord": "2.9.3", - "memize": "^2.1.0" + "@wordpress/a11y": "^4.52.0", + "@wordpress/compose": "^8.5.0", + "@wordpress/data": "^10.52.0", + "@wordpress/deprecated": "^4.52.0", + "@wordpress/dom": "^4.52.0", + "@wordpress/element": "^8.4.0", + "@wordpress/escape-html": "^3.52.0", + "@wordpress/i18n": "^6.25.0", + "@wordpress/keycodes": "^4.52.0", + "@wordpress/private-apis": "^1.52.0", + "colord": "^2.9.3" }, "engines": { "node": ">=18.12.0", "npm": ">=8.19.2" }, "peerDependencies": { - "react": "^18.0.0" + "@types/react": "^18 || ^19", + "react": "^18 || ^19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@wordpress/rich-text/node_modules/@wordpress/compose": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@wordpress/compose/-/compose-8.5.0.tgz", + "integrity": "sha512-giYS22Tbhtr3Lj4lK6lxzGqV6EkM2QeVBiP7+c9r2F9rnQN3F6A8cN4gQM5sVGdgIOlps9tw+dsn9OHzIFVwIg==", + "license": "GPL-2.0-or-later", + "dependencies": { + "@types/mousetrap": "^1.6.8", + "@wordpress/deprecated": "^4.52.0", + "@wordpress/dom": "^4.52.0", + "@wordpress/element": "^8.4.0", + "@wordpress/is-shallow-equal": "^5.52.0", + "@wordpress/keycodes": "^4.52.0", + "@wordpress/priority-queue": "^3.52.0", + "@wordpress/private-apis": "^1.52.0", + "@wordpress/undo-manager": "^1.52.0", + "change-case": "^4.1.2", + "mousetrap": "^1.6.5", + "use-memo-one": "^1.1.1" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "@types/react": "^18 || ^19", + "react": "^18 || ^19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@wordpress/rich-text/node_modules/@wordpress/element": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-8.4.0.tgz", + "integrity": "sha512-/LzW+0MpSmKfYiESoXUQtjBPqOPyvqUm17GigQR5c0Z8Wj2NiklP72x4XaF02uSkau1ru1Z9lP8NWEGjwBwLsA==", + "license": "GPL-2.0-or-later", + "dependencies": { + "@types/react": "^18.3.27", + "@types/react-dom": "^18.3.1", + "@wordpress/deprecated": "^4.52.0", + "@wordpress/escape-html": "^3.52.0", + "change-case": "^4.1.2", + "is-plain-object": "^5.0.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" } }, "node_modules/@wordpress/rich-text/node_modules/@wordpress/i18n": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.13.0.tgz", - "integrity": "sha512-Yx882uFxcg6QpB13fv8UhvM6k5NwMQGfNXKB9SVSNL/APvDWn2m/n4n+5GZYi+wOV+KJLojQZbdRpHWCnX/jFg==", + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.25.0.tgz", + "integrity": "sha512-UcyUT8CJgEkhjzEGTVV6kw0Qi4OraF0I9J9FBLmTda/1Wi1o6rLAXZBm2aJrP740ezFxPneHH92aQF4Z5xZqcQ==", "license": "GPL-2.0-or-later", "dependencies": { "@tannin/sprintf": "^1.3.2", - "@wordpress/hooks": "^4.40.0", + "@wordpress/hooks": "^4.52.0", "gettext-parser": "^1.3.1", - "memize": "^2.1.0", "tannin": "^1.2.0" }, "bin": { @@ -4700,12 +4931,12 @@ } }, "node_modules/@wordpress/undo-manager": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/@wordpress/undo-manager/-/undo-manager-1.40.0.tgz", - "integrity": "sha512-QvhHke/bVaOSPeaV5mNvsuIQpc2dJFDhXZ7gUnpuzyuNHh74Xk6Ar0vvYcfXiALst4ejKqWCoKOBi7ve1h2ppg==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@wordpress/undo-manager/-/undo-manager-1.52.0.tgz", + "integrity": "sha512-6hI0WWDOtLLVEkWqQokCoQRz5Pba9UNtgt5MV4hHGe5x0mzsowj+GWHedppf8UCVZ2ex3cde7fThaRKMCackrQ==", "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/is-shallow-equal": "^5.40.0" + "@wordpress/is-shallow-equal": "^5.52.0" }, "engines": { "node": ">=18.12.0", @@ -4713,9 +4944,9 @@ } }, "node_modules/@wordpress/url": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-4.40.0.tgz", - "integrity": "sha512-DVAJlW7bdocKfQp8G7tS73vnobAC8TBbIHHdxeLQKwzT8mOkG4W/rpzN2KTxkiJKFXUu5in4F8a6T+Cy/Lt1eQ==", + "version": "4.52.0", + "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-4.52.0.tgz", + "integrity": "sha512-4cSo0dUHiOQB2DiiPkoVIqP5aoGJdIj32LD9XgG41yeh7xG6ALdDA1I/883WFRoX2JIYySFj208MzopURDH7Yg==", "license": "GPL-2.0-or-later", "dependencies": { "remove-accents": "^0.5.0" @@ -4726,9 +4957,9 @@ } }, "node_modules/@wordpress/warning": { - "version": "3.40.0", - "resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-3.40.0.tgz", - "integrity": "sha512-0l3OFa1Z+UdhWRRHX9JWWKofo7Lbi2MqOFzzzn0MC26HOyfieQycjLVLNVNXaaodIKUhap6uDQq+JXbbHm881A==", + "version": "3.52.0", + "resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-3.52.0.tgz", + "integrity": "sha512-BjJ+Jte1g2nMITI1IHq0NwMpm/qlFOV1L+gNe0vFQmGKEcXiQ0DjrHQNtA8hV+Mgt9zSWRNS71Fgonht3r72Ew==", "license": "GPL-2.0-or-later", "engines": { "node": ">=18.12.0", @@ -8036,15 +8267,19 @@ } }, "node_modules/framer-motion": { - "version": "11.3.28", + "version": "11.18.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz", + "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==", "license": "MIT", "dependencies": { + "motion-dom": "^11.18.1", + "motion-utils": "^11.18.1", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/is-prop-valid": { @@ -9873,7 +10108,9 @@ "license": "CC0-1.0" }, "node_modules/memize": { - "version": "2.1.0", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/memize/-/memize-2.1.1.tgz", + "integrity": "sha512-8Nl+i9S5D6KXnruM03Jgjb+LwSupvR13WBr4hJegaaEyobvowCVupi79y2WSiWvO1mzBWxPwEYE5feCe8vyA5w==", "license": "MIT" }, "node_modules/memoize-one": { @@ -10032,6 +10269,21 @@ "node": "*" } }, + "node_modules/motion-dom": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", + "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==", + "license": "MIT", + "dependencies": { + "motion-utils": "^11.18.1" + } + }, + "node_modules/motion-utils": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz", + "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==", + "license": "MIT" + }, "node_modules/mousetrap": { "version": "1.6.5", "resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.5.tgz", @@ -13713,7 +13965,9 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.4.0", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -13725,14 +13979,16 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "9.0.1", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/v8-compile-cache-lib": { diff --git a/package.json b/package.json index 4d395c85b..d576992ff 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "code-snippets", "description": "Manage code snippets running on a WordPress-powered site through a graphical interface.", "homepage": "https://codesnippets.pro", - "version": "4.0.0-beta.1", + "version": "4.0.0-beta.12", "main": "src/dist/edit.js", "directories": { "test": "tests" @@ -63,7 +63,8 @@ "prismjs": "^1.29.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-select": "^5.10.0" + "react-select": "^5.10.0", + "uuid": "^11.1.0" }, "devDependencies": { "@axe-core/playwright": "^4.11.2", diff --git a/scripts/test-setup-playwright.ts b/scripts/test-setup-playwright.ts index 7ebdcd3b4..18d88b869 100644 --- a/scripts/test-setup-playwright.ts +++ b/scripts/test-setup-playwright.ts @@ -1,7 +1,7 @@ #!/usr/bin/env ts-node import { execFileSync } from 'node:child_process' -import { readFileSync } from 'node:fs' +import { existsSync, readFileSync } from 'node:fs' import { resolve } from 'node:path' const run = (cmd: string, args: readonly string[]) => { @@ -10,6 +10,22 @@ const run = (cmd: string, args: readonly string[]) => { const runWpEnvCli = (args: readonly string[]) => run('npx', ['wp-env', 'run', 'cli', ...args]) +const loadEnvFile = (): void => { + const envPath = resolve(process.cwd(), '.env') + + if (!existsSync(envPath)) { + return + } + + for (const line of readFileSync(envPath, 'utf8').split('\n')) { + const match = /^\s*(?[\w.-]+)\s*=\s*(?.*?)\s*$/u.exec(line) + + if (match?.groups && !(match.groups.key in process.env)) { + process.env[match.groups.key] = match.groups.value.replace(/^["']|["']$/u, '') + } + } +} + const getPluginSlug = (): string => { const prefix = 'wp-content/plugins/' const config = <{ mappings?: Record }>JSON.parse(readFileSync(resolve(process.cwd(), '.wp-env.json'), 'utf8')) @@ -29,6 +45,8 @@ const main = () => { // - force enable_flat_files=false so the Playwright setup test can flip it to true // - delete all DB snippets with an E2E prefix (keeps list clean across runs) + loadEnvFile() + runWpEnvCli(['sh', '-lc', 'rm -rf wp-content/code-snippets']) runWpEnvCli(['wp', 'plugin', 'activate', getPluginSlug()]) diff --git a/src/code-snippets.php b/src/code-snippets.php index 3f78bd0e5..539a75dfe 100644 --- a/src/code-snippets.php +++ b/src/code-snippets.php @@ -8,11 +8,11 @@ * License: GPL-2.0-or-later * License URI: license.txt * Text Domain: code-snippets - * Version: 4.0.0-beta.1 + * Version: 4.0.0-beta.12 * Requires PHP: 7.4 * Requires at least: 5.5 * - * @version 4.0.0-beta.1 + * @version 4.0.0-beta.12 * @package Code_Snippets * @author Shea Bunge * @copyright 2012-2026 Code Snippets Pro @@ -37,7 +37,7 @@ * * @const string */ - define( 'CODE_SNIPPETS_VERSION', '4.0.0-beta.1' ); + define( 'CODE_SNIPPETS_VERSION', '4.0.0-beta.12' ); /** * The full path to the main file of this plugin. diff --git a/src/css/admin-bar.scss b/src/css/admin-bar.scss index f3104e446..1ef7ceec1 100644 --- a/src/css/admin-bar.scss +++ b/src/css/admin-bar.scss @@ -112,8 +112,22 @@ opacity: 1; } + #wp-admin-bar-code-snippets-ran-on-this-page > .ab-sub-wrapper > .ab-submenu, #wp-admin-bar-code-snippets-active-snippets > .ab-sub-wrapper > .ab-submenu, #wp-admin-bar-code-snippets-inactive-snippets > .ab-sub-wrapper > .ab-submenu { padding-block-start: 0; } + + .code-snippets-kind-badge { + display: inline-block; + min-inline-size: 30px; + padding-inline: 5px; + border-radius: 3px; + color: #fff; + font-size: 10px; + font-weight: 600; + line-height: 1.7; + text-align: center; + text-transform: uppercase; + } } diff --git a/src/css/common/_badges.scss b/src/css/common/_badges.scss index 099aa47b3..cd0849693 100644 --- a/src/css/common/_badges.scss +++ b/src/css/common/_badges.scss @@ -77,6 +77,7 @@ $badges: ( cond: #22826f, core: #0ca0a9, pro: #f7e8e3 #df9279, + revisions: #50575e, cloud: #009fb4, bundles: #50575e, cloud_search: #d27c00, diff --git a/src/css/common/_buttons.scss b/src/css/common/_buttons.scss new file mode 100644 index 000000000..cb315d231 --- /dev/null +++ b/src/css/common/_buttons.scss @@ -0,0 +1,32 @@ +// A ` diff --git a/src/js/components/EditMenu/EditorSidebar/actions/ExportButtons.tsx b/src/js/components/EditMenu/EditorSidebar/actions/ExportButtons.tsx index 82d84aa71..a7b167465 100644 --- a/src/js/components/EditMenu/EditorSidebar/actions/ExportButtons.tsx +++ b/src/js/components/EditMenu/EditorSidebar/actions/ExportButtons.tsx @@ -11,10 +11,12 @@ import type { Snippet } from '../../../../types/Snippet' interface ExportButtonProps { name: string label: string + icon: string + title?: string makeRequest: (snippet: Snippet) => Promise } -const ExportButton: React.FC = ({ name, label, makeRequest }) => { +const ExportButton: React.FC = ({ name, label, icon, title, makeRequest }) => { const { snippet, isWorking, setIsWorking, handleRequestError } = useSnippetForm() const handleClick = () => { @@ -28,7 +30,8 @@ const ExportButton: React.FC = ({ name, label, makeRequest }) } return ( - ) @@ -43,13 +46,16 @@ export const ExportButtons: React.FC = () => { {window.CODE_SNIPPETS_EDIT?.enableDownloads && 'cond' !== getSnippetType(snippet) && ( )} diff --git a/src/js/components/EditMenu/EditorSidebar/controls/LockControl.tsx b/src/js/components/EditMenu/EditorSidebar/controls/LockControl.tsx index 84df9dcf1..6ad5800ca 100644 --- a/src/js/components/EditMenu/EditorSidebar/controls/LockControl.tsx +++ b/src/js/components/EditMenu/EditorSidebar/controls/LockControl.tsx @@ -41,6 +41,7 @@ export const LockControl: React.FC = () => { {snippet.locked ? QuickNav Active B', $active_titles[1] ); $_GET['code_snippets_ab_active_page'] = 2; @@ -241,7 +245,7 @@ public function test_snippet_listings_paginate_and_respect_query_arg(): void { $active_titles_page_2 = array_values( array_filter( $active_titles_page_2, static fn( $title ) => false !== strpos( $title, 'QuickNav Active' ) ) ); $this->assertCount( 1, $active_titles_page_2 ); - $this->assertStringContainsString( '(PHP) QuickNav Active C', $active_titles_page_2[0] ); + $this->assertStringContainsString( '>PHP QuickNav Active C', $active_titles_page_2[0] ); $_GET['code_snippets_ab_inactive_page'] = 2; @@ -264,7 +268,7 @@ public function test_snippet_listings_paginate_and_respect_query_arg(): void { ); $this->assertCount( 1, $inactive_titles_page_2 ); - $this->assertStringContainsString( '(HTML) QuickNav Inactive Z HTML', $inactive_titles_page_2[0] ); + $this->assertStringContainsString( '>HTML QuickNav Inactive Z HTML', $inactive_titles_page_2[0] ); } /** diff --git a/tests/unit/Admin/Feedback_Panel_Test.php b/tests/unit/Admin/Feedback_Panel_Test.php index 861a28b06..842bf9623 100644 --- a/tests/unit/Admin/Feedback_Panel_Test.php +++ b/tests/unit/Admin/Feedback_Panel_Test.php @@ -210,7 +210,7 @@ public function test_the_search_url_is_localised(): void { $this->assertStringContainsString( sprintf( '"searchUrl":"%s"', rest_url( Feedback_REST_Controller::get_base_route() . '/search' ) ), - (string) $data + stripslashes( $data ) ); } diff --git a/tests/unit/Admin/Menus/Manage/Manage_Menu_Assets_Test.php b/tests/unit/Admin/Menus/Manage/Manage_Menu_Assets_Test.php index 00d1be7e9..507b000e3 100644 --- a/tests/unit/Admin/Menus/Manage/Manage_Menu_Assets_Test.php +++ b/tests/unit/Admin/Menus/Manage/Manage_Menu_Assets_Test.php @@ -61,7 +61,7 @@ public function test_enqueue_localizes_manage_data(): void { } /** - * The AI Agent demo receives the site name it personalises its snippet with. + * The AI Agent demo receives the site name it personalizes its snippet with. * * @return void */ diff --git a/tests/unit/Admin/Menus/Manage/Manage_Menu_Demo_Reset_Test.php b/tests/unit/Admin/Menus/Manage/Manage_Menu_Demo_Reset_Test.php index 587b9532c..6491186fd 100644 --- a/tests/unit/Admin/Menus/Manage/Manage_Menu_Demo_Reset_Test.php +++ b/tests/unit/Admin/Menus/Manage/Manage_Menu_Demo_Reset_Test.php @@ -4,6 +4,7 @@ use Code_Snippets\REST_API\Preferences\Demos_Seen_REST_Controller; use Code_Snippets\UnitTestCase; +use ReflectionException; use ReflectionMethod; use RuntimeException; @@ -63,6 +64,8 @@ public function capture_redirect( string $location ) { * @param string $nonce Nonce to present. * * @return bool Whether the handler redirected, which it only does after resetting. + * + * @throws ReflectionException Uses reflection to access a private method. */ private function reset_request( string $nonce ): bool { $_GET[ Manage_Menu::DEMO_RESET_PARAM ] = '1'; @@ -70,8 +73,12 @@ private function reset_request( string $nonce ): bool { // The admin bootstrap does not run under PHPUnit, so the menu is built here. $menu = new Manage_Menu(); + $method = new ReflectionMethod( $menu, 'maybe_reset_demos' ); - $method->setAccessible( true ); + + if ( version_compare( PHP_VERSION, '8.1', '<' ) ) { + $method->setAccessible( true ); + } try { $method->invoke( $menu ); diff --git a/tests/unit/Admin/Notice_Filter_Test.php b/tests/unit/Admin/Notice_Filter_Test.php index f0bff06ab..8b2eeb087 100644 --- a/tests/unit/Admin/Notice_Filter_Test.php +++ b/tests/unit/Admin/Notice_Filter_Test.php @@ -3,6 +3,8 @@ namespace Code_Snippets\Admin; use Code_Snippets\AdminUnitTestCase; +use Code_Snippets\Controller\Cloud_Auth_Controller; +use Code_Snippets\Model\Basic_Cloud_Connection; use function Code_Snippets\code_snippets; /** diff --git a/tests/unit/Authorship_Test.php b/tests/unit/Authorship_Test.php new file mode 100644 index 000000000..c461692ca --- /dev/null +++ b/tests/unit/Authorship_Test.php @@ -0,0 +1,144 @@ +user->create( + [ + 'role' => 'administrator', + 'display_name' => 'Ada Author', + ] + ); + self::$editor_id = $factory->user->create( + [ + 'role' => 'administrator', + 'display_name' => 'Ed Editor', + ] + ); + } + + /** + * Start each test with an empty snippets table. + */ + public function set_up() { + parent::set_up(); + + global $wpdb; + $table_name = code_snippets()->db->get_table_name(); + $wpdb->query( "TRUNCATE TABLE $table_name" ); + } + + /** + * Create a snippet as the given user and return the stored copy. + * + * @param int $user_id User to act as. + * @param string $name Snippet name. + * + * @return Snippet + */ + private function save_as( int $user_id, string $name ): Snippet { + wp_set_current_user( $user_id ); + + $snippet = save_snippet( + new Snippet( + [ + 'name' => $name, + 'code' => "echo 'hi';", + ] + ) + ); + + return get_snippet( $snippet->id ); + } + + /** + * Stamps both authorship columns with the current user on insert. + */ + public function test_save_stamps_author_on_insert() { + $stored = $this->save_as( self::$author_id, 'Authored' ); + + $this->assertSame( self::$author_id, $stored->created_by ); + $this->assertSame( self::$author_id, $stored->updated_by ); + } + + /** + * Advances updated_by on a later save by another user while created_by stays fixed. + */ + public function test_updated_by_advances_while_created_by_is_fixed() { + $snippet = $this->save_as( self::$author_id, 'Shared' ); + + wp_set_current_user( self::$editor_id ); + $snippet->name = 'Shared (edited)'; + save_snippet( $snippet ); + + $stored = get_snippet( $snippet->id ); + $this->assertSame( self::$author_id, $stored->created_by, 'created_by is fixed at insert' ); + $this->assertSame( self::$editor_id, $stored->updated_by, 'updated_by follows the latest editor' ); + } + + /** + * Resolves a user ID to a compact display object. + */ + public function test_resolver_returns_display_object() { + $author = get_snippet_author( self::$author_id ); + + $this->assertIsArray( $author ); + $this->assertSame( self::$author_id, $author['id'] ); + $this->assertSame( 'Ada Author', $author['display_name'] ); + $this->assertArrayHasKey( 'avatar_url', $author ); + } + + /** + * Returns null for an empty or unknown user ID. + */ + public function test_resolver_returns_null_for_unknown() { + $this->assertNull( get_snippet_author( 0 ) ); + $this->assertNull( get_snippet_author( 987654 ) ); + } + + /** + * Embeds a nested author object in the snippets REST response, not a raw ID. + */ + public function test_rest_response_embeds_nested_author() { + $stored = $this->save_as( self::$author_id, 'Rest Authored' ); + + $request = new WP_REST_Request( 'GET', '/code-snippets/v1/snippets/' . $stored->id ); + $data = rest_get_server()->response_to_data( rest_do_request( $request ), false ); + + $this->assertIsArray( $data['created_by'] ); + $this->assertSame( self::$author_id, $data['created_by']['id'] ); + $this->assertSame( 'Ada Author', $data['created_by']['display_name'] ); + $this->assertArrayHasKey( 'avatar_url', $data['created_by'] ); + } +} diff --git a/tests/unit/Core/Uninstaller_Test.php b/tests/unit/Core/Uninstaller_Test.php index 389c5422e..8004b0d5d 100644 --- a/tests/unit/Core/Uninstaller_Test.php +++ b/tests/unit/Core/Uninstaller_Test.php @@ -112,7 +112,15 @@ public function test_reinstall_after_incomplete_uninstall_keeps_existing_snippet * @return void */ public function test_complete_uninstall_removes_the_snippets_table_and_settings(): void { - $db = code_snippets()->db; + $table = code_snippets()->db->table; + $drop_table_query = ''; + $filter = static function ( string $query ) use ( &$drop_table_query ): string { + if ( 0 === strpos( $query, 'DROP TABLE' ) ) { + $drop_table_query = $query; + } + + return $query; + }; update_option( 'code_snippets_settings', @@ -121,9 +129,11 @@ public function test_complete_uninstall_removes_the_snippets_table_and_settings( ] ); + add_filter( 'query', $filter, 9 ); ( new Uninstaller() )->uninstall_plugin(); + remove_filter( 'query', $filter, 9 ); - $this->assertFalse( DB::table_exists( $db->table, true ) ); + $this->assertSame( "DROP TABLE IF EXISTS $table", $drop_table_query ); $this->assertFalse( get_option( 'code_snippets_settings' ) ); } } diff --git a/tests/unit/Model/Cloud_Snippets_Test.php b/tests/unit/Model/Cloud_Snippets_Test.php index 05f2b6a53..380d01b76 100644 --- a/tests/unit/Model/Cloud_Snippets_Test.php +++ b/tests/unit/Model/Cloud_Snippets_Test.php @@ -269,4 +269,57 @@ public function test_string_fields_normalise_non_string_values(): void { $this->assertSame( '', $snippet->created ); $this->assertSame( '20260101', $snippet->updated ); } + + /** + * The page size is read from the meta block alongside the other counts. + * + * @return void + */ + public function test_reads_per_page_from_meta(): void { + $result = Cloud_Snippets::unpack_api_response( + [ + 'snippets' => [], + 'meta' => [ + 'total' => 60, + 'total_pages' => 3, + 'page' => 1, + 'per_page' => 20, + ], + ] + ); + + $this->assertSame( 20, $result->per_page ); + } + + /** + * A requested page size wins over the reported one, so callers can compare it + * against a later request even when an older cloud omits it from the meta. + * + * @return void + */ + public function test_requested_per_page_overrides_meta(): void { + $without_meta = Cloud_Snippets::unpack_api_response( [ 'snippets' => [] ], 1, 50 ); + $this->assertSame( 50, $without_meta->per_page ); + + $with_meta = Cloud_Snippets::unpack_api_response( + [ + 'snippets' => [], + 'meta' => [ 'per_page' => 20 ], + ], + 1, + 50 + ); + $this->assertSame( 50, $with_meta->per_page ); + } + + /** + * Page size defaults to zero when neither the response nor the caller states one. + * + * @return void + */ + public function test_per_page_defaults_to_zero(): void { + $result = Cloud_Snippets::unpack_api_response( [ 'snippets' => [] ] ); + + $this->assertSame( 0, $result->per_page ); + } } diff --git a/tests/unit/Plugin_Test.php b/tests/unit/Plugin_Test.php index 16fe57ace..897bebd27 100644 --- a/tests/unit/Plugin_Test.php +++ b/tests/unit/Plugin_Test.php @@ -39,7 +39,11 @@ private function get_elementor_promotion_callbacks(): array { foreach ( $callbacks as $callback ) { $function = $callback['function']; - if ( is_array( $function ) && $function[0] instanceof Elementor_Editor && 'promotion_in_custom_css_section' === $function[1] ) { + if ( + is_array( $function ) + && $function[0] instanceof Elementor_Editor + && 'promotion_in_custom_css_section' === $function[1] + ) { $promotion_callbacks[] = $function; } } diff --git a/tests/unit/REST_API/REST_API_Cloud_Test.php b/tests/unit/REST_API/Cloud/Cloud_Snippets_REST_Controller_Test.php similarity index 80% rename from tests/unit/REST_API/REST_API_Cloud_Test.php rename to tests/unit/REST_API/Cloud/Cloud_Snippets_REST_Controller_Test.php index 3fffcb4f9..329148ce8 100644 --- a/tests/unit/REST_API/REST_API_Cloud_Test.php +++ b/tests/unit/REST_API/Cloud/Cloud_Snippets_REST_Controller_Test.php @@ -1,14 +1,14 @@ requested_url = ''; $this->rest_server = $wp_rest_server ?? null; + delete_user_option( $this->get_user_id(), 'snippets_per_page' ); + add_filter( 'pre_http_request', [ $this, 'mock_cloud_search_request' ], 10, 3 ); } @@ -69,6 +71,7 @@ public function tear_down() { remove_filter( 'pre_http_request', [ $this, 'mock_cloud_search_request' ] ); delete_user_option( $this->get_user_id(), 'snippets_per_page' ); + $wp_rest_server = $this->rest_server; parent::tear_down(); @@ -84,6 +87,30 @@ public function tear_down() { * @return mixed */ public function mock_cloud_search_request( $preempt, array $parsed_args, string $url ) { + if ( false !== strpos( $url, 'private/allsnippets' ) ) { + ++$this->codevault_request_count; + + return [ + 'headers' => [], + 'body' => wp_json_encode( + [ + 'snippets' => [], + 'cloud_id_rev' => [], + 'meta' => [ + 'total' => 0, + 'total_pages' => 0, + 'page' => 1, + ], + ] + ), + 'response' => [ + 'code' => 200, + 'message' => 'OK', + ], + 'cookies' => [], + ]; + } + if ( false === strpos( $url, 'public/search' ) && false === strpos( $url, 'public/featured' ) ) { return $preempt; } @@ -145,17 +172,12 @@ public function mock_cloud_search_request( $preempt, array $parsed_args, string */ private function make_request( array $params, string $route = '' ): WP_REST_Response { global $wp_rest_server; - static $connection; - - if ( ! isset( $connection ) ) { - $connection = new Basic_Cloud_Connection(); - } $wp_rest_server = null; rest_get_server(); $request = new WP_REST_Request( 'GET', $this->endpoint . $route ); - $request->add_header( 'Access-Control', $connection->get_local_token() ); + $request->add_header( 'Access-Control', code_snippets()->cloud_connection->get_local_token() ); foreach ( $params as $key => $value ) { $request->set_param( $key, $value ); @@ -306,4 +328,49 @@ public function test_get_featured_items_reports_local_ids_for_downloaded_snippet $this->assertSame( 200, $response->get_status() ); $this->assertSame( $local->id, wp_list_pluck( $snippets, 'local_id', 'id' )[3] ?? null ); } + + /** + * The AI search method is forwarded to the cloud as s_method=ai. + */ + public function test_search_method_ai_is_forwarded_to_cloud(): void { + $response = $this->make_request( + [ + 'query' => 'make my site more secure', + 'searchMethod' => 'ai', + ] + ); + + parse_str( (string) wp_parse_url( $this->requested_url, PHP_URL_QUERY ), $query_args ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertSame( 'ai', $query_args['s_method'] ?? null ); + } + + /** + * With no method params, the search defaults to keyword matching (term). + */ + public function test_search_method_defaults_to_term(): void { + $this->make_request( [ 'query' => 'test' ] ); + + parse_str( (string) wp_parse_url( $this->requested_url, PHP_URL_QUERY ), $query_args ); + + $this->assertSame( 'term', $query_args['s_method'] ?? null ); + } + + /** + * A codevault search takes precedence over an AI search method. + */ + public function test_codevault_takes_precedence_over_ai(): void { + $this->make_request( + [ + 'query' => 'general', + 'searchByCodevault' => true, + 'searchMethod' => 'ai', + ] + ); + + parse_str( (string) wp_parse_url( $this->requested_url, PHP_URL_QUERY ), $query_args ); + + $this->assertSame( 'codevault', $query_args['s_method'] ?? null ); + } } diff --git a/tests/unit/REST_API/REST_API_Snippets_Shared_Network_Toggle_Test.php b/tests/unit/REST_API/Snippets/REST_API_Snippets_Shared_Network_Toggle_Test.php similarity index 99% rename from tests/unit/REST_API/REST_API_Snippets_Shared_Network_Toggle_Test.php rename to tests/unit/REST_API/Snippets/REST_API_Snippets_Shared_Network_Toggle_Test.php index 703c8eb11..e9f8cb2cd 100644 --- a/tests/unit/REST_API/REST_API_Snippets_Shared_Network_Toggle_Test.php +++ b/tests/unit/REST_API/Snippets/REST_API_Snippets_Shared_Network_Toggle_Test.php @@ -1,6 +1,6 @@ Date: Wed, 16 Sep 2026 19:15:46 +0300 Subject: [PATCH 08/19] Revert "Merge changes from core-beta branch" This reverts commit f7fa6219e88fdeca35cfd1a88c601e48f6587007. --- .gitignore | 7 - CHANGELOG.md | 296 ++++------ config/webpack/webpack-js.ts | 2 +- package-lock.json | 532 +++++------------- package.json | 5 +- scripts/test-setup-playwright.ts | 20 +- src/code-snippets.php | 6 +- src/css/admin-bar.scss | 14 - src/css/common/_badges.scss | 1 - src/css/common/_buttons.scss | 32 -- src/css/common/_list-table.scss | 27 - src/css/common/_modal.scss | 13 - src/css/common/_page-subtitle.scss | 13 - src/css/common/_subnav.scss | 5 +- src/css/common/_theme.scss | 34 -- src/css/common/_toolbar.scss | 70 +-- src/css/common/_tooltips.scss | 1 - src/css/common/_wp-admin.scss | 2 - src/css/common/list-table/_layout.scss | 23 +- src/css/common/list-table/_responsive.scss | 5 +- src/css/edit.scss | 3 +- src/css/edit/_conditions.scss | 5 +- src/css/edit/_form.scss | 3 +- src/css/edit/_gpt.scss | 28 + src/css/edit/_sidebar.scss | 145 ++--- src/css/import.scss | 1 - src/css/insights.scss | 1 - src/css/manage.scss | 12 +- src/css/manage/_ai-agent.scss | 46 +- src/css/manage/_blueprints.scss | 3 - src/css/manage/_cloud-community.scss | 8 +- src/css/manage/_snippets-table.scss | 109 +--- src/css/manage/blueprints/_detail.scss | 38 +- src/css/manage/blueprints/_form-layout.scss | 2 +- src/css/settings.scss | 4 +- .../ConditionModal/ConditionModalButton.tsx | 6 +- .../EditMenu/EditorSidebar/EditorSidebar.tsx | 11 +- .../EditorSidebar/actions/ExportButtons.tsx | 12 +- .../EditorSidebar/controls/LockControl.tsx | 1 - .../ManageMenu/CommunityCloud/CloudSearch.tsx | 85 ++- .../CommunityCloud/SearchResult.tsx} | 37 +- .../CommunityCloud/WithCloudSearchContext.tsx | 15 +- .../SnippetsTable/ManageSnippetCard.tsx | 38 +- .../ManageMenu/SnippetsTable/RowActions.tsx | 7 +- .../SnippetsTable/SnippetsTable.tsx | 6 +- .../ManageMenu/SnippetsTable/TableColumns.tsx | 42 +- .../WithFilteredSnippetsContext.tsx | 15 +- .../common/ListTable/TableNavigation.tsx | 2 +- .../common/LoadingStatusNotices.tsx | 5 +- src/js/components/common/SubnavTabs.tsx | 78 +-- src/js/components/common/Toolbar.tsx | 2 +- .../cloud/CloudSnippetDownloadButton.tsx | 4 +- .../common/icons/CloudUpdateIcon.tsx | 13 + .../common/snippets/ConfirmDeleteDialog.tsx | 17 +- .../common/snippets/SnippetPreviewModal.tsx | 29 +- src/js/hooks/useSnippetsAPI.tsx | 6 +- src/js/services/settings/tabs.ts | 12 +- src/js/types/Snippet.ts | 8 - src/js/types/schema/SnippetSchema.ts | 11 +- src/js/utils/errors.ts | 10 +- src/js/utils/restAPI.ts | 11 - src/js/utils/screen.ts | 3 - src/js/utils/snippets/objects.ts | 19 +- src/js/utils/snippets/snippets.ts | 2 +- src/js/utils/urls.ts | 12 +- src/php/Admin/Feedback_Panel.php | 2 +- src/php/Admin/Menus/Admin_Menu.php | 17 +- src/php/Admin/Menus/Edit_Menu.php | 4 +- .../Admin/Menus/Insights/Insights_Summary.php | 2 +- src/php/Admin/Menus/Manage/Manage_Menu.php | 9 +- .../Admin/Menus/Manage/Manage_Menu_Assets.php | 4 - .../Manage/Manage_Menu_Screen_Options.php | 17 +- src/php/Admin/Menus/Settings_Menu.php | 32 +- src/php/Client/Feedback_Client.php | 6 +- src/php/Core/DB.php | 39 +- src/php/Core/Uninstaller.php | 3 +- src/php/Core/load.php | 13 +- .../Handlers/Functions_Snippet_Handler.php | 10 +- src/php/Flat_Files/Snippet_Files.php | 10 +- src/php/Integration/Admin_Bar.php | 48 +- src/php/Integration/Evaluate_Content.php | 72 +-- src/php/Integration/Evaluate_Functions.php | 42 +- src/php/Integration/Shortcodes.php | 58 +- src/php/Model/Basic_Cloud_Connection.php | 3 +- src/php/Model/Cloud_Snippets.php | 21 +- src/php/Model/Feedback_Connection.php | 2 - src/php/Model/Model.php | 2 +- src/php/Model/Snippet.php | 7 - src/php/Plugin.php | 26 +- .../Cloud/Cloud_Snippets_REST_Controller.php | 37 +- .../Feedback/Feedback_REST_Controller.php | 2 +- .../Demos_Seen_REST_Controller.php | 2 +- .../Snippet_View_REST_Controller.php | 2 +- .../Snippets/Snippets_REST_Controller.php | 45 +- src/php/Settings/Setting_Field.php | 15 +- src/php/Settings/Settings_Fields.php | 42 +- src/php/Settings/Settings_Layout.php | 17 +- src/php/Settings/settings.php | 17 +- src/php/Utils/Validator.php | 4 +- src/php/Utils/editor.php | 2 +- src/php/Utils/options.php | 26 +- src/php/snippet-ops.php | 74 +-- src/readme.txt | 9 + tests/e2e/code-snippets-evaluation.spec.ts | 144 ----- tests/e2e/code-snippets-list.spec.ts | 62 -- tests/e2e/helpers/SnippetsTestHelper.ts | 117 ---- tests/e2e/helpers/constants.ts | 6 +- tests/unit/Admin/Admin_Bar_Test.php | 14 +- tests/unit/Admin/Feedback_Panel_Test.php | 2 +- .../Menus/Manage/Manage_Menu_Assets_Test.php | 2 +- .../Manage/Manage_Menu_Demo_Reset_Test.php | 9 +- tests/unit/Admin/Notice_Filter_Test.php | 2 - tests/unit/Authorship_Test.php | 144 ----- tests/unit/Core/Uninstaller_Test.php | 14 +- tests/unit/Model/Cloud_Snippets_Test.php | 53 -- tests/unit/Plugin_Test.php | 6 +- ...oller_Test.php => REST_API_Cloud_Test.php} | 85 +-- ...PI_Snippets_Shared_Network_Toggle_Test.php | 2 +- .../{Snippets => }/REST_API_Snippets_Test.php | 2 +- 119 files changed, 855 insertions(+), 2587 deletions(-) delete mode 100644 src/css/common/_buttons.scss delete mode 100644 src/css/common/_page-subtitle.scss delete mode 100644 src/css/manage/_blueprints.scss rename src/js/components/{common/cloud/CloudSnippetCard.tsx => ManageMenu/CommunityCloud/SearchResult.tsx} (75%) create mode 100644 src/js/components/common/icons/CloudUpdateIcon.tsx delete mode 100644 tests/unit/Authorship_Test.php rename tests/unit/REST_API/{Cloud/Cloud_Snippets_REST_Controller_Test.php => REST_API_Cloud_Test.php} (80%) rename tests/unit/REST_API/{Snippets => }/REST_API_Snippets_Shared_Network_Toggle_Test.php (99%) rename tests/unit/REST_API/{Snippets => }/REST_API_Snippets_Test.php (99%) diff --git a/.gitignore b/.gitignore index d52c8fce4..453a38177 100644 --- a/.gitignore +++ b/.gitignore @@ -40,10 +40,3 @@ tmp /.env .superpowers/ docs/superpowers/ -/docs/ - -# Local dev only (wp-env mu-plugin mappings, staging access) -/dev/ - -# Local wp-env overrides — may contain credentials, never commit -.wp-env.override.json diff --git a/CHANGELOG.md b/CHANGELOG.md index e94b17f42..73d4e8faa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,83 +3,22 @@ ## [4.0.0] (UPCOMING) ### Added -* AI Agent for building snippets from a description: it proposes a plan you can refine or approve before any code is - written, and can revise the snippets it created. (PRO) -* Snippet revisions, with a history of past versions, a side-by-side diff against the current code, and one-click - restore. (PRO) -* Display conditions for controlling where and when a snippet runs, without writing the checks by hand. (PRO) -* Per-role snippet permissions, so you can decide which roles may view, edit, activate, or delete snippets. (PRO) -* Blueprints for setting up a site from a saved collection of snippets and settings. (PRO) -* Flat file storage, for keeping snippets as files so they can be version-controlled alongside the rest of a site. (PRO) -* "Ran on this page" tracking, showing which snippets actually executed on the page you are viewing. (PRO) -* Natural-language search in Community Cloud, so you can describe what you need instead of guessing keywords. (PRO) -* Snippet deployment from Code Snippets Cloud to connected sites, including deploying several snippets under one shared - display condition. (PRO) -* Installing a cloud bundle onto a connected site as a standalone plugin. (PRO) -* Updating Code Snippets Pro on a connected site from the cloud dashboard. (PRO) -* Drift detection, which reports when a snippet on the site no longer matches the copy stored in the cloud. (PRO) -* New admin interface for managing snippets, with a cleaner layout, faster interactions, and a more consistent - experience across plugin screens. (PRO) -* Card view for browsing snippets, with a view switcher on the snippets table and Community Cloud. (PRO) -* Snippet preview modal for viewing snippet code from the snippets table without opening the editor. (PRO) -* Automatic hiding of unrelated admin notices from other plugins on Code Snippets screens. (PRO) -* Admin bar snippet drawer, based on the Deckerweb Snippets workflow, for quick access to snippets and Safe Mode from - the WordPress admin bar. (PRO) -* Snippet locking to help prevent accidental edits or deletion of important snippets. Props - to https://github.com/mgiannopoulos24. (PRO) -* Improved screen options on the main snippets table, including controls for visible columns and truncating long snippet - names or descriptions. (PRO) -* Bulk actions and bulk code download support in the redesigned snippets table. (PRO) -* Featured snippets and improved browsing in Community Cloud. (PRO) -* WordPress modern theme admin styling compatibility. (PRO) -* Clearer accessibility labels, headings, tab markup, table checkboxes, sort buttons, copy buttons, and drag-and-drop - upload controls. (PRO) -* Feedback reporter for sending bug reports, feature requests and general feedback from the plugin screens, with an - optional summary of the site environment so the team can reproduce the problem. (PRO) - -### Changed -* Redesigned the main snippets table with improved search, filtering, sorting, pagination, row actions, and bulk - selection. (PRO) -* Improved the snippet import and migration experience, including clearer file upload handling and third-party plugin - migration flows. (PRO) -* Improved snippet error handling so activation failures, validation errors, and stack traces are easier to understand. - (PRO) -* Improved Community Cloud search and filtering, including server-side filters, better result loading, and clearer empty - states. (PRO) -* Updated the welcome screen, toolbar, import screen, and cloud screens to match the new admin experience. (PRO) -* Updated internal plugin architecture to a cleaner PSR-4 structure for better long-term maintainability. (PRO) -* Improved accessibility across the snippets table, import screen, migration flow, Community Cloud, welcome screen, - toolbar, dialogs, tooltips, and code editor. (PRO) -* Improved colour contrast and reduced-motion support across admin screens. (PRO) -* Faster loading of large code vaults, which are now fetched a page at a time instead of all at once. (PRO) - -### Fixed -* Fixed AI conversations from one connected site being visible from another. (PRO) -* Fixed a snippet list stored in Code Snippets Cloud being emptied when the site was temporarily unable to read its own - snippets. (PRO) -* Fixed REST API server error responses on missing snippets. (PRO) -* Fixed redundant frontend logic, improving overall performance. (PRO) -* Fixed Community Cloud search results and pagination to respect WordPress screen options. (PRO) -* Fixed snippet saving and activation feedback to improve validation and runtime error display. (PRO) -* Fixed downloaded Community Cloud snippets appearing as not downloaded after a page reload. (PRO) -* Fixed network snippet lookups using the wrong database table on multisite. (PRO) -* Fixed the inactive snippets count including trashed snippets. (PRO) -* Fixed featured Community Cloud snippets failing to load with some cloud API responses. (PRO) -* Fixed bulk actions in Community Cloud running against an empty selection, so selected snippets were never downloaded. - (PRO) +* AI Agent demo: a guided, scripted walkthrough of the Pro AI Agent that plans, builds, and refines a welcome banner snippet named after your site. Runs entirely inside the plugin — no data leaves your site and no snippets are added to your library. +* Blueprints demo: a guided, scripted walkthrough of Pro Blueprints that steps through the "Create a Shortcode" blueprint and confirms the snippet it would generate. Runs entirely inside the plugin — no code is generated and nothing is saved. +* Cloud Library demo: a guided, scripted walkthrough of the Pro Cloud Library, showing how a cloud snippet is previewed, downloaded inactive, and then kept in sync. Runs entirely inside the plugin — the snippets shown are examples and nothing is downloaded. +* "New" badges on the AI Agent, Blueprints, and Cloud Library toolbar tabs, which soften once each demo walkthrough has been watched. ## [3.10.2] (2026-09-01) ### Added -* Added a confirmation flow for run-once snippet execution and hardened the handler to prevent failed or confusing - actions. +* Added a confirmation flow for run-once snippet execution and hardened the handler to prevent failed or confusing actions. ### Changed * Snippet names now respect the row truncation Screen Option in the admin list for better readability. * Version switching AJAX requests now validate the correct nonce, improving reliability when updating snippet versions. ### Fixed -* Fixed safe mode fatal errors caused by an undefined wp_get_current_user () call. +* Fixed safe mode fatal errors caused by an undefined wp_get_current_user() call. * Fixed PHP validation being triggered incorrectly when activating snippets in bulk. * Fixed saving issues after a user session expires. * Fixed warnings caused by aliased field names when reading modified snippet fields. @@ -94,8 +33,7 @@ ### Fixed * Fixed a fatal error affecting snippets that use a `namespace` or `declare` statement. * Fixed the snippets page rendering blank when another plugin's screen settings filter returned an invalid value. -* Fixed snippet saving on hosts that block REST API `PUT` and `PATCH` requests, by sending writes as `POST` with a - method override. +* Fixed snippet saving on hosts that block REST API `PUT` and `PATCH` requests, by sending writes as `POST` with a method override. * Fixed the Snippets List Order setting not being applied to the snippets list. * Fixed admin bar snippet scripts failing to load on the free version, including on the site front end. * Fixed snippet modified dates being sent without the correct UTC offset. @@ -106,35 +44,26 @@ ## [3.10.0] (2026-08-24) ### Added -* New admin interface for managing snippets, with a cleaner layout, faster interactions, and a more consistent - experience across plugin screens. +* New admin interface for managing snippets, with a cleaner layout, faster interactions, and a more consistent experience across plugin screens. * Card view for browsing snippets, with a view switcher on the snippets table and Community Cloud. * Snippet preview modal for viewing snippet code from the snippets table without opening the editor. * Automatic hiding of unrelated admin notices from other plugins on Code Snippets screens. -* Admin bar snippet drawer, based on the Deckerweb Snippets workflow, for quick access to snippets and Safe Mode from - the WordPress admin bar. -* Snippet locking to help prevent accidental edits or deletion of important snippets. Props - to https://github.com/mgiannopoulos24. -* Improved screen options on the main snippets table, including controls for visible columns and truncating long snippet - names or descriptions. +* Admin bar snippet drawer, based on the Deckerweb Snippets workflow, for quick access to snippets and Safe Mode from the WordPress admin bar. +* Snippet locking to help prevent accidental edits or deletion of important snippets. Props to https://github.com/mgiannopoulos24. +* Improved screen options on the main snippets table, including controls for visible columns and truncating long snippet names or descriptions. * Bulk actions and bulk code download support in the redesigned snippets table. * Featured snippets and improved browsing in Community Cloud. * WordPress modern theme admin styling compatibility. -* Clearer accessibility labels, headings, tab markup, table checkboxes, sort buttons, copy buttons, and drag-and-drop - upload controls. +* Clearer accessibility labels, headings, tab markup, table checkboxes, sort buttons, copy buttons, and drag-and-drop upload controls. ### Changed -* Redesigned the main snippets table with improved search, filtering, sorting, pagination, row actions, and bulk - selection. -* Improved the snippet import and migration experience, including clearer file upload handling and third-party plugin - migration flows. +* Redesigned the main snippets table with improved search, filtering, sorting, pagination, row actions, and bulk selection. +* Improved the snippet import and migration experience, including clearer file upload handling and third-party plugin migration flows. * Improved snippet error handling so activation failures, validation errors, and stack traces are easier to understand. -* Improved Community Cloud search and filtering, including server-side filters, better result loading, and clearer empty - states. +* Improved Community Cloud search and filtering, including server-side filters, better result loading, and clearer empty states. * Updated the welcome screen, toolbar, import screen, and cloud screens to match the new admin experience. * Updated internal plugin architecture to a cleaner PSR-4 structure for better long-term maintainability. -* Improved accessibility across the snippets table, import screen, migration flow, Community Cloud, welcome screen, - toolbar, dialogs, tooltips, and code editor. +* Improved accessibility across the snippets table, import screen, migration flow, Community Cloud, welcome screen, toolbar, dialogs, tooltips, and code editor. * Improved colour contrast and reduced-motion support across admin screens. ### Fixed @@ -162,8 +91,7 @@ ### Added * New import functionality to migrate snippets from file uploads with drag-and-drop interface. -* Support for importing snippets from other popular plugins (Header Footer Code Manager, Insert Headers and Footers, - Insert PHP Code Snippet). +* Support for importing snippets from other popular plugins (Header Footer Code Manager, Insert Headers and Footers, Insert PHP Code Snippet). * Enhanced file based execution support with improved multisite mode compatibility. ### Fixed @@ -174,8 +102,7 @@ ## [3.9.3] (2025-12-03) ### Added -* End-to-end tests to verify the toggle visual state in the snippets list page, improving UI verification and test - reliability. +* End-to-end tests to verify the toggle visual state in the snippets list page, improving UI verification and test reliability. ### Fixed * Restored missing styles styling and direction-aware layout from Manage menu. @@ -212,8 +139,7 @@ * Expanded Multisite Sharing settings for clearer control over network-wide snippet sharing ### Changed -* Modernized browser support targets and polished admin UI (clearer row-action badges, improved Pro badge hover, refined - active snippet name styling) +* Modernized browser support targets and polished admin UI (clearer row-action badges, improved Pro badge hover, refined active snippet name styling) ### Fixed * Fixed REST API pagination to return correct results and page counts @@ -325,16 +251,14 @@ * Improved UX of snippet activation toggle. ### Fixed -* Fetching active snippets on a multisite network now respects the 'priority' field above all else when ordering - snippets. +* Fetching active snippets on a multisite network now respects the 'priority' field above all else when ordering snippets. * Cloud search appears correctly and allows downloading snippets in the free version of Code Snippets. * Improved performance of loading admin menu icon. ## [3.6.9] (2025-02-17) ### Changed -* Updated `Cloud_API::get_bundles()` to properly check bundle data and return an empty array if no valid bundles are - present. +* Updated `Cloud_API::get_bundles()` to properly check bundle data and return an empty array if no valid bundles are present. * Refactored `Cloud_List_Table::fetch_snippets()` to always return a valid `Cloud_Snippets` instance. * Cleaned up bundle iteration code and improved translation handling in the bundles view. @@ -350,8 +274,7 @@ * Updated Freemius SDK to the latest version. (PRO) ### Removed -* Functionality allowing `[code_snippet]` shortcodes to be embedded recursively – it will be re-added in a future - version. +* Functionality allowing `[code_snippet]` shortcodes to be embedded recursively – it will be re-added in a future version. ### Fixed * Shortcodes embedded within `[code_snippet]` shortcodes not evaluating correctly. @@ -364,19 +287,15 @@ ### Added * Generated snippet shortcode tags will include the snippet name, for easier identification. -* Admin notices will dismiss automatically after five seconds. - ([#208](https://github.com/codesnippetspro/code-snippets/issues/208)) +* Admin notices will dismiss automatically after five seconds. ([#208](https://github.com/codesnippetspro/code-snippets/issues/208)) ### Changed * Updated CSS to use latest Sass features. -* Moved theme selector to just above editor preview on settings page (thanks to [brandonjp]). - ([#206](https://github.com/codesnippetspro/code-snippets/issues/206)) -* `[code_snippet]` shortcodes can now be nested within each other. - ([#198](https://github.com/codesnippetspro/code-snippets/issues/198)) +* Moved theme selector to just above editor preview on settings page (thanks to [brandonjp]). ([#206](https://github.com/codesnippetspro/code-snippets/issues/206)) +* `[code_snippet]` shortcodes can now be nested within each other. ([#198](https://github.com/codesnippetspro/code-snippets/issues/198)) ### Fixed -* Save buttons above editor did not follow usual validation process in Pro. (PRO) - ([#197](https://github.com/codesnippetspro/code-snippets/issues/197)) +* Save buttons above editor did not follow usual validation process in Pro. (PRO) ([#197](https://github.com/codesnippetspro/code-snippets/issues/197)) * Minor inconsistencies in consistent UI elements between Core and Pro. * Tags input not allowing input. ([#211](https://github.com/codesnippetspro/code-snippets/issues/211)) * Issue with Elementor source code widget. (PRO) ([#205](https://github.com/codesnippetspro/code-snippets/issues/205)) @@ -502,16 +421,13 @@ * Scroll new notices into view on edit menu. ### Fixed -* Error when attempting to update network shared snippets after - saving. [[#](https://wordpress.org/support/topic/activating-snippets-breaks-on-wordpress-6-3/)] +* Error when attempting to update network shared snippets after saving. [[#](https://wordpress.org/support/topic/activating-snippets-breaks-on-wordpress-6-3/)] ## [3.4.2] (2023-07-05) ### Fixed -* Issue causing export process to fail with fatal - error. [[#](https://wordpress.org/support/topic/critical-error-on-exporting-snippets/)] -* Type issue on `the_posts` filter when no posts - available. [[#](https://wordpress.org/support/topic/collision-with-plugin-xml-sitemap-google-news/)] +* Issue causing export process to fail with fatal error. [[#](https://wordpress.org/support/topic/critical-error-on-exporting-snippets/)] +* Type issue on `the_posts` filter when no posts available. [[#](https://wordpress.org/support/topic/collision-with-plugin-xml-sitemap-google-news/)] ## [3.4.1] (2023-06-29) @@ -519,18 +435,14 @@ * Added better debugging when calling REST API methods from the edit menu. ### Changed -* Escape special characters when sending snippet code through AJAX to avoid false-positives from security - modules. [[#](https://wordpress.org/support/topic/latest-3-4-0-ajax-bug-cannot-save-snippets-403-error/)] +* Escape special characters when sending snippet code through AJAX to avoid false-positives from security modules. [[#](https://wordpress.org/support/topic/latest-3-4-0-ajax-bug-cannot-save-snippets-403-error/)] * Only display the latest update or error notice on the edit page, instead of allowing them to stack. ### Fixed -* Undefined array key - error. [[#](https://wordpress.org/support/topic/after-updating-occasionally-getting-undefined-array-key-query/)] -* Potential type issue when loading - Prism. [[#](https://wordpress.org/support/topic/code-snippets-fatal-error-breaking-xml-sitemaps/)] +* Undefined array key error. [[#](https://wordpress.org/support/topic/after-updating-occasionally-getting-undefined-array-key-query/)] +* Potential type issue when loading Prism. [[#](https://wordpress.org/support/topic/code-snippets-fatal-error-breaking-xml-sitemaps/)] * Potential type issue when sorting snippets. [[#](https://github.com/codesnippetspro/code-snippets/issues/166)] -* Issue preventing asset revision numbers from updating correctly. - (PRO) [[#](https://github.com/codesnippetspro/code-snippets/issues/166)] +* Issue preventing asset revision numbers from updating correctly. (PRO) [[#](https://github.com/codesnippetspro/code-snippets/issues/166)] ## [3.4.0] (2023-05-17) @@ -541,10 +453,10 @@ ### Changed * Better compatibility with modern versions of PHP (7.0+). * Converted Edit/Add New Snippet page to use React: - - Converted action buttons to asynchronously use REST API endpoints through AJAX. - - Load page components dynamically through React. - - Added action notice queue system - - Replaced native alert dialog with proper React modal. + - Converted action buttons to asynchronously use REST API endpoints through AJAX. + - Load page components dynamically through React. + - Added action notice queue system + - Replaced native alert dialog with proper React modal. * Catch snippet execution errors to prevent site from crashing. * Display recent snippet errors in admin dashboard instead. * Updated editor block to use new REST API endpoints. (PRO) @@ -564,8 +476,7 @@ ### Added * Added additional editor shortcuts to list in tooltip. -* Filter for changing Snippets admin menu - position. [See this help article for more information.](https://codesnippets.pro/doc/snippets-menu-location/) +* Filter for changing Snippets admin menu position. [See this help article for more information.](https://codesnippets.pro/doc/snippets-menu-location/) * Ability to filter shortcode output. Thanks to contributions from [Jack Szwergold](https://github.com/JackSzwergold). ### Fixed @@ -582,12 +493,10 @@ ### Added * `Ctrl`+`/` or `Cmd`+`/` as shortcut for commenting out code in the snippet editor. -* Additional hooks to various snippet actions, thanks to contributions made - by [ancient-spirit](https://github.com/ancient-spirit). +* Additional hooks to various snippet actions, thanks to contributions made by [ancient-spirit](https://github.com/ancient-spirit). * Fold markers, additional keyboard shortcuts and keymap options to snippet editor, thanks to contributions made by [Amaral Krichman](https://github.com/karmaral). -* WP-CLI commands for retrieving, activating, deactivating, deleting, creating, updating, exporting and importing - snippets. +* WP-CLI commands for retrieving, activating, deactivating, deleting, creating, updating, exporting and importing snippets. ### Changed * Removed duplicate tables exist query. ([#](https://wordpress.org/support/topic/duplicate-queries-21)). @@ -606,8 +515,7 @@ * Support for multiple code styles in the source code Gutenberg editor block. (PRO) * Admin notice announcing release of Code Snippets Pro. * Button for copying shortcode text to clipboard. -* Option to choose from 44 different themes for the Prism code highlighter in the source editor block and Elementor - widget. (PRO) +* Option to choose from 44 different themes for the Prism code highlighter in the source editor block and Elementor widget. (PRO) ### Changed * Include Code Snippets CSS and JS source code in distributed package. @@ -726,16 +634,15 @@ ## [2.14.3] (2021-12-10) ### Fixed -* Potential security issue outputting snippets-safe-mode query variable value as-is. Thanks to Krzysztof Zając for - reporting. +* Potential security issue outputting snippets-safe-mode query variable value as-is. Thanks to Krzysztof Zając for reporting. ## [2.14.2] (2021-09-09) ### Added * Added translations: - - Spanish by [Ibidem Group](https://www.ibidemgroup.com) - - Urdu by [Samuel Badree](https://mobilemall.pk/) - - Greek by [Toni Bishop from Jrop](https://www.jrop.com/) + - Spanish by [Ibidem Group](https://www.ibidemgroup.com) + - Urdu by [Samuel Badree](https://mobilemall.pk/) + - Greek by [Toni Bishop from Jrop](https://www.jrop.com/) * Support for `:class` syntax to the code validator. * PHP8 support to the code linter. * Color picker feature to the code editor. @@ -758,8 +665,7 @@ * Code validator now supports `function_exists` and `class_exists` checks. * Code validator now supports anonymous functions. * Issue with saving the hidden columns setting. -* Replaced the outdated tag-it library with [tagger](https://github.com/jcubic/tagger) for powering the snippet tags - editor. +* Replaced the outdated tag-it library with [tagger](https://github.com/jcubic/tagger) for powering the snippet tags editor. ## [2.14.0] (2020-01-26) @@ -780,8 +686,7 @@ * Fixed a bug preventing the editor theme from being set to default. * Ensure that imported snippets are always inactive. * Check the referer on the import menu to prevent CSRF attacks. - Thanks to [Chloe with the Wordfence Threat Intelligence team](https://www.wordfence.com/blog/author/wfchloe/) for - reporting. + Thanks to [Chloe with the Wordfence Threat Intelligence team](https://www.wordfence.com/blog/author/wfchloe/) for reporting. * Ensure that individual snippet action links use proper verification. ## [2.13.3] (2019-03-13) @@ -828,8 +733,7 @@ ## [2.13.0] (2018-12-17) ### Added -* Search/replace functionality to the snippet - editor. [See here for a list of keyboard shortcuts.](https://codemirror.net/demo/search.html) [[#](https://wordpress.org/support/topic/feature-request-codemirror-search-and-replace/)] +* Search/replace functionality to the snippet editor. [See here for a list of keyboard shortcuts.](https://codemirror.net/demo/search.html) [[#](https://wordpress.org/support/topic/feature-request-codemirror-search-and-replace/)] * Option to make admin menu more compact. * Added additional styles to editor settings preview. * PHP linter to code editor. @@ -849,8 +753,7 @@ * CodeMirror updated to version 5.41.0. * Attempt to create database columns that might be missing after a table upgrade. * Streamlined upgrade process. -* Made search box appear at top of page on - mobile. [[#](https://wordpress.org/support/topic/small-modification-for-mobile-ux/)] +* Made search box appear at top of page on mobile. [[#](https://wordpress.org/support/topic/small-modification-for-mobile-ux/)] * Updated screenshots. ### Fixed @@ -907,7 +810,7 @@ ### Fixed * Prevent errors when trying to export no snippets. -* Use wp_json_encode () to encode export data. +* Use wp_json_encode() to encode export data. * Check both the file extension and MIME type of uploaded import files. ## [2.10.0] (2018-01-18) @@ -979,7 +882,7 @@ ### Changed * Moved code to disable snippet execution into a filter hook. -* execute_active_snippets () function updated with improved efficiency. +* execute_active_snippets() function updated with improved efficiency. * Renamed Snippet class to avoid name collisions with other plugins. * Don't hide output when executing a snippet. @@ -997,10 +900,8 @@ ## [2.8.6] (2017-05-14) ### Fixed -* Fixed snippet description field alias not mapping correctly, causing snippet descriptions to not be displayed in the - table or when editing a snippet. -* Ensured that get_snippets () function retrieves snippets with the correct 'network' setting. Fixes snippet edit links - in network admin. +* Fixed snippet description field alias not mapping correctly, causing snippet descriptions to not be displayed in the table or when editing a snippet. +* Ensured that get_snippets() function retrieves snippets with the correct 'network' setting. Fixes snippet edit links in network admin. ## [2.8.5] (2017-05-13) @@ -1044,8 +945,7 @@ ### Fixed * Fixed admin menu items not translating. * Corrected editor alignment on RTL sites. ([#](https://wordpress.org/support/topic/suggestion-css-fix-for-rtl-sites/)) -* Fixed bulk actions running when Filter button is clicked. - ([#](https://wordpress.org/support/topic/bug-with-filtering-action-buttons/)) +* Fixed bulk actions running when Filter button is clicked. ([#](https://wordpress.org/support/topic/bug-with-filtering-action-buttons/)) ## [2.8.0] (2016-12-14) @@ -1084,8 +984,7 @@ * Updated CodeMirror to version 5.19.0. ### Security -* Ensured that the editor theme setting is properly validated. Thanks to [Netsparker](https://www.netsparker.com) for - reporting. +* Ensured that the editor theme setting is properly validated. Thanks to [Netsparker](https://www.netsparker.com) for reporting. * Ensured that snippet tags are properly escaped. Thanks to [Netsparker](https://www.netsparker.com) for reporting. ## [2.7.0] (2016-07-23) @@ -1101,11 +1000,9 @@ ### Fixed * Fixed plugin translations being loaded. * Fixed description field not being imported. -* Fixed issue with CodeMirror rubyblue - theme. [[#](https://wordpress.org/support/topic/a-problem-with-the-cursor-color-and-the-fix-that-worked-for-me)] +* Fixed issue with CodeMirror rubyblue theme. [[#](https://wordpress.org/support/topic/a-problem-with-the-cursor-color-and-the-fix-that-worked-for-me)] * Fixed snippet fields not importing. -* Fixed a minor XSS vulnerability discovered by Burak - Kelebek. [[#](https://wordpress.org/support/topic/security-vulnerability-20)] +* Fixed a minor XSS vulnerability discovered by Burak Kelebek. [[#](https://wordpress.org/support/topic/security-vulnerability-20)] ## [2.6.1] (2016-02-10) @@ -1167,8 +1064,7 @@ ## [2.4.1] (2015-09-17) ### Fixed -* Fixed CodeMirror themes not being detected on settings - page [[#](https://wordpress.org/support/topic/updated-to-240-now-i-cant-switch-theme)] +* Fixed CodeMirror themes not being detected on settings page [[#](https://wordpress.org/support/topic/updated-to-240-now-i-cant-switch-theme)] ## [2.4.0] (2015-09-17) @@ -1194,8 +1090,7 @@ ### Added * Added icons for admin and front-end snippets to manage table. -* Added filter switch to prevent a snippet from executing. - ([#25](https://github.com/codesnippetspro/code-snippets/issues/25)) +* Added filter switch to prevent a snippet from executing. ([#25](https://github.com/codesnippetspro/code-snippets/issues/25)) ### Changed * Improved settings retrieval by caching settings. @@ -1231,7 +1126,7 @@ ### Fixed * Resolved JavaScript error on edit snippet pages. -* Added polyfill for array_replace_recursive () function for PHP 5.2. +* Added polyfill for array_replace_recursive() function for PHP 5.2. ## [2.2.1] (2015-05-10) @@ -1304,8 +1199,7 @@ * Added Russian translation by Alexander Samsonov. * Added Slovak translation by [Ján Fajčák] from [WordPress Slovakia](https://wp.sk). * Added setting to always save and activate snippets by default. -* Added braces to single-line conditionals in line - with [new coding standards](https://make.wordpress.org/core/2013/11/13/proposed-coding-standards-change-always-require-braces/). +* Added braces to single-line conditionals in line with [new coding standards](https://make.wordpress.org/core/2013/11/13/proposed-coding-standards-change-always-require-braces/). ### Changed * Improved plugin file structure. @@ -1347,8 +1241,7 @@ ### Added * Added French translation thanks to translator [oWEB](http://office-web.net). -* Added 'Save & Deactivate' button to the edit snippet page. - ([#](https://wordpress.org/support/topic/deactivate-button-in-edit-snippet-page)) +* Added 'Save & Deactivate' button to the edit snippet page. ([#](https://wordpress.org/support/topic/deactivate-button-in-edit-snippet-page)) * Added nonce to edit snippet page. * Added a fallback MP6 icon. @@ -1358,10 +1251,8 @@ * Updated CodeMirror to version 3.19. * Updated WordPress.org plugin banner. * Add and remove network capabilities as super admins are added and removed. -* Replaced buggy trim `` functionality with a much more reliable regex method. - ([#](https://wordpress.org/support/topic/character-gets-cut)) -* Make the title of each snippet on the manage page a clickable link to edit the snippet - ([#](https://wordpress.org/support/topic/deactivate-button-in-edit-snippet-page?replies=9#post-4682757)) +* Replaced buggy trim `` functionality with a much more reliable regex method. ([#](https://wordpress.org/support/topic/character-gets-cut)) +* Make the title of each snippet on the manage page a clickable link to edit the snippet ([#](https://wordpress.org/support/topic/deactivate-button-in-edit-snippet-page?replies=9#post-4682757)) * Hide row actions on manage snippet page by default. * Use the proper WordPress database APIs consistently. * Rewritten export functionality. @@ -1372,9 +1263,7 @@ * Removed CodeMirror bundled with plugin. ### Fixed -* Fixed snippet failing to save when code contains `%` character, props - to [nikan06](https://wordpress.org/support/profile/nikan06). - ([#](https://wordpress.org/support/topic/percent-sign-bug)) +* Fixed snippet failing to save when code contains `%` character, props to [nikan06](https://wordpress.org/support/profile/nikan06). ([#](https://wordpress.org/support/topic/percent-sign-bug)) * Fixed HTML breaking in export files. ([#](https://wordpress.org/support/topic/import-problem-7)) * Fixed incorrect export filename. * Fixed CodeMirror incompatibility with the WP Editor plugin. @@ -1400,9 +1289,7 @@ * Added error message handling for import snippets page. ### Changed -* Improved database table creation method: on a single-site install, the snippets table will always be created. On a - multisite install, the network snippets table will always be created; the site-specific table will always be created - for the main site; for sub-sites the snippets table will only be created on a visit to a snippets admin page. +* Improved database table creation method: on a single-site install, the snippets table will always be created. On a multisite install, the network snippets table will always be created; the site-specific table will always be created for the main site; for sub-sites the snippets table will only be created on a visit to a snippets admin page. * Updated to CodeMirror 3.14. * Allow no snippet name or code to be set. * Prevented an error on fresh multisite installations. @@ -1428,8 +1315,7 @@ ### Added * Added German translation thanks to [David Decker](https://deckerweb.de) -* Allow or deny site administrators access to snippet admin menus. Set your preference in the **Enable Administration - Menus** setting under the *Settings > Network Settings* network admin menu. +* Allow or deny site administrators access to snippet admin menus. Set your preference in the **Enable Administration Menus** setting under the *Settings > Network Settings* network admin menu. ### Changed * Updated PHP Documentation completely. [[View online](https://bungeshea.github.io/code-snippets/api)] @@ -1444,8 +1330,7 @@ ### Added * Added icon for the new MP6 admin UI ([#](https://wordpress.org/support/topic/icon-disappears-with-mp6)) -* Allow plugin to be activated on individual sites on multisite - ([#](https://wordpress.org/support/topic/dont-work-at-multisite)) +* Allow plugin to be activated on individual sites on multisite ([#](https://wordpress.org/support/topic/dont-work-at-multisite)) * Strip PHP tags from the beginning and end of a snippet on save ([#](https://wordpress.org/support/topic/php-tags)) * Change label in admin menu when editing a snippet. @@ -1461,14 +1346,12 @@ * Removed HTML, CSS and JavaScript CodeMirror modes that were messing things up. ### Fixed -* Fixed a bug with saving snippets per page option - ([#](https://wordpress.org/support/topic/plugin-code-snippets-snippets-per-page-does-not-work#post-3710991)) +* Fixed a bug with saving snippets per page option ([#](https://wordpress.org/support/topic/plugin-code-snippets-snippets-per-page-does-not-work#post-3710991)) ## [1.6.1] (2012-12-29) ### Fixed -* Fixed a bug with permissions not being applied on install - ([#](https://wordpress.org/support/topic/permissions-problem-after-install)) +* Fixed a bug with permissions not being applied on install ([#](https://wordpress.org/support/topic/permissions-problem-after-install)) * Fixed a bug in the uninstall method ([#](https://wordpress.org/support/topic/bug-in-delete-script)) ## [1.6.0] (2012-12-22) @@ -1495,22 +1378,19 @@ ### Added * Added custom capabilities. -* Added 'Export to PHP' feature. - ([#](https://wordpress.org/support/topic/plugin-code-snippets-suggestion-bulk-export-to-php)) +* Added 'Export to PHP' feature. ([#](https://wordpress.org/support/topic/plugin-code-snippets-suggestion-bulk-export-to-php)) * Added i18n. ### Changed * Updated CodeMirror to version 2.33. * Updated the 'Manage Snippets' page to use the WP_List_Table class: - - Added 'Screen Options' tab to 'Manage Snippets' page. - - Added search capability to 'Manage Snippets' page. - - Added views to easily filter activated, deactivated and recently activated snippets. - - Added ID column to 'Manage Snippets' page. - - Added sortable name and ID column on 'Manage Snippets' page - ([#](https://wordpress.org/support/topic/plugin-code-snippets-suggestion-sort-by-snippet-name)) + - Added 'Screen Options' tab to 'Manage Snippets' page. + - Added search capability to 'Manage Snippets' page. + - Added views to easily filter activated, deactivated and recently activated snippets. + - Added ID column to 'Manage Snippets' page. + - Added sortable name and ID column on 'Manage Snippets' page ([#](https://wordpress.org/support/topic/plugin-code-snippets-suggestion-sort-by-snippet-name)) * Improved API. -* Lengthened snippet name field to 64 characters. - ([#](https://wordpress.org/support/topic/plugin-code-snippets-snippet-title-limited-to-36-characters)) +* Lengthened snippet name field to 64 characters. ([#](https://wordpress.org/support/topic/plugin-code-snippets-snippet-title-limited-to-36-characters)) ## [1.4.0] (2012-08-20) @@ -1554,11 +1434,9 @@ ## [1.1.0] (2012-06-24) ### Fixed -* Fixed a permissions bug with `DISALLOW_FILE_EDIT` being set to true. - ([#](https://wordpress.org/support/topic/plugin-code-snippets-cant-add-new)) +* Fixed a permissions bug with `DISALLOW_FILE_EDIT` being set to true. ([#](https://wordpress.org/support/topic/plugin-code-snippets-cant-add-new)) * Fixed a bug with the page title reading 'Add New Snippet' on the 'Edit Snippets' page. -* Fixed a bug not allowing the plugin to be Network Activated. - ([#](https://wordpress.org/support/topic/plugin-code-snippets-network-activate-does-not-create-snippets-tables)) +* Fixed a bug not allowing the plugin to be Network Activated. ([#](https://wordpress.org/support/topic/plugin-code-snippets-network-activate-does-not-create-snippets-tables)) ## [1.0.0] (2012-06-13) @@ -1568,6 +1446,22 @@ [brandonjp]: https://github.com/brandonjp [unreleased]: https://github.com/codesnippetspro/code-snippets/tree/core +[3.10.0]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.10.0 +[3.9.6]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.9.6 +[3.9.5]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.9.5 +[3.9.4]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.9.4 +[3.9.3]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.9.3 +[3.9.2]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.9.2 +[3.9.1]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.9.1 +[3.9.0]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.9.0 +[3.9.0-beta.2]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.9.0-beta.2 +[3.9.0-beta.1]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.9.0-beta.1 +[3.8.2]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.8.2 +[3.8.1]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.8.1 +[3.8.0]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.8.0 +[3.7.1-beta.3]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.7.1-beta.3 +[3.7.1-beta.2]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.7.1-beta.2 +[3.7.1-beta.1]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.7.1-beta.1 [3.7.0]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.7.0 [3.6.7]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.6.7 [3.6.6.1]: https://github.com/codesnippetspro/code-snippets/releases/tag/v3.6.6.1 diff --git a/config/webpack/webpack-js.ts b/config/webpack/webpack-js.ts index 6c51a3ce5..62981acea 100644 --- a/config/webpack/webpack-js.ts +++ b/config/webpack/webpack-js.ts @@ -24,6 +24,7 @@ const babelConfig = { export const jsWebpackConfig: Configuration = { entry: { + 'admin-bar': `${SOURCE_DIR}/admin-bar.ts`, 'edit': { import: `${SOURCE_DIR}/edit.ts`, dependOn: 'editor' }, 'editor': `${SOURCE_DIR}/editor.ts`, 'feedback': `${SOURCE_DIR}/feedback.ts`, @@ -34,7 +35,6 @@ export const jsWebpackConfig: Configuration = { 'mce': `${SOURCE_DIR}/mce.ts`, 'prism': `${SOURCE_DIR}/prism.ts`, 'settings': { import: `${SOURCE_DIR}/settings.ts`, dependOn: 'editor' }, - 'admin-bar': `${SOURCE_DIR}/admin-bar.ts`, 'welcome': `${SOURCE_DIR}/welcome.ts` }, output: { diff --git a/package-lock.json b/package-lock.json index 8e437da30..9f98a02e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "code-snippets", - "version": "4.0.0-beta.12", + "version": "4.0.0-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "code-snippets", - "version": "4.0.0-beta.12", + "version": "4.0.0-beta.1", "license": "GPL-2.0-or-later", "dependencies": { "@codemirror/fold": "^0.19.4", @@ -23,8 +23,7 @@ "prismjs": "^1.29.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-select": "^5.10.0", - "uuid": "^11.1.0" + "react-select": "^5.10.0" }, "devDependencies": { "@axe-core/playwright": "^4.11.2", @@ -100,23 +99,15 @@ "node": ">=6.0.0" } }, - "node_modules/@ariakit/components": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/@ariakit/components/-/components-0.1.9.tgz", - "integrity": "sha512-Rmj5gcdfNQ4r4z5FzHHeC9OFRu9HSCVXDCDz8ak/vNHBrGmjeZ6Q3LOup61Eh+/GsT6cc2TDIQ6i8X1aB6y0BA==", - "license": "MIT", - "dependencies": { - "@ariakit/store": "0.1.7", - "@ariakit/utils": "0.1.5" - } + "node_modules/@ariakit/core": { + "version": "0.4.14", + "license": "MIT" }, "node_modules/@ariakit/react": { - "version": "0.4.36", - "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.36.tgz", - "integrity": "sha512-QKxSc6KvTHObB9khmaQlr6XOvjKyZSHr4JWnZTDwRFDgUAzz79a+p2vApCHFlqadqV1QeGezap5cxHrOysoNOg==", + "version": "0.4.15", "license": "MIT", "dependencies": { - "@ariakit/react-components": "0.4.0" + "@ariakit/react-core": "0.4.15" }, "funding": { "type": "opencollective", @@ -127,67 +118,19 @@ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/@ariakit/react-components": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@ariakit/react-components/-/react-components-0.4.0.tgz", - "integrity": "sha512-dYymiYvAbyu6a/ehqYN2ZeieiMSYt0CMAj2BnDRUI/dZ9vLF32cv9PQMcgXXyqn0ILpqu79PQs53vVEZ+Z+0rQ==", + "node_modules/@ariakit/react-core": { + "version": "0.4.15", "license": "MIT", "dependencies": { - "@ariakit/components": "0.1.9", - "@ariakit/react-store": "0.1.8", - "@ariakit/react-utils": "0.2.3", - "@ariakit/store": "0.1.7", - "@ariakit/utils": "0.1.5", - "@floating-ui/dom": "^1.0.0" + "@ariakit/core": "0.4.14", + "@floating-ui/dom": "^1.0.0", + "use-sync-external-store": "^1.2.0" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/@ariakit/react-store": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@ariakit/react-store/-/react-store-0.1.8.tgz", - "integrity": "sha512-VYZ1LTUVMrNUi4jP37Npvhe3mcAzKdznqnBSVeh1Jjsbcgw0JlN88oy6UpdoJPvLKWOZHVYr62Sqn7xE0GrsqQ==", - "license": "MIT", - "dependencies": { - "@ariakit/react-utils": "0.2.3", - "@ariakit/store": "0.1.7", - "@ariakit/utils": "0.1.5", - "use-sync-external-store": "^1.6.0" - }, - "peerDependencies": { - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@ariakit/react-utils": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@ariakit/react-utils/-/react-utils-0.2.3.tgz", - "integrity": "sha512-fDaheb/7QEusanZb2oRT7mO55GTpQUyBOdjvQF5RPh3/CM15lm0TejLKN5bl1obmU5HRuByvckQmQvSyr8n/dw==", - "license": "MIT", - "dependencies": { - "@ariakit/store": "0.1.7", - "@ariakit/utils": "0.1.5" - }, - "peerDependencies": { - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@ariakit/store": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/@ariakit/store/-/store-0.1.7.tgz", - "integrity": "sha512-/GcxscA9QTo2F+IFbFPvoyj1N8hzXBnaYsQt9UxRiJgCFPQ2jIe4i6QgPXdOZEZUuqYdyuvjcQrg7MDm9vpvCA==", - "license": "MIT", - "dependencies": { - "@ariakit/utils": "0.1.5" - } - }, - "node_modules/@ariakit/utils": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@ariakit/utils/-/utils-0.1.5.tgz", - "integrity": "sha512-BQebYH9nV1VZttZwoq/fsxcxIJjc8oW2bNV6yJDHLZ8OF8UtM15drFN0JOL8Jwn7jeeD7Ev+tIC8pUijJEditQ==", - "license": "MIT" - }, "node_modules/@axe-core/playwright": { "version": "4.11.3", "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.3.tgz", @@ -2051,16 +1994,14 @@ } }, "node_modules/@emotion/css": { - "version": "11.13.5", - "resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.13.5.tgz", - "integrity": "sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w==", + "version": "11.11.2", "license": "MIT", "dependencies": { - "@emotion/babel-plugin": "^11.13.5", - "@emotion/cache": "^11.13.5", - "@emotion/serialize": "^1.3.3", - "@emotion/sheet": "^1.4.0", - "@emotion/utils": "^1.4.2" + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.2", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1" } }, "node_modules/@emotion/hash": { @@ -2116,9 +2057,7 @@ "license": "MIT" }, "node_modules/@emotion/styled": { - "version": "11.14.1", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", - "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "version": "11.14.0", "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.3", @@ -2324,31 +2263,25 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", - "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "version": "1.5.0", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.12" + "@floating-ui/utils": "^0.1.3" } }, "node_modules/@floating-ui/dom": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", - "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "version": "1.5.3", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.8.0", - "@floating-ui/utils": "^0.2.12" + "@floating-ui/core": "^1.4.2", + "@floating-ui/utils": "^0.1.3" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", - "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", + "version": "2.1.1", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.8.0" + "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", @@ -2356,9 +2289,7 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", - "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "version": "0.1.6", "license": "MIT" }, "node_modules/@humanfs/core": { @@ -4215,13 +4146,13 @@ } }, "node_modules/@wordpress/a11y": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@wordpress/a11y/-/a11y-4.52.0.tgz", - "integrity": "sha512-0KlDa/vSASriu84+z+a/XA2teaau6t6rCH6PEqMXoW5a6EbP2e/REpZRumbu0VzV5MRO5kpxfD+fVYNYz9BdlA==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@wordpress/a11y/-/a11y-4.40.0.tgz", + "integrity": "sha512-WhBuBgJTvanbBMNeflgCvwQLOU9ToITdYSzOvWg0kzz1i/e138NlCxrVpcXGUc6MQulduKhOWOtjizSdotaQRA==", "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/dom-ready": "^4.52.0", - "@wordpress/i18n": "^6.25.0" + "@wordpress/dom-ready": "^4.40.0", + "@wordpress/i18n": "^6.13.0" }, "engines": { "node": ">=18.12.0", @@ -4229,14 +4160,15 @@ } }, "node_modules/@wordpress/a11y/node_modules/@wordpress/i18n": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.25.0.tgz", - "integrity": "sha512-UcyUT8CJgEkhjzEGTVV6kw0Qi4OraF0I9J9FBLmTda/1Wi1o6rLAXZBm2aJrP740ezFxPneHH92aQF4Z5xZqcQ==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.13.0.tgz", + "integrity": "sha512-Yx882uFxcg6QpB13fv8UhvM6k5NwMQGfNXKB9SVSNL/APvDWn2m/n4n+5GZYi+wOV+KJLojQZbdRpHWCnX/jFg==", "license": "GPL-2.0-or-later", "dependencies": { "@tannin/sprintf": "^1.3.2", - "@wordpress/hooks": "^4.52.0", + "@wordpress/hooks": "^4.40.0", "gettext-parser": "^1.3.1", + "memize": "^2.1.0", "tannin": "^1.2.0" }, "bin": { @@ -4343,20 +4275,6 @@ "react-dom": "^18.0.0" } }, - "node_modules/@wordpress/components/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/@wordpress/compose": { "version": "7.40.0", "resolved": "https://registry.npmjs.org/@wordpress/compose/-/compose-7.40.0.tgz", @@ -4385,19 +4303,19 @@ } }, "node_modules/@wordpress/data": { - "version": "10.52.0", - "resolved": "https://registry.npmjs.org/@wordpress/data/-/data-10.52.0.tgz", - "integrity": "sha512-7Nx66TfUkYZDdXL4wn2/DjIJOYRsKdi8Gvv24Warv9rZ1pwTNoFXFDagvc3UoCOjn0pM8+pFNbd7a9TJH8oQzg==", + "version": "10.40.0", + "resolved": "https://registry.npmjs.org/@wordpress/data/-/data-10.40.0.tgz", + "integrity": "sha512-wwqkMc9iLteRO1zNxL/R3COWnijsdC5TIjenmd2JivReUmdA4ulAN3Tq7QiHkhwOV4jzZkuWW7DgR2ynxf55lw==", "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/compose": "^8.5.0", - "@wordpress/deprecated": "^4.52.0", - "@wordpress/element": "^8.4.0", - "@wordpress/is-shallow-equal": "^5.52.0", - "@wordpress/priority-queue": "^3.52.0", - "@wordpress/private-apis": "^1.52.0", - "@wordpress/redux-routine": "^5.52.0", - "deepmerge": "^4.3.1", + "@wordpress/compose": "^7.40.0", + "@wordpress/deprecated": "^4.40.0", + "@wordpress/element": "^6.40.0", + "@wordpress/is-shallow-equal": "^5.40.0", + "@wordpress/priority-queue": "^3.40.0", + "@wordpress/private-apis": "^1.40.0", + "@wordpress/redux-routine": "^5.40.0", + "deepmerge": "^4.3.0", "equivalent-key-map": "^0.2.2", "is-plain-object": "^5.0.0", "is-promise": "^4.0.0", @@ -4410,75 +4328,16 @@ "npm": ">=8.19.2" }, "peerDependencies": { - "@types/react": "^18 || ^19", - "react": "^18 || ^19" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@wordpress/data/node_modules/@wordpress/compose": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@wordpress/compose/-/compose-8.5.0.tgz", - "integrity": "sha512-giYS22Tbhtr3Lj4lK6lxzGqV6EkM2QeVBiP7+c9r2F9rnQN3F6A8cN4gQM5sVGdgIOlps9tw+dsn9OHzIFVwIg==", - "license": "GPL-2.0-or-later", - "dependencies": { - "@types/mousetrap": "^1.6.8", - "@wordpress/deprecated": "^4.52.0", - "@wordpress/dom": "^4.52.0", - "@wordpress/element": "^8.4.0", - "@wordpress/is-shallow-equal": "^5.52.0", - "@wordpress/keycodes": "^4.52.0", - "@wordpress/priority-queue": "^3.52.0", - "@wordpress/private-apis": "^1.52.0", - "@wordpress/undo-manager": "^1.52.0", - "change-case": "^4.1.2", - "mousetrap": "^1.6.5", - "use-memo-one": "^1.1.1" - }, - "engines": { - "node": ">=18.12.0", - "npm": ">=8.19.2" - }, - "peerDependencies": { - "@types/react": "^18 || ^19", - "react": "^18 || ^19" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@wordpress/data/node_modules/@wordpress/element": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-8.4.0.tgz", - "integrity": "sha512-/LzW+0MpSmKfYiESoXUQtjBPqOPyvqUm17GigQR5c0Z8Wj2NiklP72x4XaF02uSkau1ru1Z9lP8NWEGjwBwLsA==", - "license": "GPL-2.0-or-later", - "dependencies": { - "@types/react": "^18.3.27", - "@types/react-dom": "^18.3.1", - "@wordpress/deprecated": "^4.52.0", - "@wordpress/escape-html": "^3.52.0", - "change-case": "^4.1.2", - "is-plain-object": "^5.0.0", - "react": "^18.3.1", - "react-dom": "^18.3.1" - }, - "engines": { - "node": ">=18.12.0", - "npm": ">=8.19.2" + "react": "^18.0.0" } }, "node_modules/@wordpress/date": { - "version": "5.52.0", - "resolved": "https://registry.npmjs.org/@wordpress/date/-/date-5.52.0.tgz", - "integrity": "sha512-JRAXv2CQwUiJq9k2n/iUqblo9rX3LUFJ03lA4zRy4+bxcSiMdpXvYrpZfUTp3W4I1Oc/fPZIYQY0HB3XNyjUWw==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@wordpress/date/-/date-5.43.0.tgz", + "integrity": "sha512-8DiFlE7YzP7F/P59Hr6h5fWJxJlvt6eZgU1C7huM9XhANh8Y3dZfepsySL6K7h1yE66SQDSq07cEefFQgJW31g==", "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/deprecated": "^4.52.0", + "@wordpress/deprecated": "^4.43.0", "moment": "^2.29.4", "moment-timezone": "^0.5.40" }, @@ -4488,12 +4347,12 @@ } }, "node_modules/@wordpress/deprecated": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@wordpress/deprecated/-/deprecated-4.52.0.tgz", - "integrity": "sha512-94oHBKPty4pp6L5510LWDwJwNqFhikxLfwN6VUcDOQzj3itkc0MQ4ZQhmsfBy3Y6IRxSGx+p2bjSQvA4D+M96A==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@wordpress/deprecated/-/deprecated-4.43.0.tgz", + "integrity": "sha512-Pxn+nUmCVAaKBiZun2tEVweVdevMvWFWyCRqIqsAKdWCLsD8Uk6o27EwXc1u8BlO65VmK8D2zF9uWKGKfdZbCw==", "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/hooks": "^4.52.0" + "@wordpress/hooks": "^4.43.0" }, "engines": { "node": ">=18.12.0", @@ -4501,12 +4360,12 @@ } }, "node_modules/@wordpress/dom": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@wordpress/dom/-/dom-4.52.0.tgz", - "integrity": "sha512-LUY9nI9h6blk4xBuG83KgyfTnx7PMs/g6ZVzY/SOaCcOc2P3zOfzacADq/vxQydbZpcwtLM6juzgnNta6AX95w==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@wordpress/dom/-/dom-4.40.0.tgz", + "integrity": "sha512-JBF1sRjJMFgLn0pet0tmPzO1kNaa35/DwAAtG81zzjikctR1PzE3EK8o6ZGPtUY1sTa9l7aB1Lxfcum/eroyRg==", "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/deprecated": "^4.52.0" + "@wordpress/deprecated": "^4.40.0" }, "engines": { "node": ">=18.12.0", @@ -4514,9 +4373,9 @@ } }, "node_modules/@wordpress/dom-ready": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@wordpress/dom-ready/-/dom-ready-4.52.0.tgz", - "integrity": "sha512-dAsch1oeyRV4kwoQuqdr6dXisGAs09OtvGOk09Ke+QC0fTTPzxdKUw+c4M1nk0AmCeyZfqAqYYGCMm4n241QeQ==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@wordpress/dom-ready/-/dom-ready-4.40.0.tgz", + "integrity": "sha512-mHVy4P6yc0XLmGgnccxptMKg83TwcbYKfYrQH8pTcIu43P24zONTd44eZFjkfz7c/b+RLJg1Kj+d5mKh1xqH1A==", "license": "GPL-2.0-or-later", "engines": { "node": ">=18.12.0", @@ -4627,9 +4486,9 @@ } }, "node_modules/@wordpress/escape-html": { - "version": "3.52.0", - "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-3.52.0.tgz", - "integrity": "sha512-sW8X839Xu8aiJlCGF48MM3iYrcXspuiY0By8iTpXKpnUdd2u0SL9HRR0ALSAsOf2ujOeWm/x58ODMSchovRWGw==", + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-3.40.0.tgz", + "integrity": "sha512-DD6xWVbnw4fGGgO6DFDTJiLj52om0OG4cYHLz7ZhuipmOlEUGljPYOcrj8uxtlh5EFrqHCIPkOya+qQXUHUSBw==", "license": "GPL-2.0-or-later", "engines": { "node": ">=18.12.0", @@ -4637,9 +4496,9 @@ } }, "node_modules/@wordpress/hooks": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-4.52.0.tgz", - "integrity": "sha512-EbV/nJTerhqwNW3DLvvGutJfNyXcmBHXuWyJpv1NypzT80k21jPGP79HBE5Z0A2oAI2kIBp6Klaa4O8uEjq/sw==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-4.43.0.tgz", + "integrity": "sha512-BY7GPjEwhOlgkavVak40E3RtA8Z9ehydqTZckRoesMRjXYfxKSzr1C1FT4wAPS5uXM1pNlWivfofMaJjVNQu5w==", "license": "GPL-2.0-or-later", "engines": { "node": ">=18.12.0", @@ -4647,9 +4506,9 @@ } }, "node_modules/@wordpress/html-entities": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@wordpress/html-entities/-/html-entities-4.52.0.tgz", - "integrity": "sha512-SM0XH+EgCi20Ptqv5WGkM8d2CAlThWRO/BVMeXzu6RLlcHKLMXNQZvW2waXCZtUpto8hlVZT9Dzg8z37sj4OiQ==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@wordpress/html-entities/-/html-entities-4.40.0.tgz", + "integrity": "sha512-bsJrwZk22On8gNhUd84yyWKt/nrNZtACNZpXmkpyue/oTlFqNenLfhqRkvTKJzjbLxrrcUPsXlskbPcS7mxwTQ==", "license": "GPL-2.0-or-later", "engines": { "node": ">=18.12.0", @@ -4693,9 +4552,9 @@ } }, "node_modules/@wordpress/is-shallow-equal": { - "version": "5.52.0", - "resolved": "https://registry.npmjs.org/@wordpress/is-shallow-equal/-/is-shallow-equal-5.52.0.tgz", - "integrity": "sha512-LMIBLLTZ+vvq0DlYebL4d9XaCu16PNnhPGSn8QqsMPmBzwBYOjO0/btMLpYbs9rRc8k1QKBDWvw8eddvRVsYxA==", + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@wordpress/is-shallow-equal/-/is-shallow-equal-5.40.0.tgz", + "integrity": "sha512-IU11xOcHIGqDLxx9X+8RIk4WFo0qqba0bpeLqrVKsQXNGjP7tXSo2ufylxE9K9CEYXFMF0C65k83XpRZtEkA8g==", "license": "GPL-2.0-or-later", "engines": { "node": ">=18.12.0", @@ -4703,35 +4562,28 @@ } }, "node_modules/@wordpress/keycodes": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-4.52.0.tgz", - "integrity": "sha512-KRvwViTTdxUDgikYaCm+qnXSH2UlFuCDjAIvtjjcen8oRviSkeMv6bPn1T2vqFRKcVgt88xTtKlvI8l5U99o2Q==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-4.40.0.tgz", + "integrity": "sha512-laLkfjwkhMdreCl/KQdHucBIQAYwSjkyk3BToq/PCrcxFJBwWK2NgEtSl/t1CEw2HJwe0H2ne3FEWtipY4iDrA==", "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/i18n": "^6.25.0" + "@wordpress/i18n": "^6.13.0" }, "engines": { "node": ">=18.12.0", "npm": ">=8.19.2" - }, - "peerDependencies": { - "@types/react": "^18 || ^19" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } } }, "node_modules/@wordpress/keycodes/node_modules/@wordpress/i18n": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.25.0.tgz", - "integrity": "sha512-UcyUT8CJgEkhjzEGTVV6kw0Qi4OraF0I9J9FBLmTda/1Wi1o6rLAXZBm2aJrP740ezFxPneHH92aQF4Z5xZqcQ==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.13.0.tgz", + "integrity": "sha512-Yx882uFxcg6QpB13fv8UhvM6k5NwMQGfNXKB9SVSNL/APvDWn2m/n4n+5GZYi+wOV+KJLojQZbdRpHWCnX/jFg==", "license": "GPL-2.0-or-later", "dependencies": { "@tannin/sprintf": "^1.3.2", - "@wordpress/hooks": "^4.52.0", + "@wordpress/hooks": "^4.40.0", "gettext-parser": "^1.3.1", + "memize": "^2.1.0", "tannin": "^1.2.0" }, "bin": { @@ -4743,12 +4595,12 @@ } }, "node_modules/@wordpress/primitives": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@wordpress/primitives/-/primitives-4.52.0.tgz", - "integrity": "sha512-rgB+Q1i97AY8AECLAEhvV/bGw9sOV6CKtUaY90t7nuv6M+ZgXqwQLRRWVaXWFUsthcZ2y4sTIbKfxwVJV+JQ6g==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@wordpress/primitives/-/primitives-4.40.0.tgz", + "integrity": "sha512-0gOw3n3kSUsAPo91xNDS9J4GGTrNXU90XmuWn7mNfXAl5uRAMRnxgkfL+pwd0ng0rmdPtjPqrJpljnP2oy3K2w==", "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/element": "^8.4.0", + "@wordpress/element": "^6.40.0", "clsx": "^2.1.1" }, "engines": { @@ -4756,39 +4608,13 @@ "npm": ">=8.19.2" }, "peerDependencies": { - "@types/react": "^18 || ^19", - "react": "^18 || ^19" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@wordpress/primitives/node_modules/@wordpress/element": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-8.4.0.tgz", - "integrity": "sha512-/LzW+0MpSmKfYiESoXUQtjBPqOPyvqUm17GigQR5c0Z8Wj2NiklP72x4XaF02uSkau1ru1Z9lP8NWEGjwBwLsA==", - "license": "GPL-2.0-or-later", - "dependencies": { - "@types/react": "^18.3.27", - "@types/react-dom": "^18.3.1", - "@wordpress/deprecated": "^4.52.0", - "@wordpress/escape-html": "^3.52.0", - "change-case": "^4.1.2", - "is-plain-object": "^5.0.0", - "react": "^18.3.1", - "react-dom": "^18.3.1" - }, - "engines": { - "node": ">=18.12.0", - "npm": ">=8.19.2" + "react": "^18.0.0" } }, "node_modules/@wordpress/priority-queue": { - "version": "3.52.0", - "resolved": "https://registry.npmjs.org/@wordpress/priority-queue/-/priority-queue-3.52.0.tgz", - "integrity": "sha512-aYnVXxV5j4D73tRld2n1D3G6cKLfSwKWcTnktJ7XlmiswW7xlaAqvn4FyBjwmIe20pZw0A7xLH5xCuQcqOM7nw==", + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@wordpress/priority-queue/-/priority-queue-3.40.0.tgz", + "integrity": "sha512-85km9+I7RWi7P73BU/yom41gpdu0watdQ1GscQhQBel6BjHOXO5qWG6P9i3sEH47bz7EyO248l4LC/h8oHqpfQ==", "license": "GPL-2.0-or-later", "dependencies": { "requestidlecallback": "^0.3.0" @@ -4799,9 +4625,9 @@ } }, "node_modules/@wordpress/private-apis": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/@wordpress/private-apis/-/private-apis-1.52.0.tgz", - "integrity": "sha512-gFcmBXSti73Y4uEx++5qUNDaH/o0/2ijrHEJkY5e/df4RtmxVSQOIVxftRiI4m9thCWfJMoehMMreJxhwx6Qtg==", + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@wordpress/private-apis/-/private-apis-1.40.0.tgz", + "integrity": "sha512-68cwZKVq8Xy8GBzKoDRuV4b3pQ4nJFItY689HXp+poc0XXrnAeC4ZhjeSgS1qGRpFo6RVvLjjcaZsN2OrSSMvQ==", "license": "GPL-2.0-or-later", "engines": { "node": ">=18.12.0", @@ -4809,9 +4635,9 @@ } }, "node_modules/@wordpress/redux-routine": { - "version": "5.52.0", - "resolved": "https://registry.npmjs.org/@wordpress/redux-routine/-/redux-routine-5.52.0.tgz", - "integrity": "sha512-MpyNAKpfQAk8IWJ3Cwzc/p1dVEUl/iYxL6GCTX/y7K3c8qYd7csU4o5kuK9lfeqgVVI+d0y6rzBZiTaL2Z6Ojw==", + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@wordpress/redux-routine/-/redux-routine-5.40.0.tgz", + "integrity": "sha512-V+c1yCBl4i7qvRsWtQpGevbFCGtrRlzDe++4bwnrYJUiu79wbSXWRrmiSFr/EQie2KNM680t2MeFcfO7nsDVoA==", "license": "GPL-2.0-or-later", "dependencies": { "is-plain-object": "^5.0.0", @@ -4827,99 +4653,42 @@ } }, "node_modules/@wordpress/rich-text": { - "version": "7.52.0", - "resolved": "https://registry.npmjs.org/@wordpress/rich-text/-/rich-text-7.52.0.tgz", - "integrity": "sha512-zyd7w4hs8TV+nWrC/hbULg4lKJQyxpd5AolFfOHAJmKM2LrU/Lvlobz9oyGuTrZ/HXdkFs3wzPMwS/VGydSjHw==", - "license": "GPL-2.0-or-later", - "dependencies": { - "@wordpress/a11y": "^4.52.0", - "@wordpress/compose": "^8.5.0", - "@wordpress/data": "^10.52.0", - "@wordpress/deprecated": "^4.52.0", - "@wordpress/dom": "^4.52.0", - "@wordpress/element": "^8.4.0", - "@wordpress/escape-html": "^3.52.0", - "@wordpress/i18n": "^6.25.0", - "@wordpress/keycodes": "^4.52.0", - "@wordpress/private-apis": "^1.52.0", - "colord": "^2.9.3" - }, - "engines": { - "node": ">=18.12.0", - "npm": ">=8.19.2" - }, - "peerDependencies": { - "@types/react": "^18 || ^19", - "react": "^18 || ^19" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@wordpress/rich-text/node_modules/@wordpress/compose": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@wordpress/compose/-/compose-8.5.0.tgz", - "integrity": "sha512-giYS22Tbhtr3Lj4lK6lxzGqV6EkM2QeVBiP7+c9r2F9rnQN3F6A8cN4gQM5sVGdgIOlps9tw+dsn9OHzIFVwIg==", + "version": "7.40.0", + "resolved": "https://registry.npmjs.org/@wordpress/rich-text/-/rich-text-7.40.0.tgz", + "integrity": "sha512-eHImTvzPEg4GWAuzcagyc2tArc6neA2sbqvybpd5JzhEpgv/Q0zcKwLfUKI05kYaaPI/Rg5WXgeXDxjGYpq5hA==", "license": "GPL-2.0-or-later", "dependencies": { - "@types/mousetrap": "^1.6.8", - "@wordpress/deprecated": "^4.52.0", - "@wordpress/dom": "^4.52.0", - "@wordpress/element": "^8.4.0", - "@wordpress/is-shallow-equal": "^5.52.0", - "@wordpress/keycodes": "^4.52.0", - "@wordpress/priority-queue": "^3.52.0", - "@wordpress/private-apis": "^1.52.0", - "@wordpress/undo-manager": "^1.52.0", - "change-case": "^4.1.2", - "mousetrap": "^1.6.5", - "use-memo-one": "^1.1.1" + "@wordpress/a11y": "^4.40.0", + "@wordpress/compose": "^7.40.0", + "@wordpress/data": "^10.40.0", + "@wordpress/deprecated": "^4.40.0", + "@wordpress/dom": "^4.40.0", + "@wordpress/element": "^6.40.0", + "@wordpress/escape-html": "^3.40.0", + "@wordpress/i18n": "^6.13.0", + "@wordpress/keycodes": "^4.40.0", + "@wordpress/private-apis": "^1.40.0", + "colord": "2.9.3", + "memize": "^2.1.0" }, "engines": { "node": ">=18.12.0", "npm": ">=8.19.2" }, "peerDependencies": { - "@types/react": "^18 || ^19", - "react": "^18 || ^19" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@wordpress/rich-text/node_modules/@wordpress/element": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-8.4.0.tgz", - "integrity": "sha512-/LzW+0MpSmKfYiESoXUQtjBPqOPyvqUm17GigQR5c0Z8Wj2NiklP72x4XaF02uSkau1ru1Z9lP8NWEGjwBwLsA==", - "license": "GPL-2.0-or-later", - "dependencies": { - "@types/react": "^18.3.27", - "@types/react-dom": "^18.3.1", - "@wordpress/deprecated": "^4.52.0", - "@wordpress/escape-html": "^3.52.0", - "change-case": "^4.1.2", - "is-plain-object": "^5.0.0", - "react": "^18.3.1", - "react-dom": "^18.3.1" - }, - "engines": { - "node": ">=18.12.0", - "npm": ">=8.19.2" + "react": "^18.0.0" } }, "node_modules/@wordpress/rich-text/node_modules/@wordpress/i18n": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.25.0.tgz", - "integrity": "sha512-UcyUT8CJgEkhjzEGTVV6kw0Qi4OraF0I9J9FBLmTda/1Wi1o6rLAXZBm2aJrP740ezFxPneHH92aQF4Z5xZqcQ==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.13.0.tgz", + "integrity": "sha512-Yx882uFxcg6QpB13fv8UhvM6k5NwMQGfNXKB9SVSNL/APvDWn2m/n4n+5GZYi+wOV+KJLojQZbdRpHWCnX/jFg==", "license": "GPL-2.0-or-later", "dependencies": { "@tannin/sprintf": "^1.3.2", - "@wordpress/hooks": "^4.52.0", + "@wordpress/hooks": "^4.40.0", "gettext-parser": "^1.3.1", + "memize": "^2.1.0", "tannin": "^1.2.0" }, "bin": { @@ -4931,12 +4700,12 @@ } }, "node_modules/@wordpress/undo-manager": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/@wordpress/undo-manager/-/undo-manager-1.52.0.tgz", - "integrity": "sha512-6hI0WWDOtLLVEkWqQokCoQRz5Pba9UNtgt5MV4hHGe5x0mzsowj+GWHedppf8UCVZ2ex3cde7fThaRKMCackrQ==", + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@wordpress/undo-manager/-/undo-manager-1.40.0.tgz", + "integrity": "sha512-QvhHke/bVaOSPeaV5mNvsuIQpc2dJFDhXZ7gUnpuzyuNHh74Xk6Ar0vvYcfXiALst4ejKqWCoKOBi7ve1h2ppg==", "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/is-shallow-equal": "^5.52.0" + "@wordpress/is-shallow-equal": "^5.40.0" }, "engines": { "node": ">=18.12.0", @@ -4944,9 +4713,9 @@ } }, "node_modules/@wordpress/url": { - "version": "4.52.0", - "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-4.52.0.tgz", - "integrity": "sha512-4cSo0dUHiOQB2DiiPkoVIqP5aoGJdIj32LD9XgG41yeh7xG6ALdDA1I/883WFRoX2JIYySFj208MzopURDH7Yg==", + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-4.40.0.tgz", + "integrity": "sha512-DVAJlW7bdocKfQp8G7tS73vnobAC8TBbIHHdxeLQKwzT8mOkG4W/rpzN2KTxkiJKFXUu5in4F8a6T+Cy/Lt1eQ==", "license": "GPL-2.0-or-later", "dependencies": { "remove-accents": "^0.5.0" @@ -4957,9 +4726,9 @@ } }, "node_modules/@wordpress/warning": { - "version": "3.52.0", - "resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-3.52.0.tgz", - "integrity": "sha512-BjJ+Jte1g2nMITI1IHq0NwMpm/qlFOV1L+gNe0vFQmGKEcXiQ0DjrHQNtA8hV+Mgt9zSWRNS71Fgonht3r72Ew==", + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-3.40.0.tgz", + "integrity": "sha512-0l3OFa1Z+UdhWRRHX9JWWKofo7Lbi2MqOFzzzn0MC26HOyfieQycjLVLNVNXaaodIKUhap6uDQq+JXbbHm881A==", "license": "GPL-2.0-or-later", "engines": { "node": ">=18.12.0", @@ -8267,19 +8036,15 @@ } }, "node_modules/framer-motion": { - "version": "11.18.2", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz", - "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==", + "version": "11.3.28", "license": "MIT", "dependencies": { - "motion-dom": "^11.18.1", - "motion-utils": "^11.18.1", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" + "react": "^18.0.0", + "react-dom": "^18.0.0" }, "peerDependenciesMeta": { "@emotion/is-prop-valid": { @@ -10108,9 +9873,7 @@ "license": "CC0-1.0" }, "node_modules/memize": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/memize/-/memize-2.1.1.tgz", - "integrity": "sha512-8Nl+i9S5D6KXnruM03Jgjb+LwSupvR13WBr4hJegaaEyobvowCVupi79y2WSiWvO1mzBWxPwEYE5feCe8vyA5w==", + "version": "2.1.0", "license": "MIT" }, "node_modules/memoize-one": { @@ -10269,21 +10032,6 @@ "node": "*" } }, - "node_modules/motion-dom": { - "version": "11.18.1", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", - "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==", - "license": "MIT", - "dependencies": { - "motion-utils": "^11.18.1" - } - }, - "node_modules/motion-utils": { - "version": "11.18.1", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz", - "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==", - "license": "MIT" - }, "node_modules/mousetrap": { "version": "1.6.5", "resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.5.tgz", @@ -13965,9 +13713,7 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "version": "1.4.0", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -13979,16 +13725,14 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", - "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "version": "9.0.1", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { - "uuid": "dist/esm/bin/uuid" + "uuid": "dist/bin/uuid" } }, "node_modules/v8-compile-cache-lib": { diff --git a/package.json b/package.json index d576992ff..4d395c85b 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "code-snippets", "description": "Manage code snippets running on a WordPress-powered site through a graphical interface.", "homepage": "https://codesnippets.pro", - "version": "4.0.0-beta.12", + "version": "4.0.0-beta.1", "main": "src/dist/edit.js", "directories": { "test": "tests" @@ -63,8 +63,7 @@ "prismjs": "^1.29.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-select": "^5.10.0", - "uuid": "^11.1.0" + "react-select": "^5.10.0" }, "devDependencies": { "@axe-core/playwright": "^4.11.2", diff --git a/scripts/test-setup-playwright.ts b/scripts/test-setup-playwright.ts index 18d88b869..7ebdcd3b4 100644 --- a/scripts/test-setup-playwright.ts +++ b/scripts/test-setup-playwright.ts @@ -1,7 +1,7 @@ #!/usr/bin/env ts-node import { execFileSync } from 'node:child_process' -import { existsSync, readFileSync } from 'node:fs' +import { readFileSync } from 'node:fs' import { resolve } from 'node:path' const run = (cmd: string, args: readonly string[]) => { @@ -10,22 +10,6 @@ const run = (cmd: string, args: readonly string[]) => { const runWpEnvCli = (args: readonly string[]) => run('npx', ['wp-env', 'run', 'cli', ...args]) -const loadEnvFile = (): void => { - const envPath = resolve(process.cwd(), '.env') - - if (!existsSync(envPath)) { - return - } - - for (const line of readFileSync(envPath, 'utf8').split('\n')) { - const match = /^\s*(?[\w.-]+)\s*=\s*(?.*?)\s*$/u.exec(line) - - if (match?.groups && !(match.groups.key in process.env)) { - process.env[match.groups.key] = match.groups.value.replace(/^["']|["']$/u, '') - } - } -} - const getPluginSlug = (): string => { const prefix = 'wp-content/plugins/' const config = <{ mappings?: Record }>JSON.parse(readFileSync(resolve(process.cwd(), '.wp-env.json'), 'utf8')) @@ -45,8 +29,6 @@ const main = () => { // - force enable_flat_files=false so the Playwright setup test can flip it to true // - delete all DB snippets with an E2E prefix (keeps list clean across runs) - loadEnvFile() - runWpEnvCli(['sh', '-lc', 'rm -rf wp-content/code-snippets']) runWpEnvCli(['wp', 'plugin', 'activate', getPluginSlug()]) diff --git a/src/code-snippets.php b/src/code-snippets.php index 539a75dfe..3f78bd0e5 100644 --- a/src/code-snippets.php +++ b/src/code-snippets.php @@ -8,11 +8,11 @@ * License: GPL-2.0-or-later * License URI: license.txt * Text Domain: code-snippets - * Version: 4.0.0-beta.12 + * Version: 4.0.0-beta.1 * Requires PHP: 7.4 * Requires at least: 5.5 * - * @version 4.0.0-beta.12 + * @version 4.0.0-beta.1 * @package Code_Snippets * @author Shea Bunge * @copyright 2012-2026 Code Snippets Pro @@ -37,7 +37,7 @@ * * @const string */ - define( 'CODE_SNIPPETS_VERSION', '4.0.0-beta.12' ); + define( 'CODE_SNIPPETS_VERSION', '4.0.0-beta.1' ); /** * The full path to the main file of this plugin. diff --git a/src/css/admin-bar.scss b/src/css/admin-bar.scss index 1ef7ceec1..f3104e446 100644 --- a/src/css/admin-bar.scss +++ b/src/css/admin-bar.scss @@ -112,22 +112,8 @@ opacity: 1; } - #wp-admin-bar-code-snippets-ran-on-this-page > .ab-sub-wrapper > .ab-submenu, #wp-admin-bar-code-snippets-active-snippets > .ab-sub-wrapper > .ab-submenu, #wp-admin-bar-code-snippets-inactive-snippets > .ab-sub-wrapper > .ab-submenu { padding-block-start: 0; } - - .code-snippets-kind-badge { - display: inline-block; - min-inline-size: 30px; - padding-inline: 5px; - border-radius: 3px; - color: #fff; - font-size: 10px; - font-weight: 600; - line-height: 1.7; - text-align: center; - text-transform: uppercase; - } } diff --git a/src/css/common/_badges.scss b/src/css/common/_badges.scss index cd0849693..099aa47b3 100644 --- a/src/css/common/_badges.scss +++ b/src/css/common/_badges.scss @@ -77,7 +77,6 @@ $badges: ( cond: #22826f, core: #0ca0a9, pro: #f7e8e3 #df9279, - revisions: #50575e, cloud: #009fb4, bundles: #50575e, cloud_search: #d27c00, diff --git a/src/css/common/_buttons.scss b/src/css/common/_buttons.scss deleted file mode 100644 index cb315d231..000000000 --- a/src/css/common/_buttons.scss +++ /dev/null @@ -1,32 +0,0 @@ -// A ` diff --git a/src/js/components/EditMenu/EditorSidebar/actions/ExportButtons.tsx b/src/js/components/EditMenu/EditorSidebar/actions/ExportButtons.tsx index a7b167465..82d84aa71 100644 --- a/src/js/components/EditMenu/EditorSidebar/actions/ExportButtons.tsx +++ b/src/js/components/EditMenu/EditorSidebar/actions/ExportButtons.tsx @@ -11,12 +11,10 @@ import type { Snippet } from '../../../../types/Snippet' interface ExportButtonProps { name: string label: string - icon: string - title?: string makeRequest: (snippet: Snippet) => Promise } -const ExportButton: React.FC = ({ name, label, icon, title, makeRequest }) => { +const ExportButton: React.FC = ({ name, label, makeRequest }) => { const { snippet, isWorking, setIsWorking, handleRequestError } = useSnippetForm() const handleClick = () => { @@ -30,8 +28,7 @@ const ExportButton: React.FC = ({ name, label, icon, title, m } return ( - ) @@ -46,16 +43,13 @@ export const ExportButtons: React.FC = () => { {window.CODE_SNIPPETS_EDIT?.enableDownloads && 'cond' !== getSnippetType(snippet) && ( )} diff --git a/src/js/components/EditMenu/EditorSidebar/controls/LockControl.tsx b/src/js/components/EditMenu/EditorSidebar/controls/LockControl.tsx index 6ad5800ca..84df9dcf1 100644 --- a/src/js/components/EditMenu/EditorSidebar/controls/LockControl.tsx +++ b/src/js/components/EditMenu/EditorSidebar/controls/LockControl.tsx @@ -41,7 +41,6 @@ export const LockControl: React.FC = () => { {snippet.locked ? QuickNav Active B', $active_titles[1] ); + $this->assertStringContainsString( '(PHP) QuickNav Active A', $active_titles[0] ); + $this->assertStringContainsString( '(PHP) QuickNav Active B', $active_titles[1] ); $_GET['code_snippets_ab_active_page'] = 2; @@ -245,7 +241,7 @@ public function test_snippet_listings_paginate_and_respect_query_arg(): void { $active_titles_page_2 = array_values( array_filter( $active_titles_page_2, static fn( $title ) => false !== strpos( $title, 'QuickNav Active' ) ) ); $this->assertCount( 1, $active_titles_page_2 ); - $this->assertStringContainsString( '>PHP QuickNav Active C', $active_titles_page_2[0] ); + $this->assertStringContainsString( '(PHP) QuickNav Active C', $active_titles_page_2[0] ); $_GET['code_snippets_ab_inactive_page'] = 2; @@ -268,7 +264,7 @@ public function test_snippet_listings_paginate_and_respect_query_arg(): void { ); $this->assertCount( 1, $inactive_titles_page_2 ); - $this->assertStringContainsString( '>HTML QuickNav Inactive Z HTML', $inactive_titles_page_2[0] ); + $this->assertStringContainsString( '(HTML) QuickNav Inactive Z HTML', $inactive_titles_page_2[0] ); } /** diff --git a/tests/unit/Admin/Feedback_Panel_Test.php b/tests/unit/Admin/Feedback_Panel_Test.php index 842bf9623..861a28b06 100644 --- a/tests/unit/Admin/Feedback_Panel_Test.php +++ b/tests/unit/Admin/Feedback_Panel_Test.php @@ -210,7 +210,7 @@ public function test_the_search_url_is_localised(): void { $this->assertStringContainsString( sprintf( '"searchUrl":"%s"', rest_url( Feedback_REST_Controller::get_base_route() . '/search' ) ), - stripslashes( $data ) + (string) $data ); } diff --git a/tests/unit/Admin/Menus/Manage/Manage_Menu_Assets_Test.php b/tests/unit/Admin/Menus/Manage/Manage_Menu_Assets_Test.php index 507b000e3..00d1be7e9 100644 --- a/tests/unit/Admin/Menus/Manage/Manage_Menu_Assets_Test.php +++ b/tests/unit/Admin/Menus/Manage/Manage_Menu_Assets_Test.php @@ -61,7 +61,7 @@ public function test_enqueue_localizes_manage_data(): void { } /** - * The AI Agent demo receives the site name it personalizes its snippet with. + * The AI Agent demo receives the site name it personalises its snippet with. * * @return void */ diff --git a/tests/unit/Admin/Menus/Manage/Manage_Menu_Demo_Reset_Test.php b/tests/unit/Admin/Menus/Manage/Manage_Menu_Demo_Reset_Test.php index 6491186fd..587b9532c 100644 --- a/tests/unit/Admin/Menus/Manage/Manage_Menu_Demo_Reset_Test.php +++ b/tests/unit/Admin/Menus/Manage/Manage_Menu_Demo_Reset_Test.php @@ -4,7 +4,6 @@ use Code_Snippets\REST_API\Preferences\Demos_Seen_REST_Controller; use Code_Snippets\UnitTestCase; -use ReflectionException; use ReflectionMethod; use RuntimeException; @@ -64,8 +63,6 @@ public function capture_redirect( string $location ) { * @param string $nonce Nonce to present. * * @return bool Whether the handler redirected, which it only does after resetting. - * - * @throws ReflectionException Uses reflection to access a private method. */ private function reset_request( string $nonce ): bool { $_GET[ Manage_Menu::DEMO_RESET_PARAM ] = '1'; @@ -73,12 +70,8 @@ private function reset_request( string $nonce ): bool { // The admin bootstrap does not run under PHPUnit, so the menu is built here. $menu = new Manage_Menu(); - $method = new ReflectionMethod( $menu, 'maybe_reset_demos' ); - - if ( version_compare( PHP_VERSION, '8.1', '<' ) ) { - $method->setAccessible( true ); - } + $method->setAccessible( true ); try { $method->invoke( $menu ); diff --git a/tests/unit/Admin/Notice_Filter_Test.php b/tests/unit/Admin/Notice_Filter_Test.php index 8b2eeb087..f0bff06ab 100644 --- a/tests/unit/Admin/Notice_Filter_Test.php +++ b/tests/unit/Admin/Notice_Filter_Test.php @@ -3,8 +3,6 @@ namespace Code_Snippets\Admin; use Code_Snippets\AdminUnitTestCase; -use Code_Snippets\Controller\Cloud_Auth_Controller; -use Code_Snippets\Model\Basic_Cloud_Connection; use function Code_Snippets\code_snippets; /** diff --git a/tests/unit/Authorship_Test.php b/tests/unit/Authorship_Test.php deleted file mode 100644 index c461692ca..000000000 --- a/tests/unit/Authorship_Test.php +++ /dev/null @@ -1,144 +0,0 @@ -user->create( - [ - 'role' => 'administrator', - 'display_name' => 'Ada Author', - ] - ); - self::$editor_id = $factory->user->create( - [ - 'role' => 'administrator', - 'display_name' => 'Ed Editor', - ] - ); - } - - /** - * Start each test with an empty snippets table. - */ - public function set_up() { - parent::set_up(); - - global $wpdb; - $table_name = code_snippets()->db->get_table_name(); - $wpdb->query( "TRUNCATE TABLE $table_name" ); - } - - /** - * Create a snippet as the given user and return the stored copy. - * - * @param int $user_id User to act as. - * @param string $name Snippet name. - * - * @return Snippet - */ - private function save_as( int $user_id, string $name ): Snippet { - wp_set_current_user( $user_id ); - - $snippet = save_snippet( - new Snippet( - [ - 'name' => $name, - 'code' => "echo 'hi';", - ] - ) - ); - - return get_snippet( $snippet->id ); - } - - /** - * Stamps both authorship columns with the current user on insert. - */ - public function test_save_stamps_author_on_insert() { - $stored = $this->save_as( self::$author_id, 'Authored' ); - - $this->assertSame( self::$author_id, $stored->created_by ); - $this->assertSame( self::$author_id, $stored->updated_by ); - } - - /** - * Advances updated_by on a later save by another user while created_by stays fixed. - */ - public function test_updated_by_advances_while_created_by_is_fixed() { - $snippet = $this->save_as( self::$author_id, 'Shared' ); - - wp_set_current_user( self::$editor_id ); - $snippet->name = 'Shared (edited)'; - save_snippet( $snippet ); - - $stored = get_snippet( $snippet->id ); - $this->assertSame( self::$author_id, $stored->created_by, 'created_by is fixed at insert' ); - $this->assertSame( self::$editor_id, $stored->updated_by, 'updated_by follows the latest editor' ); - } - - /** - * Resolves a user ID to a compact display object. - */ - public function test_resolver_returns_display_object() { - $author = get_snippet_author( self::$author_id ); - - $this->assertIsArray( $author ); - $this->assertSame( self::$author_id, $author['id'] ); - $this->assertSame( 'Ada Author', $author['display_name'] ); - $this->assertArrayHasKey( 'avatar_url', $author ); - } - - /** - * Returns null for an empty or unknown user ID. - */ - public function test_resolver_returns_null_for_unknown() { - $this->assertNull( get_snippet_author( 0 ) ); - $this->assertNull( get_snippet_author( 987654 ) ); - } - - /** - * Embeds a nested author object in the snippets REST response, not a raw ID. - */ - public function test_rest_response_embeds_nested_author() { - $stored = $this->save_as( self::$author_id, 'Rest Authored' ); - - $request = new WP_REST_Request( 'GET', '/code-snippets/v1/snippets/' . $stored->id ); - $data = rest_get_server()->response_to_data( rest_do_request( $request ), false ); - - $this->assertIsArray( $data['created_by'] ); - $this->assertSame( self::$author_id, $data['created_by']['id'] ); - $this->assertSame( 'Ada Author', $data['created_by']['display_name'] ); - $this->assertArrayHasKey( 'avatar_url', $data['created_by'] ); - } -} diff --git a/tests/unit/Core/Uninstaller_Test.php b/tests/unit/Core/Uninstaller_Test.php index 8004b0d5d..389c5422e 100644 --- a/tests/unit/Core/Uninstaller_Test.php +++ b/tests/unit/Core/Uninstaller_Test.php @@ -112,15 +112,7 @@ public function test_reinstall_after_incomplete_uninstall_keeps_existing_snippet * @return void */ public function test_complete_uninstall_removes_the_snippets_table_and_settings(): void { - $table = code_snippets()->db->table; - $drop_table_query = ''; - $filter = static function ( string $query ) use ( &$drop_table_query ): string { - if ( 0 === strpos( $query, 'DROP TABLE' ) ) { - $drop_table_query = $query; - } - - return $query; - }; + $db = code_snippets()->db; update_option( 'code_snippets_settings', @@ -129,11 +121,9 @@ public function test_complete_uninstall_removes_the_snippets_table_and_settings( ] ); - add_filter( 'query', $filter, 9 ); ( new Uninstaller() )->uninstall_plugin(); - remove_filter( 'query', $filter, 9 ); - $this->assertSame( "DROP TABLE IF EXISTS $table", $drop_table_query ); + $this->assertFalse( DB::table_exists( $db->table, true ) ); $this->assertFalse( get_option( 'code_snippets_settings' ) ); } } diff --git a/tests/unit/Model/Cloud_Snippets_Test.php b/tests/unit/Model/Cloud_Snippets_Test.php index 380d01b76..05f2b6a53 100644 --- a/tests/unit/Model/Cloud_Snippets_Test.php +++ b/tests/unit/Model/Cloud_Snippets_Test.php @@ -269,57 +269,4 @@ public function test_string_fields_normalise_non_string_values(): void { $this->assertSame( '', $snippet->created ); $this->assertSame( '20260101', $snippet->updated ); } - - /** - * The page size is read from the meta block alongside the other counts. - * - * @return void - */ - public function test_reads_per_page_from_meta(): void { - $result = Cloud_Snippets::unpack_api_response( - [ - 'snippets' => [], - 'meta' => [ - 'total' => 60, - 'total_pages' => 3, - 'page' => 1, - 'per_page' => 20, - ], - ] - ); - - $this->assertSame( 20, $result->per_page ); - } - - /** - * A requested page size wins over the reported one, so callers can compare it - * against a later request even when an older cloud omits it from the meta. - * - * @return void - */ - public function test_requested_per_page_overrides_meta(): void { - $without_meta = Cloud_Snippets::unpack_api_response( [ 'snippets' => [] ], 1, 50 ); - $this->assertSame( 50, $without_meta->per_page ); - - $with_meta = Cloud_Snippets::unpack_api_response( - [ - 'snippets' => [], - 'meta' => [ 'per_page' => 20 ], - ], - 1, - 50 - ); - $this->assertSame( 50, $with_meta->per_page ); - } - - /** - * Page size defaults to zero when neither the response nor the caller states one. - * - * @return void - */ - public function test_per_page_defaults_to_zero(): void { - $result = Cloud_Snippets::unpack_api_response( [ 'snippets' => [] ] ); - - $this->assertSame( 0, $result->per_page ); - } } diff --git a/tests/unit/Plugin_Test.php b/tests/unit/Plugin_Test.php index 897bebd27..16fe57ace 100644 --- a/tests/unit/Plugin_Test.php +++ b/tests/unit/Plugin_Test.php @@ -39,11 +39,7 @@ private function get_elementor_promotion_callbacks(): array { foreach ( $callbacks as $callback ) { $function = $callback['function']; - if ( - is_array( $function ) - && $function[0] instanceof Elementor_Editor - && 'promotion_in_custom_css_section' === $function[1] - ) { + if ( is_array( $function ) && $function[0] instanceof Elementor_Editor && 'promotion_in_custom_css_section' === $function[1] ) { $promotion_callbacks[] = $function; } } diff --git a/tests/unit/REST_API/Cloud/Cloud_Snippets_REST_Controller_Test.php b/tests/unit/REST_API/REST_API_Cloud_Test.php similarity index 80% rename from tests/unit/REST_API/Cloud/Cloud_Snippets_REST_Controller_Test.php rename to tests/unit/REST_API/REST_API_Cloud_Test.php index 329148ce8..3fffcb4f9 100644 --- a/tests/unit/REST_API/Cloud/Cloud_Snippets_REST_Controller_Test.php +++ b/tests/unit/REST_API/REST_API_Cloud_Test.php @@ -1,14 +1,14 @@ requested_url = ''; $this->rest_server = $wp_rest_server ?? null; - delete_user_option( $this->get_user_id(), 'snippets_per_page' ); - add_filter( 'pre_http_request', [ $this, 'mock_cloud_search_request' ], 10, 3 ); } @@ -71,7 +69,6 @@ public function tear_down() { remove_filter( 'pre_http_request', [ $this, 'mock_cloud_search_request' ] ); delete_user_option( $this->get_user_id(), 'snippets_per_page' ); - $wp_rest_server = $this->rest_server; parent::tear_down(); @@ -87,30 +84,6 @@ public function tear_down() { * @return mixed */ public function mock_cloud_search_request( $preempt, array $parsed_args, string $url ) { - if ( false !== strpos( $url, 'private/allsnippets' ) ) { - ++$this->codevault_request_count; - - return [ - 'headers' => [], - 'body' => wp_json_encode( - [ - 'snippets' => [], - 'cloud_id_rev' => [], - 'meta' => [ - 'total' => 0, - 'total_pages' => 0, - 'page' => 1, - ], - ] - ), - 'response' => [ - 'code' => 200, - 'message' => 'OK', - ], - 'cookies' => [], - ]; - } - if ( false === strpos( $url, 'public/search' ) && false === strpos( $url, 'public/featured' ) ) { return $preempt; } @@ -172,12 +145,17 @@ public function mock_cloud_search_request( $preempt, array $parsed_args, string */ private function make_request( array $params, string $route = '' ): WP_REST_Response { global $wp_rest_server; + static $connection; + + if ( ! isset( $connection ) ) { + $connection = new Basic_Cloud_Connection(); + } $wp_rest_server = null; rest_get_server(); $request = new WP_REST_Request( 'GET', $this->endpoint . $route ); - $request->add_header( 'Access-Control', code_snippets()->cloud_connection->get_local_token() ); + $request->add_header( 'Access-Control', $connection->get_local_token() ); foreach ( $params as $key => $value ) { $request->set_param( $key, $value ); @@ -328,49 +306,4 @@ public function test_get_featured_items_reports_local_ids_for_downloaded_snippet $this->assertSame( 200, $response->get_status() ); $this->assertSame( $local->id, wp_list_pluck( $snippets, 'local_id', 'id' )[3] ?? null ); } - - /** - * The AI search method is forwarded to the cloud as s_method=ai. - */ - public function test_search_method_ai_is_forwarded_to_cloud(): void { - $response = $this->make_request( - [ - 'query' => 'make my site more secure', - 'searchMethod' => 'ai', - ] - ); - - parse_str( (string) wp_parse_url( $this->requested_url, PHP_URL_QUERY ), $query_args ); - - $this->assertSame( 200, $response->get_status() ); - $this->assertSame( 'ai', $query_args['s_method'] ?? null ); - } - - /** - * With no method params, the search defaults to keyword matching (term). - */ - public function test_search_method_defaults_to_term(): void { - $this->make_request( [ 'query' => 'test' ] ); - - parse_str( (string) wp_parse_url( $this->requested_url, PHP_URL_QUERY ), $query_args ); - - $this->assertSame( 'term', $query_args['s_method'] ?? null ); - } - - /** - * A codevault search takes precedence over an AI search method. - */ - public function test_codevault_takes_precedence_over_ai(): void { - $this->make_request( - [ - 'query' => 'general', - 'searchByCodevault' => true, - 'searchMethod' => 'ai', - ] - ); - - parse_str( (string) wp_parse_url( $this->requested_url, PHP_URL_QUERY ), $query_args ); - - $this->assertSame( 'codevault', $query_args['s_method'] ?? null ); - } } diff --git a/tests/unit/REST_API/Snippets/REST_API_Snippets_Shared_Network_Toggle_Test.php b/tests/unit/REST_API/REST_API_Snippets_Shared_Network_Toggle_Test.php similarity index 99% rename from tests/unit/REST_API/Snippets/REST_API_Snippets_Shared_Network_Toggle_Test.php rename to tests/unit/REST_API/REST_API_Snippets_Shared_Network_Toggle_Test.php index e9f8cb2cd..703c8eb11 100644 --- a/tests/unit/REST_API/Snippets/REST_API_Snippets_Shared_Network_Toggle_Test.php +++ b/tests/unit/REST_API/REST_API_Snippets_Shared_Network_Toggle_Test.php @@ -1,6 +1,6 @@ Date: Fri, 18 Sep 2026 16:16:42 +0300 Subject: [PATCH 09/19] feat: remove tests --- .../unit/REST_API/REST_API_Snippets_Test.php | 61 ------------------- 1 file changed, 61 deletions(-) diff --git a/tests/unit/REST_API/REST_API_Snippets_Test.php b/tests/unit/REST_API/REST_API_Snippets_Test.php index 902d03293..8ecf21ed5 100644 --- a/tests/unit/REST_API/REST_API_Snippets_Test.php +++ b/tests/unit/REST_API/REST_API_Snippets_Test.php @@ -125,67 +125,6 @@ public function test_get_all_snippets_without_pagination() { $this->assertArrayHasKey( 'code', $response[0] ); } - /** - * A missing snippet is reported as a 404 instead of an internal error. - * - * @return void - */ - public function test_getting_a_missing_snippet_returns_404(): void { - $request = new WP_REST_Request( 'GET', "/$this->namespace/$this->base_route/999999" ); - $request->set_param( 'network', false ); - $response = rest_do_request( $request ); - $data = $response->get_data(); - - $this->assertSame( 404, $response->get_status() ); - $this->assertSame( 'rest_cannot_get', $data['code'] ); - $this->assertSame( 'The snippet could not be found.', $data['message'] ); - } - - /** - * Snippets can be created, read, updated, trashed, and permanently deleted. - * - * @return void - */ - public function test_snippet_crud_lifecycle(): void { - $endpoint = "/$this->namespace/$this->base_route"; - $created = $this->make_mutating_request( - 'POST', - $endpoint, - [ - 'name' => 'REST CRUD fixture', - 'code' => '// REST CRUD fixture', - 'scope' => 'global', - 'active' => false, - 'network' => false, - ] - ); - $snippet_id = $created['id']; - - $this->assertGreaterThan( 0, $snippet_id ); - $this->assertSame( 'REST CRUD fixture', $this->make_request( "$endpoint/$snippet_id", [ 'network' => false ] )['name'] ); - - $updated = $this->make_mutating_request( - 'PUT', - "$endpoint/$snippet_id", - [ - 'name' => 'Updated REST CRUD fixture', - 'network' => false, - ] - ); - - $this->assertSame( 'Updated REST CRUD fixture', $updated['name'] ); - - $trashed = $this->make_mutating_request( 'DELETE', "$endpoint/$snippet_id", [ 'network' => false ] ); - $this->assertTrue( $trashed['trashed'] ); - - $request = new WP_REST_Request( 'DELETE', "$endpoint/$snippet_id" ); - $request->set_param( 'network', false ); - $response = rest_do_request( $request ); - - $this->assertSame( 204, $response->get_status() ); - $this->assertSame( 0, get_snippet( $snippet_id )->id ); - } - /** * Test pagination with per_page parameter only (first page). */ From 251297be65c86083f2b30a54e5141ba89e793bd3 Mon Sep 17 00:00:00 2001 From: Rami Yushuvaev Date: Fri, 18 Sep 2026 16:19:15 +0300 Subject: [PATCH 10/19] feat: add new tests --- .../Snippets/REST_API_Snippets_Test.php | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/tests/unit/REST_API/Snippets/REST_API_Snippets_Test.php b/tests/unit/REST_API/Snippets/REST_API_Snippets_Test.php index 24d5ea668..961da9aa1 100644 --- a/tests/unit/REST_API/Snippets/REST_API_Snippets_Test.php +++ b/tests/unit/REST_API/Snippets/REST_API_Snippets_Test.php @@ -125,6 +125,67 @@ public function test_get_all_snippets_without_pagination() { $this->assertArrayHasKey( 'code', $response[0] ); } + /** + * A missing snippet is reported as a 404 instead of an internal error. + * + * @return void + */ + public function test_getting_a_missing_snippet_returns_404(): void { + $request = new WP_REST_Request( 'GET', "/$this->namespace/$this->base_route/999999" ); + $request->set_param( 'network', false ); + $response = rest_do_request( $request ); + $data = $response->get_data(); + + $this->assertSame( 404, $response->get_status() ); + $this->assertSame( 'rest_cannot_get', $data['code'] ); + $this->assertSame( 'The snippet could not be found.', $data['message'] ); + } + + /** + * Snippets can be created, read, updated, trashed, and permanently deleted. + * + * @return void + */ + public function test_snippet_crud_lifecycle(): void { + $endpoint = "/$this->namespace/$this->base_route"; + $created = $this->make_mutating_request( + 'POST', + $endpoint, + [ + 'name' => 'REST CRUD fixture', + 'code' => '// REST CRUD fixture', + 'scope' => 'global', + 'active' => false, + 'network' => false, + ] + ); + $snippet_id = $created['id']; + + $this->assertGreaterThan( 0, $snippet_id ); + $this->assertSame( 'REST CRUD fixture', $this->make_request( "$endpoint/$snippet_id", [ 'network' => false ] )['name'] ); + + $updated = $this->make_mutating_request( + 'PUT', + "$endpoint/$snippet_id", + [ + 'name' => 'Updated REST CRUD fixture', + 'network' => false, + ] + ); + + $this->assertSame( 'Updated REST CRUD fixture', $updated['name'] ); + + $trashed = $this->make_mutating_request( 'DELETE', "$endpoint/$snippet_id", [ 'network' => false ] ); + $this->assertTrue( $trashed['trashed'] ); + + $request = new WP_REST_Request( 'DELETE', "$endpoint/$snippet_id" ); + $request->set_param( 'network', false ); + $response = rest_do_request( $request ); + + $this->assertSame( 204, $response->get_status() ); + $this->assertSame( 0, get_snippet( $snippet_id )->id ); + } + /** * Test pagination with per_page parameter only (first page). */ From f422c5e0e5059665addce30ec18a3a606e9ce96b Mon Sep 17 00:00:00 2001 From: Rami Yushuvaev Date: Fri, 18 Sep 2026 16:36:17 +0300 Subject: [PATCH 11/19] test: ensure proper cleanup during complete uninstall --- tests/unit/Core/Uninstaller_Test.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/unit/Core/Uninstaller_Test.php b/tests/unit/Core/Uninstaller_Test.php index 389c5422e..4da5610b4 100644 --- a/tests/unit/Core/Uninstaller_Test.php +++ b/tests/unit/Core/Uninstaller_Test.php @@ -120,8 +120,13 @@ public function test_complete_uninstall_removes_the_snippets_table_and_settings( 'general' => [ 'complete_uninstall' => true ], ] ); + remove_filter( 'query', [ $this, '_drop_temporary_tables' ] ); - ( new Uninstaller() )->uninstall_plugin(); + try { + ( new Uninstaller() )->uninstall_plugin(); + } finally { + add_filter( 'query', [ $this, '_drop_temporary_tables' ] ); + } $this->assertFalse( DB::table_exists( $db->table, true ) ); $this->assertFalse( get_option( 'code_snippets_settings' ) ); From 3547032664070e9d665d44972c4fe6a88fa7bb7c Mon Sep 17 00:00:00 2001 From: Rami Yushuvaev Date: Fri, 18 Sep 2026 16:50:13 +0300 Subject: [PATCH 12/19] test: ensure temporary tables are removed during teardown --- tests/unit/Core/Uninstaller_Test.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/Core/Uninstaller_Test.php b/tests/unit/Core/Uninstaller_Test.php index 4da5610b4..8ba959337 100644 --- a/tests/unit/Core/Uninstaller_Test.php +++ b/tests/unit/Core/Uninstaller_Test.php @@ -22,6 +22,7 @@ class Uninstaller_Test extends UnitTestCase { * @return void */ public function tear_down() { + remove_filter( 'query', [ $this, '_create_temporary_tables' ] ); code_snippets()->db->create_or_upgrade_tables(); delete_option( Snippet_View_REST_Controller::OPTION_NAME ); delete_option( Insights_View_Rest_Controller::OPTION_NAME ); From 5689fab0838847aae7065094279efe9900b6960a Mon Sep 17 00:00:00 2001 From: Rami Yushuvaev Date: Fri, 18 Sep 2026 17:34:26 +0300 Subject: [PATCH 13/19] tests: update button name for download action and improve snippet cleanup --- tests/e2e/code-snippets-edit.spec.ts | 2 +- tests/e2e/code-snippets-evaluation.spec.ts | 44 +++++++--------------- 2 files changed, 15 insertions(+), 31 deletions(-) diff --git a/tests/e2e/code-snippets-edit.spec.ts b/tests/e2e/code-snippets-edit.spec.ts index c682c2934..17c541acf 100644 --- a/tests/e2e/code-snippets-edit.spec.ts +++ b/tests/e2e/code-snippets-edit.spec.ts @@ -254,7 +254,7 @@ test.describe('Code Snippets Admin', () => { const download = await Promise.all([ page.waitForEvent('download'), - page.getByRole('button', { name: 'Download Code' }).click() + page.getByRole('button', { name: 'Download' }).click() ]).then(([event]) => event) expect(download.suggestedFilename()).toMatch(/\.php$/) diff --git a/tests/e2e/code-snippets-evaluation.spec.ts b/tests/e2e/code-snippets-evaluation.spec.ts index 2facad907..e464c8b16 100644 --- a/tests/e2e/code-snippets-evaluation.spec.ts +++ b/tests/e2e/code-snippets-evaluation.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test' import { DEFAULT_E2E_SNIPPET_BASE_NAME, SnippetsTestHelper } from './helpers/SnippetsTestHelper' -import { SELECTORS, TIMEOUTS, URLS } from './helpers/constants' +import { SELECTORS, URLS } from './helpers/constants' import { wpCli } from './helpers/wpCli' import type { Page } from '@playwright/test' @@ -161,20 +161,20 @@ test.describe('Code Snippets Evaluation', () => { test('Safe mode query disables front-end snippet execution', async ({ page }) => { const safeModeClass = `safe-mode-${Date.now()}` - await helper.createAndActivateSnippet({ - name: snippetName, - code: `add_filter('body_class', function($classes) { $classes[] = '${safeModeClass}'; return $classes; });` - }) - - await page.goto(URLS.FRONTEND) - await expect(page.locator('body')).toHaveClass(new RegExp(safeModeClass)) + try { + await helper.createAndActivateSnippet({ + name: snippetName, + code: `add_filter('body_class', function($classes) { $classes[] = '${safeModeClass}'; return $classes; });` + }) - await page.goto(`${URLS.FRONTEND}?snippets-safe-mode=1`) - await expect(page.locator('body')).not.toHaveClass(new RegExp(safeModeClass)) + await page.goto(URLS.FRONTEND) + await expect(page.locator('body')).toHaveClass(new RegExp(safeModeClass)) - await page.goto(`${URLS.SNIPPETS_ADMIN}&snippets-safe-mode=1`) - await page.getByRole('link', { name: 'Add New' }).click() - await expect(page).toHaveURL(/snippets-safe-mode=1/, { timeout: TIMEOUTS.SHORT }) + await page.goto(`${URLS.FRONTEND}?snippets-safe-mode=1`) + await expect(page.locator('body')).not.toHaveClass(new RegExp(safeModeClass)) + } finally { + await helper.cleanupSnippet(snippetName) + } }) test('Safe mode constant disables snippets while keeping the editor accessible', async ({ page }) => { @@ -265,20 +265,6 @@ test.describe('Code Snippets Evaluation', () => { } }) - test('asks for confirmation before running a single-use snippet', async ({ page }) => { - await SnippetsTestHelper.createSnippetViaCli({ - name: snippetName, - active: false, - scope: 'single-use', - code: '// A harmless Run Once confirmation fixture.' - }) - await helper.navigateToSnippetsAdmin() - - const row = page.locator(SELECTORS.SNIPPET_ROW).filter({ hasText: snippetName }).first() - await row.getByRole('link', { name: 'Run Once' }).click() - await expect(page.getByRole('dialog', { name: /Run Once/ })).toBeVisible({ timeout: TIMEOUTS.SHORT }) - }) - test('PHP snippets execute in priority order', async ({ page }) => { const outputPrefix = `snippet-priority-${Date.now()}` const highPriorityId = `${outputPrefix}-high` @@ -301,9 +287,7 @@ test.describe('Code Snippets Evaluation', () => { }) await helper.navigateToFrontend() - await expect(page.locator(`#${lowPriorityId}`)).toBeAttached() - await expect(page.locator(`#${highPriorityId}`)).toBeAttached() - expect(await page.locator(`span[id^="${outputPrefix}"]`).evaluateAll(elements => + await expect.poll(() => page.locator(`span[id^="${outputPrefix}"]`).evaluateAll(elements => elements.map(({ id }) => id) )).toEqual([lowPriorityId, highPriorityId]) } finally { From 7f2520ec37b19109223bb2c9c91d320e280f503f Mon Sep 17 00:00:00 2001 From: Rami Yushuvaev Date: Fri, 18 Sep 2026 18:02:30 +0300 Subject: [PATCH 14/19] fix: show save shortcut in code editor help --- .../EditMenu/SnippetForm/fields/CodeEditorShortcuts.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/components/EditMenu/SnippetForm/fields/CodeEditorShortcuts.tsx b/src/js/components/EditMenu/SnippetForm/fields/CodeEditorShortcuts.tsx index 5e44a7a27..c065c996c 100644 --- a/src/js/components/EditMenu/SnippetForm/fields/CodeEditorShortcuts.tsx +++ b/src/js/components/EditMenu/SnippetForm/fields/CodeEditorShortcuts.tsx @@ -151,9 +151,9 @@ export const CodeEditorShortcuts: React.FC = ({ editor const keyMapName = codeEditorInstance.codemirror.getOption('keyMap') const combinedKeyMap: KeyMap = { - ...isMacOS() ? fallbackKeyMapMac : fallbackKeyMap, ...keyMapName && getKeyMap(keyMapName), - ...'object' === typeof extraKeys ? extraKeys : undefined + ...'object' === typeof extraKeys ? extraKeys : undefined, + ...isMacOS() ? fallbackKeyMapMac : fallbackKeyMap } return unpackKeyMap(combinedKeyMap) From e43f57428c4f8a345d654650477ea3dbef8cae84 Mon Sep 17 00:00:00 2001 From: Rami Yushuvaev Date: Fri, 18 Sep 2026 18:06:29 +0300 Subject: [PATCH 15/19] fix: handle empty tag filter value and reset filters in search results --- .../SnippetsTable/SnippetsTableControls.tsx | 2 +- .../ManageMenu/SnippetsTable/SnippetsTableSearch.tsx | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/js/components/ManageMenu/SnippetsTable/SnippetsTableControls.tsx b/src/js/components/ManageMenu/SnippetsTable/SnippetsTableControls.tsx index 3d9efa60c..9aa5d91fc 100644 --- a/src/js/components/ManageMenu/SnippetsTable/SnippetsTableControls.tsx +++ b/src/js/components/ManageMenu/SnippetsTable/SnippetsTableControls.tsx @@ -101,7 +101,7 @@ const FilterByTagControl: React.FC = ({ visibleSnippets