diff --git a/packages/browser-test-runtime/package.json b/packages/browser-test-runtime/package.json index febe5357e..9e7ca2ff3 100644 --- a/packages/browser-test-runtime/package.json +++ b/packages/browser-test-runtime/package.json @@ -20,12 +20,8 @@ "scripts": { "ci:playwright": "cd ../../submodules/playwright && npm ci --ignore-scripts", "generate-injected:core": "cd ../../submodules/playwright && node ./utils/generate_injected.js", - "ci:core:bundles:utils": "cd ../../submodules/playwright/packages/playwright-core/bundles/utils && npm ci", - "ci:core:bundles:zip": "cd ../../submodules/playwright/packages/playwright-core/bundles/zip && npm ci", - "ci:core:bundles:mcp": "cd ../../submodules/playwright/packages/playwright-core/bundles/mcp && npm ci", - "ci:test:bundles:expect": "cd ../../submodules/playwright/packages/playwright/bundles/expect && npm ci", "ci:bundles:pngjs": "cd bundles/pngjs && npm ci", - "ci:bundles": "npm run ci:core:bundles:utils && npm run ci:core:bundles:zip && npm run ci:core:bundles:mcp && npm run ci:test:bundles:expect && npm run ci:bundles:pngjs", + "ci:bundles": "npm run ci:bundles:pngjs", "build:bundles": "node utils/build_bundles.js", "build": "node ../../scripts/with-patch.mjs playwright npm run build:patched", "build:patched": "npm run ci:playwright && npm run generate-injected:core && npm run ci:bundles && npm run build:bundles && vite build" diff --git a/packages/browser-test-runtime/src/index.ts b/packages/browser-test-runtime/src/index.ts index eb10b30df..8869c805b 100644 --- a/packages/browser-test-runtime/src/index.ts +++ b/packages/browser-test-runtime/src/index.ts @@ -1,18 +1,18 @@ import fs from 'fs'; +import {asLocatorDescription} from '@isomorphic/locatorGenerators'; +import {ManualPromise} from '@isomorphic/manualPromise'; import { - asLocatorDescription, - currentZone, - ManualPromise, + getActionGroup, renderTitleForCall, - setTimeOrigin, - timeOrigin, -} from 'playwright-core/lib/utils'; +} from '@isomorphic/protocolFormatter'; +import {setTimeOrigin, timeOrigin} from '@isomorphic/time'; +import {currentZone} from '@utils/zones'; import {loadConfig} from 'playwright/lib/common/configLoader'; import { currentTestInfo, setCurrentlyLoadingFileSuite, -} from 'playwright/lib/common/globals'; +} from 'playwright/lib/globals'; import {bindFileSuiteToProject} from 'playwright/lib/common/suiteUtils'; import {Suite, TestCase} from 'playwright/lib/common/test'; import {rootTestType} from 'playwright/lib/common/testType'; @@ -146,7 +146,8 @@ class TestWorker extends WorkerMain { configCLIOverrides: {timeout: 5000, ...options}, }, artifactsDir: '/tmp/tests', - recoverFromStepErrors: false, + pauseOnError: false, + pauseAtEnd: false, }); } @@ -219,7 +220,11 @@ export class TestRunner { const {retry} = this.testContext; const [result] = await Promise.all([ worker.result(), - worker.runTestGroup({file, entries: [{testId, retry}]}), + // plan annotations are applied by the test runner, which does not run here + worker.runTestGroup({ + file, + entries: [{testId, retry, planAnnotations: []}], + }), ]); if ( result.status === 'failed' && @@ -254,7 +259,8 @@ const expectApiListener: ClientInstrumentationListener = { const zone = currentZone().data('stepZone'); if (zone?.category === 'expect') { if (zone.apiName) data.apiName = zone.apiName; - if (zone.title) data.title = zone.title; + if (zone.shortTitle || zone.title) + data.title = zone.shortTitle ?? zone.title; data.stepId = zone.stepId; return; } @@ -271,6 +277,7 @@ const expectApiListener: ClientInstrumentationListener = { ), apiName: data.apiName, params: channel.params, + group: getActionGroup({type: channel.type, method: channel.method}), }, tracingGroupSteps[tracingGroupSteps.length - 1], ); diff --git a/packages/browser-test-runtime/src/mocks/coreBundle.ts b/packages/browser-test-runtime/src/mocks/coreBundle.ts new file mode 100644 index 000000000..ede495965 --- /dev/null +++ b/packages/browser-test-runtime/src/mocks/coreBundle.ts @@ -0,0 +1,6 @@ +// `playwright-core/lib/coreBundle` re-exports all of playwright-core (CLI, MCP tools, servers). +// Workers code only needs these exports, so the full barrel is not bundled. +export { + getPlaywrightVersion, + getUserAgent, +} from 'playwright-core/lib/server/userAgent'; diff --git a/packages/browser-test-runtime/src/mocks/esmLoaderHost.ts b/packages/browser-test-runtime/src/mocks/esmLoaderHost.ts deleted file mode 100644 index 61bc7cea0..000000000 --- a/packages/browser-test-runtime/src/mocks/esmLoaderHost.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function registerESMLoader() { - return false; -} - -export function configureESMLoader() {} - -export function configureESMLoaderTransformConfig() {} diff --git a/packages/browser-test-runtime/src/mocks/transform.ts b/packages/browser-test-runtime/src/mocks/transform.ts index a75813ca1..5ed275fdb 100644 --- a/packages/browser-test-runtime/src/mocks/transform.ts +++ b/packages/browser-test-runtime/src/mocks/transform.ts @@ -1,4 +1,4 @@ -import {currentlyLoadingFileSuite} from 'playwright/lib/common/globals'; +import {currentlyLoadingFileSuite} from 'playwright/lib/globals'; import {configLocation, playwrightTestConfig} from '../state'; diff --git a/packages/browser-test-runtime/src/state.ts b/packages/browser-test-runtime/src/state.ts index 05fe7df81..1456ed89c 100644 --- a/packages/browser-test-runtime/src/state.ts +++ b/packages/browser-test-runtime/src/state.ts @@ -11,6 +11,8 @@ export const playwrightTestConfig = { testDir: '/bundle', }, ], + // same as submodules/playwright/tests/library/playwright.config.ts + expect: {timeout: 10000}, }; export const configLocation = { diff --git a/packages/browser-test-runtime/utils/build_bundles.js b/packages/browser-test-runtime/utils/build_bundles.js index aa645a401..6d6b4b41d 100644 --- a/packages/browser-test-runtime/utils/build_bundles.js +++ b/packages/browser-test-runtime/utils/build_bundles.js @@ -3,16 +3,38 @@ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; -const bundles = { - // pngjs needs to bundle browserify-zlib, it throws an error when trying to use workers runtime zlib: - // Error: Class constructor Inflate cannot be invoked without 'new' - // It needs to build before other bundles, because they may depend on it - 'pngjs': '../bundles/pngjs', - 'utilsBundleImpl': '../../../submodules/playwright/packages/playwright-core/bundles/utils', - 'zipBundleImpl': '../../../submodules/playwright/packages/playwright-core/bundles/zip', - 'mcpBundleImpl': '../../../submodules/playwright/packages/playwright-core/bundles/mcp', - 'expectBundleImpl': '../../../submodules/playwright/packages/playwright/bundles/expect', -}; +const basedir = path.dirname(fileURLToPath(import.meta.url)); +const playwrightDir = path.join(basedir, '..', '..', '..', 'submodules', 'playwright'); +const jestDir = path.join(basedir, '..', '..', '..', 'submodules', 'jest'); +const outDir = path.join(basedir, '../src/bundles'); + +const bundles = [ + { + // pngjs needs to bundle browserify-zlib, it throws an error when trying to use workers runtime zlib: + // Error: Class constructor Inflate cannot be invoked without 'new' + // It needs to build before other bundles, because they may depend on it + name: 'pngjs', + root: path.join(basedir, '../bundles/pngjs'), + entry: { pngjs: path.join(basedir, '../bundles/pngjs/src/pngjs.ts') }, + }, + { + // Upstream vendors all npm dependencies of playwright-core through this single barrel. + name: 'utilsBundleImpl', + root: playwrightDir, + entry: { utilsBundleImpl: path.join(playwrightDir, 'packages/playwright-core/src/utilsBundle.ts') }, + }, + { + // Jest helpers used by playwright's inlined expect library (matchers/expectLibrary.ts). + // Each entry keeps the original package exports, so imports can be aliased one-to-one. + name: 'jest', + root: playwrightDir, + entry: { + 'jest-matcher-utils': path.join(jestDir, 'packages/jest-matcher-utils/src/index.ts'), + 'jest-message-util': path.join(jestDir, 'packages/jest-message-util/src/index.ts'), + 'jest-expect-utils': path.join(jestDir, 'packages/expect-utils/src/index.ts'), + }, + }, +]; const external = [ 'assert', @@ -37,6 +59,7 @@ const external = [ 'tls', 'url', 'util', + 'readline', // Node.js prefixed modules 'node:crypto', 'node:url', @@ -56,36 +79,39 @@ const external = [ 'node:string_decoder', 'node:tls', 'node:os', + 'node:readline', ]; -const basedir = path.dirname(fileURLToPath(import.meta.url)); +function listNodeModules(root) { + const nodeModulesDir = path.join(root, 'node_modules'); + return fs.readdirSync(nodeModulesDir, { withFileTypes: true }) + .filter(dirent => dirent.isDirectory()) + .flatMap(dirent => dirent.name.startsWith('@') + // expand scoped packages (the ones that start with @) + ? fs.readdirSync(path.join(nodeModulesDir, dirent.name), { withFileTypes: true }) + .filter(subdirent => subdirent.isDirectory()) + .map(subdirent => `${dirent.name}/${subdirent.name}`) + : [dirent.name]); +} -(async () => { - for (const [name, bundleDir] of Object.entries(bundles)) { - const root = path.join(basedir, bundleDir); - const nodeModulesDir = path.join(root, 'node_modules'); - const nodeModulesLibs = fs.readdirSync(nodeModulesDir, { withFileTypes: true }) - .filter(dirent => dirent.isDirectory()) - .flatMap(dirent => dirent.name.startsWith('@') - // expand scoped packages (the ones that start with @) - ? fs.readdirSync(path.join(nodeModulesDir, dirent.name), { withFileTypes: true }) - .filter(subdirent => subdirent.isDirectory()) - .map(subdirent => `${dirent.name}/${subdirent.name}`) - : [dirent.name]); - const jestDir = path.join(basedir, '..', '..', '..', 'submodules', 'jest'); +// we'll use jest packages source code instead of npm packages +const jestPackages = Object.fromEntries(fs.readdirSync(path.join(jestDir, 'packages'), { withFileTypes: true }) + .filter(dirent => dirent.isDirectory() && + fs.existsSync(path.join(jestDir, 'packages', dirent.name, 'package.json')) && + fs.existsSync(path.join(jestDir, 'packages', dirent.name, 'src', 'index.ts'))) + .map(dirent => [ + JSON.parse(fs.readFileSync(path.join(jestDir, 'packages', dirent.name, 'package.json'), 'utf-8')).name, + path.join(jestDir, 'packages', dirent.name, 'src', 'index.ts') + ])); - // we'll use jest packages source code instead of npm packages - const jestPackages = Object.fromEntries(fs.readdirSync(path.join(jestDir, 'packages'), { withFileTypes: true }) - .filter(dirent => dirent.isDirectory() && - fs.existsSync(path.join(jestDir, 'packages', dirent.name, 'package.json')) && - fs.existsSync(path.join(jestDir, 'packages', dirent.name, 'src', 'index.ts'))) - .map(dirent => [ - JSON.parse(fs.readFileSync(path.join(jestDir, 'packages', dirent.name, 'package.json'), 'utf-8')).name, - path.join(jestDir, 'packages', dirent.name, 'src', 'index.ts') - ])); +(async () => { + for (const { name, root, entry } of bundles) { + // jest sources live outside the root, so their npm dependencies must resolve from the root node_modules + const nodeModulesLibs = name === 'jest' ? listNodeModules(root).filter(lib => !(lib in jestPackages)) : []; await build({ root, + configFile: false, // Jest is source input here, not a separately installed project. esbuild: { tsconfigRaw: {} }, resolve: { @@ -98,15 +124,19 @@ const basedir = path.dirname(fileURLToPath(import.meta.url)); 'node:process': 'process', 'node:string_decoder': 'string_decoder', - // jest npm package is commonjs, + // jest npm package is commonjs, ...jestPackages, 'commander': path.join(basedir, '../src/mocks/commander'), 'socks-proxy-agent': path.join(basedir, '../src/mocks/socksProxyAgent'), 'open': path.join(basedir, '../src/mocks/open'), + // optional native macOS dependency of chokidar + 'fsevents': path.join(basedir, '../src/mocks/empty'), + // same replacement upstream uses when bundling utilsBundle (utils/build/build.js) + 'raw-body': path.join(playwrightDir, 'utils/build/raw-body.ts'), ...(name === 'pngjs' ? { 'zlib': 'browserify-zlib' } : - { 'pngjs': path.join(basedir, `../src/bundles/pngjs.js` ) }), + { 'pngjs': path.join(outDir, 'pngjs.js') }), }, }, build: { @@ -116,23 +146,26 @@ const basedir = path.dirname(fileURLToPath(import.meta.url)); target: 'esnext', lib: { name, - entry: path.join(root, `./src/${name}.ts`), + entry, formats: ['es'], }, rollupOptions: { external: [ + /^node:/, ...(name === 'pngjs' ? external : [...external, 'zlib', 'pngjs']) ], output: { - dir: path.join(basedir, '../src/bundles'), - entryFileNames: `${name}.js`, + dir: outDir, + entryFileNames: '[name].js', + chunkFileNames: `${name}-[name].js`, }, }, commonjsOptions: { transformMixedEsModules: true, extensions: ['.ts', '.js'], include: [ + ...Object.values(entry), path.join(root, './src/**/*'), path.join(root, './node_modules/**/*'), ], diff --git a/packages/browser-test-runtime/utils/vite-playwright.js b/packages/browser-test-runtime/utils/vite-playwright.js new file mode 100644 index 000000000..7001161bd --- /dev/null +++ b/packages/browser-test-runtime/utils/vite-playwright.js @@ -0,0 +1,135 @@ +// Shared vite configuration for builds that bundle the playwright submodule sources. +import fs from 'fs'; +import { createRequire } from 'module'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const basedir = path.dirname(fileURLToPath(import.meta.url)); +export const playwrightDir = path.resolve(basedir, '../../../submodules/playwright'); +const playwrightPackagesDir = path.join(playwrightDir, 'packages'); +const bundlesDir = path.resolve(basedir, '../src/bundles'); +const utilsBundleImpl = path.join(bundlesDir, 'utilsBundleImpl.js'); + +// Path aliases from the submodule tsconfig.json that runtime code imports. +export const playwrightPathAliases = [ + { find: /^@isomorphic\/(.*)$/, replacement: `${playwrightPackagesDir}/isomorphic/$1` }, + { find: /^@utils\/(.*)$/, replacement: `${playwrightPackagesDir}/utils/$1` }, + { find: /^@protocol\/(.*)$/, replacement: `${playwrightPackagesDir}/protocol/src/$1` }, + { find: /^@testIsomorphic\/(.*)$/, replacement: `${playwrightPackagesDir}/playwright/src/isomorphic/$1` }, +]; + +// Must come before the `playwright-core/lib` alias. +export const playwrightCoreBundleAlias = { + find: /^playwright-core\/lib\/coreBundle$/, + replacement: path.resolve(basedir, '../src/mocks/coreBundle.ts'), +}; + +// Prebuilt bundles from utils/build_bundles.js. +export const playwrightBundleAliases = [ + { find: /^jest-matcher-utils$/, replacement: path.join(bundlesDir, 'jest-matcher-utils.js') }, + { find: /^jest-message-util$/, replacement: path.join(bundlesDir, 'jest-message-util.js') }, + { find: /^@jest\/expect-utils$/, replacement: path.join(bundlesDir, 'jest-expect-utils.js') }, + { find: /^pngjs$/, replacement: path.join(bundlesDir, 'pngjs.js') }, +]; + +// Replaces submodule modules with mocks, matched by the resolved file path rather than by the import +// specifier, so the mock still applies when upstream moves the importer. Fails the build if a mocked +// module no longer exists upstream, instead of silently bundling nothing or the real module. +// `mocks` maps paths relative to submodules/playwright/packages (without extension) to mock files. +export function playwrightModuleMocks(mocks) { + const targets = new Map(); + for (const [target, mock] of Object.entries(mocks)) { + const file = path.join(playwrightPackagesDir, target); + if (!fs.existsSync(`${file}.ts`)) + throw new Error(`Mocked module ${target}.ts does not exist in submodules/playwright/packages`); + targets.set(file, mock); + } + return { + name: 'playwright-module-mocks', + enforce: 'pre', + resolveId(source, importer) { + if (!importer || !source.startsWith('.')) + return null; + const file = path.resolve(path.dirname(importer), source).replace(/\.(ts|js)$/, ''); + return targets.get(file) ?? null; + }, + }; +} + +// Upstream sources import their npm dependencies directly (`import debug from 'debug'`), and the +// upstream build rewrites those imports to keys of `playwright-core/lib/utilsBundle` +// (see `_rewriteVendoredImports` in submodules/playwright/utils/build/build.js). +// This plugin does the same rewrite, so the dependencies come from the prebuilt utilsBundleImpl. +// `target` is the module that provides the utilsBundle exports (defaults to the prebuilt bundle; set +// it to an external module id to reuse a bundle at runtime). `include` lists the directories to rewrite. +export function playwrightVendoredImports({ target = utilsBundleImpl, external = false, include = [playwrightPackagesDir] } = {}) { + const require = createRequire(import.meta.url); + const { MAPPING, VENDORED_PACKAGES } = require(path.join(playwrightDir, 'utils/build/utilsBundleMapping.js')); + const escape = value => value.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&'); + const packagesPattern = [...VENDORED_PACKAGES].map(escape).join('|'); + const importRe = new RegExp(`^import\\s+(\\{[^}]*\\}|\\*\\s+as\\s+\\w+|\\w+(?:\\s*,\\s*\\{[^}]*\\})?)\\s+from\\s+'(${packagesPattern})';?`, 'gm'); + const sourceRe = new RegExp(`^import\\s+[^'";]*\\s+from\\s+'(${packagesPattern})'`, 'm'); + + const parseNamed = braced => braced.replace(/^\s*\{|\}\s*$/g, '').split(',') + .map(spec => spec.trim()) + .filter(spec => spec && !spec.startsWith('type ')) + .map(spec => { + const match = spec.match(/^(\w+)(?:\s+as\s+(\w+))?$/); + if (!match) + throw new Error(`Cannot parse import specifier "${spec}"`); + return { src: match[1], alias: match[2] || match[1] }; + }); + + class UnmappedImportError extends Error {} + + const rewrite = (file, clause, pkg) => { + const def = MAPPING[pkg]; + const specifiers = []; + const fail = () => { + throw new UnmappedImportError(`${file}: "import ${clause} from '${pkg}'" has no utilsBundle mapping`); + }; + if (clause.startsWith('{')) { + for (const { src, alias } of parseNamed(clause)) + specifiers.push(`${def.named?.[src] ?? fail()} as ${alias}`); + } else if (clause.startsWith('*')) { + specifiers.push(`${def.namespace ?? fail()} as ${clause.match(/\*\s+as\s+(\w+)/)[1]}`); + } else { + const [, defaultName, named] = clause.match(/^(\w+)(?:\s*,\s*(\{[^}]*\}))?$/); + specifiers.push(`${def.default ?? fail()} as ${defaultName}`); + for (const { src, alias } of named ? parseNamed(named) : []) + specifiers.push(`${def.named?.[src] ?? fail()} as ${alias}`); + } + return `import { ${specifiers.join(', ')} } from ${JSON.stringify(target)};`; + }; + + return { + name: 'playwright-vendored-imports', + enforce: 'pre', + resolveId(source) { + // `utilsBundle` (the upstream barrel) is replaced by the prebuilt bundle + if (/(^|\/)utilsBundle(\.ts|\.js)?$/.test(source)) + return { id: target, external }; + return null; + }, + transform(code, id) { + if (!include.some(dir => id.startsWith(dir)) || id.includes('/node_modules/') || !/\.ts$/.test(id)) + return null; + if (!sourceRe.test(code)) + return null; + return { + code: code.replace(importRe, (full, clause, pkg) => { + try { + return rewrite(id, clause, pkg); + } catch (error) { + // Upstream rewrites every import in its sources, so an unmapped one there is a bug. Other + // files (upstream tests) may import packages directly; those are bundled as they are. + if (error instanceof UnmappedImportError && !id.startsWith(playwrightPackagesDir)) + return full; + throw error; + } + }), + map: null, + }; + }, + }; +} diff --git a/packages/browser-test-runtime/vite.config.ts b/packages/browser-test-runtime/vite.config.ts index 66ac1997e..3dac97b72 100644 --- a/packages/browser-test-runtime/vite.config.ts +++ b/packages/browser-test-runtime/vite.config.ts @@ -2,112 +2,110 @@ import path from 'path'; import {defineConfig} from 'vite'; +import { + playwrightBundleAliases, + playwrightCoreBundleAlias, + playwrightModuleMocks, + playwrightPathAliases, + playwrightVendoredImports, +} from './utils/vite-playwright.js'; + +const mocks = path.resolve(__dirname, './src/mocks'); + +const aliases = { + 'playwright-core/lib': path.resolve( + __dirname, + '../../submodules/playwright/packages/playwright-core/src', + ), + 'playwright/lib': path.resolve( + __dirname, + '../../submodules/playwright/packages/playwright/src', + ), + 'playwright-core': path.resolve( + __dirname, + './src/mocks/playwrightCore', + ), + 'async_hooks': 'node:async_hooks', + 'assert': 'node:assert', + 'buffer': 'node:buffer', + 'constants': 'node:constants', + 'crypto': 'node:crypto', + 'dns': 'node:dns', + 'events': 'node:events', + 'fs': 'node:fs', + 'http': 'node:http', + 'http2': 'node:http2', + 'https': 'node:https', + 'module': 'node:module', + 'net': 'node:net', + 'os': 'node:os', + 'path': 'node:path', + 'process': 'node:process', + 'stream': 'node:stream', + 'string_decoder': 'node:string_decoder', + 'tls': 'node:tls', + 'url': 'node:url', + 'util': 'node:util', + 'zlib': 'node:zlib', + 'child_process': path.resolve( + __dirname, + './src/mocks/childProcess', + ), + 'node:child_process': path.resolve( + __dirname, + './src/mocks/childProcess', + ), + 'readline': path.resolve(__dirname, './src/mocks/readline'), + 'node:readline': path.resolve( + __dirname, + './src/mocks/readline', + ), + 'inspector': path.resolve(__dirname, './src/mocks/inspector'), + 'node:inspector': path.resolve( + __dirname, + './src/mocks/inspector', + ), + './transport': path.resolve( + __dirname, + './src/cloudflare/webSocketTransport', + ), + '../transport': path.resolve( + __dirname, + './src/cloudflare/webSocketTransport', + ), + '@playwright-cloudflare': path.resolve(__dirname, './src'), + // IMPORTANT `require('../playwright')` in `recorderApp.ts` causes a circular dependency, + // so we need to mock it (it's not needed, it's related with recorder). + '../playwright': path.resolve(__dirname, './src/mocks/empty'), + 'electron/index.js': path.resolve( + __dirname, + './src/mocks/empty', + ), +}; + export default defineConfig({ + plugins: [ + playwrightModuleMocks({ + 'playwright/src/common/testLoader': `${mocks}/testLoader.ts`, + 'playwright/src/transform/transform': `${mocks}/transform.ts`, + 'playwright/src/transform/compilationCache': `${mocks}/compilationCache.ts`, + 'playwright-core/src/server/bidi/bidiOverCdp': `${mocks}/empty.ts`, + }), + playwrightVendoredImports(), + ], resolve: { - alias: { - 'playwright-core/lib': path.resolve( - __dirname, - '../../submodules/playwright/packages/playwright-core/src', - ), - 'playwright/lib': path.resolve( - __dirname, - '../../submodules/playwright/packages/playwright/src', - ), - 'playwright-core': path.resolve( - __dirname, - './src/mocks/playwrightCore', - ), - 'async_hooks': 'node:async_hooks', - 'assert': 'node:assert', - 'buffer': 'node:buffer', - 'constants': 'node:constants', - 'crypto': 'node:crypto', - 'dns': 'node:dns', - 'events': 'node:events', - 'fs': 'node:fs', - 'http': 'node:http', - 'http2': 'node:http2', - 'https': 'node:https', - 'module': 'node:module', - 'net': 'node:net', - 'os': 'node:os', - 'path': 'node:path', - 'process': 'node:process', - 'stream': 'node:stream', - 'string_decoder': 'node:string_decoder', - 'tls': 'node:tls', - 'url': 'node:url', - 'util': 'node:util', - 'zlib': 'node:zlib', - './utilsBundleImpl': path.resolve( - __dirname, - './src/bundles/utilsBundleImpl.js', - ), - './zipBundleImpl': path.resolve( - __dirname, - './src/bundles/zipBundleImpl.js', - ), - './expectBundleImpl': path.resolve( - __dirname, - './src/bundles/expectBundleImpl.js', - ), - './mcpBundleImpl': path.resolve( - __dirname, - './src/bundles/mcpBundleImpl.js', - ), - 'pngjs': path.resolve(__dirname, './src/bundles/pngjs.js'), - 'child_process': path.resolve( - __dirname, - './src/mocks/childProcess', - ), - 'node:child_process': path.resolve( - __dirname, - './src/mocks/childProcess', - ), - 'readline': path.resolve(__dirname, './src/mocks/readline'), - 'node:readline': path.resolve( - __dirname, - './src/mocks/readline', - ), - 'inspector': path.resolve(__dirname, './src/mocks/inspector'), - 'node:inspector': path.resolve( - __dirname, - './src/mocks/inspector', - ), - './transport': path.resolve( - __dirname, - './src/cloudflare/webSocketTransport', - ), - '../transport': path.resolve( - __dirname, - './src/cloudflare/webSocketTransport', - ), - '@playwright-cloudflare': path.resolve(__dirname, './src'), - './transform': path.resolve(__dirname, './src/mocks/transform'), - '../transform/transform': path.resolve(__dirname, './src/mocks/transform'), - '../transform/compilationCache': path.resolve( - __dirname, - './src/mocks/compilationCache', - ), - '../common/testLoader': path.resolve( - __dirname, - './src/mocks/testLoader', - ), - '../common/esmLoaderHost': path.resolve( - __dirname, - './src/mocks/esmLoaderHost', - ), - './esmLoaderHost': path.resolve( - __dirname, - './src/mocks/esmLoaderHost', - ), - '../playwright': path.resolve(__dirname, './src/mocks/empty'), - './bidiOverCdp': path.resolve(__dirname, './src/mocks/empty'), - 'electron/index.js': path.resolve( - __dirname, - './src/mocks/empty', - ), - }, + alias: [ + playwrightCoreBundleAlias, + ...Object.entries(aliases).map(([find, replacement]) => ({ + find, + replacement, + })), + ...playwrightPathAliases, + ...playwrightBundleAliases, + ], + }, + define: { + __dirname: JSON.stringify('/'), }, build: { minify: false, @@ -141,6 +139,18 @@ export default defineConfig({ __dirname, '../../submodules/playwright/packages/playwright/src/**/*', ), + path.resolve( + __dirname, + '../../submodules/playwright/packages/utils/**/*', + ), + path.resolve( + __dirname, + '../../submodules/playwright/packages/isomorphic/**/*', + ), + path.resolve( + __dirname, + '../../submodules/playwright/packages/protocol/src/**/*', + ), /node_modules/, ], }, diff --git a/packages/playwright-cloudflare/internal.d.ts b/packages/playwright-cloudflare/internal.d.ts index af6610aff..8ce6757ea 100644 --- a/packages/playwright-cloudflare/internal.d.ts +++ b/packages/playwright-cloudflare/internal.d.ts @@ -1,5 +1,3 @@ -import {isUnderTest} from 'playwright-core/lib/utils'; - export {mergeTests} from './types/test'; interface Debug { @@ -9,4 +7,6 @@ interface Debug { } export const debug: Debug; -export {isUnderTest}; +export function isUnderTest(): boolean; +export function getPlaywrightVersion(majorMinorOnly?: boolean): string; +export function getUserAgent(): string; diff --git a/packages/playwright-cloudflare/src/cloudflare/wrapClientApis.ts b/packages/playwright-cloudflare/src/cloudflare/wrapClientApis.ts index 8fd047419..bb9d429c7 100644 --- a/packages/playwright-cloudflare/src/cloudflare/wrapClientApis.ts +++ b/packages/playwright-cloudflare/src/cloudflare/wrapClientApis.ts @@ -19,42 +19,32 @@ // some types are commented out because they are not used in the extension // or they were not released in this version of Playwright -import { - Browser, - BrowserContext, - BrowserType, - Clock, - ConsoleMessage, - Coverage, - Dialog, - Download, - // Electron, - // ElectronApplication, - Locator, - FrameLocator, - ElementHandle, - FileChooser, - TimeoutError, - Frame, - Keyboard, - Mouse, - Touchscreen, - JSHandle, - Route, - WebSocket, - WebSocketRoute, - APIRequest, - APIRequestContext, - APIResponse, - Page, - Selectors, - Tracing, - Video, - Worker, - CDPSession, - Playwright, - WebError, -} from 'playwright-core/lib/client/api'; +import { Browser } from 'playwright-core/lib/client/browser'; +import { BrowserContext } from 'playwright-core/lib/client/browserContext'; +import { BrowserType } from 'playwright-core/lib/client/browserType'; +import { Clock } from 'playwright-core/lib/client/clock'; +import { ConsoleMessage } from 'playwright-core/lib/client/consoleMessage'; +import { Coverage } from 'playwright-core/lib/client/coverage'; +import { Dialog } from 'playwright-core/lib/client/dialog'; +import { Download } from 'playwright-core/lib/client/download'; +// import { Electron, ElectronApplication } from 'playwright-core/lib/client/electron'; +import { FrameLocator, Locator } from 'playwright-core/lib/client/locator'; +import { ElementHandle } from 'playwright-core/lib/client/elementHandle'; +import { FileChooser } from 'playwright-core/lib/client/fileChooser'; +import { TimeoutError } from 'playwright-core/lib/client/errors'; +import { Frame } from 'playwright-core/lib/client/frame'; +import { Keyboard, Mouse, Touchscreen } from 'playwright-core/lib/client/input'; +import { JSHandle } from 'playwright-core/lib/client/jsHandle'; +import { Route, WebSocket, WebSocketRoute } from 'playwright-core/lib/client/network'; +import { APIRequest, APIRequestContext, APIResponse } from 'playwright-core/lib/client/fetch'; +import { Page } from 'playwright-core/lib/client/page'; +import { Selectors } from 'playwright-core/lib/client/selectors'; +import { Tracing } from 'playwright-core/lib/client/tracing'; +import { Video } from 'playwright-core/lib/client/video'; +import { Worker } from 'playwright-core/lib/client/worker'; +import { CDPSession } from 'playwright-core/lib/client/cdpSession'; +import { Playwright } from 'playwright-core/lib/client/playwright'; +import { WebError } from 'playwright-core/lib/client/webError'; import { apiCallZone } from './apiCallZone'; @@ -112,7 +102,7 @@ const apis: { [ApiK in keyof ApiTypeMap]: [ApiTypeMap[ApiK], { [K in KeysOfAsync // androidWebView: [AndroidWebView.prototype], // androidInput: [AndroidInput.prototype], // androidSocket: [AndroidSocket.prototype], - browser: [Browser.prototype, { newContext: true, newPage: true, newBrowserCDPSession: true, startTracing: true, stopTracing: true, close: true }], + browser: [Browser.prototype, { newContext: true, newPage: true, newBrowserCDPSession: true, startTracing: true, stopTracing: true, close: true, bind: true, unbind: true }], browserContext: [BrowserContext.prototype, { newPage: true, cookies: true, @@ -124,6 +114,7 @@ const apis: { [ApiK in keyof ApiTypeMap]: [ApiTypeMap[ApiK], { [K in KeysOfAsync setExtraHTTPHeaders: true, setOffline: true, setHTTPCredentials: true, + setStorageState: true, addInitScript: true, exposeBinding: true, exposeFunction: true, @@ -190,7 +181,11 @@ const apis: { [ApiK in keyof ApiTypeMap]: [ApiTypeMap[ApiK], { [K in KeysOfAsync allInnerTexts: true, allTextContents: true, waitFor: true, - ariaSnapshot: true + ariaSnapshot: true, + drop: true, + normalize: true, + hideHighlight: true, + waitForFunction: true, }], frameLocator: [FrameLocator.prototype, {}], elementHandle: [ElementHandle.prototype, { @@ -241,7 +236,7 @@ const apis: { [ApiK in keyof ApiTypeMap]: [ApiTypeMap[ApiK], { [K in KeysOfAsync waitForSelector: true, }], fileChooser: [FileChooser.prototype, { setFiles: true }], - timeoutError: [TimeoutError.prototype, {}], + timeoutError: [TimeoutError.prototype, { details: false }], frame: [Frame.prototype, { goto: true, waitForNavigation: true, @@ -298,7 +293,7 @@ const apis: { [ApiK in keyof ApiTypeMap]: [ApiTypeMap[ApiK], { [K in KeysOfAsync webSocketRoute: [WebSocketRoute.prototype, { close: true }], request: [APIRequest.prototype, { newContext: true }], requestContext: [APIRequestContext.prototype, { dispose: true, delete: true, head: true, get: true, patch: true, post: true, put: true, fetch: true, storageState: true }], - response: [APIResponse.prototype, { body: true, json: true, text: true, dispose: true }], + response: [APIResponse.prototype, { body: true, json: true, text: true, dispose: true, securityDetails: true, serverAddr: true }], page: [Page.prototype, { opener: true, waitForSelector: true, @@ -373,9 +368,15 @@ const apis: { [ApiK in keyof ApiTypeMap]: [ApiTypeMap[ApiK], { [K in KeysOfAsync consoleMessages: true, pageErrors: true, requests: true, + ariaSnapshot: true, + pickLocator: true, + cancelPickLocator: true, + hideHighlight: true, + clearConsoleMessages: true, + clearPageErrors: true, }], selectors: [Selectors.prototype, { register: true }], - tracing: [Tracing.prototype, { start: true, startChunk: true, stop: true, stopChunk: true, group: false, groupEnd: false }], + tracing: [Tracing.prototype, { start: true, startChunk: true, stop: true, stopChunk: true, group: false, groupEnd: false, startHar: true, stopHar: true }], video: [Video.prototype, { delete: true, path: true, saveAs: true }], worker: [Worker.prototype, { evaluate: true, evaluateHandle: true, waitForEvent: true }], session: [CDPSession.prototype, { send: true, detach: true }], diff --git a/packages/playwright-cloudflare/src/index.ts b/packages/playwright-cloudflare/src/index.ts index fd3668c25..51215286a 100644 --- a/packages/playwright-cloudflare/src/index.ts +++ b/packages/playwright-cloudflare/src/index.ts @@ -1,7 +1,7 @@ -import { createInProcessPlaywright } from 'playwright-core/lib/inProcessFactory'; +import { playwright } from 'playwright-core/lib/inprocess'; import { kBrowserCloseMessageId } from 'playwright-core/lib/server/chromium/crConnection'; import { env } from 'cloudflare:workers'; -import { setTimeOrigin, timeOrigin } from 'playwright-core/lib/utils/isomorphic/time'; +import { setTimeOrigin, timeOrigin } from '@isomorphic/time'; import { transportZone, WebSocketTransport } from './cloudflare/webSocketTransport'; import { wrapClientApis } from './cloudflare/wrapClientApis'; @@ -22,7 +22,6 @@ function resetMonotonicTime() { setTimeOrigin(Date.now()); } -const playwright = createInProcessPlaywright(); unsupportedOperations(playwright); wrapClientApis(); @@ -34,7 +33,7 @@ const WS_FAKE_HOST = 'ws://fake.host'; // Browser Run binding every property is an RPC stub, so it is always a function. const rpcBindings = new WeakSet(); -const originalConnectOverCDP = playwright.chromium.connectOverCDP; +const originalConnectOverCDP = playwright.chromium.connectOverCDP as (endpointURL: string, options?: ConnectOverCDPOptions) => Promise; // HACK this is a major hack, but we need it to make playwright-mcp and stagehand work without modifying their code extensively. // Both playwright-mcp and stagehand use playwright.chromium.connectOverCDP if a CDP endpoint is passed, // so we need to override it to use our own connectOverCDP implementation. diff --git a/packages/playwright-cloudflare/src/internal.ts b/packages/playwright-cloudflare/src/internal.ts index fca2c33fb..ed1f94c89 100644 --- a/packages/playwright-cloudflare/src/internal.ts +++ b/packages/playwright-cloudflare/src/internal.ts @@ -1,10 +1,12 @@ import {debug} from 'playwright-core/lib/utilsBundle'; -export {asLocator, isUnderTest} from 'playwright-core/lib/utils'; +export {asLocator} from '@isomorphic/locatorGenerators'; +export {isUnderTest} from '@utils/debug'; +export {getPlaywrightVersion, getUserAgent} from 'playwright-core/lib/server/userAgent'; export {debug} from 'playwright-core/lib/utilsBundle'; export {mergeTests} from 'playwright/lib/common/testType'; export * from 'playwright-core/lib/utilsBundle'; -export * from 'playwright-core/lib/zipBundle'; +export {extractZip as extract} from '@utils/third_party/extractZip'; // console.log is unavailable during Worker module initialization. debug.log = (...args: any[]) => console.log(...args); diff --git a/packages/playwright-cloudflare/src/mocks/transform.ts b/packages/playwright-cloudflare/src/mocks/transform.ts index 3ce0df127..958c97d36 100644 --- a/packages/playwright-cloudflare/src/mocks/transform.ts +++ b/packages/playwright-cloudflare/src/mocks/transform.ts @@ -1,4 +1,4 @@ -import { currentlyLoadingFileSuite } from 'playwright/lib/common/globals'; +import { currentlyLoadingFileSuite } from 'playwright/lib/globals'; import {configLocation, playwrightTestConfig} from './testState'; diff --git a/packages/playwright-cloudflare/tests/src/server/coreBundle.ts b/packages/playwright-cloudflare/tests/src/server/coreBundle.ts new file mode 100644 index 000000000..84e0aec05 --- /dev/null +++ b/packages/playwright-cloudflare/tests/src/server/coreBundle.ts @@ -0,0 +1,21 @@ +// Replaces `playwright-core/lib/coreBundle` for the upstream test files. The real barrel +// re-exports all of playwright-core (CLI, MCP tools, servers), and the tests only use these parts. +import { deviceDescriptors } from '@isomorphic/deviceDescriptors'; +import { nullProgress } from '../../../../../submodules/playwright/packages/playwright-core/src/server/progress'; +import { DirTraceLoaderBackend, extractTrace } from '../../../../../submodules/playwright/packages/playwright-core/src/tools/trace/traceParser'; + +export * as iso from '@isomorphic/index'; +export * as utils from '@utils/index'; +// from the runtime build, because the sources read package.json relative to __dirname +export { getPlaywrightVersion, getUserAgent } from '@cloudflare/playwright/internal'; + +export const server = { deviceDescriptors, nullProgress }; +export const tools = { DirTraceLoaderBackend, extractTrace }; +// library/videoPlayer.ts decodes videos with the ffmpeg executable, which Workers cannot run. +export const registry = { + registry: { + findExecutable(name: string): never { + throw new Error(`Executable ${name} is not available in Cloudflare Workers`); + }, + }, +}; diff --git a/packages/playwright-cloudflare/tests/src/server/workerFixtures.ts b/packages/playwright-cloudflare/tests/src/server/workerFixtures.ts index 820bd0e6d..6f4d6f4f8 100644 --- a/packages/playwright-cloudflare/tests/src/server/workerFixtures.ts +++ b/packages/playwright-cloudflare/tests/src/server/workerFixtures.ts @@ -103,6 +103,10 @@ class TestServer { this._testInfo.skip(true, 'setRoute not supported, skipping'); } + setContent() { + this._testInfo.skip(true, 'setContent not supported, skipping'); + } + waitForRequest() { this._testInfo.skip(true, 'waitForRequest not supported, skipping'); } @@ -115,6 +119,10 @@ class TestServer { this._testInfo.skip(true, 'waitForWebSocket not supported, skipping'); } + waitForWebSocketConnectionRequest() { + this._testInfo.skip(true, 'waitForWebSocketConnectionRequest not supported, skipping'); + } + enableGzip() { this._testInfo.skip(true, 'enableGzip not supported, skipping'); } @@ -174,6 +182,7 @@ export type BrowserTestWorkerFixtures = PageWorkerFixtures & { isAndroid: boolean; isElectron: boolean; isHeadlessShell: boolean; + isFrozenWebkit: boolean; nodeVersion: { major: number, minor: number, patch: number }; isBidi: boolean; }; @@ -269,6 +278,7 @@ export const test = platformTest.extend should set window.screen.orientation.type for mobile devices', 'library/capabilities.spec.ts > should not auto play audio', 'library/capabilities.spec.ts > should not crash on feature detection for PublicKeyCredential', + // These load the assets with a file:// URL, which the remote browser cannot open. Upstream + // v1.58 skipped them in service mode, v1.62 does not. + 'library/capabilities.spec.ts > should play video @smoke', + 'library/capabilities.spec.ts > should play webm video @smoke', 'library/download.spec.ts > should be able to download a inline PDF file via response interception', 'library/download.spec.ts > should be able to download a PDF file', 'library/har.spec.ts > should attach content', @@ -33,6 +37,11 @@ export const skipTests: string[] = [ 'library/har.spec.ts > should report the correct response body size when the bodySize is 0', 'library/ignorehttpserrors.spec.ts > should isolate contexts', 'library/ignorehttpserrors.spec.ts > should isolated contexts that share network process', + // The test source locations are not available in the bundled Worker tests. + 'library/debugger.spec.ts > should pause at next and resume', + 'library/debugger.spec.ts > should run to location', + // Also fails on main (v1.58) in the full suite against the deployed test Worker. + 'library/geolocation.spec.ts > should use context options for popup', 'library/page-event-crash.spec.ts > should emit crash event when page crashes', 'library/page-event-crash.spec.ts > should be able to close context when page crashes', 'library/page-event-crash.spec.ts > should cancel waitForEvent when page crashes', @@ -64,6 +73,8 @@ export const skipTests: string[] = [ 'library/emulation-focus.spec.ts > should not affect screenshots', 'library/shared-worker.spec.ts > should survive shared worker restart', 'library/tracing.spec.ts > should include context API requests', + // Reads the local artifacts folder. Upstream v1.58 skipped it in service mode, v1.62 does not. + 'library/tracing.spec.ts > should not flush console events', 'page/elementhandle-screenshot.spec.ts > element screenshot > should capture full element when larger than viewport', 'page/expect-boolean.spec.ts > toBeChecked > fail missing', 'page/expect-boolean.spec.ts > toBeChecked > fail with checked:false', @@ -93,17 +104,34 @@ export const skipTests: string[] = [ // Cache interception relies on a local history lifecycle that remote sessions do not expose. 'page/interception.spec.ts > should disable memory cache when intercepting', 'page/locator-misc-1.spec.ts > should upload the file', + // Also fails on main (v1.58) in the full suite against the deployed test Worker. + 'page/page-add-locator-handler.spec.ts > should work', 'page/page-autowaiting-no-hang.spec.ts > assigning location to about:blank', 'page/page-basic.spec.ts > should have sane user agent', + // New in v1.62. The result depends on the Browser Run browser: these pass with wrangler dev, + // but fail against the deployed test Worker. (hover: hover) needs the --blink-settings flags + // that Playwright passes when it launches a headless browser. 20 page.requestGC() calls exceed + // the test timeout. + 'page/page-emulate-media.spec.ts > should report hover and fine pointer for desktop', + 'page/page-evaluate.spec.ts > promise collected', 'page/page-evaluate.spec.ts > should respect use strict expression', 'page/page-evaluate.spec.ts > should work with function shorthands', 'page/page-event-popup.spec.ts > should be able to capture alert', + // The test creates large files in the Worker, and the Worker throws an exception. Upstream + // v1.58 skipped these tests in service mode, v1.62 does not. + 'page/page-filechooser.spec.ts > should upload multiple large files', 'page/page-goto.spec.ts > should fail when main resources failed to load', 'page/page-goto.spec.ts > should fail when navigating to bad SSL', // The Worker fixture is HTTPS-only, so it cannot exercise implicit HTTP URL completion. 'page/page-goto.spec.ts > should use http for no protocol', // The Worker fixture cannot emulate the separate failing HTTPS server used by this test. 'page/page-goto.spec.ts > should fail when navigating and show the url at the error message', + // Browser Run blocks file:// URLs (net::ERR_BLOCKED_BY_ADMINISTRATOR). Upstream v1.58 skipped + // these tests in service mode, v1.62 does not. + 'page/page-goto.spec.ts > should work with file URL', + 'page/page-goto.spec.ts > should work with file URL with subframes', + 'page/page-history.spec.ts > page.goBack should work for file urls', + 'page/page-network-response.spec.ts > should provide a Response with a file URL', // Browser Rendering does not expose a stable back-forward cache lifecycle. 'page/page-history.spec.ts > goBack/goForward should work with bfcache-able pages', // Browser Rendering does not preserve renderer-initiated history entries reliably. @@ -115,12 +143,24 @@ export const skipTests: string[] = [ 'page/page-network-sizes.spec.ts > should set bodySize to 0 when there was no response body', 'page/page-request-intercept.spec.ts > should fulfill intercepted response using alias', 'page/page-request-intercept.spec.ts > should give access to the intercepted response', + // Also fails on main (v1.58) with a page teardown timeout. route.continue fails the same way. + 'page/page-route.spec.ts > route.fulfill should throw if called twice', + 'page/page-route.spec.ts > route.continue should throw if called twice', + // Also fails on main (v1.58) in the full suite against the deployed test Worker. + 'page/page-screenshot.spec.ts > page screenshot > should take fullPage screenshots during navigation', 'page/page-screenshot.spec.ts > page screenshot animations > should wait for fonts to load', + // WebP snapshot comparison loads a WASM codec from the file system, which Workers cannot do. + // Page.screenshot({ type: 'webp' }) works, because Chromium encodes the image. + 'page/page-screenshot.spec.ts > page screenshot > should produce a valid webp screenshot', + 'page/page-screenshot.spec.ts > page screenshot > path option should detect webp', 'page/page-set-input-files.spec.ts > should throw an error if the file does not exist', 'page/page-set-input-files.spec.ts > should upload a file after popup', 'page/page-set-input-files.spec.ts > should upload a folder', 'page/page-set-input-files.spec.ts > should upload the file with spaces in name', 'page/page-set-input-files.spec.ts > should upload the file', + // See page-filechooser.spec.ts > should upload multiple large files. + 'page/page-set-input-files.spec.ts > should upload large file', + 'page/page-set-input-files.spec.ts > should upload large file with relative path', 'page/page-wait-for-navigation.spec.ts > should work with clicking on links which do not commit navigation', 'page/page-wait-for-response.spec.ts > should work with re-rendered cached IMG elements', 'page/page-wait-for-selector-2.spec.ts > should have correct stack trace for timeout', @@ -135,6 +175,9 @@ export const skipTests: string[] = [ 'page/workers.spec.ts > Page.workers @smoke', 'page/workers.spec.ts > should emit created and destroyed events', 'page/workers.spec.ts > should report worker script as network request', + // server.waitForRequest skips the test while page.waitForEvent('worker') is still pending. The + // pending wait then rejects during teardown and fails the test. + 'page/workers.spec.ts > should support extra http headers', ]; export const skipErrorMessages: Array = [ diff --git a/packages/playwright-cloudflare/tests/vite.config.ts b/packages/playwright-cloudflare/tests/vite.config.ts index d6d8d91a2..3b37c8caa 100644 --- a/packages/playwright-cloudflare/tests/vite.config.ts +++ b/packages/playwright-cloudflare/tests/vite.config.ts @@ -3,6 +3,8 @@ import fs from 'fs'; import { defineConfig, Plugin } from 'vite'; +import { playwrightPathAliases, playwrightVendoredImports, playwrightDir } from '../../browser-test-runtime/utils/vite-playwright.js'; + const basedir = __dirname.replace(/\\/g, '/'); const cloudflareSourceTestsDir = path.join(basedir, 'src'); const sourceTestsDir = path.join(basedir, '..', '..', '..', 'submodules', 'playwright', 'tests'); @@ -54,10 +56,22 @@ function setTestFilePlugin() { // https://vitejs.dev/config/ export default defineConfig({ - plugins: [setTestFilePlugin(), resolvePlaywrightPackageImports()], + plugins: [ + setTestFilePlugin(), + resolvePlaywrightPackageImports(), + // npm dependencies of upstream sources come from the utilsBundle exports of @cloudflare/playwright/internal + playwrightVendoredImports({ + target: '@cloudflare/playwright/internal', + external: true, + include: [path.join(playwrightDir, 'packages'), sourceTestsDir], + }), + ], root: sourceTestsDir, resolve: { - alias: { + alias: [ + // upstream tests import parts of playwright-core through this barrel + { find: /^(\.\.\/)+packages\/playwright-core\/lib\/coreBundle$/, replacement: path.resolve(basedir, './src/server/coreBundle') }, + ...Object.entries({ // https://workers-nodejs-compat-matrix.pages.dev/ 'async_hooks': 'node:async_hooks', 'assert': 'node:assert', @@ -109,9 +123,9 @@ export default defineConfig({ 'playwright': '@cloudflare/playwright', 'playwright-core': '@cloudflare/playwright', '@playwright/test': path.resolve(basedir, './src/server/workerFixtures'), - '@isomorphic': path.resolve(basedir, '../../../submodules/playwright/packages/playwright-core/src/utils/isomorphic'), - '@testIsomorphic': path.resolve(basedir, '../../../submodules/playwright/packages/playwright/src/isomorphic'), - }, + }).map(([find, replacement]) => ({ find, replacement })), + ...playwrightPathAliases, + ], }, build: { emptyOutDir: false, diff --git a/packages/playwright-cloudflare/tsconfig.json b/packages/playwright-cloudflare/tsconfig.json index 39b2aeec4..6e285d296 100644 --- a/packages/playwright-cloudflare/tsconfig.json +++ b/packages/playwright-cloudflare/tsconfig.json @@ -13,7 +13,10 @@ "lib": ["esnext", "DOM"], "paths": { "playwright-core/lib/*": ["../../submodules/playwright/packages/playwright-core/src/*"], - "playwright/lib/*": ["../../submodules/playwright/packages/playwright/src/*"] + "playwright/lib/*": ["../../submodules/playwright/packages/playwright/src/*"], + "@isomorphic/*": ["../../submodules/playwright/packages/isomorphic/*"], + "@utils/*": ["../../submodules/playwright/packages/utils/*"], + "@protocol/*": ["../../submodules/playwright/packages/protocol/src/*"] }, "baseUrl": ".", "types": ["@cloudflare/workers-types"] diff --git a/packages/playwright-cloudflare/utils/generate_worker_tests.js b/packages/playwright-cloudflare/utils/generate_worker_tests.js index eac919dba..adeecdfb1 100644 --- a/packages/playwright-cloudflare/utils/generate_worker_tests.js +++ b/packages/playwright-cloudflare/utils/generate_worker_tests.js @@ -13,10 +13,15 @@ const excludedFiles = [ 'library/client-certificates.spec.ts', 'library/debug-controller.spec.ts', 'library/headful.spec.ts', + // inspects the Node.js heap through node:inspector, which Workers do not provide + 'library/heap.spec.ts', 'library/har.spec.ts', 'library/launcher.spec.ts', + // tests the @playwright/client bundle from the file system (reads __dirname at module scope) + 'library/playwright-client.spec.ts', 'library/snapshotter.spec.ts', 'library/trace-viewer.spec.ts', + 'library/trace-viewer-scrub.spec.ts', 'library/video.spec.ts', 'library/web-socket.spec.ts', ]; diff --git a/packages/playwright-cloudflare/vite.config.ts b/packages/playwright-cloudflare/vite.config.ts index 24da3fe38..94e22e195 100644 --- a/packages/playwright-cloudflare/vite.config.ts +++ b/packages/playwright-cloudflare/vite.config.ts @@ -3,6 +3,14 @@ import path from 'path'; import picomatch from 'picomatch'; import { defineConfig } from 'vite'; +import { + playwrightBundleAliases, + playwrightCoreBundleAlias, + playwrightModuleMocks, + playwrightPathAliases, + playwrightVendoredImports, +} from '../browser-test-runtime/utils/vite-playwright.js'; + const baseDir = __dirname.replace(/\\/g, '/'); const browserTestRuntime = path.resolve(__dirname, '../browser-test-runtime'); @@ -18,11 +26,14 @@ const pathMappings = [ { glob: '**/submodules/playwright/packages/playwright-core/*.json', target: 'playwright-core' }, { glob: '**/submodules/playwright/packages/playwright/src/**', target: 'playwright' }, { glob: '**/submodules/playwright/packages/playwright/*.json', target: 'playwright' }, + { glob: '**/submodules/playwright/packages/utils/**', target: 'utils' }, + { glob: '**/submodules/playwright/packages/isomorphic/**', target: 'isomorphic' }, + { glob: '**/submodules/playwright/packages/protocol/src/**', target: 'protocol' }, ]; // Pre-compile globs to regexes (capture: true creates capture groups for **/*) const compiledMappings = pathMappings.map(({ glob, target }) => ({ - regex: picomatch.makeRe(glob, { capture: true }), + regex: picomatch.makeRe(glob, { capture: true, dot: true }), target, })); @@ -61,75 +72,74 @@ function remapOutputPath(facadeModuleId?: string): string { return '[name].js'; } +const aliases = { + 'playwright-core/lib': path.resolve(__dirname, '../../submodules/playwright/packages/playwright-core/src'), + 'playwright/lib': path.resolve(__dirname, '../../submodules/playwright/packages/playwright/src'), + 'playwright-core': path.resolve(__dirname, './src/index'), + + // https://workers-nodejs-compat-matrix.pages.dev/ + 'async_hooks': 'node:async_hooks', + 'assert': 'node:assert', + 'buffer': 'node:buffer', + 'constants': 'node:constants', + 'crypto': 'node:crypto', + 'dns': 'node:dns', + 'events': 'node:events', + 'fs': 'node:fs', + 'http': 'node:http', + 'http2': 'node:http2', + 'https': 'node:https', + 'module': 'node:module', + 'net': 'node:net', + 'os': 'node:os', + 'path': 'node:path', + 'process': 'node:process', + 'stream': 'node:stream', + 'string_decoder': 'node:string_decoder', + 'tls': 'node:tls', + 'url': 'node:url', + 'util': 'node:util', + 'zlib': 'node:zlib', + + "child_process": path.resolve(browserTestRuntime, './src/mocks/childProcess'), + "node:child_process": path.resolve(browserTestRuntime, './src/mocks/childProcess'), + "readline": path.resolve(browserTestRuntime, './src/mocks/readline'), + "node:readline": path.resolve(browserTestRuntime, './src/mocks/readline'), + "inspector": path.resolve(browserTestRuntime, './src/mocks/inspector'), + "node:inspector": path.resolve(browserTestRuntime, './src/mocks/inspector'), + + // replace playwright transport with cloudflare workers transport + './transport': path.resolve(__dirname, './src/cloudflare/webSocketTransport'), + '../transport': path.resolve(__dirname, './src/cloudflare/webSocketTransport'), + + // cloudflare-specific modules added by patch + '@playwright-cloudflare': path.resolve(__dirname, './src'), + + // IMPORTANT `require('../playwright')` in `recorderApp.ts` causes a circular dependency, + // so we need to mock it (it's not needed, it's related with recorder). + '../playwright': path.resolve(browserTestRuntime, './src/mocks/empty'), + 'electron/index.js': path.resolve(browserTestRuntime, './src/mocks/empty'), +}; + // https://vitejs.dev/config/ export default defineConfig({ - + plugins: [ + playwrightModuleMocks({ + // Workers do not load or transpile test files, and this way babelBundle is not bundled + 'playwright/src/transform/transform': path.resolve(__dirname, './src/mocks/transform.ts'), + 'playwright/src/transform/compilationCache': path.resolve(browserTestRuntime, './src/mocks/compilationCache.ts'), + 'playwright/src/common/testLoader': path.resolve(__dirname, './src/mocks/testLoader.ts'), + 'playwright-core/src/server/bidi/bidiOverCdp': path.resolve(browserTestRuntime, './src/mocks/empty.ts'), + }), + playwrightVendoredImports(), + ], resolve: { - alias: { - 'playwright-core/lib': path.resolve(__dirname, '../../submodules/playwright/packages/playwright-core/src'), - 'playwright/lib': path.resolve(__dirname, '../../submodules/playwright/packages/playwright/src'), - 'playwright-core': path.resolve(__dirname, './src/index'), - - // https://workers-nodejs-compat-matrix.pages.dev/ - 'async_hooks': 'node:async_hooks', - 'assert': 'node:assert', - 'buffer': 'node:buffer', - 'constants': 'node:constants', - 'crypto': 'node:crypto', - 'dns': 'node:dns', - 'events': 'node:events', - 'fs': 'node:fs', - 'http': 'node:http', - 'http2': 'node:http2', - 'https': 'node:https', - 'module': 'node:module', - 'net': 'node:net', - 'os': 'node:os', - 'path': 'node:path', - 'process': 'node:process', - 'stream': 'node:stream', - 'string_decoder': 'node:string_decoder', - 'tls': 'node:tls', - 'url': 'node:url', - 'util': 'node:util', - 'zlib': 'node:zlib', - - // bundles - './utilsBundleImpl': path.resolve(__dirname, '../browser-test-runtime/src/bundles/utilsBundleImpl'), - './zipBundleImpl': path.resolve(__dirname, '../browser-test-runtime/src/bundles/zipBundleImpl'), - './mcpBundleImpl': path.resolve(__dirname, '../browser-test-runtime/src/bundles/mcpBundleImpl'), - './expectBundleImpl': path.resolve(__dirname, '../browser-test-runtime/src/bundles/expectBundleImpl'), - 'pngjs': path.resolve(__dirname, '../browser-test-runtime/src/bundles/pngjs'), - - "child_process": path.resolve(browserTestRuntime, './src/mocks/childProcess'), - "node:child_process": path.resolve(browserTestRuntime, './src/mocks/childProcess'), - "readline": path.resolve(browserTestRuntime, './src/mocks/readline'), - "node:readline": path.resolve(browserTestRuntime, './src/mocks/readline'), - "inspector": path.resolve(browserTestRuntime, './src/mocks/inspector'), - "node:inspector": path.resolve(browserTestRuntime, './src/mocks/inspector'), - - // replace playwright transport with cloudflare workers transport - './transport': path.resolve(__dirname, './src/cloudflare/webSocketTransport'), - '../transport': path.resolve(__dirname, './src/cloudflare/webSocketTransport'), - - // cloudflare-specific modules added by patch - '@playwright-cloudflare': path.resolve(__dirname, './src'), - - // It's not needed and this way we don't need to build and import utilsBundleImpl and babelBundleImpl - './transform': path.resolve(__dirname, './src/mocks/transform'), - '../transform/transform': path.resolve(__dirname, './src/mocks/transform'), - - '../transform/compilationCache': path.resolve(browserTestRuntime, './src/mocks/compilationCache'), - '../common/testLoader': path.resolve(__dirname, './src/mocks/testLoader'), - '../common/esmLoaderHost': path.resolve(browserTestRuntime, './src/mocks/esmLoaderHost'), - './esmLoaderHost': path.resolve(browserTestRuntime, './src/mocks/esmLoaderHost'), - - // IMPORTANT `require('../playwright')` in `recorderApp.ts` causes a circular dependency, - // so we need to mock it (it's not needed, it's related with recorder). - '../playwright': path.resolve(browserTestRuntime, './src/mocks/empty'), - './bidiOverCdp': path.resolve(browserTestRuntime, './src/mocks/empty'), - 'electron/index.js': path.resolve(browserTestRuntime, './src/mocks/empty'), - }, + alias: [ + playwrightCoreBundleAlias, + ...Object.entries(aliases).map(([find, replacement]) => ({ find, replacement })), + ...playwrightPathAliases, + ...playwrightBundleAliases, + ], }, define: { '__dirname': `'${baseDir}'`, @@ -182,6 +192,7 @@ export default defineConfig({ 'node:url', 'node:util', 'node:zlib', + /^node:/, 'cloudflare:workers', ] }, @@ -194,6 +205,9 @@ export default defineConfig({ include: [ path.resolve(__dirname, '../../submodules/playwright/packages/playwright-core/src/**/*'), path.resolve(__dirname, '../../submodules/playwright/packages/playwright/src/**/*'), + path.resolve(__dirname, '../../submodules/playwright/packages/utils/**/*'), + path.resolve(__dirname, '../../submodules/playwright/packages/isomorphic/**/*'), + path.resolve(__dirname, '../../submodules/playwright/packages/protocol/src/**/*'), /node_modules/, ], } diff --git a/patches/playwright.patch b/patches/playwright.patch index 889f8e77a..71034c7bb 100644 --- a/patches/playwright.patch +++ b/patches/playwright.patch @@ -1,8 +1,8 @@ diff --git a/packages/playwright-core/src/client/browser.ts b/packages/playwright-core/src/client/browser.ts -index f4b803891..457006a6a 100644 +index 368e518bb..c198ff67d 100644 --- a/packages/playwright-core/src/client/browser.ts +++ b/packages/playwright-core/src/client/browser.ts -@@ -55,7 +55,13 @@ export class Browser extends ChannelOwner implements ap +@@ -60,7 +60,13 @@ export class Browser extends ChannelOwner implements ap return this._browserType; } @@ -18,36 +18,36 @@ index f4b803891..457006a6a 100644 } diff --git a/packages/playwright-core/src/client/channelOwner.ts b/packages/playwright-core/src/client/channelOwner.ts -index 7bd097b3f..33af318f8 100644 +index f6ed9d166..07faa7d31 100644 --- a/packages/playwright-core/src/client/channelOwner.ts +++ b/packages/playwright-core/src/client/channelOwner.ts -@@ -19,6 +19,7 @@ import { ValidationError, maybeFindValidator } from '../protocol/validator'; - import { methodMetainfo } from '../utils/isomorphic/protocolMetainfo'; +@@ -22,6 +22,7 @@ import { currentZone } from '@utils/zones'; + import { ValidationError, maybeFindValidator } from '@protocol/validator'; + import { EventEmitter } from './eventEmitter'; import { captureLibraryStackTrace } from './clientStackTrace'; - import { stringifyStackFrames } from '../utils/isomorphic/stackTrace'; +import { apiCallZone } from '@playwright-cloudflare/cloudflare/apiCallZone'; import type { ClientInstrumentation } from './clientInstrumentation'; import type { Connection } from './connection'; -@@ -179,8 +180,10 @@ export abstract class ChannelOwner await func(apiZone)); + const result = await currentZone().with('apiZone', apiZone).run(async () => await func(apiZone)); diff --git a/packages/playwright-core/src/client/clientHelper.ts b/packages/playwright-core/src/client/clientHelper.ts -index 6c9b8aad1..9233f26ce 100644 +index 89037e342..2b2cb7ce3 100644 --- a/packages/playwright-core/src/client/clientHelper.ts +++ b/packages/playwright-core/src/client/clientHelper.ts -@@ -30,7 +30,9 @@ export function envObjectToArray(env: NodeJS.ProcessEnv): { name: string, value: +@@ -32,7 +32,9 @@ export function envObjectToArray(env: NodeJS.ProcessEnv): { name: string, value: - export async function evaluationScript(platform: Platform, fun: Function | string | { path?: string, content?: string }, arg?: any, addSourceUrl: boolean = true): Promise { + export async function evaluationScript(fun: Function | string | { path?: string, content?: string }, arg?: any, addSourceUrl: boolean = true): Promise { if (typeof fun === 'function') { - const source = fun.toString(); + // function is most likely bundled with wrangler, which uses esbuild with keepNames enabled. @@ -56,38 +56,81 @@ index 6c9b8aad1..9233f26ce 100644 const argString = Object.is(arg, undefined) ? 'undefined' : JSON.stringify(arg); return `(${source})(${argString})`; } +@@ -64,7 +66,10 @@ export async function initScriptSourceWithExposedFunctions(fun: Function, arg: a + await Promise.all(exposePromises); + // Bindings backing the functions are registered through their own init scripts + // that are guaranteed to run first, so the controller is available here. +- return `(${fun.toString()})(globalThis['${kBindingsControllerProperty}'].parseInitScriptArg(${JSON.stringify(serialized)}))`; ++ // function is most likely bundled with wrangler, which uses esbuild with keepNames enabled. ++ // See: https://github.com/cloudflare/workers-sdk/issues/7107 ++ const source = `((__name => (${fun.toString()}))(t => t))`; ++ return `(${source})(globalThis['${kBindingsControllerProperty}'].parseInitScriptArg(${JSON.stringify(serialized)}))`; + } + + export function addSourceUrlToScript(source: string, path: string): string { +diff --git a/packages/playwright-core/src/package.ts b/packages/playwright-core/src/package.ts +index 180f16392..2e2bbee9b 100644 +--- a/packages/playwright-core/src/package.ts ++++ b/packages/playwright-core/src/package.ts +@@ -19,7 +19,8 @@ import path from 'path'; + // Use a dynamic path so esbuild does not statically resolve and inline + // package.json into coreBundle.js. + export const packageRoot = path.join(__dirname, '..'); +-export const packageJSON = require(path.join(packageRoot, 'package.json')); ++// PATCH static require, so the bundler can include package.json (Workers cannot read it at runtime) ++export const packageJSON = require('../package.json'); + export const binPath = path.join(packageRoot, 'bin'); + + export function libPath(...parts: string[]): string { diff --git a/packages/playwright-core/src/server/chromium/chromium.ts b/packages/playwright-core/src/server/chromium/chromium.ts -index bcc69d40f..ec9f63d44 100644 +index 68d0a9096..57671e7f5 100644 --- a/packages/playwright-core/src/server/chromium/chromium.ts +++ b/packages/playwright-core/src/server/chromium/chromium.ts -@@ -108,7 +108,9 @@ export class Chromium extends BrowserType { - chromeTransport = await WebSocketTransport.connect(progress, wsEndpoint, { headers: headersMap }); +@@ -112,10 +112,12 @@ export class Chromium extends BrowserType { + throw e; + } + const closeAndWait = async () => await chromeTransport.closeAndWait(); +- return this._connectOverCDPImpl(progress, chromeTransport, closeAndWait, options, onClose); ++ // Only connections with ?persistent in the endpoint URL get a persistent default context. ++ const persistent = new URL(endpointURL).searchParams.has('persistent'); ++ return this._connectOverCDPImpl(progress, chromeTransport, closeAndWait, { ...options, persistent }, onClose); + } + +- private async _connectOverCDPImpl(progress: Progress, transport: ConnectionTransport, closeAndWait: () => Promise, options: types.LaunchOptions & { isLocal?: boolean, noDefaults?: boolean, artifactsDir?: string }, onClose?: () => Promise) { ++ private async _connectOverCDPImpl(progress: Progress, transport: ConnectionTransport, closeAndWait: () => Promise, options: types.LaunchOptions & { isLocal?: boolean, noDefaults?: boolean, artifactsDir?: string, persistent?: boolean }, onClose?: () => Promise) { + let artifactsDir: string; + const tempDirectories: string[] = []; + if (options.artifactsDir) { +@@ -138,8 +140,10 @@ export class Chromium extends BrowserType { + try { const browserProcess: BrowserProcess = { close: doClose, kill: doClose }; -- const persistent: types.BrowserContextOptions = { noDefaultViewport: true }; +- const persistent: types.BrowserContextOptions = { +- noDefaultViewport: true, + // in cloudflare workers, noDefaultViewport will set height to 1px, which is not what we want + const noDefaultViewport = globalThis.navigator?.userAgent !== 'Cloudflare-Workers'; -+ const persistent = new URL(endpointURL).searchParams.has('persistent') ? { noDefaultViewport } : undefined; ++ const persistent: types.BrowserContextOptions | undefined = options.persistent === false ? undefined : { ++ noDefaultViewport, + ...(options.noDefaults ? { acceptDownloads: 'internal-browser-default' as const } : {}), + }; const browserOptions: BrowserOptions = { - slowMo: options.slowMo, - name: 'chromium', -@@ -122,7 +124,8 @@ export class Chromium extends BrowserType { - tracesDir: options.tracesDir || artifactsDir, +@@ -156,7 +160,8 @@ export class Chromium extends BrowserType { originalLaunchOptions: {}, + noDefaults: options.noDefaults, }; - validateBrowserContextOptions(persistent, browserOptions); + if (persistent) + validateBrowserContextOptions(persistent, browserOptions); - const browser = await progress.race(CRBrowser.connect(this.attribution.playwright, chromeTransport, browserOptions)); + const browser = await progress.race(CRBrowser.connect(this.attribution.playwright, transport, browserOptions)); if (!options.isLocal) - browser._isCollocatedWithServer = false; + browser._isBrowserCollocatedWithServer = false; diff --git a/packages/playwright-core/src/server/chromium/crBrowser.ts b/packages/playwright-core/src/server/chromium/crBrowser.ts -index 83cb12d2e..1ba89f4aa 100644 +index ba563a4b5..03b9b8da2 100644 --- a/packages/playwright-core/src/server/chromium/crBrowser.ts +++ b/packages/playwright-core/src/server/chromium/crBrowser.ts -@@ -557,6 +557,9 @@ export class CRBrowserContext extends BrowserContext { - return; - } +@@ -562,6 +562,9 @@ export class CRBrowserContext extends BrowserContext { + // Ongoing downloads cause crashes in Edge, so cancel them first. + await Promise.all([...this._downloads].map(download => download.cancel().catch(() => {}))); + // PATCH ensure all pages are closed before closing the context + await Promise.all(this._crPages().map(page => page.closePage(true))).catch(() => {}); @@ -96,7 +139,7 @@ index 83cb12d2e..1ba89f4aa 100644 this._browser._contexts.delete(this._browserContextId); for (const [targetId, serviceWorker] of this._browser._serviceWorkers) { diff --git a/packages/playwright-core/src/server/chromium/crDragDrop.ts b/packages/playwright-core/src/server/chromium/crDragDrop.ts -index 893968917..db81281b3 100644 +index e104741a9..453e60721 100644 --- a/packages/playwright-core/src/server/chromium/crDragDrop.ts +++ b/packages/playwright-core/src/server/chromium/crDragDrop.ts @@ -93,7 +93,8 @@ export class DragManager { @@ -107,13 +150,13 @@ index 893968917..db81281b3 100644 + const scriptSource = `((__name => (${setupDragListeners.toString()}))(t => t))`; + await progress.race(this._crPage._page.safeNonStallingEvaluateInAllFrames(`(${scriptSource})()`, 'utility')); client.on('Input.dragIntercepted', onDragIntercepted!); - await client.send('Input.setInterceptDrags', { enabled: true }); + await progress.race(client.send('Input.setInterceptDrags', { enabled: true })); try { diff --git a/packages/playwright-core/src/server/chromium/crNetworkManager.ts b/packages/playwright-core/src/server/chromium/crNetworkManager.ts -index f6d338470..d3a1f0346 100644 +index 1c720b582..8b5523403 100644 --- a/packages/playwright-core/src/server/chromium/crNetworkManager.ts +++ b/packages/playwright-core/src/server/chromium/crNetworkManager.ts -@@ -531,6 +531,15 @@ export class CRNetworkManager { +@@ -515,6 +515,15 @@ export class CRNetworkManager { return; this._maybeUpdateRequestSession(sessionInfo, request); const response = request.request._existingResponse(); @@ -130,10 +173,10 @@ index f6d338470..d3a1f0346 100644 response.setTransferSize(null); response.setEncodedBodySize(null); diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts -index 2cb3df967..ab3207f8a 100644 +index b462b0b22..cefe2e862 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts -@@ -552,6 +552,10 @@ class FrameSession { +@@ -566,6 +566,10 @@ class FrameSession { const response = await this._client.send('Page.navigate', { url, referrer, frameId: frame._id, referrerPolicy: 'unsafeUrl' }); if (response.isDownload) throw new frames.NavigationAbortedError(response.loaderId, 'Download is starting'); @@ -144,50 +187,11 @@ index 2cb3df967..ab3207f8a 100644 if (response.errorText) throw new frames.NavigationAbortedError(response.loaderId, `${response.errorText} at ${url}`); return { newDocumentId: response.loaderId }; -diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts -index 22f65daf0..38976e072 100644 ---- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts -+++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts -@@ -27,7 +27,8 @@ import { RouteDispatcher, WebSocketDispatcher } from './networkDispatchers'; - import { WebSocketRouteDispatcher } from './webSocketRouteDispatcher'; - import { SdkObject } from '../instrumentation'; - import { urlMatches } from '../../utils/isomorphic/urlMatch'; --import { PageAgentDispatcher } from './pageAgentDispatcher'; -+// TODO enable in the future -+// import { PageAgentDispatcher } from './pageAgentDispatcher'; - - import type { Artifact } from '../artifact'; - import type { BrowserContext } from '../browserContext'; -@@ -362,7 +363,8 @@ export class PageDispatcher extends Dispatcher { -- return { agent: new PageAgentDispatcher(this, params) }; -+ // return { agent: new PageAgentDispatcher(this, params) }; -+ throw new Error('Agent not implemented'); - } - - _onFrameAttached(frame: Frame) { -diff --git a/packages/playwright-core/src/server/har/harRecorder.ts b/packages/playwright-core/src/server/har/harRecorder.ts -index 7d99a3779..fefdcd98e 100644 ---- a/packages/playwright-core/src/server/har/harRecorder.ts -+++ b/packages/playwright-core/src/server/har/harRecorder.ts -@@ -86,7 +86,9 @@ export class HarRecorder implements HarTracerDelegate { - (this._zipFile as unknown as EventEmitter).on('error', error => result.reject(error)); - this._zipFile.addBuffer(Buffer.from(harFileContent, 'utf-8'), 'har.har'); - this._zipFile.end(); -- this._zipFile.outputStream.pipe(fs.createWriteStream(this._artifact.localPath())).on('close', () => { -+ const chunks: Buffer[] = []; -+ this._zipFile.outputStream.on('data', data => chunks.push(data)).on('close', () => { -+ fs.writeFileSync(this._artifact.localPath(), Buffer.concat(chunks)); - result.resolve(); - }); - await result; diff --git a/packages/playwright-core/src/server/javascript.ts b/packages/playwright-core/src/server/javascript.ts -index bc9261b8d..fe61e610f 100644 +index 17aaf1683..b60703b6d 100644 --- a/packages/playwright-core/src/server/javascript.ts +++ b/packages/playwright-core/src/server/javascript.ts -@@ -285,24 +285,77 @@ export function parseUnserializableValue(unserializableValue: string): any { +@@ -303,24 +303,77 @@ export function parseUnserializableValue(unserializableValue: string): any { return -0; } @@ -279,10 +283,10 @@ index bc9261b8d..fe61e610f 100644 } } diff --git a/packages/playwright-core/src/server/localUtils.ts b/packages/playwright-core/src/server/localUtils.ts -index 685a0f64f..cea0c1018 100644 +index c2b36758a..469aba38b 100644 --- a/packages/playwright-core/src/server/localUtils.ts +++ b/packages/playwright-core/src/server/localUtils.ts -@@ -82,10 +82,15 @@ export async function zip(progress: Progress, stackSessions: Map { @@ -296,28 +300,40 @@ index 685a0f64f..cea0c1018 100644 + promise.resolve(); + }) .on('error', error => promise.reject(error)); -- }); -+ }); + }); await progress.race(promise); - await deleteStackSession(progress, stackSessions, params.stacksId); - return; -@@ -111,8 +116,10 @@ export async function zip(progress: Progress, stackSessions: Map { -- zipFile.outputStream.pipe(fs.createWriteStream(params.zipFile)).on('close', () => { -+ const chunks: Buffer[] = []; -+ zipFile.outputStream.on('data', data => chunks.push(data)).on('close', () => { +@@ -106,9 +111,12 @@ export async function zip(progress: Progress, stackSessions: Map { + zipFile.end(undefined, () => { +- zipFile.outputStream.pipe(fs.createWriteStream(params.zipFile)) ++ const chunks: Buffer[] = []; ++ zipFile.outputStream ++ .on('data', data => chunks.push(data)) + .on('close', () => { fs.promises.unlink(tempFile).then(() => { + fs.writeFileSync(params.zipFile, Buffer.concat(chunks)); promise.resolve(); }).catch(error => promise.reject(error)); - }); + }) +diff --git a/packages/playwright-core/src/server/registry/index.ts b/packages/playwright-core/src/server/registry/index.ts +index 2b6e0141a..7ef2a581c 100644 +--- a/packages/playwright-core/src/server/registry/index.ts ++++ b/packages/playwright-core/src/server/registry/index.ts +@@ -1568,6 +1568,7 @@ function lowercaseAllKeys(json: any): any { + return result; + } + +-export const registry = new Registry(require(path.join(packageRoot, 'browsers.json'))); ++// PATCH static require, so the bundler can include browsers.json (Workers cannot read it at runtime) ++export const registry = new Registry(require('../../../browsers.json')); + + export { runOopDownloadBrowserMain } from './oopDownloadBrowserMain'; diff --git a/packages/playwright-core/src/server/screenshotter.ts b/packages/playwright-core/src/server/screenshotter.ts -index 93765cc33..f8e62d5c1 100644 +index d96c32c5f..d9561092e 100644 --- a/packages/playwright-core/src/server/screenshotter.ts +++ b/packages/playwright-core/src/server/screenshotter.ts -@@ -254,7 +254,8 @@ export class Screenshotter { +@@ -252,7 +252,8 @@ export class Screenshotter { if (disableAnimations) progress.log(' disabled all CSS animations'); const syncAnimations = this._page.delegate.shouldToggleStyleSheetToSyncAnimations(); @@ -328,25 +344,53 @@ index 93765cc33..f8e62d5c1 100644 if (!process.env.PW_TEST_SCREENSHOT_NO_FONTS_READY) { progress.log('waiting for fonts to load...'); diff --git a/packages/playwright-core/src/server/trace/recorder/snapshotter.ts b/packages/playwright-core/src/server/trace/recorder/snapshotter.ts -index 9b5543905..6aa2a56ef 100644 +index 90c2f3432..88181532a 100644 --- a/packages/playwright-core/src/server/trace/recorder/snapshotter.ts +++ b/packages/playwright-core/src/server/trace/recorder/snapshotter.ts -@@ -91,7 +91,8 @@ export class Snapshotter { +@@ -94,7 +94,8 @@ export class Snapshotter { ]; const { javaScriptEnabled } = this._context._options; - const initScriptSource = `(${frameSnapshotStreamer})("${this._snapshotStreamer}", ${javaScriptEnabled || javaScriptEnabled === undefined})`; + const initScript = `((__name => (${frameSnapshotStreamer}))(t => t))`; + const initScriptSource = `(${initScript})("${this._snapshotStreamer}", ${javaScriptEnabled || javaScriptEnabled === undefined})`; - this._initScript = await this._context.addInitScript(undefined, initScriptSource); - await this._context.safeNonStallingEvaluateInAllFrames(initScriptSource, 'main'); + this._initScript = await this._context.addInitScript(progress, initScriptSource); + await progress.race(this._context.safeNonStallingEvaluateInAllFrames(initScriptSource, 'main')); } -diff --git a/packages/playwright-core/src/server/utils/crypto.ts b/packages/playwright-core/src/server/utils/crypto.ts -index ab51dbb25..2cf1fe999 100644 ---- a/packages/playwright-core/src/server/utils/crypto.ts -+++ b/packages/playwright-core/src/server/utils/crypto.ts +diff --git a/packages/playwright/src/common/configLoader.ts b/packages/playwright/src/common/configLoader.ts +index f9ec65451..6ba9d2c91 100644 +--- a/packages/playwright/src/common/configLoader.ts ++++ b/packages/playwright/src/common/configLoader.ts +@@ -118,7 +118,8 @@ export async function loadConfig(location: ConfigLocation, overrides?: ConfigCLI + // 3. Load transform options from the playwright config. + const babelPlugins = (userConfig as any)['@playwright/test']?.babelPlugins || []; + const external = userConfig.build?.external || []; +- const jsxImportSource = path.dirname(require.resolve('playwright')); ++ // PATCH require.resolve is not available in Cloudflare Workers, where the transform is not used ++ const jsxImportSource = typeof require.resolve === 'function' ? path.dirname(require.resolve('playwright')) : undefined; + await setTransformConfig({ babelPlugins, external, jsxImportSource }); + if (!overrides?.tsconfig) + await setSingleTSConfig(fullConfig?.singleTSConfigPath); +diff --git a/packages/playwright/src/package.ts b/packages/playwright/src/package.ts +index fdd2c47f2..4b2ba3f87 100644 +--- a/packages/playwright/src/package.ts ++++ b/packages/playwright/src/package.ts +@@ -19,7 +19,8 @@ import path from 'path'; + // Use a dynamic path so esbuild does not statically resolve and inline + // package.json into coreBundle.js. + export const packageRoot = path.join(__dirname, '..'); +-export const packageJSON = require(path.join(packageRoot, 'package.json')); ++// PATCH static require, so the bundler can include package.json (Workers cannot read it at runtime) ++export const packageJSON = require('../package.json'); + + export function libPath(...parts: string[]): string { + return path.join(packageRoot, 'lib', ...parts); +diff --git a/packages/utils/crypto.ts b/packages/utils/crypto.ts +index c5ed49e8a..ca1c42a8d 100644 +--- a/packages/utils/crypto.ts ++++ b/packages/utils/crypto.ts @@ -19,7 +19,13 @@ import crypto from 'crypto'; - import { assert } from '../../utils/isomorphic/assert'; + import { assert } from '@isomorphic/assert'; export function createGuid(): string { - return crypto.randomBytes(16).toString('hex'); @@ -360,11 +404,11 @@ index ab51dbb25..2cf1fe999 100644 } export function calculateSha1(buffer: Buffer | string): string { -diff --git a/packages/playwright-core/src/server/utils/fileUtils.ts b/packages/playwright-core/src/server/utils/fileUtils.ts -index 3eab2c036..fab20db31 100644 ---- a/packages/playwright-core/src/server/utils/fileUtils.ts -+++ b/packages/playwright-core/src/server/utils/fileUtils.ts -@@ -193,9 +193,13 @@ export class SerializedFS { +diff --git a/packages/utils/serializedFS.ts b/packages/utils/serializedFS.ts +index 994ebf22e..bec9e2908 100644 +--- a/packages/utils/serializedFS.ts ++++ b/packages/utils/serializedFS.ts +@@ -165,9 +165,13 @@ export class SerializedFS { for (const entry of op.entries) zipFile.addFile(entry.value, entry.name); zipFile.end(); @@ -380,54 +424,3 @@ index 3eab2c036..fab20db31 100644 .on('error', error => result.reject(error)); await result; return; -diff --git a/packages/playwright-core/src/server/utils/nodePlatform.ts b/packages/playwright-core/src/server/utils/nodePlatform.ts -index 9350b5797..634b126c2 100644 ---- a/packages/playwright-core/src/server/utils/nodePlatform.ts -+++ b/packages/playwright-core/src/server/utils/nodePlatform.ts -@@ -25,7 +25,7 @@ import { colors } from '../../utilsBundle'; - import { debugLogger } from './debugLogger'; - import { currentZone, emptyZone } from './zones'; - import { debugMode, isUnderTest } from './debug'; --import { zodToJsonSchema as zodToJsonSchemaV3, z } from '../../mcpBundle'; -+// import { zodToJsonSchema as zodToJsonSchemaV3, z } from '../../mcpBundle'; - import type zod3 from 'zod/v3'; - import type zod4 from 'zod'; - -@@ -64,7 +64,7 @@ export function setBoxedStackPrefixes(prefixes: string[]) { - boxedStackPrefixes = prefixes; - } - --const coreDir = path.dirname(require.resolve('../../../package.json')); -+const coreDir = path.dirname('.'); - - export const nodePlatform: Platform = { - name: 'node', -@@ -128,9 +128,10 @@ export const nodePlatform: Platform = { - - zodToJsonSchema: (schema: zod3.Schema | zod4.Schema): any => { - // https://zod.dev/library-authors?id=how-to-support-zod-3-and-zod-4-simultaneously -- if ('_zod' in schema) -- return z.toJSONSchema(schema); -- return zodToJsonSchemaV3(schema); -+ // if ('_zod' in schema) -+ // return z.toJSONSchema(schema); -+ // return zodToJsonSchemaV3(schema); -+ throw new Error('Not implemented'); - }, - - zones: { -diff --git a/packages/playwright/src/util.ts b/packages/playwright/src/util.ts -index bb15fecee..47c2b9f08 100644 ---- a/packages/playwright/src/util.ts -+++ b/packages/playwright/src/util.ts -@@ -28,8 +28,8 @@ import type { StackFrame } from '@protocol/channels'; - import type { RawStack } from 'playwright-core/lib/utils'; - import type { TestCase } from './common/test'; - --const PLAYWRIGHT_TEST_PATH = path.join(__dirname, '..'); --const PLAYWRIGHT_CORE_PATH = path.dirname(require.resolve('playwright-core/package.json')); -+const PLAYWRIGHT_TEST_PATH = '.'; -+const PLAYWRIGHT_CORE_PATH = '.'; - - export function filterStackTrace(e: Error): { message: string, stack: string, cause?: ReturnType } { - const name = e.name ? e.name + ': ' : ''; diff --git a/submodules/jest b/submodules/jest index 855864e3f..b3b4a09ed 160000 --- a/submodules/jest +++ b/submodules/jest @@ -1 +1 @@ -Subproject commit 855864e3f9751366455246790be2bf912d4d0dac +Subproject commit b3b4a09ed3005369dacc7466d1d2122797283785 diff --git a/submodules/playwright b/submodules/playwright index ce480a952..26a9e470a 160000 --- a/submodules/playwright +++ b/submodules/playwright @@ -1 +1 @@ -Subproject commit ce480a952553175eae75342aad2c5e86cdf2cbba +Subproject commit 26a9e470a7b3c7822084b09fb7f13902c5f37b51