diff --git a/.eslintignore b/.eslintignore index c47a41f9f9..b3ac4e5866 100644 --- a/.eslintignore +++ b/.eslintignore @@ -10,4 +10,5 @@ packages/playground/sync/src/test/wp-* packages/php-wasm/node/src/test/__test* *.timestamp-1678999213403.mjs .local -.vscode \ No newline at end of file +.vscode +playwright-report \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b73a9bd70..1c8b611eb5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,22 @@ jobs: with: name: cypress-screenshots path: dist/cypress/packages/playground/website/screenshots - + test-e2e-playwright: + runs-on: ubuntu-latest + needs: [lint-and-typecheck] + # Run as root to allow node to bind to port 80 + steps: + - uses: actions/checkout@v3 + - uses: ./.github/actions/prepare-playground + - name: Install Playwright Browsers + run: sudo npx playwright install --with-deps + - name: Run Playwright tests + run: sudo CI=true npx nx run playground-website:e2e:playwright:ci + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ build: runs-on: ubuntu-latest needs: [lint-and-typecheck] diff --git a/.gitignore b/.gitignore index 7544500691..f9bc1bc23b 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,6 @@ Thumbs.db # Playground artifacts php.js.bak +**/test-results/ +**/playwright-report/ +**/blob-report/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e8aae0351..3f170c4fb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,17 +25,17 @@ The following contributors merged PRs in this release: ### Documentation -- Docs: Better paths for links. ([#1765](https://github.com/WordPress/wordpress-playground/pull/1765)) -- Docs: I18n setup. ([#1766](https://github.com/WordPress/wordpress-playground/pull/1766)) -- Docs: Remove the outdated "data rependencies" page. ([#1785](https://github.com/WordPress/wordpress-playground/pull/1785)) +- Docs: Better paths for links. ([#1765](https://github.com/WordPress/wordpress-playground/pull/1765)) +- Docs: I18n setup. ([#1766](https://github.com/WordPress/wordpress-playground/pull/1766)) +- Docs: Remove the outdated "data rependencies" page. ([#1785](https://github.com/WordPress/wordpress-playground/pull/1785)) ### Website -- Fix troubleshoot-and-debug link. ([#1782](https://github.com/WordPress/wordpress-playground/pull/1782)) +- Fix troubleshoot-and-debug link. ([#1782](https://github.com/WordPress/wordpress-playground/pull/1782)) ### Various -- Update link for contributor day. ([#1775](https://github.com/WordPress/wordpress-playground/pull/1775)) +- Update link for contributor day. ([#1775](https://github.com/WordPress/wordpress-playground/pull/1775)) ### Contributors @@ -43,12 +43,11 @@ The following contributors merged PRs in this release: @adamziel @bgrgicak @juanmaguitar @n8finch - -## [v0.9.42] (2024-09-17) +## [v0.9.42] (2024-09-17) ### PHP WebAssembly -- FS: Use the correct rm/rmdir method when moving files between mounts. ([#1770](https://github.com/WordPress/wordpress-playground/pull/1770)) +- FS: Use the correct rm/rmdir method when moving files between mounts. ([#1770](https://github.com/WordPress/wordpress-playground/pull/1770)) ### Contributors @@ -56,50 +55,46 @@ The following contributors merged PRs in this release: @adamziel +## [v0.9.41] (2024-09-16) -## [v0.9.41] (2024-09-16) - - - - -## [v0.9.40] (2024-09-16) +## [v0.9.40] (2024-09-16) ### Enhancements -- Extend allowable resources available via WordPress/WordPress. ([#1721](https://github.com/WordPress/wordpress-playground/pull/1721)) +- Extend allowable resources available via WordPress/WordPress. ([#1721](https://github.com/WordPress/wordpress-playground/pull/1721)) ### Tools -- Update actions/upload-artifact version to 4. ([#1748](https://github.com/WordPress/wordpress-playground/pull/1748)) +- Update actions/upload-artifact version to 4. ([#1748](https://github.com/WordPress/wordpress-playground/pull/1748)) ### Documentation -- Docs/Blueprints resources: Grammar and typo fixes. ([#1741](https://github.com/WordPress/wordpress-playground/pull/1741)) +- Docs/Blueprints resources: Grammar and typo fixes. ([#1741](https://github.com/WordPress/wordpress-playground/pull/1741)) ### PHP WebAssembly -- @php-wasm/universal : Add Phar support in php-wasm. ([#1716](https://github.com/WordPress/wordpress-playground/pull/1716)) +- @php-wasm/universal : Add Phar support in php-wasm. ([#1716](https://github.com/WordPress/wordpress-playground/pull/1716)) ### Website -- Add the `components` package with PathMappingControl. ([#1608](https://github.com/WordPress/wordpress-playground/pull/1608)) +- Add the `components` package with PathMappingControl. ([#1608](https://github.com/WordPress/wordpress-playground/pull/1608)) ### Bug Fixes -- Fix CLI --skipWordPressSetup option. ([#1760](https://github.com/WordPress/wordpress-playground/pull/1760)) +- Fix CLI --skipWordPressSetup option. ([#1760](https://github.com/WordPress/wordpress-playground/pull/1760)) ### Reliability -- Improve Playground CLI logging and fix quiet mode. ([#1751](https://github.com/WordPress/wordpress-playground/pull/1751)) +- Improve Playground CLI logging and fix quiet mode. ([#1751](https://github.com/WordPress/wordpress-playground/pull/1751)) ### Various -- Docs/Guides: Guides introductions and some minor adjustments. ([#1754](https://github.com/WordPress/wordpress-playground/pull/1754)) -- Docs/Guides: Normalized and fixed guides links. ([#1756](https://github.com/WordPress/wordpress-playground/pull/1756)) -- Docs/Guides: Providing content for your demo. ([#1747](https://github.com/WordPress/wordpress-playground/pull/1747)) -- Docs/Guides: WordPress Playground for plugin developers. ([#1750](https://github.com/WordPress/wordpress-playground/pull/1750)) -- Docs/Guides: WordPress Playground for theme developers. ([#1732](https://github.com/WordPress/wordpress-playground/pull/1732)) -- Docs: Links redirections. ([#1758](https://github.com/WordPress/wordpress-playground/pull/1758)) +- Docs/Guides: Guides introductions and some minor adjustments. ([#1754](https://github.com/WordPress/wordpress-playground/pull/1754)) +- Docs/Guides: Normalized and fixed guides links. ([#1756](https://github.com/WordPress/wordpress-playground/pull/1756)) +- Docs/Guides: Providing content for your demo. ([#1747](https://github.com/WordPress/wordpress-playground/pull/1747)) +- Docs/Guides: WordPress Playground for plugin developers. ([#1750](https://github.com/WordPress/wordpress-playground/pull/1750)) +- Docs/Guides: WordPress Playground for theme developers. ([#1732](https://github.com/WordPress/wordpress-playground/pull/1732)) +- Docs: Links redirections. ([#1758](https://github.com/WordPress/wordpress-playground/pull/1758)) ### Contributors @@ -107,7 +102,6 @@ The following contributors merged PRs in this release: @adamziel @bgrgicak @brandonpayton @juanmaguitar @mho22 @peterwilsoncc - ## [v0.9.39] (2024-09-09) ### Bug Fixes diff --git a/package-lock.json b/package-lock.json index d0f672d6a6..458976e7e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,11 +31,13 @@ "octokit-plugin-create-pull-request": "5.1.1", "react": "^18.2.25", "react-dom": "^18.2.25", + "react-hook-form": "7.53.0", "react-modal": "^3.16.1", "react-redux": "8.1.3", "react-transition-group": "4.4.5", "unzipper": "0.10.11", "vite-plugin-api": "1.0.4", + "wouter": "3.3.5", "xterm": "5.3.0", "xterm-addon-fit": "0.8.0", "yargs": "17.7.2" @@ -63,6 +65,7 @@ "@nx/web": "16.9.0", "@nx/webpack": "16.9.0", "@nx/workspace": "16.9.0", + "@playwright/test": "1.47.1", "@rollup/plugin-url": "^8.0.1", "@swc-node/register": "~1.6.7", "@swc/core": "~1.3.85", @@ -13985,6 +13988,22 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.47.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.1.tgz", + "integrity": "sha512-dbWpcNQZ5nj16m+A5UNScYx7HX5trIy7g4phrcitn+Nk83S32EBX/CLU4hiF4RGKX/yRc93AAqtfaXB7JWBd4Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.47.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.21", "dev": true, @@ -34390,6 +34409,12 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/mkdirp": { "version": "1.0.4", "devOptional": true, @@ -36862,6 +36887,38 @@ "node": ">=4" } }, + "node_modules/playwright": { + "version": "1.47.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.1.tgz", + "integrity": "sha512-SUEKi6947IqYbKxRiqnbUobVZY4bF1uu+ZnZNJX9DfU1tlf2UhWfvVjLf01pQx9URsOr18bFVUKXmanYWhbfkw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.47.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.47.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.1.tgz", + "integrity": "sha512-i1iyJdLftqtt51mEk6AhYFaAJCDx0xQ/O5NU8EKaWFgMjItPVma542Nh/Aq8aLCjIJSzjaiEQGW/nyqLkGF1OQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/portfinder": { "version": "1.0.32", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", @@ -38496,6 +38553,22 @@ "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-hook-form": { + "version": "7.53.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.0.tgz", + "integrity": "sha512-M1n3HhqCww6S2hxLxciEXy2oISPnAzxY7gvwVPrtlczTM/1dDadXgUxDpHMrMTblDOcm/AXtXxHwZ3jpg1mqKQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-is": { "version": "17.0.2", "dev": true, @@ -39479,6 +39552,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regexparam": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-3.0.0.tgz", + "integrity": "sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/regexpu-core": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", @@ -46583,6 +46665,20 @@ "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", "dev": true }, + "node_modules/wouter": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/wouter/-/wouter-3.3.5.tgz", + "integrity": "sha512-bx3fLQAMn+EhYbBdY3W1gw9ZfO/uchudxYMwOIBzF3HVgqNEEIT199vEoh7FLTC0Vz5+rpMO6NdFsOkGX1QQCw==", + "license": "Unlicense", + "dependencies": { + "mitt": "^3.0.1", + "regexparam": "^3.0.0", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "license": "MIT", diff --git a/package.json b/package.json index cc439fb3d6..d3763de549 100644 --- a/package.json +++ b/package.json @@ -72,11 +72,13 @@ "octokit-plugin-create-pull-request": "5.1.1", "react": "^18.2.25", "react-dom": "^18.2.25", + "react-hook-form": "7.53.0", "react-modal": "^3.16.1", "react-redux": "8.1.3", "react-transition-group": "4.4.5", "unzipper": "0.10.11", "vite-plugin-api": "1.0.4", + "wouter": "3.3.5", "xterm": "5.3.0", "xterm-addon-fit": "0.8.0", "yargs": "17.7.2" @@ -104,6 +106,7 @@ "@nx/web": "16.9.0", "@nx/webpack": "16.9.0", "@nx/workspace": "16.9.0", + "@playwright/test": "1.47.1", "@rollup/plugin-url": "^8.0.1", "@swc-node/register": "~1.6.7", "@swc/core": "~1.3.85", diff --git a/packages/docs/site/docs/developers/06-apis/query-api/01-index.md b/packages/docs/site/docs/developers/06-apis/query-api/01-index.md index 34e81fb78f..22d2d65dc0 100644 --- a/packages/docs/site/docs/developers/06-apis/query-api/01-index.md +++ b/packages/docs/site/docs/developers/06-apis/query-api/01-index.md @@ -35,10 +35,9 @@ You can go ahead and try it out. The Playground will automatically install the t | `lazy` | | Defer loading the Playground assets until someone clicks on the "Run" button. Does not accept any values. If `lazy` is added as a URL parameter, loading will be deferred. | | `login` | `yes` | Log the user in as an admin. Accepts `yes` or `no`. | | `multisite` | `no` | Enables the WordPress multisite mode. Accepts `yes` or `no`. | -| `storage` | `none` | Selects the storage for Playground: `none` gets erased on page refresh, `browser` is stored in the browser, and `device` is stored in the selected directory on a device. The last two protect the user from accidentally losing their work upon page refresh. | | `import-site` | | Imports site files and database from a ZIP file specified by a URL. | | `import-wxr` | | Imports site content from a WXR file specified by a URL. It uses the WordPress Importer plugin, so the default admin user must be logged in. | -| `site-slug` | | Selects which site to load from browser storage. This must be used in combination with `storage=browser`. | +| `site-slug` | | Selects which site to load from browser storage. | | `language` | `en_US` | Sets the locale for the WordPress instance. This must be used in combination with `networking=yes` otherwise WordPress won't be able to download translations. | For example, the following code embeds a Playground with a preinstalled Gutenberg plugin and opens the post editor: diff --git a/packages/docs/site/docs/developers/24-limitations/01-index.md b/packages/docs/site/docs/developers/24-limitations/01-index.md index 55e995f0e1..b57c7dfd4f 100644 --- a/packages/docs/site/docs/developers/24-limitations/01-index.md +++ b/packages/docs/site/docs/developers/24-limitations/01-index.md @@ -16,7 +16,7 @@ Playground [disables network connections](/blueprints/data-format#features) by d ### Temporary by design -As Playground [streams rather than serves](/about#streamed-not-served) WordPress, all database changes and uploads will be gone when you refresh the page. To avoid losing your work, either [export your work](/quick-start-guide#save-your-site) before or enable storage in the browser/device via the [Query API](/developers/apis/query-api#available-options) or the UI. +As Playground [streams rather than serves](/about#streamed-not-served) WordPress, all database changes and uploads will be gone when you refresh the page. To avoid losing your work, either [export your work](/quick-start-guide#save-your-site) before or enable storage in the browser/device via the "Save" button in the UI. ## When developing with Playground diff --git a/packages/docs/site/docs/main/_assets/customize-playground.png b/packages/docs/site/docs/main/_assets/customize-playground.png index 3b8a7d356a..4d6d0b5105 100644 Binary files a/packages/docs/site/docs/main/_assets/customize-playground.png and b/packages/docs/site/docs/main/_assets/customize-playground.png differ diff --git a/packages/docs/site/docs/main/changelog.md b/packages/docs/site/docs/main/changelog.md index b36439c998..8682d372f1 100644 --- a/packages/docs/site/docs/main/changelog.md +++ b/packages/docs/site/docs/main/changelog.md @@ -30,17 +30,17 @@ The following contributors merged PRs in this release: ### Documentation -- Docs: Better paths for links. ([#1765](https://github.com/WordPress/wordpress-playground/pull/1765)) -- Docs: I18n setup. ([#1766](https://github.com/WordPress/wordpress-playground/pull/1766)) -- Docs: Remove the outdated "data rependencies" page. ([#1785](https://github.com/WordPress/wordpress-playground/pull/1785)) +- Docs: Better paths for links. ([#1765](https://github.com/WordPress/wordpress-playground/pull/1765)) +- Docs: I18n setup. ([#1766](https://github.com/WordPress/wordpress-playground/pull/1766)) +- Docs: Remove the outdated "data rependencies" page. ([#1785](https://github.com/WordPress/wordpress-playground/pull/1785)) ### Website -- Fix troubleshoot-and-debug link. ([#1782](https://github.com/WordPress/wordpress-playground/pull/1782)) +- Fix troubleshoot-and-debug link. ([#1782](https://github.com/WordPress/wordpress-playground/pull/1782)) ### Various -- Update link for contributor day. ([#1775](https://github.com/WordPress/wordpress-playground/pull/1775)) +- Update link for contributor day. ([#1775](https://github.com/WordPress/wordpress-playground/pull/1775)) ### Contributors @@ -48,12 +48,11 @@ The following contributors merged PRs in this release: @adamziel @bgrgicak @juanmaguitar @n8finch - -## [v0.9.42] (2024-09-17) +## [v0.9.42] (2024-09-17) ### PHP WebAssembly -- FS: Use the correct rm/rmdir method when moving files between mounts. ([#1770](https://github.com/WordPress/wordpress-playground/pull/1770)) +- FS: Use the correct rm/rmdir method when moving files between mounts. ([#1770](https://github.com/WordPress/wordpress-playground/pull/1770)) ### Contributors @@ -61,50 +60,46 @@ The following contributors merged PRs in this release: @adamziel +## [v0.9.41] (2024-09-16) -## [v0.9.41] (2024-09-16) - - - - -## [v0.9.40] (2024-09-16) +## [v0.9.40] (2024-09-16) ### Enhancements -- Extend allowable resources available via WordPress/WordPress. ([#1721](https://github.com/WordPress/wordpress-playground/pull/1721)) +- Extend allowable resources available via WordPress/WordPress. ([#1721](https://github.com/WordPress/wordpress-playground/pull/1721)) ### Tools -- Update actions/upload-artifact version to 4. ([#1748](https://github.com/WordPress/wordpress-playground/pull/1748)) +- Update actions/upload-artifact version to 4. ([#1748](https://github.com/WordPress/wordpress-playground/pull/1748)) ### Documentation -- Docs/Blueprints resources: Grammar and typo fixes. ([#1741](https://github.com/WordPress/wordpress-playground/pull/1741)) +- Docs/Blueprints resources: Grammar and typo fixes. ([#1741](https://github.com/WordPress/wordpress-playground/pull/1741)) ### PHP WebAssembly -- @php-wasm/universal : Add Phar support in php-wasm. ([#1716](https://github.com/WordPress/wordpress-playground/pull/1716)) +- @php-wasm/universal : Add Phar support in php-wasm. ([#1716](https://github.com/WordPress/wordpress-playground/pull/1716)) ### Website -- Add the `components` package with PathMappingControl. ([#1608](https://github.com/WordPress/wordpress-playground/pull/1608)) +- Add the `components` package with PathMappingControl. ([#1608](https://github.com/WordPress/wordpress-playground/pull/1608)) ### Bug Fixes -- Fix CLI --skipWordPressSetup option. ([#1760](https://github.com/WordPress/wordpress-playground/pull/1760)) +- Fix CLI --skipWordPressSetup option. ([#1760](https://github.com/WordPress/wordpress-playground/pull/1760)) ### Reliability -- Improve Playground CLI logging and fix quiet mode. ([#1751](https://github.com/WordPress/wordpress-playground/pull/1751)) +- Improve Playground CLI logging and fix quiet mode. ([#1751](https://github.com/WordPress/wordpress-playground/pull/1751)) ### Various -- Docs/Guides: Guides introductions and some minor adjustments. ([#1754](https://github.com/WordPress/wordpress-playground/pull/1754)) -- Docs/Guides: Normalized and fixed guides links. ([#1756](https://github.com/WordPress/wordpress-playground/pull/1756)) -- Docs/Guides: Providing content for your demo. ([#1747](https://github.com/WordPress/wordpress-playground/pull/1747)) -- Docs/Guides: WordPress Playground for plugin developers. ([#1750](https://github.com/WordPress/wordpress-playground/pull/1750)) -- Docs/Guides: WordPress Playground for theme developers. ([#1732](https://github.com/WordPress/wordpress-playground/pull/1732)) -- Docs: Links redirections. ([#1758](https://github.com/WordPress/wordpress-playground/pull/1758)) +- Docs/Guides: Guides introductions and some minor adjustments. ([#1754](https://github.com/WordPress/wordpress-playground/pull/1754)) +- Docs/Guides: Normalized and fixed guides links. ([#1756](https://github.com/WordPress/wordpress-playground/pull/1756)) +- Docs/Guides: Providing content for your demo. ([#1747](https://github.com/WordPress/wordpress-playground/pull/1747)) +- Docs/Guides: WordPress Playground for plugin developers. ([#1750](https://github.com/WordPress/wordpress-playground/pull/1750)) +- Docs/Guides: WordPress Playground for theme developers. ([#1732](https://github.com/WordPress/wordpress-playground/pull/1732)) +- Docs: Links redirections. ([#1758](https://github.com/WordPress/wordpress-playground/pull/1758)) ### Contributors @@ -112,7 +107,6 @@ The following contributors merged PRs in this release: @adamziel @bgrgicak @brandonpayton @juanmaguitar @mho22 @peterwilsoncc - ## [v0.9.39] (2024-09-09) ### Bug Fixes diff --git a/packages/docs/site/docs/main/web-instance.md b/packages/docs/site/docs/main/web-instance.md index ef14bed928..c79cf1f88d 100644 --- a/packages/docs/site/docs/main/web-instance.md +++ b/packages/docs/site/docs/main/web-instance.md @@ -25,7 +25,6 @@ From the Playground website there are also available some toolbars to customize The options available from the "Customize Playground" window correpond to the following [Query API options](/developers/apis/query-api#available-options): -- `storage` - `php` - `php-extension-bundle` - `networking` diff --git a/packages/php-wasm/web/src/lib/directory-handle-mount.ts b/packages/php-wasm/web/src/lib/directory-handle-mount.ts index 9f8fba9f5a..70ddd061fc 100644 --- a/packages/php-wasm/web/src/lib/directory-handle-mount.ts +++ b/packages/php-wasm/web/src/lib/directory-handle-mount.ts @@ -188,15 +188,55 @@ export async function copyMemfsToOpfs( } await mirrorMemfsDirectoryinOpfs(memfsRoot, opfsRoot); - // Now let's create all the required files in OPFS. This is quite slow - // so we report progress. - let i = 0; - const filesCreated = filesToCreate.map(([opfsDir, memfsPath, entryName]) => - overwriteOpfsFile(opfsDir, entryName, FS, memfsPath).then(() => { - onProgress?.({ files: ++i, total: filesToCreate.length }); - }) - ); - await Promise.all(filesCreated); + // Now let's create all the required files in OPFS. This can be quite slow + // so we report progress. Throttle the progress callback to avoid flooding + // the main thread with excessive updates. + let numFilesCompleted = 0; + const throttledProgressCallback = onProgress && throttle(onProgress, 100); + + // Limit max concurrent writes because Safari may otherwise encounter + // an error like "UnknownError: Invalid platform file handle" after opening + // a sufficient number of FileSyncAccessHandles (near 128). + // 2024-09-21: This limit was chosen based on perceived performance while + // testing with Safari, Chrome, and Firefox. It felt like a sweet spot. + // Writing one-at-a-time with no concurrency had similar performance + // but felt slightly slower. We can revisit and take better measurements + // if needed. + const maxConcurrentWrites = 100; + const concurrentWrites = new Set(); + + try { + for (const [opfsDir, memfsPath, entryName] of filesToCreate) { + const promise = overwriteOpfsFile( + opfsDir, + entryName, + FS, + memfsPath + ).then(() => { + numFilesCompleted++; + concurrentWrites.delete(promise); + + throttledProgressCallback?.({ + files: numFilesCompleted, + total: filesToCreate.length, + }); + }); + concurrentWrites.add(promise); + + if (concurrentWrites.size >= maxConcurrentWrites) { + await Promise.race(concurrentWrites); + throttledProgressCallback?.({ + files: numFilesCompleted, + total: filesToCreate.length, + }); + } + } + } finally { + // Make sure all FS-related activity has completed one way or another + // before returning. Otherwise, an error followed by a retry might lead + // to a conflict with writes from the earlier attempt. + await Promise.allSettled(concurrentWrites); + } } function isMemfsDir(FS: Emscripten.RootFS, path: string) { @@ -391,3 +431,26 @@ async function resolveParent( } return handle as any; } + +function throttle any>( + fn: T, + debounceMs: number +): T { + let lastCallTime = 0; + let timeoutId: ReturnType | undefined; + let pendingArgs: Parameters | undefined; + + return function throttledCallback(...args: Parameters) { + pendingArgs = args; + + const timeSinceLastCall = Date.now() - lastCallTime; + if (timeoutId === undefined) { + const delay = Math.max(0, debounceMs - timeSinceLastCall); + timeoutId = setTimeout(() => { + timeoutId = undefined; + lastCallTime = Date.now(); + fn(...pendingArgs!); + }, delay); + } + } as T; +} diff --git a/packages/playground/blueprints/src/lib/compile.ts b/packages/playground/blueprints/src/lib/compile.ts index 8b55b6a27e..45af9211a7 100644 --- a/packages/playground/blueprints/src/lib/compile.ts +++ b/packages/playground/blueprints/src/lib/compile.ts @@ -85,6 +85,9 @@ export function compileBlueprint( onStepCompleted = () => {}, }: CompileBlueprintOptions = {} ): CompiledBlueprint { + // Deep clone the blueprint to avoid mutating the input + blueprint = structuredClone(blueprint); + blueprint = { ...blueprint, steps: (blueprint.steps || []) @@ -158,7 +161,10 @@ export function compileBlueprint( // Default to the "kitchen sink" PHP extensions bundle if no // other bundles are specified. if (blueprint.phpExtensionBundles.length === 0) { - blueprint.phpExtensionBundles.push('kitchen-sink'); + blueprint.phpExtensionBundles = [ + ...blueprint.phpExtensionBundles, + 'kitchen-sink', + ]; } /** diff --git a/packages/playground/blueprints/src/lib/steps/enable-multisite.ts b/packages/playground/blueprints/src/lib/steps/enable-multisite.ts index 2608ccdf3a..16db673ea7 100644 --- a/packages/playground/blueprints/src/lib/steps/enable-multisite.ts +++ b/packages/playground/blueprints/src/lib/steps/enable-multisite.ts @@ -199,9 +199,18 @@ echo json_encode($deactivated_plugins); // Reactivate any previously deactivated plugins for (const plugin of deactivatedPlugins) { - await activatePlugin(playground, { - pluginPath: plugin, - }); + try { + await activatePlugin(playground, { + pluginPath: plugin, + }); + } catch (error) { + /** + * This can happen if the plugin is already active. + * For example, the Hello Dolly plugin exists as hello.php and hello/hello.php. + * The second activation will fail, but that's fine. + */ + logger.error('Error activating plugin', plugin, error); + } } }; diff --git a/packages/playground/client/src/index.ts b/packages/playground/client/src/index.ts index 666f9059d8..26b2de4b3c 100644 --- a/packages/playground/client/src/index.ts +++ b/packages/playground/client/src/index.ts @@ -66,7 +66,6 @@ export interface StartPlaygroundOptions { * @returns */ onBeforeBlueprint?: () => Promise; - siteSlug?: string; mounts?: Array; shouldInstallWordPress?: boolean; } diff --git a/packages/playground/remote/src/lib/offline-mode-cache.ts b/packages/playground/remote/src/lib/offline-mode-cache.ts index 3c78beb297..8c4034f8f4 100644 --- a/packages/playground/remote/src/lib/offline-mode-cache.ts +++ b/packages/playground/remote/src/lib/offline-mode-cache.ts @@ -73,6 +73,7 @@ export class OfflineModeCache { if ( url.href.startsWith('http://127.0.0.1:5400/') || url.href.startsWith('http://localhost:5400/') || + url.href.startsWith('https://playground.test/') || url.pathname.startsWith('/website-server/') ) { return false; diff --git a/packages/playground/storage/src/lib/browser-fs.ts b/packages/playground/storage/src/lib/browser-fs.ts index c50e6935fb..889745b7ba 100644 --- a/packages/playground/storage/src/lib/browser-fs.ts +++ b/packages/playground/storage/src/lib/browser-fs.ts @@ -1,4 +1,6 @@ import type { MountDevice } from '@php-wasm/web'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import type * as pleaseLoadTypes from 'wicg-file-system-access'; export async function directoryHandleFromMountDevice( device: MountDevice @@ -16,7 +18,7 @@ export async function opfsPathToDirectoryHandle( const parts = opfsPath.split('/').filter((p) => p.length > 0); let handle = await navigator.storage.getDirectory(); for (const part of parts) { - handle = await handle.getDirectoryHandle(part); + handle = await handle.getDirectoryHandle(part, { create: true }); } return handle; } diff --git a/packages/playground/website/cypress.config.ts b/packages/playground/website/cypress.config.ts index 46f7775328..a1b6646c83 100644 --- a/packages/playground/website/cypress.config.ts +++ b/packages/playground/website/cypress.config.ts @@ -1,10 +1,8 @@ import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; import { defineConfig } from 'cypress'; -// @ts-ignore -const currentPath = new URL(import.meta.url).pathname; export default defineConfig({ - e2e: nxE2EPreset(currentPath, { + e2e: nxE2EPreset(__dirname, { cypressDir: 'cypress', }), // Playground may be slow on GitHub CI diff --git a/packages/playground/website/cypress/e2e/blueprints.cy.ts b/packages/playground/website/cypress/e2e/blueprints.cy.ts index 72da2deaba..7e8701a0e9 100644 --- a/packages/playground/website/cypress/e2e/blueprints.cy.ts +++ b/packages/playground/website/cypress/e2e/blueprints.cy.ts @@ -60,29 +60,6 @@ describe('Blueprints', () => { cy.wordPressDocument().its('body').should('contain.text', 'My Sites'); }); - it('enableMultisite step should re-activate the plugins', () => { - const blueprint: Blueprint = { - landingPage: '/wp-admin/plugins.php', - steps: [ - { step: 'login' }, - { - step: 'installPlugin', - pluginZipFile: { - resource: 'wordpress.org/plugins', - slug: 'hello-dolly', - }, - options: { activate: true }, - }, - { step: 'enableMultisite' }, - ], - }; - cy.visit('/#' + JSON.stringify(blueprint)); - cy.wordPressDocument() - .its('body') - .find('[data-slug="hello-dolly"].active') - .should('exist'); - }); - it('wp-cli step should create a post', () => { const blueprint: Blueprint = { landingPage: '/wp-admin/post.php', diff --git a/packages/playground/website/cypress/e2e/query-api.cy.ts b/packages/playground/website/cypress/e2e/query-api.cy.ts index 720c74e948..ddc3d38edb 100644 --- a/packages/playground/website/cypress/e2e/query-api.cy.ts +++ b/packages/playground/website/cypress/e2e/query-api.cy.ts @@ -199,47 +199,6 @@ describe('Query API', () => { }); }); - describe('option `storage`', () => { - describe('storage=none', () => { - it('should reset Playground data after every refresh', () => { - // Create a Playground site with a custom title - cy.visit( - '/?storage=none#{"siteOptions":{"blogname":"persistent storage"}}' - ); - cy.wordPressDocument().its('body').should('have.class', 'home'); - cy.wordPressDocument() - .its('body') - .should('contain', 'persistent storage'); - - // Reload the page and verify that the title is not there anymore - cy.visit('/?storage=none'); - cy.wordPressDocument().its('body').should('have.class', 'home'); - cy.wordPressDocument() - .its('body') - .should('not.contain', 'persistent storage'); - }); - }); - describe('storage=browser', () => { - it('should store the Playground data in the browser', () => { - // Create a Playground site with a custom title - cy.visit( - '/?storage=browser#{"siteOptions":{"blogname":"persistent storage"}}' - ); - cy.wordPressDocument().its('body').should('have.class', 'home'); - cy.wordPressDocument() - .its('body') - .should('contain', 'persistent storage'); - - // Reload the page and verify that the title is still there - cy.visit('/?storage=browser'); - cy.wordPressDocument().its('body').should('have.class', 'home'); - cy.wordPressDocument() - .its('body') - .should('contain', 'persistent storage'); - }); - }); - }); - describe('Patching Gutenberg editor frame', () => { it('should patch the editor frame in WordPress', () => { cy.visit('/?plugin=gutenberg&url=/wp-admin/post-new.php'); @@ -251,18 +210,18 @@ describe('Query API', () => { checkIfGutenbergIsPatched(); }); - it('should patch Gutenberg brought over by importing a site', () => { - cy.visit('/'); - // Get the current URL - cy.url().then((url) => { - url = url.replace(/\/$/, '').replace('/website-server', ''); - // Import a site that has Gutenberg installed - cy.visit( - `/?import-site=${url}/test-fixtures/site-with-unpatched-gutenberg.zip&url=/wp-admin/post-new.php` - ); - checkIfGutenbergIsPatched(); - }); - }); + // it('should patch Gutenberg brought over by importing a site', () => { + // cy.visit('/'); + // // Get the current URL + // cy.url().then((url) => { + // url = url.replace(/\/$/, ''); + // // Import a site that has Gutenberg installed + // cy.visit( + // `/?import-site=${url}/test-fixtures/site-with-unpatched-gutenberg.zip&url=/wp-admin/post-new.php` + // ); + // checkIfGutenbergIsPatched(); + // }); + // }); function checkIfGutenbergIsPatched() { // Check if the inserter button is styled. @@ -282,53 +241,6 @@ describe('Query API', () => { .should('not.have.css', 'background-color', undefined); } }); - - describe('Site switching', () => { - it('should switch between sites', () => { - cy.visit( - '/?storage=browser#' + - JSON.stringify({ - landingPage: '/', - steps: [ - { - step: 'runPHP', - code: ` { const testedStorageOptions = [ 'none', // TODO: Re-enable this option once the tests are more stable - //'browser' + //'opfs' ]; testedStorageOptions.forEach((storage) => { @@ -15,7 +15,7 @@ describe('Remote Assets', () => { cy.visit(`/?storage=${storage}#${blueprint}`); runAssertions(); - if (storage === 'browser') { + if (storage === 'opfs') { // Reload and re-assert to test when loading from browser storage cy.reload(); runAssertions(); diff --git a/packages/playground/website/cypress/e2e/website-ui.cy.ts b/packages/playground/website/cypress/e2e/website-ui.cy.ts deleted file mode 100644 index 7dbaf6b7a4..0000000000 --- a/packages/playground/website/cypress/e2e/website-ui.cy.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { - SupportedPHPVersions, - LatestSupportedPHPVersion, -} from '@php-wasm/universal'; - -// We can't import the WordPress versions directly from the remote package -// because of ESModules vs CommonJS incompatibilities. Let's just import the -// JSON file directly. @ts-ignore -// eslint-disable-next-line @nx/enforce-module-boundaries -import * as MinifiedWordPressVersions from '../../../wordpress-builds/src/wordpress/wp-versions.json'; -import { Blueprint } from '@wp-playground/blueprints'; - -describe('Playground website UI', () => { - beforeEach(() => cy.visit('/?networking=no')); - - it('should reflect the URL update from the navigation bar in the WordPress site', () => { - cy.setWordPressUrl('/wp-admin'); - cy.wordpressPath().should('contain', '/wp-admin'); - }); - - // Test all PHP versions for completeness - describe('PHP version switcher', () => { - SupportedPHPVersions.forEach((version) => { - /** - * WordPress 6.6 dropped support for PHP 7.0 and 7.1 so we need to skip these versions. - * @see https://make.wordpress.org/core/2024/04/08/dropping-support-for-php-7-1/ - */ - if (['7.0', '7.1'].includes(version)) { - return; - } - it('should switch PHP version to ' + version, () => { - // Update settings in Playground configurator - cy.get('button#configurator').click(); - cy.get('select#php-version').select(version); - cy.get('#modal-content button[type=submit]').click(); - // Wait for the page to finish reloading - cy.url().should('contain', `php=${version}`); - cy.document().should('exist'); - - // Go to phpinfo - cy.setWordPressUrl('/phpinfo.php'); - cy.wordPressDocument() - .find('h1') - .should('contain', 'PHP Version ' + version); - }); - }); - }); - - // Only test the latest PHP version to save time - describe('PHP extensions bundle', () => { - it('should load additional PHP extensions when requested', () => { - // Update settings in Playground configurator - cy.get('button#configurator').click(); - cy.get('select#php-version').select(LatestSupportedPHPVersion); - cy.get('input[name=with-extensions]').check(); - cy.get('#modal-content button[type=submit]').click(); - // Wait for the page to finish loading - cy.document().should('exist'); - - // Go to phpinfo - cy.setWordPressUrl('/phpinfo.php'); - cy.wordPressDocument() - .its('body') - .should('contain', '--enable-xmlwriter'); - }); - - it('should not load additional PHP extensions when not requested', () => { - // Update settings in Playground configurator - cy.get('button#configurator').click(); - cy.get('select#php-version').select(LatestSupportedPHPVersion); - cy.get('input[name=with-extensions]').uncheck(); - cy.get('#modal-content button[type=submit]').click(); - // Wait for the page to finish loading - cy.document().should('exist'); - - // Go to phpinfo - cy.setWordPressUrl('/phpinfo.php'); - cy.wordPressDocument() - .its('body') - .should('contain', '--without-libxml'); - }); - }); - - // Test all WordPress versions for completeness - describe('WordPress version selector', () => { - for (const version in MinifiedWordPressVersions) { - if (version === 'beta') { - continue; - } - // @ts-ignore - let versionMessage = 'Version ' + version; - if (version === 'nightly') { - versionMessage = 'You are using a development version'; - } - - it('should switch WordPress version to ' + version, () => { - // Update settings in Playground configurator - cy.get('button#configurator').click(); - cy.get(`select#wp-version option[value="${version}"]`).should( - 'exist' - ); - cy.get('select#wp-version').select(`${version}`); - cy.get('#modal-content button[type=submit]').click(); - // Wait for the page to finish loading - cy.url().should('contain', `&wp=${version}`); - - // Go to phpinfo - cy.setWordPressUrl('/wp-admin'); - cy.wordPressDocument() - .find('#footer-upgrade') - .should('contain', versionMessage); - }); - } - }); -}); - -/** - * These tests only check if the modal UI updates the URL correctly. - * The actual networking functionality is tested in the Query API tests. - */ -describe('Website UI – Networking support', () => { - it('should display an unchecked networking checkbox by default', () => { - cy.visit('/'); - - cy.get('button#configurator').click(); - cy.get('input[name=with-networking]').should('not.be.checked'); - }); - - it('should display a checked networking checkbox when networking is enabled', () => { - cy.visit('/?networking=yes'); - - cy.get('button#configurator').click(); - cy.get('input[name=with-networking]').should('be.checked'); - }); - - it('should display PHP output even when a fatal error is hit', () => { - const blueprint: Blueprint = { - landingPage: '/err.php', - login: true, - steps: [ - { - step: 'writeFile', - path: '/wordpress/err.php', - data: " { - cy.visit('/'); - - // Update settings in Playground configurator - cy.get('button#configurator').click(); - cy.get('input[name=with-networking]').check(); - cy.get('#modal-content button[type=submit]').click(); - - // Wait for the page to reload - cy.document().should('exist'); - cy.get('#modal-content button[type=submit]').should('not.exist'); - - // Confirm the URL was updated correctly - cy.relativeUrl().should('contain', 'networking=yes'); - }); - - it('should disable networking when requested', () => { - cy.visit('/?networking=yes'); - - // Update settings in Playground configurator - cy.get('button#configurator').click(); - cy.get('input[name=with-networking]').uncheck(); - cy.get('#modal-content button[type=submit]').click(); - - // Wait for the page to reload - cy.document().should('exist'); - cy.get('#modal-content button[type=submit]').should('not.exist'); - - // Confirm the URL was updated correctly - cy.relativeUrl().should('not.contain', 'networking=yes'); - }); -}); diff --git a/packages/playground/website/index.html b/packages/playground/website/index.html index 69404d93f7..e7392029e0 100644 --- a/packages/playground/website/index.html +++ b/packages/playground/website/index.html @@ -43,7 +43,7 @@ -
+
-
+