diff --git a/packages/php-wasm/web/src/lib/index.ts b/packages/php-wasm/web/src/lib/index.ts index af054e52c56..16171f2787a 100644 --- a/packages/php-wasm/web/src/lib/index.ts +++ b/packages/php-wasm/web/src/lib/index.ts @@ -4,10 +4,6 @@ export type { LoaderOptions as PHPWebLoaderOptions } from './load-runtime'; export { loadWebRuntime } from './load-runtime'; export { getPHPLoaderModule } from './get-php-loader-module'; -export { - registerServiceWorker, - setPhpInstanceUsedByServiceWorker, -} from './register-service-worker'; export { setupPostMessageRelay } from './setup-post-message-relay'; export { spawnPHPWorkerThread } from './worker-thread/spawn-php-worker-thread'; diff --git a/packages/php-wasm/web/src/lib/register-service-worker.ts b/packages/php-wasm/web/src/lib/register-service-worker.ts deleted file mode 100644 index 4b6eddce437..00000000000 --- a/packages/php-wasm/web/src/lib/register-service-worker.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { PHPWorker } from '@php-wasm/universal'; -import { PhpWasmError } from '@php-wasm/util'; -import { responseTo } from '@php-wasm/web-service-worker'; -import { Remote } from 'comlink'; -import { logger } from '@php-wasm/logger'; - -export interface Client extends Remote {} - -/** - * Resolves when the PHP API client is set. - * - * This allows us to wait for the PHP API client to be set before proxying - * service worker messages to the web worker. - */ -let resolvePhpApi: (api: Client) => void; -export const phpApiPromise = new Promise((resolve) => { - resolvePhpApi = resolve; -}); - -/** - * Sets the PHP API client. - * - * @param {Client} api The PHP API client. - * - */ -export function setPhpInstanceUsedByServiceWorker(api: Client) { - if (!api) { - throw new PhpWasmError('PHP API client must be a valid client object.'); - } - resolvePhpApi(api); -} - -/** - * Run this in the main application to register the service worker or - * reload the registered worker if the app expects a different version - * than the currently registered one. - * - * @param scriptUrl The URL of the service worker script. - */ -export async function registerServiceWorker(scriptUrl: string) { - const sw = navigator.serviceWorker; - if (!sw) { - /** - * Service workers may only run in secure contexts. - * See https://w3c.github.io/webappsec-secure-contexts/ - */ - if (window.isSecureContext) { - throw new PhpWasmError( - 'Service workers are not supported in your browser.' - ); - } else { - throw new PhpWasmError( - 'WordPress Playground uses service workers and may only work on HTTPS and http://localhost/ sites, but the current site is neither.' - ); - } - } - - const registration = await sw.register(scriptUrl, { - type: 'module', - // Always bypass HTTP cache when fetching the new Service Worker script: - updateViaCache: 'none', - }); - - // Check if there's a new service worker available and, if so, enqueue - // the update: - try { - await registration.update(); - } catch (e) { - // registration.update() throws if it can't reach the server. - // We're swallowing the error to keep the app working in offline mode - // or when playground.wordpress.net is down. We can be sure we have a - // functional service worker at this point because sw.register() succeeded. - logger.error('Failed to update service worker.', e); - } - - return { - /** - * Establishes the communication bridge between the service worker and the web worker - * where the current site is running. - * - * @param scope The string prefix used in the site URL served by the currently - * running remote.html. E.g. for a prefix like `/scope:playground/`, - * the scope would be `playground`. See the `@php-wasm/scopes` package - * for more details. - */ - startServiceWorkerCommunicationBridge({ scope }: { scope: string }) { - // Proxy the service worker messages to the web worker: - navigator.serviceWorker.addEventListener( - 'message', - async function onMessage(event) { - /** - * Ignore events meant for other PHP instances to - * avoid handling the same event twice. - * - * This is important because the service worker posts the - * same message to all application instances across all browser tabs. - */ - if (scope && event.data.scope !== scope) { - return; - } - - // Wait for the PHP API client to be set by bootPlaygroundRemote - const phpApi = await phpApiPromise; - - const args = event.data.args || []; - const method = event.data.method as keyof Client; - const result = await (phpApi[method] as Function)(...args); - event.source!.postMessage( - responseTo(event.data.requestId, result) - ); - } - ); - sw.startMessages(); - }, - }; -} diff --git a/packages/playground/remote/service-worker.ts b/packages/playground/remote/service-worker.ts index 1f5f5a3c5ff..036e4f9cbc9 100644 --- a/packages/playground/remote/service-worker.ts +++ b/packages/playground/remote/service-worker.ts @@ -6,8 +6,9 @@ * ## Playground must be upgraded as early as possible after a new release * * New service workers call .skipWaiting(), immediately claim all the clients - * that were controlled by the previous service worker, and forcibly refreshes - * them. + * that were controlled by the previous service worker and clears the offline + * cache. The claimed clients are not forcibly refreshed. They just continue + * running under the new service worker. * * Why? * @@ -16,22 +17,12 @@ * the previous webapp version. Therefore, we can't allow the previous version * to run when a new version becomes available. * - * ### Push notifications - * - * It would be supremely useful to proactively notify the webapp after a fresh deployment. - * Playground doesn't do that yet but it likely will in the future. - * * ## Caching strategy * - * Playground relies on the **Cache only** strategy. It loads assets from - * the network, caches them, and serves them from the cache. The assumption - * is that all network requests yield the most recent version of the remote file. - * - * This helps us avoid the HTTP cache problem. + * Playground uses caching heavily to achieve great loading speeds and provide + * an offline mode. * - * ### Cache layers - * - * We're dealing with the following cache layers: + * Caching is a complex beast. Playground deals with the following cache layers: * * * HTTP cache in the browser * * CacheStorage in the service worker @@ -52,17 +43,34 @@ * * ### CacheStorage in the service worker * - * This servive worker uses a **Cache only** strategy to ensure all the loaded assets - * come from the same webapp build. + * Playground primarily relies on the **Cache first** strategy. This means assets are: + * + * 1. Loaded from the network without using any HTTP caching. + * 2. Stored in the CacheStorage. + * 3. Served from the CacheStorage on subsequent requests. + * + * While this strategy enables fast load times and an offline experience, it also + * creates a substantial challenge. * - * The **Cache only** strategy means Playground only loads each assets from - * the network once, caches it, and serves it from the cache from that point on. + * When a new Playground version is deployed, all the clients will load an old + * version of the `remote.html` file on their next visit. Unfortunately, that old + * `remote.html` file contains hardcoded references to assets that may not be + * cached and no longer exist in the new webapp build. * - * The only times Playground reaches to the network are: + * To solve this problem, we use the **Network first** strategy when `remote.html` + * is requested. This introduces a small network overhead, but it guarantees loading + * the most recent version of `remote.html` and all the referenced assets. * - * * Before the service worker is installed. - * * When the service worker is being activated. - * * On CacheStorage cache miss occurs. + * Similarly, we use the **Network first** strategy for the `/` path. This is + * useful in situations where the user didn't visit Playground in a while, + * they have a stale version of the `/` route cached, and they open Playground. + * If we loaded the cached version, they'd see the old Playground website on their + * first visit and then the new Playground website only on their second visit. + * + * There's still a small window of time between loading the remote.html file and + * fetching the new assets when a new deployment would break the application. + * This should be very rare, but when it happens we provide an error message asking + * the user to reload the page. * * ### Edge Cache on playground.wordpress.net * @@ -87,7 +95,7 @@ * * * PR that turned off HTTP caching: https://github.com/WordPress/wordpress-playground/pull/1822 * * Exploring all the cache layers: https://github.com/WordPress/wordpress-playground/issues/1774 - * * Cache only strategy: https://web.dev/articles/offline-cookbook#cache-only + * * Cache first strategy: https://web.dev/articles/offline-cookbook#cache-falling-back-to-network * * Service worker caching and HTTP caching: https://web.dev/articles/service-worker-caching-and-http-caching */ @@ -105,7 +113,8 @@ import { wordPressRewriteRules } from '@wp-playground/wordpress'; import { reportServiceWorkerMetrics } from '@php-wasm/logger'; import { - cachedFetch, + cacheFirstFetch, + networkFirstFetch, cacheOfflineModeAssetsForCurrentRelease, isCurrentServiceWorkerActive, purgeEverythingFromPreviousRelease, @@ -168,17 +177,6 @@ self.addEventListener('install', (event) => { * registration. It shouldn't have unwanted side effects in our case. All these * pages would get controlled eventually anyway. * - * ## Upgrading other browser tabs - * - * This activation hook upgrades all the Playground browser tabs to the latest - * service worker version, and that service worker upgrades them the latest version - * of the webapp. - * - * The moment a new Playground version is deployed, the existing browser tabs - * won't be able to load assets from the network. The older Playground version - * they're running contains hardcoded URLs to assets that no longer exist on - * the server. - * * See: * * The service worker lifecycle https://web.dev/articles/service-worker-lifecycle * * Clients.claim() docs https://developer.mozilla.org/en-US/docs/Web/API/Clients/claim @@ -191,35 +189,6 @@ self.addEventListener('activate', function (event) { await purgeEverythingFromPreviousRelease(); cacheOfflineModeAssetsForCurrentRelease(); } - - // Reload all clients that were controlled by the previous service worker - // so they can load the new version of the app without any stale assets - // whatsoever. - const windowClients = await self.clients.matchAll({ - type: 'window', - includeUncontrolled: true, - }); - - for (const client of windowClients) { - let url; - try { - url = new URL(client.url); - } catch (e) { - // Ignore - return; - } - - if ( - url.pathname.startsWith('/remote.html') || - url.pathname.startsWith('/scope:') - ) { - return; - } - - // @TODO: Store temporary sites in OPFS to avoid destroying in-memory - // changes in tabs that are already open. - client.navigate(client.url); - } } event.waitUntil(doActivate()); }); @@ -273,8 +242,41 @@ self.addEventListener('fetch', (event) => { return; } - // Use Cache Only strategy to serve regular static assets. - return event.respondWith(cachedFetch(event.request)); + /** + * Always fetch the fresh version of `/remote.html` and `/` from the network. + * + * This is the secret sauce that enables seamless upgrades of the + * running Playground clients when a new version is deployed on + * the server. + * + * ## The problem with deployments + * + * App deployments remove all the static assets associated with the + * previous app version. Meanwhile, the remote.html file we've cached + * for offline usage still holds references to those assets. + * + * If we just loaded the cached remote.html file, the site would crash + * with seemingly random errors. + * + * Instead, we fetch the most recent version of remote.html from the network. + * It references the static assets that are now available on the server and + * should work just fine. + * + * Relatedly, loading the `/` path using the network first strategy ensures + * that the user sees the latest version of the webapp even if they aleady + * have the previous version cached in CacheStorage. + * + * This very simple resolution took multiple iterations to get right. See + * https://github.com/WordPress/wordpress-playground/issues/1821 for more + * details. + */ + if (url.pathname === '/remote.html' || url.pathname === '/') { + event.respondWith(networkFirstFetch(event.request)); + return; + } + + // Use cache first strategy to serve regular static assets. + return event.respondWith(cacheFirstFetch(event.request)); }); /** diff --git a/packages/playground/remote/src/lib/boot-playground-remote.ts b/packages/playground/remote/src/lib/boot-playground-remote.ts index 76aec132b3d..657c0b40f0d 100644 --- a/packages/playground/remote/src/lib/boot-playground-remote.ts +++ b/packages/playground/remote/src/lib/boot-playground-remote.ts @@ -1,7 +1,5 @@ import { MessageListener } from '@php-wasm/universal'; import { - registerServiceWorker, - setPhpInstanceUsedByServiceWorker, spawnPHPWorkerThread, exposeAPI, consumeAPI, @@ -32,6 +30,9 @@ export const workerUrl: string = new URL(moduleWorkerUrl, origin) + ''; import serviceWorkerPath from '../../service-worker.ts?worker&url'; import { FilesystemOperation } from '@php-wasm/fs-journal'; import { setupFetchNetworkTransport } from './setup-fetch-network-transport'; +import { logger } from '@php-wasm/logger'; +import { PhpWasmError } from '@php-wasm/util'; +import { responseTo } from '@php-wasm/web-service-worker'; export const serviceWorkerUrl = new URL(serviceWorkerPath, origin); // Prevent Vite from hot-reloading this file – it would @@ -56,14 +57,44 @@ export async function bootPlaygroundRemote() { bar = new ProgressBar(); document.body.prepend(bar.element); } + const sw = navigator.serviceWorker; + if (!sw) { + /** + * Service workers may only run in secure contexts. + * See https://w3c.github.io/webappsec-secure-contexts/ + */ + if (window.isSecureContext) { + throw new PhpWasmError( + 'Service workers are not supported in your browser.' + ); + } else { + throw new PhpWasmError( + 'WordPress Playground uses service workers and may only work on HTTPS and http://localhost/ sites, but the current site is neither.' + ); + } + } + + const registration = await sw.register(serviceWorkerUrl + '', { + type: 'module', + // Always bypass HTTP cache when fetching the new Service Worker script: + updateViaCache: 'none', + }); - const { startServiceWorkerCommunicationBridge } = - await registerServiceWorker(serviceWorkerUrl + ''); + // Check if there's a new service worker available and, if so, enqueue + // the update: + try { + await registration.update(); + } catch (e) { + // registration.update() throws if it can't reach the server. + // We're swallowing the error to keep the app working in offline mode + // or when playground.wordpress.net is down. We can be sure we have a + // functional service worker at this point because sw.register() succeeded. + logger.error('Failed to update service worker.', e); + } const phpWorkerApi = consumeAPI( await spawnPHPWorkerThread(workerUrl) ); - setPhpInstanceUsedByServiceWorker(phpWorkerApi); const wpFrame = document.querySelector('#wp') as HTMLIFrameElement; const phpRemoteApi: WebClientMixin = { @@ -222,9 +253,35 @@ export async function bootPlaygroundRemote() { async boot(options) { await phpWorkerApi.boot(options); - startServiceWorkerCommunicationBridge({ - scope: options.scope, - }); + + // Proxy the service worker messages to the web worker: + navigator.serviceWorker.addEventListener( + 'message', + async function onMessage(event) { + /** + * Ignore events meant for other PHP instances to + * avoid handling the same event twice. + * + * This is important because the service worker posts the + * same message to all application instances across all browser tabs. + */ + if (options.scope && event.data.scope !== options.scope) { + return; + } + + // Wait for the PHP API client to be set by bootPlaygroundRemote + const args = event.data.args || []; + const method = event.data + .method as keyof PlaygroundWorkerEndpoint; + const result = await (phpWorkerApi[method] as Function)( + ...args + ); + event.source!.postMessage( + responseTo(event.data.requestId, result) + ); + } + ); + sw.startMessages(); try { await phpWorkerApi.isReady(); diff --git a/packages/playground/remote/src/lib/offline-mode-cache.ts b/packages/playground/remote/src/lib/offline-mode-cache.ts index e7a331abaa2..9aa4512fdb0 100644 --- a/packages/playground/remote/src/lib/offline-mode-cache.ts +++ b/packages/playground/remote/src/lib/offline-mode-cache.ts @@ -9,7 +9,7 @@ const LATEST_CACHE_NAME = `${CACHE_NAME_PREFIX}-${buildVersion}`; // a Service Worker module which does not allow top-level await. const promisedOfflineModeCache = caches.open(LATEST_CACHE_NAME); -export async function cachedFetch(request: Request): Promise { +export async function cacheFirstFetch(request: Request): Promise { const offlineModeCache = await promisedOfflineModeCache; let response = await offlineModeCache.match(request, { ignoreSearch: true, @@ -40,6 +40,36 @@ export async function cachedFetch(request: Request): Promise { return response; } +export async function networkFirstFetch(request: Request): Promise { + const offlineModeCache = await promisedOfflineModeCache; + const cachedResponse = await offlineModeCache.match(request, { + ignoreSearch: true, + }); + + let response: Response | undefined = undefined; + try { + response = await fetch(request, { + cache: 'no-cache', + }); + } catch (e) { + if (cachedResponse) { + return cachedResponse; + } + throw e; + } + + if (response.ok) { + await offlineModeCache.put(request, response.clone()); + return response; + } + + if (cachedResponse) { + return cachedResponse; + } + + return response; +} + /** * For offline mode to work we need to cache all required assets. * @@ -80,8 +110,6 @@ export async function cacheOfflineModeAssetsForCurrentRelease(): Promise { * `buildVersionPlugin` for more details on how it's generated. */ export async function purgeEverythingFromPreviousRelease() { - // @TODO: Ensure an older service worker won't ever remove the assets of a newer service worker, - // even if this is accidentally called in the older worker. const keys = await caches.keys(); const oldKeys = keys.filter( (key) => key.startsWith(CACHE_NAME_PREFIX) && key !== LATEST_CACHE_NAME diff --git a/packages/playground/website/bin/version-switching-server.ts b/packages/playground/website/bin/version-switching-server.ts index 4736568d7db..042793f5f39 100644 --- a/packages/playground/website/bin/version-switching-server.ts +++ b/packages/playground/website/bin/version-switching-server.ts @@ -1,6 +1,6 @@ import { startVersionSwitchingServer } from '../playwright/version-switching-server'; -const [, , oldVersionDir, newVersionDir, port] = process.argv; +const [, , oldVersionDir, midVersionDir, newVersionDir, port] = process.argv; if (!oldVersionDir || !newVersionDir || !port) { console.error( @@ -12,6 +12,7 @@ if (!oldVersionDir || !newVersionDir || !port) { const server = await startVersionSwitchingServer({ oldVersionDirectory: oldVersionDir, newVersionDirectory: newVersionDir, + midVersionDirectory: midVersionDir, port: parseInt(port, 10), }); diff --git a/packages/playground/website/playwright/deploy-e2e-mid-release.zip b/packages/playground/website/playwright/deploy-e2e-mid-release.zip new file mode 100644 index 00000000000..319bae1b70c Binary files /dev/null and b/packages/playground/website/playwright/deploy-e2e-mid-release.zip differ diff --git a/packages/playground/website/playwright/e2e/deployment.spec.ts b/packages/playground/website/playwright/e2e/deployment.spec.ts index 2c83234332b..914a13e0774 100644 --- a/packages/playground/website/playwright/e2e/deployment.spec.ts +++ b/packages/playground/website/playwright/e2e/deployment.spec.ts @@ -17,6 +17,10 @@ test.beforeEach(async () => { __dirname, '../../../../../dist/packages/playground/wasm-wordpress-net-old' ), + midVersionDirectory: path.join( + __dirname, + '../../../../../dist/packages/playground/wasm-wordpress-net-mid' + ), newVersionDirectory: path.join( __dirname, '../../../../../dist/packages/playground/wasm-wordpress-net-new' @@ -35,7 +39,7 @@ test.afterEach(async () => { for (const cachingEnabled of [true, false]) { test(`When a new website version is deployed, it should be loaded upon a regular page refresh (with HTTP caching ${ cachingEnabled ? 'enabled' : 'disabled' - })`, async ({ website, page }) => { + })`, async ({ website, page, wordpress }) => { server!.setHttpCacheEnabled(cachingEnabled); await page.goto(url); @@ -47,52 +51,56 @@ for (const cachingEnabled of [true, false]) { server!.switchToNewVersion(); await page.goto(url); await website.waitForNestedIframes(); - await expect(page).toHaveScreenshot('website-new.png', { - maxDiffPixels, - }); + await expect( + website.page.getByLabel('Open Site Manager') + ).toBeVisible(); + await expect(wordpress.locator('body')).toContainText('Edit site'); }); } test( - 'When a new website version is deployed while the old version is still opened in two browser tabs, ' + - 'both tabs should be upgraded to the new app version upon a regular page refresh', - async ({ website, page, browser, browserName }) => { - test.skip( - browserName === 'webkit', - `Playwright creates separate ephemeral browser contexts for each Safari page, ` + - `which means they don't actually share the service worker and the first tab won't` + - `be refreshed when the second tab updates its service worker registration.` - ); - await page.goto(url); - await website.waitForNestedIframes(); - await expect(page).toHaveScreenshot('website-old.png', { - maxDiffPixels, - }); + 'When a new website version is deployed while the old version is still loaded, ' + + 'creating a new site should still work.', + async ({ website, page, wordpress }) => { + server!.setHttpCacheEnabled(true); + server!.switchToMidVersion(); - const page2 = await browser.newPage(); - await page2.goto(url); - await website.waitForNestedIframes(page2); - await expect(page2).toHaveScreenshot('website-old.png', { - maxDiffPixels, - }); + await page.goto(`${url}/?wp=6.5`); + await website.waitForNestedIframes(); + // Switching to the new app version does not trigger a page reload, + // but it deletes all the stale assets from the server. server!.switchToNewVersion(); - await page.goto(url); - await website.waitForNestedIframes(page); - await expect(page).toHaveScreenshot('website-new.png', { - maxDiffPixels, - }); + // The non-reloaded tab should still work. The remote.html iframes + // that are already loaded should continue to work, and the newly + // loaded remote.html iframes should pull in the latest Playground version. + const siteManagerHeading = website.page.locator( + '[class*="_site-manager-site-info"]' + ); + if (await siteManagerHeading.isHidden({ timeout: 5000 })) { + await website.page.getByLabel('Open Site Manager').click(); + } + await expect(siteManagerHeading).toBeVisible(); - await website.waitForNestedIframes(page2); - await expect(page2).toHaveScreenshot('website-new.png', { - maxDiffPixels, - }); + await website.page.getByText('Add Playground').click(); + + const modal = website.page.locator('.components-modal__frame'); + await modal.getByLabel('PHP version').selectOption('7.4'); + await modal.getByLabel('WordPress version').selectOption('6.5'); + await modal.getByLabel('Language').selectOption('pl_PL'); + await website.page.getByText('Create a temporary Playground').click(); + + await website.waitForNestedIframes(); + + // Confirm we're looking at the Polish site. + expect(wordpress.locator('body')).toContainText('Edytuj witrynę'); } ); test('offline mode – the app should load even when the server goes offline', async ({ website, + wordpress, page, browserName, }) => { @@ -112,15 +120,14 @@ test('offline mode – the app should load even when the server goes offline', a await page.goto(`${url}`); await website.waitForNestedIframes(); - // @TODO a better check – screenshot comparisons will be annoying to maintain - await expect(page).toHaveScreenshot('website-online.png', { - maxDiffPixels, - }); + + await expect(website.page.getByLabel('Open Site Manager')).toBeVisible(); + expect(wordpress.locator('body')).toContainText('Edit site'); server!.kill(); await page.reload(); await website.waitForNestedIframes(); - await expect(page).toHaveScreenshot('website-online.png', { - maxDiffPixels, - }); + + await expect(website.page.getByLabel('Open Site Manager')).toBeVisible(); + expect(wordpress.locator('body')).toContainText('Edit site'); }); diff --git a/packages/playground/website/playwright/e2e/deployment.spec.ts-snapshots/website-new-chromium-linux.png b/packages/playground/website/playwright/e2e/deployment.spec.ts-snapshots/website-new-chromium-linux.png deleted file mode 100644 index 4c86e0e2734..00000000000 Binary files a/packages/playground/website/playwright/e2e/deployment.spec.ts-snapshots/website-new-chromium-linux.png and /dev/null differ diff --git a/packages/playground/website/playwright/e2e/deployment.spec.ts-snapshots/website-new-firefox-linux.png b/packages/playground/website/playwright/e2e/deployment.spec.ts-snapshots/website-new-firefox-linux.png deleted file mode 100644 index 5688dc9caf2..00000000000 Binary files a/packages/playground/website/playwright/e2e/deployment.spec.ts-snapshots/website-new-firefox-linux.png and /dev/null differ diff --git a/packages/playground/website/playwright/e2e/deployment.spec.ts-snapshots/website-new-webkit-linux.png b/packages/playground/website/playwright/e2e/deployment.spec.ts-snapshots/website-new-webkit-linux.png deleted file mode 100644 index 9cba30f9896..00000000000 Binary files a/packages/playground/website/playwright/e2e/deployment.spec.ts-snapshots/website-new-webkit-linux.png and /dev/null differ diff --git a/packages/playground/website/playwright/e2e/deployment.spec.ts-snapshots/website-online-chromium-linux.png b/packages/playground/website/playwright/e2e/deployment.spec.ts-snapshots/website-online-chromium-linux.png deleted file mode 100644 index 4c86e0e2734..00000000000 Binary files a/packages/playground/website/playwright/e2e/deployment.spec.ts-snapshots/website-online-chromium-linux.png and /dev/null differ diff --git a/packages/playground/website/playwright/version-switching-server.ts b/packages/playground/website/playwright/version-switching-server.ts index 30955883bd0..ebae04b3209 100644 --- a/packages/playground/website/playwright/version-switching-server.ts +++ b/packages/playground/website/playwright/version-switching-server.ts @@ -8,6 +8,7 @@ export async function startVersionSwitchingServer({ port = 7999, oldVersionDirectory, newVersionDirectory, + midVersionDirectory, }) { const app = express(); @@ -34,8 +35,20 @@ export async function startVersionSwitchingServer({ }; app.use(noCacheMiddleware); - app.use((req, res, next) => { + if (req.method === 'GET' && req.path.startsWith('/switch-versions/')) { + const version = req.path.split('/').pop(); + staticDirectory = path.resolve( + version === 'old' + ? oldVersionDirectory + : version === 'mid' + ? midVersionDirectory + : newVersionDirectory + ); + res.send('Version switched'); + next(); + return; + } express.static(staticDirectory)(req, res, next); }); @@ -60,6 +73,9 @@ export async function startVersionSwitchingServer({ switchToNewVersion: () => { staticDirectory = newVersionDirectory; }, + switchToMidVersion: () => { + staticDirectory = midVersionDirectory; + }, switchToOldVersion: () => { staticDirectory = oldVersionDirectory; }, diff --git a/packages/playground/website/playwright/website-page.ts b/packages/playground/website/playwright/website-page.ts index b254c774ddb..a56ec229d7c 100644 --- a/packages/playground/website/playwright/website-page.ts +++ b/packages/playground/website/playwright/website-page.ts @@ -17,6 +17,18 @@ export class WebsitePage { ).not.toBeEmpty(); } + wordpress(page = this.page) { + return ( + page + /* There are multiple viewports possible, so we need to select + the one that is visible. */ + .frameLocator( + '#playground-viewport:visible,.playground-viewport:visible' + ) + .frameLocator('#wp') + ); + } + async goto(url: string, options?: any) { const originalGoto = this.page.goto.bind(this.page); const response = await originalGoto(url, options); diff --git a/packages/playground/website/project.json b/packages/playground/website/project.json index b1dac65d321..7f69d1e0e9e 100644 --- a/packages/playground/website/project.json +++ b/packages/playground/website/project.json @@ -164,14 +164,15 @@ "executor": "nx:noop", "dependsOn": [ "e2e:playwright:prepare-app-deploy-and-offline-mode:build-current-version", - "e2e:playwright:prepare-app-deploy-and-offline-mode:unzip-old-version" + "e2e:playwright:prepare-app-deploy-and-offline-mode:unzip-old-versions" ] }, - "e2e:playwright:prepare-app-deploy-and-offline-mode:unzip-old-version": { + "e2e:playwright:prepare-app-deploy-and-offline-mode:unzip-old-versions": { "executor": "nx:run-commands", "options": { "commands": [ - "unzip ./packages/playground/website/playwright/deploy-e2e-old-release.zip" + "unzip ./packages/playground/website/playwright/deploy-e2e-old-release.zip", + "unzip ./packages/playground/website/playwright/deploy-e2e-mid-release.zip" ] } },