From 52f1dad415e246277f56fcd225055d45498b2913 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 17 Nov 2025 16:45:53 +0100 Subject: [PATCH 1/6] Reproduce max call stack in fs journal --- .../fs-journal/src/test/fs-journal.spec.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/packages/php-wasm/fs-journal/src/test/fs-journal.spec.ts b/packages/php-wasm/fs-journal/src/test/fs-journal.spec.ts index 79f77e140f..d0ec08e6a3 100644 --- a/packages/php-wasm/fs-journal/src/test/fs-journal.spec.ts +++ b/packages/php-wasm/fs-journal/src/test/fs-journal.spec.ts @@ -1,5 +1,11 @@ /* eslint-disable @nx/enforce-module-boundaries */ import { PHP } from '@php-wasm/universal'; +import { build } from 'esbuild'; +import { spawnSync } from 'node:child_process'; +import { mkdirSync } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; import type { FilesystemOperation } from '../lib/fs-journal'; import { journalFSEvents, @@ -9,6 +15,28 @@ import { import { LatestSupportedPHPVersion } from '@php-wasm/universal'; import { loadNodeRuntime } from '@php-wasm/node'; +const SPEC_DIR = path.dirname(fileURLToPath(import.meta.url)); +const NORMALIZE_ENTRY = path.resolve(SPEC_DIR, 'fixtures/normalize-entry.ts'); +const NORMALIZE_BUNDLE_DIR = path.join(os.tmpdir(), 'wp-playground-fs-journal'); +const NORMALIZE_BUNDLE_PATH = path.join( + NORMALIZE_BUNDLE_DIR, + 'normalize-bundle.mjs' +); + +async function buildNormalizeBundle() { + mkdirSync(NORMALIZE_BUNDLE_DIR, { recursive: true }); + await build({ + entryPoints: [NORMALIZE_ENTRY], + outfile: NORMALIZE_BUNDLE_PATH, + bundle: true, + platform: 'node', + format: 'esm', + logLevel: 'silent', + sourcemap: false, + }); + return NORMALIZE_BUNDLE_PATH; +} + describe('Journal MemFS', () => { let php: PHP; beforeEach(async () => { @@ -453,4 +481,29 @@ describe('normalizeFilesystemOperations()', () => { ]) ).toEqual([]); }); + it('Normalizes long rename sequences without overflowing the stack', async () => { + const bundlePath = await buildNormalizeBundle(); + const bundleUrl = pathToFileURL(bundlePath).href; + const renameCount = 512; + const script = ` +import { normalizeFilesystemOperations } from '${bundleUrl}'; +const journal = []; +for (let i = 0; i < ${renameCount}; i++) { + journal.push({ operation: 'CREATE', path: '/file-' + i, nodeType: 'file' }); + journal.push({ + operation: 'RENAME', + path: '/file-' + i, + toPath: '/renamed-' + i, + nodeType: 'file' + }); +} +normalizeFilesystemOperations(journal); + `; + const result = spawnSync( + process.execPath, + ['--stack_size=128', '--input-type=module', '-e', script], + { encoding: 'utf-8' } + ); + expect(result.status).toBe(0); + }); }); From ebbac39b660fe9917ef2cc02bce5753078d4866e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 17 Nov 2025 17:16:27 +0100 Subject: [PATCH 2/6] Failing test for normalizeFilesystemOperations --- .../fs-journal/src/test/fs-journal.spec.ts | 72 ++++++------------- 1 file changed, 22 insertions(+), 50 deletions(-) diff --git a/packages/php-wasm/fs-journal/src/test/fs-journal.spec.ts b/packages/php-wasm/fs-journal/src/test/fs-journal.spec.ts index d0ec08e6a3..ab46359fcd 100644 --- a/packages/php-wasm/fs-journal/src/test/fs-journal.spec.ts +++ b/packages/php-wasm/fs-journal/src/test/fs-journal.spec.ts @@ -1,11 +1,5 @@ /* eslint-disable @nx/enforce-module-boundaries */ import { PHP } from '@php-wasm/universal'; -import { build } from 'esbuild'; -import { spawnSync } from 'node:child_process'; -import { mkdirSync } from 'node:fs'; -import os from 'node:os'; -import path from 'node:path'; -import { fileURLToPath, pathToFileURL } from 'node:url'; import type { FilesystemOperation } from '../lib/fs-journal'; import { journalFSEvents, @@ -15,26 +9,11 @@ import { import { LatestSupportedPHPVersion } from '@php-wasm/universal'; import { loadNodeRuntime } from '@php-wasm/node'; -const SPEC_DIR = path.dirname(fileURLToPath(import.meta.url)); -const NORMALIZE_ENTRY = path.resolve(SPEC_DIR, 'fixtures/normalize-entry.ts'); -const NORMALIZE_BUNDLE_DIR = path.join(os.tmpdir(), 'wp-playground-fs-journal'); -const NORMALIZE_BUNDLE_PATH = path.join( - NORMALIZE_BUNDLE_DIR, - 'normalize-bundle.mjs' -); - -async function buildNormalizeBundle() { - mkdirSync(NORMALIZE_BUNDLE_DIR, { recursive: true }); - await build({ - entryPoints: [NORMALIZE_ENTRY], - outfile: NORMALIZE_BUNDLE_PATH, - bundle: true, - platform: 'node', - format: 'esm', - logLevel: 'silent', - sourcemap: false, - }); - return NORMALIZE_BUNDLE_PATH; +function runWithLimitedStack(depth: number, fn: () => T): T { + if (depth === 0) { + return fn(); + } + return runWithLimitedStack(depth - 1, fn); } describe('Journal MemFS', () => { @@ -481,29 +460,22 @@ describe('normalizeFilesystemOperations()', () => { ]) ).toEqual([]); }); - it('Normalizes long rename sequences without overflowing the stack', async () => { - const bundlePath = await buildNormalizeBundle(); - const bundleUrl = pathToFileURL(bundlePath).href; - const renameCount = 512; - const script = ` -import { normalizeFilesystemOperations } from '${bundleUrl}'; -const journal = []; -for (let i = 0; i < ${renameCount}; i++) { - journal.push({ operation: 'CREATE', path: '/file-' + i, nodeType: 'file' }); - journal.push({ - operation: 'RENAME', - path: '/file-' + i, - toPath: '/renamed-' + i, - nodeType: 'file' - }); -} -normalizeFilesystemOperations(journal); - `; - const result = spawnSync( - process.execPath, - ['--stack_size=128', '--input-type=module', '-e', script], - { encoding: 'utf-8' } - ); - expect(result.status).toBe(0); + it('Normalizes long rename sequences without overflowing the stack', () => { + const renameCount = 350; + const journal: FilesystemOperation[] = []; + for (let i = 0; i < renameCount; i++) { + journal.push({ + operation: 'CREATE', + path: `/file-${i}`, + nodeType: 'file', + }); + journal.push({ + operation: 'RENAME', + path: `/file-${i}`, + toPath: `/renamed-${i}`, + nodeType: 'file', + }); + } + runWithLimitedStack(7000, () => normalizeFilesystemOperations(journal)); }); }); From 18d1fa6775134d8a6570fc083978c0dc8a82a940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 17 Nov 2025 17:33:48 +0100 Subject: [PATCH 3/6] Find the problematic recursive case --- .../fs-journal/src/test/fs-journal.spec.ts | 66 +++++++++++++++++-- 1 file changed, 60 insertions(+), 6 deletions(-) diff --git a/packages/php-wasm/fs-journal/src/test/fs-journal.spec.ts b/packages/php-wasm/fs-journal/src/test/fs-journal.spec.ts index ab46359fcd..b06b4665e3 100644 --- a/packages/php-wasm/fs-journal/src/test/fs-journal.spec.ts +++ b/packages/php-wasm/fs-journal/src/test/fs-journal.spec.ts @@ -9,11 +9,36 @@ import { import { LatestSupportedPHPVersion } from '@php-wasm/universal'; import { loadNodeRuntime } from '@php-wasm/node'; -function runWithLimitedStack(depth: number, fn: () => T): T { - if (depth === 0) { - return fn(); +let cachedMaxStackDepth: number | null = null; +function getMaxStackDepth() { + if (cachedMaxStackDepth !== null) { + return cachedMaxStackDepth; } - return runWithLimitedStack(depth - 1, fn); + let depth = 0; + function dive() { + depth++; + dive(); + } + try { + dive(); + } catch (error) { + if (!(error instanceof RangeError)) { + throw error; + } + } + cachedMaxStackDepth = depth; + return depth; +} + +function runWithLimitedStack(budget: number, fn: () => T): T { + const maxDepth = getMaxStackDepth(); + const framesToConsume = Math.max(0, maxDepth - budget); + let wrapped = fn; + for (let i = 0; i < framesToConsume; i++) { + const next = wrapped; + wrapped = () => next(); + } + return wrapped(); } describe('Journal MemFS', () => { @@ -460,7 +485,7 @@ describe('normalizeFilesystemOperations()', () => { ]) ).toEqual([]); }); - it('Normalizes long rename sequences without overflowing the stack', () => { + it('Still overflows the stack on long rename sequences', () => { const renameCount = 350; const journal: FilesystemOperation[] = []; for (let i = 0; i < renameCount; i++) { @@ -476,6 +501,35 @@ describe('normalizeFilesystemOperations()', () => { nodeType: 'file', }); } - runWithLimitedStack(7000, () => normalizeFilesystemOperations(journal)); + expect(() => + runWithLimitedStack(512, () => + normalizeFilesystemOperations(journal) + ) + ).toThrow(RangeError); + }); + it('Overflows the stack even with a handful of recursive rewrites', () => { + const journal: FilesystemOperation[] = [ + { operation: 'CREATE', path: '/dir', nodeType: 'directory' }, + { operation: 'CREATE', path: '/dir/a', nodeType: 'directory' }, + { + operation: 'RENAME', + path: '/dir', + toPath: '/dir/a', + nodeType: 'directory', + }, + { operation: 'DELETE', path: '/dir/a', nodeType: 'directory' }, + { + operation: 'RENAME', + path: '/dir/a', + toPath: '/dir/a/b', + nodeType: 'directory', + }, + { operation: 'DELETE', path: '/dir/a/b', nodeType: 'directory' }, + ]; + expect(() => + runWithLimitedStack(20, () => + normalizeFilesystemOperations([...journal]) + ) + ).toThrow(RangeError); }); }); From 7b94f5a42daf9e71b01a8669af7aea04c80be997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 18 Nov 2025 01:19:36 +0100 Subject: [PATCH 4/6] [Website] Limit the callstack size in filesystem journal Replaces recursion with a loop in the filesystem journal code to avoid the following error when installing Woo on an OPFS site: ``` Uncaught (in promise) RangeError: Maximum call stack size exceeded at normalizeFilesystemOperations (fs-journal.ts:364:17) at normalizeFilesystemOperations (fs-journal.ts:473:11) ... ``` \## Implementation `normalizeFilesystemOperations()` used to call itself in a way that significantly inflated the callstack when either there was either a lot of operations to normalize, or the normalized operations referenced each other recursively. This PR replaces the recursive call with a while(true) loop which limits the callstack size and allows the normalization to finish. \## Testing instructions Create a new site, save it "to this browser", go to wp-admin, loop up Woo, click install, confirm it worked. --- .../php-wasm/fs-journal/src/lib/fs-journal.ts | 208 +++++++++--------- .../fs-journal/src/test/fs-journal.spec.ts | 59 ++--- 2 files changed, 122 insertions(+), 145 deletions(-) diff --git a/packages/php-wasm/fs-journal/src/lib/fs-journal.ts b/packages/php-wasm/fs-journal/src/lib/fs-journal.ts index 395d7a5b7c..3d311c111d 100644 --- a/packages/php-wasm/fs-journal/src/lib/fs-journal.ts +++ b/packages/php-wasm/fs-journal/src/lib/fs-journal.ts @@ -362,118 +362,124 @@ function normalizePath(path: string) { * @returns The normalized journal. */ export function normalizeFilesystemOperations( - journal: FilesystemOperation[] + input: FilesystemOperation[] ): FilesystemOperation[] { - const substitutions: Record = {}; - for (let i = journal.length - 1; i >= 0; i--) { - for (let j = i - 1; j >= 0; j--) { - const formerType = checkRelationship(journal[i], journal[j]); - if (formerType === 'none') { - continue; - } + let journal = input; + while (true) { + const substitutions: Record = {}; + for (let i = journal.length - 1; i >= 0; i--) { + for (let j = i - 1; j >= 0; j--) { + const formerType = checkRelationship(journal[i], journal[j]); + if (formerType === 'none') { + continue; + } - const latter = journal[i]; - const former = journal[j]; - if ( - latter.operation === 'RENAME' && - former.operation === 'RENAME' - ) { - // Normalizing a double rename is a complex scenario so let's just give - // up. There's just too many possible scenarios to handle. - // - // For example, the following scenario may not be possible to normalize: - // RENAME /dir_a /dir_b - // RENAME /dir_b/subdir /dir_c - // RENAME /dir_b /dir_d - // - // Similarly, how should we normalize the following list? - // CREATE_FILE /file - // CREATE_DIR /dir_a - // RENAME /file /dir_a/file - // RENAME /dir_a /dir_b - // RENAME /dir_b/file /dir_b/file_2 - // - // The shortest way to recreate the same structure would be this: - // CREATE_DIR /dir_b - // CREATE_FILE /dir_b/file_2 - // - // But that's not a straightforward transformation so let's just not - // handle it for now. - logger.warn( - '[FS Journal] Normalizing a double rename is not yet supported:', - { - current: latter, - last: former, - } - ); - continue; - } + const latter = journal[i]; + const former = journal[j]; + if ( + latter.operation === 'RENAME' && + former.operation === 'RENAME' + ) { + // Normalizing a double rename is a complex scenario so let's just give + // up. There's just too many possible scenarios to handle. + // + // For example, the following scenario may not be possible to normalize: + // RENAME /dir_a /dir_b + // RENAME /dir_b/subdir /dir_c + // RENAME /dir_b /dir_d + // + // Similarly, how should we normalize the following list? + // CREATE_FILE /file + // CREATE_DIR /dir_a + // RENAME /file /dir_a/file + // RENAME /dir_a /dir_b + // RENAME /dir_b/file /dir_b/file_2 + // + // The shortest way to recreate the same structure would be this: + // CREATE_DIR /dir_b + // CREATE_FILE /dir_b/file_2 + // + // But that's not a straightforward transformation so let's just not + // handle it for now. + logger.warn( + '[FS Journal] Normalizing a double rename is not yet supported:', + { + current: latter, + last: former, + } + ); + continue; + } - if (former.operation === 'CREATE' || former.operation === 'WRITE') { - if (latter.operation === 'RENAME') { - if (formerType === 'same_node') { - // Creating a node and then renaming it is equivalent to creating - // it in the new location. + if ( + former.operation === 'CREATE' || + former.operation === 'WRITE' + ) { + if (latter.operation === 'RENAME') { + if (formerType === 'same_node') { + // Creating a node and then renaming it is equivalent to creating + // it in the new location. + substitutions[j] = []; + substitutions[i] = [ + { + ...former, + path: latter.toPath, + }, + ...(substitutions[i] || []), + ]; + } else if (formerType === 'descendant') { + // Creating a node and then renaming its parent directory is + // equivalent to creating it in the new location. + substitutions[j] = []; + substitutions[i] = [ + { + ...former, + path: joinPaths( + latter.toPath, + former.path.substring( + latter.path.length + ) + ), + }, + ...(substitutions[i] || []), + ]; + } + } else if ( + latter.operation === 'WRITE' && + formerType === 'same_node' + ) { + // Updating the same node twice is equivalent to updating it once + // at the later time. substitutions[j] = []; - substitutions[i] = [ - { - ...former, - path: latter.toPath, - }, - ...(substitutions[i] || []), - ]; - } else if (formerType === 'descendant') { - // Creating a node and then renaming its parent directory is - // equivalent to creating it in the new location. + } else if ( + latter.operation === 'DELETE' && + formerType === 'same_node' + ) { + // A CREATE/WRITE followed by a DELETE on the same node. + // The CREATE/WRITE is redundant. substitutions[j] = []; - substitutions[i] = [ - { - ...former, - path: joinPaths( - latter.toPath, - former.path.substring(latter.path.length) - ), - }, - ...(substitutions[i] || []), - ]; - } - } else if ( - latter.operation === 'WRITE' && - formerType === 'same_node' - ) { - // Updating the same node twice is equivalent to updating it once - // at the later time. - substitutions[j] = []; - } else if ( - latter.operation === 'DELETE' && - formerType === 'same_node' - ) { - // A CREATE/WRITE followed by a DELETE on the same node. - // The CREATE/WRITE is redundant. - substitutions[j] = []; - - // The DELETE is redundant only if the node was created - // in this journal. - if (former.operation === 'CREATE') { - substitutions[i] = []; + + // The DELETE is redundant only if the node was created + // in this journal. + if (former.operation === 'CREATE') { + substitutions[i] = []; + } } } } } - // Any substitutions? Apply them and start over. - // We can't just continue as the current operation may - // have been replaced. - if (Object.entries(substitutions).length > 0) { - const updated = journal.flatMap((op, index) => { - if (!(index in substitutions)) { - return [op]; - } - return substitutions[index]; - }); - return normalizeFilesystemOperations(updated); + + if (Object.keys(substitutions).length === 0) { + return journal; } + + journal = journal.flatMap((op, index) => { + if (!(index in substitutions)) { + return [op]; + } + return substitutions[index]; + }); } - return journal; } type RelatedOperationInfo = 'same_node' | 'ancestor' | 'descendant' | 'none'; diff --git a/packages/php-wasm/fs-journal/src/test/fs-journal.spec.ts b/packages/php-wasm/fs-journal/src/test/fs-journal.spec.ts index b06b4665e3..3457f5dc9a 100644 --- a/packages/php-wasm/fs-journal/src/test/fs-journal.spec.ts +++ b/packages/php-wasm/fs-journal/src/test/fs-journal.spec.ts @@ -9,38 +9,6 @@ import { import { LatestSupportedPHPVersion } from '@php-wasm/universal'; import { loadNodeRuntime } from '@php-wasm/node'; -let cachedMaxStackDepth: number | null = null; -function getMaxStackDepth() { - if (cachedMaxStackDepth !== null) { - return cachedMaxStackDepth; - } - let depth = 0; - function dive() { - depth++; - dive(); - } - try { - dive(); - } catch (error) { - if (!(error instanceof RangeError)) { - throw error; - } - } - cachedMaxStackDepth = depth; - return depth; -} - -function runWithLimitedStack(budget: number, fn: () => T): T { - const maxDepth = getMaxStackDepth(); - const framesToConsume = Math.max(0, maxDepth - budget); - let wrapped = fn; - for (let i = 0; i < framesToConsume; i++) { - const next = wrapped; - wrapped = () => next(); - } - return wrapped(); -} - describe('Journal MemFS', () => { let php: PHP; beforeEach(async () => { @@ -485,7 +453,7 @@ describe('normalizeFilesystemOperations()', () => { ]) ).toEqual([]); }); - it('Still overflows the stack on long rename sequences', () => { + it('Normalizes long rename sequences without overflowing the stack', () => { const renameCount = 350; const journal: FilesystemOperation[] = []; for (let i = 0; i < renameCount; i++) { @@ -501,13 +469,16 @@ describe('normalizeFilesystemOperations()', () => { nodeType: 'file', }); } - expect(() => - runWithLimitedStack(512, () => - normalizeFilesystemOperations(journal) - ) - ).toThrow(RangeError); + const normalized = normalizeFilesystemOperations(journal); + expect(normalized).toEqual( + Array.from({ length: renameCount }, (_, i) => ({ + operation: 'CREATE', + path: `/renamed-${i}`, + nodeType: 'file', + })) + ); }); - it('Overflows the stack even with a handful of recursive rewrites', () => { + it('Normalizes even a handful of recursive rewrites', () => { const journal: FilesystemOperation[] = [ { operation: 'CREATE', path: '/dir', nodeType: 'directory' }, { operation: 'CREATE', path: '/dir/a', nodeType: 'directory' }, @@ -526,10 +497,10 @@ describe('normalizeFilesystemOperations()', () => { }, { operation: 'DELETE', path: '/dir/a/b', nodeType: 'directory' }, ]; - expect(() => - runWithLimitedStack(20, () => - normalizeFilesystemOperations([...journal]) - ) - ).toThrow(RangeError); + const normalized = normalizeFilesystemOperations([...journal]); + expect(normalized).toEqual([ + { operation: 'CREATE', path: '/dir/a', nodeType: 'directory' }, + { operation: 'CREATE', path: '/dir/a/a', nodeType: 'directory' }, + ]); }); }); From 00b965f887c67218869053d1988f2fd4b05946c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 18 Nov 2025 01:36:59 +0100 Subject: [PATCH 5/6] Add an E2E test to confirm Woo can be installed on an OPFS site --- .../website/playwright/e2e/website-ui.spec.ts | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/packages/playground/website/playwright/e2e/website-ui.spec.ts b/packages/playground/website/playwright/e2e/website-ui.spec.ts index f2c0be5e7a..5ed2d58269 100644 --- a/packages/playground/website/playwright/e2e/website-ui.spec.ts +++ b/packages/playground/website/playwright/e2e/website-ui.spec.ts @@ -228,6 +228,52 @@ test('should rename a saved Playground and persist after reload', async ({ ).toContainText(newName); }); +test('installs WooCommerce via wp-admin after saving the site', async ({ + website, + wordpress, + browserName, +}) => { + test.skip( + browserName !== 'chromium', + `This test relies on OPFS-backed saves, which aren't available in Playwright's flavor of ${browserName}.` + ); + + await website.goto('./'); + await website.ensureSiteManagerIsOpen(); + await saveSiteViaModal(website.page); + await website.ensureSiteManagerIsClosed(); + + await website.goto('./?url=/wp-admin/plugins.php'); + await website.ensureSiteManagerIsClosed(); + + await wordpress.getByRole('link', { name: 'Add New', exact: true }).click(); + const searchInput = wordpress.locator('#plugin-search-input'); + await searchInput.fill('woo'); + await searchInput.press('Enter'); + + const wooCard = wordpress.locator('.plugin-card-woocommerce'); + await expect(wooCard).toBeVisible({ timeout: 60000 }); + + const installButton = wooCard.getByRole('button', { + name: /Install Now/i, + }); + await installButton.click(); + + await expect( + wooCard.getByRole('button', { name: /Activate/i }) + ).toBeVisible({ timeout: 120000 }); + + await wordpress.getByRole('link', { name: 'Plugins', exact: true }).click(); + await expect(wordpress.locator('.wp-heading-inline')).toContainText( + 'Plugins' + ); + + await wordpress.getByRole('link', { name: 'Posts', exact: true }).click(); + await expect(wordpress.locator('.wp-heading-inline')).toContainText( + 'Posts' + ); +}); + test('should show save site modal with correct elements', async ({ website, browserName, From 74854a497fb6ea10c0e3e133ae278320681b2101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 18 Nov 2025 02:34:55 +0100 Subject: [PATCH 6/6] Remove WooCommerce installation test from e2e specs Removed the test for installing WooCommerce via wp-admin after saving the site. --- .../website/playwright/e2e/website-ui.spec.ts | 46 ------------------- 1 file changed, 46 deletions(-) diff --git a/packages/playground/website/playwright/e2e/website-ui.spec.ts b/packages/playground/website/playwright/e2e/website-ui.spec.ts index 5ed2d58269..f2c0be5e7a 100644 --- a/packages/playground/website/playwright/e2e/website-ui.spec.ts +++ b/packages/playground/website/playwright/e2e/website-ui.spec.ts @@ -228,52 +228,6 @@ test('should rename a saved Playground and persist after reload', async ({ ).toContainText(newName); }); -test('installs WooCommerce via wp-admin after saving the site', async ({ - website, - wordpress, - browserName, -}) => { - test.skip( - browserName !== 'chromium', - `This test relies on OPFS-backed saves, which aren't available in Playwright's flavor of ${browserName}.` - ); - - await website.goto('./'); - await website.ensureSiteManagerIsOpen(); - await saveSiteViaModal(website.page); - await website.ensureSiteManagerIsClosed(); - - await website.goto('./?url=/wp-admin/plugins.php'); - await website.ensureSiteManagerIsClosed(); - - await wordpress.getByRole('link', { name: 'Add New', exact: true }).click(); - const searchInput = wordpress.locator('#plugin-search-input'); - await searchInput.fill('woo'); - await searchInput.press('Enter'); - - const wooCard = wordpress.locator('.plugin-card-woocommerce'); - await expect(wooCard).toBeVisible({ timeout: 60000 }); - - const installButton = wooCard.getByRole('button', { - name: /Install Now/i, - }); - await installButton.click(); - - await expect( - wooCard.getByRole('button', { name: /Activate/i }) - ).toBeVisible({ timeout: 120000 }); - - await wordpress.getByRole('link', { name: 'Plugins', exact: true }).click(); - await expect(wordpress.locator('.wp-heading-inline')).toContainText( - 'Plugins' - ); - - await wordpress.getByRole('link', { name: 'Posts', exact: true }).click(); - await expect(wordpress.locator('.wp-heading-inline')).toContainText( - 'Posts' - ); -}); - test('should show save site modal with correct elements', async ({ website, browserName,