Compare commits

...

15 commits

Author SHA1 Message Date
Max Schmitt a4096676ab
chore: mark 1.41.2 (#29293) 2024-02-01 20:24:16 +01:00
Playwright Service 414affaed4
cherry-pick(#29271): Revert "chore: remove fake error from expect calls (#28112)" (#29296)
This PR cherry-picks the following commits:

- 622153db18

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2024-02-01 18:11:50 +01:00
Yury Semikhatsky 7a72adc33c
cherry-pick(#29180): fix: interception id not found error in route.co… (#29222)
…ntinue

We stopped catching all exceptions in
https://github.com/microsoft/playwright/pull/28539 in hope that we'll
get loadingFailed even before Fetch.continue/fulfill command's error.
Turns out this is racy and may fail if the test cancels the request
while we are continuing it. The following test could in theory reproduce
it if stars align and the timing is good:

```js
it('page.continue on canceled request', async ({ page }) => {
  let resolveRoute;
  const routePromise = new Promise<Route>(f => resolveRoute = f);
  await page.route('http://test.com/x', resolveRoute);

  const evalPromise = page.evaluate(async () => {
    const abortController = new AbortController();
    (window as any).abortController = abortController;
    return fetch('http://test.com/x', { signal: abortController.signal }).catch(e => 'cancelled');
  });
  const route = await routePromise;
  void page.evaluate(() => (window as any).abortController.abort());
  await new Promise(f => setTimeout(f, 10));
  await route.continue();
  const req = await evalPromise;
  expect(req).toBe('cancelled');
});
```

Fixes https://github.com/microsoft/playwright/issues/29123
2024-01-29 09:33:09 -08:00
Max Schmitt 8f0163f3f4
chore: mark 1.41.1 (#29075) 2024-01-20 00:05:04 +01:00
Playwright Service 98a661824d
cherry-pick(#29069): Revert "feat(codegen): add range input recording support (#28767)" (#29074)
This PR cherry-picks the following commits:

- e551506c9e

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2024-01-19 22:08:43 +01:00
Sander 50f1f08e9c cherry-pick(#29070): chore(ct): revert export mount result 2024-01-19 11:12:07 -08:00
Pavel Feldman e5d201b459 cherry-pick(#29031): fix(ct): allow passing date, url, bigint as properties 2024-01-18 11:44:08 -08:00
Pavel Feldman 8ee2d81143 cherry-pick(#29026): test: add a props.children test 2024-01-18 11:43:45 -08:00
Pavel Feldman 50a8e4f52a cherry-pick(#29016): chore: add an image import component test 2024-01-18 11:43:21 -08:00
Pavel Feldman cb6c64cc33 cherry-pick(#28986): fix(ct): move import list into the compilation cache data 2024-01-18 11:42:56 -08:00
Pavel Feldman 06518b2091 cherry-pick(#28978): chore: build import registry source 2024-01-18 11:42:04 -08:00
Pavel Feldman d47ed6a076 cherry-pick(#28975): chore: refactor import processing in ct 2024-01-18 11:41:24 -08:00
Playwright Service 4d9f923dfe
cherry-pick(#29034): docs: fix typo for stylePath (#29035)
This PR cherry-picks the following commits:

- a217d6a08d

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2024-01-18 10:06:32 +01:00
Dmitry Gozman ece2a97702 Revert "feat(trace): allow Trace Viewer to include credentials when fetching traces cross-origin (#28502)" (#29024)
This reverts commit 3f3f332060.

References #29019.
2024-01-17 10:41:44 -08:00
Max Schmitt 38d699f64f
chore: mark 1.41.0 (#29007) 2024-01-16 19:27:23 +01:00
54 changed files with 1050 additions and 1025 deletions

View file

@ -4,6 +4,7 @@ test/assets/modernizr.js
/packages/playwright-core/src/generated/* /packages/playwright-core/src/generated/*
/packages/playwright-core/src/third_party/ /packages/playwright-core/src/third_party/
/packages/playwright-core/types/* /packages/playwright-core/types/*
/packages/playwright-ct-core/src/generated/*
/index.d.ts /index.d.ts
utils/generate_types/overrides.d.ts utils/generate_types/overrides.d.ts
utils/generate_types/test/test.ts utils/generate_types/test/test.ts

3
.gitignore vendored
View file

@ -9,7 +9,8 @@ node_modules/
.vscode .vscode
.idea .idea
yarn.lock yarn.lock
/packages/playwright-core/src/generated/* /packages/playwright-core/src/generated
/packages/playwright-ct-core/src/generated
packages/*/lib/ packages/*/lib/
drivers/ drivers/
.android-sdk/ .android-sdk/

View file

@ -98,7 +98,7 @@ import { test, expect } from '@playwright/test';
test('example test', async ({ page }) => { test('example test', async ({ page }) => {
await page.goto('https://playwright.dev'); await page.goto('https://playwright.dev');
await expect(page).toHaveScreenshot({ styleFile: path.join(__dirname, 'screenshot.css') }); await expect(page).toHaveScreenshot({ stylePath: path.join(__dirname, 'screenshot.css') });
}); });
``` ```
@ -109,7 +109,7 @@ import { defineConfig } from '@playwright/test';
export default defineConfig({ export default defineConfig({
expect: { expect: {
toHaveScreenshot: { toHaveScreenshot: {
styleFile: './screenshot.css' stylePath: './screenshot.css'
}, },
}, },
}); });

100
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "playwright-internal", "name": "playwright-internal",
"version": "1.41.0-next", "version": "1.41.2",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "playwright-internal", "name": "playwright-internal",
"version": "1.41.0-next", "version": "1.41.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
@ -7530,10 +7530,10 @@
} }
}, },
"packages/playwright": { "packages/playwright": {
"version": "1.41.0-next", "version": "1.41.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.41.0-next" "playwright-core": "1.41.2"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -7547,11 +7547,11 @@
}, },
"packages/playwright-browser-chromium": { "packages/playwright-browser-chromium": {
"name": "@playwright/browser-chromium", "name": "@playwright/browser-chromium",
"version": "1.41.0-next", "version": "1.41.2",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.41.0-next" "playwright-core": "1.41.2"
}, },
"engines": { "engines": {
"node": ">=16" "node": ">=16"
@ -7559,11 +7559,11 @@
}, },
"packages/playwright-browser-firefox": { "packages/playwright-browser-firefox": {
"name": "@playwright/browser-firefox", "name": "@playwright/browser-firefox",
"version": "1.41.0-next", "version": "1.41.2",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.41.0-next" "playwright-core": "1.41.2"
}, },
"engines": { "engines": {
"node": ">=16" "node": ">=16"
@ -7571,22 +7571,22 @@
}, },
"packages/playwright-browser-webkit": { "packages/playwright-browser-webkit": {
"name": "@playwright/browser-webkit", "name": "@playwright/browser-webkit",
"version": "1.41.0-next", "version": "1.41.2",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.41.0-next" "playwright-core": "1.41.2"
}, },
"engines": { "engines": {
"node": ">=16" "node": ">=16"
} }
}, },
"packages/playwright-chromium": { "packages/playwright-chromium": {
"version": "1.41.0-next", "version": "1.41.2",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.41.0-next" "playwright-core": "1.41.2"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -7596,7 +7596,7 @@
} }
}, },
"packages/playwright-core": { "packages/playwright-core": {
"version": "1.41.0-next", "version": "1.41.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"playwright-core": "cli.js" "playwright-core": "cli.js"
@ -7607,11 +7607,11 @@
}, },
"packages/playwright-ct-core": { "packages/playwright-ct-core": {
"name": "@playwright/experimental-ct-core", "name": "@playwright/experimental-ct-core",
"version": "1.41.0-next", "version": "1.41.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright": "1.41.0-next", "playwright": "1.41.2",
"playwright-core": "1.41.0-next", "playwright-core": "1.41.2",
"vite": "^4.4.12" "vite": "^4.4.12"
}, },
"bin": { "bin": {
@ -7623,10 +7623,10 @@
}, },
"packages/playwright-ct-react": { "packages/playwright-ct-react": {
"name": "@playwright/experimental-ct-react", "name": "@playwright/experimental-ct-react",
"version": "1.41.0-next", "version": "1.41.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.41.0-next", "@playwright/experimental-ct-core": "1.41.2",
"@vitejs/plugin-react": "^4.0.0" "@vitejs/plugin-react": "^4.0.0"
}, },
"bin": { "bin": {
@ -7655,10 +7655,10 @@
}, },
"packages/playwright-ct-react17": { "packages/playwright-ct-react17": {
"name": "@playwright/experimental-ct-react17", "name": "@playwright/experimental-ct-react17",
"version": "1.41.0-next", "version": "1.41.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.41.0-next", "@playwright/experimental-ct-core": "1.41.2",
"@vitejs/plugin-react": "^4.0.0" "@vitejs/plugin-react": "^4.0.0"
}, },
"bin": { "bin": {
@ -7687,10 +7687,10 @@
}, },
"packages/playwright-ct-solid": { "packages/playwright-ct-solid": {
"name": "@playwright/experimental-ct-solid", "name": "@playwright/experimental-ct-solid",
"version": "1.41.0-next", "version": "1.41.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.41.0-next", "@playwright/experimental-ct-core": "1.41.2",
"vite-plugin-solid": "^2.7.0" "vite-plugin-solid": "^2.7.0"
}, },
"bin": { "bin": {
@ -7705,10 +7705,10 @@
}, },
"packages/playwright-ct-svelte": { "packages/playwright-ct-svelte": {
"name": "@playwright/experimental-ct-svelte", "name": "@playwright/experimental-ct-svelte",
"version": "1.41.0-next", "version": "1.41.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.41.0-next", "@playwright/experimental-ct-core": "1.41.2",
"@sveltejs/vite-plugin-svelte": "^3.0.1" "@sveltejs/vite-plugin-svelte": "^3.0.1"
}, },
"bin": { "bin": {
@ -7972,10 +7972,10 @@
}, },
"packages/playwright-ct-vue": { "packages/playwright-ct-vue": {
"name": "@playwright/experimental-ct-vue", "name": "@playwright/experimental-ct-vue",
"version": "1.41.0-next", "version": "1.41.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.41.0-next", "@playwright/experimental-ct-core": "1.41.2",
"@vitejs/plugin-vue": "^4.2.1" "@vitejs/plugin-vue": "^4.2.1"
}, },
"bin": { "bin": {
@ -8023,10 +8023,10 @@
}, },
"packages/playwright-ct-vue2": { "packages/playwright-ct-vue2": {
"name": "@playwright/experimental-ct-vue2", "name": "@playwright/experimental-ct-vue2",
"version": "1.41.0-next", "version": "1.41.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.41.0-next", "@playwright/experimental-ct-core": "1.41.2",
"@vitejs/plugin-vue2": "^2.2.0" "@vitejs/plugin-vue2": "^2.2.0"
}, },
"bin": { "bin": {
@ -8040,11 +8040,11 @@
} }
}, },
"packages/playwright-firefox": { "packages/playwright-firefox": {
"version": "1.41.0-next", "version": "1.41.2",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.41.0-next" "playwright-core": "1.41.2"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -8078,10 +8078,10 @@
}, },
"packages/playwright-test": { "packages/playwright-test": {
"name": "@playwright/test", "name": "@playwright/test",
"version": "1.41.0-next", "version": "1.41.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright": "1.41.0-next" "playwright": "1.41.2"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -8091,11 +8091,11 @@
} }
}, },
"packages/playwright-webkit": { "packages/playwright-webkit": {
"version": "1.41.0-next", "version": "1.41.2",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.41.0-next" "playwright-core": "1.41.2"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -8973,33 +8973,33 @@
"@playwright/browser-chromium": { "@playwright/browser-chromium": {
"version": "file:packages/playwright-browser-chromium", "version": "file:packages/playwright-browser-chromium",
"requires": { "requires": {
"playwright-core": "1.41.0-next" "playwright-core": "1.41.2"
} }
}, },
"@playwright/browser-firefox": { "@playwright/browser-firefox": {
"version": "file:packages/playwright-browser-firefox", "version": "file:packages/playwright-browser-firefox",
"requires": { "requires": {
"playwright-core": "1.41.0-next" "playwright-core": "1.41.2"
} }
}, },
"@playwright/browser-webkit": { "@playwright/browser-webkit": {
"version": "file:packages/playwright-browser-webkit", "version": "file:packages/playwright-browser-webkit",
"requires": { "requires": {
"playwright-core": "1.41.0-next" "playwright-core": "1.41.2"
} }
}, },
"@playwright/experimental-ct-core": { "@playwright/experimental-ct-core": {
"version": "file:packages/playwright-ct-core", "version": "file:packages/playwright-ct-core",
"requires": { "requires": {
"playwright": "1.41.0-next", "playwright": "1.41.2",
"playwright-core": "1.41.0-next", "playwright-core": "1.41.2",
"vite": "^4.4.12" "vite": "^4.4.12"
} }
}, },
"@playwright/experimental-ct-react": { "@playwright/experimental-ct-react": {
"version": "file:packages/playwright-ct-react", "version": "file:packages/playwright-ct-react",
"requires": { "requires": {
"@playwright/experimental-ct-core": "1.41.0-next", "@playwright/experimental-ct-core": "1.41.2",
"@vitejs/plugin-react": "^4.0.0" "@vitejs/plugin-react": "^4.0.0"
}, },
"dependencies": { "dependencies": {
@ -9019,7 +9019,7 @@
"@playwright/experimental-ct-react17": { "@playwright/experimental-ct-react17": {
"version": "file:packages/playwright-ct-react17", "version": "file:packages/playwright-ct-react17",
"requires": { "requires": {
"@playwright/experimental-ct-core": "1.41.0-next", "@playwright/experimental-ct-core": "1.41.2",
"@vitejs/plugin-react": "^4.0.0" "@vitejs/plugin-react": "^4.0.0"
}, },
"dependencies": { "dependencies": {
@ -9039,7 +9039,7 @@
"@playwright/experimental-ct-solid": { "@playwright/experimental-ct-solid": {
"version": "file:packages/playwright-ct-solid", "version": "file:packages/playwright-ct-solid",
"requires": { "requires": {
"@playwright/experimental-ct-core": "1.41.0-next", "@playwright/experimental-ct-core": "1.41.2",
"solid-js": "^1.7.0", "solid-js": "^1.7.0",
"vite-plugin-solid": "^2.7.0" "vite-plugin-solid": "^2.7.0"
} }
@ -9047,7 +9047,7 @@
"@playwright/experimental-ct-svelte": { "@playwright/experimental-ct-svelte": {
"version": "file:packages/playwright-ct-svelte", "version": "file:packages/playwright-ct-svelte",
"requires": { "requires": {
"@playwright/experimental-ct-core": "1.41.0-next", "@playwright/experimental-ct-core": "1.41.2",
"@sveltejs/vite-plugin-svelte": "^3.0.1", "@sveltejs/vite-plugin-svelte": "^3.0.1",
"svelte": "^4.2.8" "svelte": "^4.2.8"
}, },
@ -9203,7 +9203,7 @@
"@playwright/experimental-ct-vue": { "@playwright/experimental-ct-vue": {
"version": "file:packages/playwright-ct-vue", "version": "file:packages/playwright-ct-vue",
"requires": { "requires": {
"@playwright/experimental-ct-core": "1.41.0-next", "@playwright/experimental-ct-core": "1.41.2",
"@vitejs/plugin-vue": "^4.2.1" "@vitejs/plugin-vue": "^4.2.1"
}, },
"dependencies": { "dependencies": {
@ -9237,7 +9237,7 @@
"@playwright/experimental-ct-vue2": { "@playwright/experimental-ct-vue2": {
"version": "file:packages/playwright-ct-vue2", "version": "file:packages/playwright-ct-vue2",
"requires": { "requires": {
"@playwright/experimental-ct-core": "1.41.0-next", "@playwright/experimental-ct-core": "1.41.2",
"@vitejs/plugin-vue2": "^2.2.0", "@vitejs/plugin-vue2": "^2.2.0",
"vue": "^2.7.14" "vue": "^2.7.14"
} }
@ -9245,7 +9245,7 @@
"@playwright/test": { "@playwright/test": {
"version": "file:packages/playwright-test", "version": "file:packages/playwright-test",
"requires": { "requires": {
"playwright": "1.41.0-next" "playwright": "1.41.2"
} }
}, },
"@rollup/rollup-android-arm-eabi": { "@rollup/rollup-android-arm-eabi": {
@ -12246,13 +12246,13 @@
"version": "file:packages/playwright", "version": "file:packages/playwright",
"requires": { "requires": {
"fsevents": "2.3.2", "fsevents": "2.3.2",
"playwright-core": "1.41.0-next" "playwright-core": "1.41.2"
} }
}, },
"playwright-chromium": { "playwright-chromium": {
"version": "file:packages/playwright-chromium", "version": "file:packages/playwright-chromium",
"requires": { "requires": {
"playwright-core": "1.41.0-next" "playwright-core": "1.41.2"
} }
}, },
"playwright-core": { "playwright-core": {
@ -12261,13 +12261,13 @@
"playwright-firefox": { "playwright-firefox": {
"version": "file:packages/playwright-firefox", "version": "file:packages/playwright-firefox",
"requires": { "requires": {
"playwright-core": "1.41.0-next" "playwright-core": "1.41.2"
} }
}, },
"playwright-webkit": { "playwright-webkit": {
"version": "file:packages/playwright-webkit", "version": "file:packages/playwright-webkit",
"requires": { "requires": {
"playwright-core": "1.41.0-next" "playwright-core": "1.41.2"
} }
}, },
"postcss": { "postcss": {

View file

@ -1,7 +1,7 @@
{ {
"name": "playwright-internal", "name": "playwright-internal",
"private": true, "private": true,
"version": "1.41.0-next", "version": "1.41.2",
"description": "A high-level API to automate web browsers", "description": "A high-level API to automate web browsers",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/browser-chromium", "name": "@playwright/browser-chromium",
"version": "1.41.0-next", "version": "1.41.2",
"description": "Playwright package that automatically installs Chromium", "description": "Playwright package that automatically installs Chromium",
"repository": { "repository": {
"type": "git", "type": "git",
@ -27,6 +27,6 @@
"install": "node install.js" "install": "node install.js"
}, },
"dependencies": { "dependencies": {
"playwright-core": "1.41.0-next" "playwright-core": "1.41.2"
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/browser-firefox", "name": "@playwright/browser-firefox",
"version": "1.41.0-next", "version": "1.41.2",
"description": "Playwright package that automatically installs Firefox", "description": "Playwright package that automatically installs Firefox",
"repository": { "repository": {
"type": "git", "type": "git",
@ -27,6 +27,6 @@
"install": "node install.js" "install": "node install.js"
}, },
"dependencies": { "dependencies": {
"playwright-core": "1.41.0-next" "playwright-core": "1.41.2"
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/browser-webkit", "name": "@playwright/browser-webkit",
"version": "1.41.0-next", "version": "1.41.2",
"description": "Playwright package that automatically installs WebKit", "description": "Playwright package that automatically installs WebKit",
"repository": { "repository": {
"type": "git", "type": "git",
@ -27,6 +27,6 @@
"install": "node install.js" "install": "node install.js"
}, },
"dependencies": { "dependencies": {
"playwright-core": "1.41.0-next" "playwright-core": "1.41.2"
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "playwright-chromium", "name": "playwright-chromium",
"version": "1.41.0-next", "version": "1.41.2",
"description": "A high-level API to automate Chromium", "description": "A high-level API to automate Chromium",
"repository": { "repository": {
"type": "git", "type": "git",
@ -30,6 +30,6 @@
"install": "node install.js" "install": "node install.js"
}, },
"dependencies": { "dependencies": {
"playwright-core": "1.41.0-next" "playwright-core": "1.41.2"
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "playwright-core", "name": "playwright-core",
"version": "1.41.0-next", "version": "1.41.2",
"description": "A high-level API to automate web browsers", "description": "A high-level API to automate web browsers",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -28,6 +28,7 @@ import type * as types from '../types';
import type { CRPage } from './crPage'; import type { CRPage } from './crPage';
import { assert, headersObjectToArray } from '../../utils'; import { assert, headersObjectToArray } from '../../utils';
import type { CRServiceWorker } from './crServiceWorker'; import type { CRServiceWorker } from './crServiceWorker';
import { isProtocolError } from '../protocolError';
type SessionInfo = { type SessionInfo = {
session: CRSession; session: CRSession;
@ -571,13 +572,16 @@ class RouteImpl implements network.RouteDelegate {
method: overrides.method, method: overrides.method,
postData: overrides.postData ? overrides.postData.toString('base64') : undefined postData: overrides.postData ? overrides.postData.toString('base64') : undefined
}; };
await catchDisallowedErrors(async () => {
await this._session.send('Fetch.continueRequest', this._alreadyContinuedParams); await this._session.send('Fetch.continueRequest', this._alreadyContinuedParams);
});
} }
async fulfill(response: types.NormalizedFulfillResponse) { async fulfill(response: types.NormalizedFulfillResponse) {
const body = response.isBase64 ? response.body : Buffer.from(response.body).toString('base64'); const body = response.isBase64 ? response.body : Buffer.from(response.body).toString('base64');
const responseHeaders = splitSetCookieHeader(response.headers); const responseHeaders = splitSetCookieHeader(response.headers);
await catchDisallowedErrors(async () => {
await this._session.send('Fetch.fulfillRequest', { await this._session.send('Fetch.fulfillRequest', {
requestId: this._interceptionId!, requestId: this._interceptionId!,
responseCode: response.status, responseCode: response.status,
@ -585,20 +589,33 @@ class RouteImpl implements network.RouteDelegate {
responseHeaders, responseHeaders,
body, body,
}); });
});
} }
async abort(errorCode: string = 'failed') { async abort(errorCode: string = 'failed') {
const errorReason = errorReasons[errorCode]; const errorReason = errorReasons[errorCode];
assert(errorReason, 'Unknown error code: ' + errorCode); assert(errorReason, 'Unknown error code: ' + errorCode);
// In certain cases, protocol will return error if the request was already canceled await catchDisallowedErrors(async () => {
// or the page was closed. We should tolerate these errors. await this._session.send('Fetch.failRequest', {
await this._session._sendMayFail('Fetch.failRequest', {
requestId: this._interceptionId!, requestId: this._interceptionId!,
errorReason errorReason
}); });
});
} }
} }
// In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors but propagate other.
async function catchDisallowedErrors(callback: () => Promise<void>) {
try {
return await callback();
} catch (e) {
if (isProtocolError(e) && e.message.includes('Invalid http status code or phrase'))
throw e;
}
}
function splitSetCookieHeader(headers: types.HeadersArray): types.HeadersArray { function splitSetCookieHeader(headers: types.HeadersArray): types.HeadersArray {
const index = headers.findIndex(({ name }) => name.toLowerCase() === 'set-cookie'); const index = headers.findIndex(({ name }) => name.toLowerCase() === 'set-cookie');
if (index === -1) if (index === -1)

View file

@ -262,6 +262,8 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameChannel, Br
const result = await this._frame.expect(metadata, params.selector, { ...params, expectedValue }); const result = await this._frame.expect(metadata, params.selector, { ...params, expectedValue });
if (result.received !== undefined) if (result.received !== undefined)
result.received = serializeResult(result.received); result.received = serializeResult(result.received);
if (result.matches === params.isNot)
metadata.error = { error: { name: 'Expect', message: 'Expect failed' } };
return result; return result;
} }
} }

View file

@ -188,7 +188,7 @@ class RecordActionTool implements RecorderTool {
return; return;
if (this._actionInProgress(event)) if (this._actionInProgress(event))
return; return;
if (this._consumedDueWrongTarget(event, this._hoveredModel)) if (this._consumedDueToNoModel(event, this._hoveredModel))
return; return;
const checkbox = asCheckbox(this._recorder.deepEventTarget(event)); const checkbox = asCheckbox(this._recorder.deepEventTarget(event));
@ -283,7 +283,7 @@ class RecordActionTool implements RecorderTool {
} }
// Non-navigating actions are simply recorded by Playwright. // Non-navigating actions are simply recorded by Playwright.
if (this._consumedDueWrongTarget(event, this._activeModel)) if (this._consumedDueWrongTarget(event))
return; return;
this._recorder.delegate.recordAction?.({ this._recorder.delegate.recordAction?.({
name: 'fill', name: 'fill',
@ -313,7 +313,7 @@ class RecordActionTool implements RecorderTool {
this._expectProgrammaticKeyUp = true; this._expectProgrammaticKeyUp = true;
return; return;
} }
if (this._consumedDueWrongTarget(event, this._activeModel)) if (this._consumedDueWrongTarget(event))
return; return;
// Similarly to click, trigger checkbox on key event, not input. // Similarly to click, trigger checkbox on key event, not input.
if (event.key === ' ') { if (event.key === ' ') {
@ -373,7 +373,7 @@ class RecordActionTool implements RecorderTool {
const nodeName = target.nodeName; const nodeName = target.nodeName;
if (nodeName === 'SELECT' || nodeName === 'OPTION') if (nodeName === 'SELECT' || nodeName === 'OPTION')
return true; return true;
if (nodeName === 'INPUT' && ['date', 'range'].includes((target as HTMLInputElement).type)) if (nodeName === 'INPUT' && ['date'].includes((target as HTMLInputElement).type))
return true; return true;
return false; return false;
} }
@ -387,8 +387,15 @@ class RecordActionTool implements RecorderTool {
return false; return false;
} }
private _consumedDueWrongTarget(event: Event, model: HighlightModel | null): boolean { private _consumedDueToNoModel(event: Event, model: HighlightModel | null): boolean {
if (model && model.elements[0] === this._recorder.deepEventTarget(event)) if (model)
return false;
consumeEvent(event);
return true;
}
private _consumedDueWrongTarget(event: Event): boolean {
if (this._activeModel && this._activeModel.elements[0] === this._recorder.deepEventTarget(event))
return false; return false;
consumeEvent(event); consumeEvent(event);
return true; return true;

View file

@ -134,18 +134,6 @@ export class Request extends SdkObject {
this._waitForResponsePromise.resolve(null); this._waitForResponsePromise.resolve(null);
} }
async _waitForRequestFailure() {
const response = await this._waitForResponsePromise;
// If response is null it was a failure an we are done.
if (!response)
return;
await response._finishedPromise;
if (this.failure())
return;
// If request finished without errors, we stall.
await new Promise(() => {});
}
_setOverrides(overrides: types.NormalizedContinueOverrides) { _setOverrides(overrides: types.NormalizedContinueOverrides) {
this._overrides = overrides; this._overrides = overrides;
this._updateHeadersMap(); this._updateHeadersMap();
@ -270,13 +258,7 @@ export class Route extends SdkObject {
async abort(errorCode: string = 'failed') { async abort(errorCode: string = 'failed') {
this._startHandling(); this._startHandling();
this._request._context.emit(BrowserContext.Events.RequestAborted, this._request); this._request._context.emit(BrowserContext.Events.RequestAborted, this._request);
await Promise.race([ await this._delegate.abort(errorCode);
this._delegate.abort(errorCode),
// If the request is already cancelled by the page before we handle the route,
// we'll receive loading failed event and will ignore route handling error.
this._request._waitForRequestFailure()
]);
this._endHandling(); this._endHandling();
} }
@ -304,17 +286,12 @@ export class Route extends SdkObject {
const headers = [...(overrides.headers || [])]; const headers = [...(overrides.headers || [])];
this._maybeAddCorsHeaders(headers); this._maybeAddCorsHeaders(headers);
this._request._context.emit(BrowserContext.Events.RequestFulfilled, this._request); this._request._context.emit(BrowserContext.Events.RequestFulfilled, this._request);
await Promise.race([ await this._delegate.fulfill({
this._delegate.fulfill({
status: overrides.status || 200, status: overrides.status || 200,
headers, headers,
body, body: body!,
isBase64, isBase64,
}), });
// If the request is already cancelled by the page before we handle the route,
// we'll receive loading failed event and will ignore route handling error.
this._request._waitForRequestFailure()
]);
this._endHandling(); this._endHandling();
} }
@ -347,13 +324,7 @@ export class Route extends SdkObject {
this._request._setOverrides(overrides); this._request._setOverrides(overrides);
if (!overrides.isFallback) if (!overrides.isFallback)
this._request._context.emit(BrowserContext.Events.RequestContinued, this._request); this._request._context.emit(BrowserContext.Events.RequestContinued, this._request);
await Promise.race([ await this._delegate.continue(this._request, overrides);
this._delegate.continue(this._request, overrides),
// If the request is already cancelled by the page before we handle the route,
// we'll receive loading failed event and will ignore route handling error.
this._request._waitForRequestFailure()
]);
this._endHandling(); this._endHandling();
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-core", "name": "@playwright/experimental-ct-core",
"version": "1.41.0-next", "version": "1.41.2",
"description": "Playwright Component Testing Helpers", "description": "Playwright Component Testing Helpers",
"repository": { "repository": {
"type": "git", "type": "git",
@ -27,9 +27,9 @@
} }
}, },
"dependencies": { "dependencies": {
"playwright-core": "1.41.0-next", "playwright-core": "1.41.2",
"vite": "^4.4.12", "vite": "^4.4.12",
"playwright": "1.41.0-next" "playwright": "1.41.2"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"

View file

@ -0,0 +1,6 @@
[vitePlugin.ts]
generated/indexSource.ts
[mount.ts]
generated/serializers.ts
injected/**

View file

@ -0,0 +1,49 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type ImportRef = {
__pw_type: 'importRef',
id: string,
property?: string,
};
export function isImportRef(value: any): value is ImportRef {
return typeof value === 'object' && value && value.__pw_type === 'importRef';
}
export class ImportRegistry {
private _registry = new Map<string, () => Promise<any>>();
initialize(components: Record<string, () => Promise<any>>) {
for (const [name, value] of Object.entries(components))
this._registry.set(name, value);
}
async resolveImportRef(importRef: ImportRef): Promise<any> {
const importFunction = this._registry.get(importRef.id);
if (!importFunction)
throw new Error(`Unregistered component: ${importRef.id}. Following components are registered: ${[...this._registry.keys()]}`);
let importedObject = await importFunction();
if (!importedObject)
throw new Error(`Could not resolve component: ${importRef.id}.`);
if (importRef.property) {
importedObject = importedObject[importRef.property];
if (!importedObject)
throw new Error(`Could not instantiate component: ${importRef.id}.${importRef.property}.`);
}
return importedObject;
}
}

View file

@ -0,0 +1,22 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ImportRegistry } from './importRegistry';
import { transformObject, unwrapObject } from './serializers';
window.__pwRegistry = new ImportRegistry();
window.__pwUnwrapObject = unwrapObject;
window.__pwTransformObject = transformObject;

View file

@ -0,0 +1,93 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { isImportRef } from './importRegistry';
type FunctionRef = {
__pw_type: 'function';
ordinal: number;
};
function isFunctionRef(value: any): value is FunctionRef {
return value && typeof value === 'object' && value.__pw_type === 'function';
}
export function wrapObject(value: any, callbacks: Function[]): any {
return transformObject(value, (v: any) => {
if (typeof v === 'function') {
const ordinal = callbacks.length;
callbacks.push(v as Function);
const result: FunctionRef = {
__pw_type: 'function',
ordinal,
};
return { result };
}
});
}
export async function unwrapObject(value: any): Promise<any> {
return transformObjectAsync(value, async (v: any) => {
if (isFunctionRef(v)) {
const result = (...args: any[]) => {
window.__ctDispatchFunction(v.ordinal, args);
};
return { result };
}
if (isImportRef(v))
return { result: await window.__pwRegistry.resolveImportRef(v) };
});
}
export function transformObject(value: any, mapping: (v: any) => { result: any } | undefined): any {
const result = mapping(value);
if (result)
return result.result;
if (value === null || typeof value !== 'object')
return value;
if (value instanceof Date || value instanceof RegExp || value instanceof URL)
return value;
if (Array.isArray(value)) {
const result = [];
for (const item of value)
result.push(transformObject(item, mapping));
return result;
}
const result2: any = {};
for (const [key, prop] of Object.entries(value))
result2[key] = transformObject(prop, mapping);
return result2;
}
export async function transformObjectAsync(value: any, mapping: (v: any) => Promise<{ result: any } | undefined>): Promise<any> {
const result = await mapping(value);
if (result)
return result.result;
if (value === null || typeof value !== 'object')
return value;
if (value instanceof Date || value instanceof RegExp || value instanceof URL)
return value;
if (Array.isArray(value)) {
const result = [];
for (const item of value)
result.push(await transformObjectAsync(item, mapping));
return result;
}
const result2: any = {};
for (const [key, prop] of Object.entries(value))
result2[key] = await transformObjectAsync(prop, mapping);
return result2;
}

View file

@ -15,8 +15,10 @@
*/ */
import type { Fixtures, Locator, Page, BrowserContextOptions, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, BrowserContext } from 'playwright/test'; import type { Fixtures, Locator, Page, BrowserContextOptions, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, BrowserContext } from 'playwright/test';
import type { Component, JsxComponent, MountOptions } from '../types/component'; import type { Component, JsxComponent, MountOptions, ObjectComponentOptions } from '../types/component';
import type { ContextReuseMode, FullConfigInternal } from '../../playwright/src/common/config'; import type { ContextReuseMode, FullConfigInternal } from '../../playwright/src/common/config';
import type { ImportRef } from './injected/importRegistry';
import { wrapObject } from './injected/serializers';
let boundCallbacksForMount: Function[] = []; let boundCallbacksForMount: Function[] = [];
@ -25,12 +27,16 @@ interface MountResult extends Locator {
update(options: Omit<MountOptions, 'hooksConfig'> | string | JsxComponent): Promise<void>; update(options: Omit<MountOptions, 'hooksConfig'> | string | JsxComponent): Promise<void>;
} }
export const fixtures: Fixtures< type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & {
PlaywrightTestArgs & PlaywrightTestOptions & {
mount: (component: any, options: any) => Promise<MountResult>; mount: (component: any, options: any) => Promise<MountResult>;
}, };
PlaywrightWorkerArgs & PlaywrightWorkerOptions & { _ctWorker: { context: BrowserContext | undefined, hash: string } }, type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & { _ctWorker: { context: BrowserContext | undefined, hash: string } };
{ _contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>, _contextReuseMode: ContextReuseMode }> = { type BaseTestFixtures = {
_contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>,
_contextReuseMode: ContextReuseMode
};
export const fixtures: Fixtures<TestFixtures, WorkerFixtures, BaseTestFixtures> = {
_contextReuseMode: 'when-possible', _contextReuseMode: 'when-possible',
@ -42,7 +48,7 @@ export const fixtures: Fixtures<
if (!((info as any)._configInternal as FullConfigInternal).defineConfigWasUsed) if (!((info as any)._configInternal as FullConfigInternal).defineConfigWasUsed)
throw new Error('Component testing requires the use of the defineConfig() in your playwright-ct.config.{ts,js}: https://aka.ms/playwright/ct-define-config'); throw new Error('Component testing requires the use of the defineConfig() in your playwright-ct.config.{ts,js}: https://aka.ms/playwright/ct-define-config');
await (page as any)._wrapApiCall(async () => { await (page as any)._wrapApiCall(async () => {
await page.exposeFunction('__ct_dispatch', (ordinal: number, args: any[]) => { await page.exposeFunction('__ctDispatchFunction', (ordinal: number, args: any[]) => {
boundCallbacksForMount[ordinal](...args); boundCallbacksForMount[ordinal](...args);
}); });
await page.goto(process.env.PLAYWRIGHT_TEST_BASE_URL!); await page.goto(process.env.PLAYWRIGHT_TEST_BASE_URL!);
@ -51,9 +57,9 @@ export const fixtures: Fixtures<
}, },
mount: async ({ page }, use) => { mount: async ({ page }, use) => {
await use(async (component: JsxComponent | string, options?: MountOptions) => { await use(async (componentRef: JsxComponent | ImportRef, options?: ObjectComponentOptions & MountOptions) => {
const selector = await (page as any)._wrapApiCall(async () => { const selector = await (page as any)._wrapApiCall(async () => {
return await innerMount(page, component, options); return await innerMount(page, componentRef, options);
}, true); }, true);
const locator = page.locator(selector); const locator = page.locator(selector);
return Object.assign(locator, { return Object.assign(locator, {
@ -63,10 +69,10 @@ export const fixtures: Fixtures<
await window.playwrightUnmount(rootElement); await window.playwrightUnmount(rootElement);
}); });
}, },
update: async (options: JsxComponent | Omit<MountOptions, 'hooksConfig'>) => { update: async (options: JsxComponent | ObjectComponentOptions) => {
if (isJsxApi(options)) if (isJsxComponent(options))
return await innerUpdate(page, options); return await innerUpdate(page, options);
await innerUpdate(page, component, options); await innerUpdate(page, componentRef, options);
} }
}); });
}); });
@ -74,63 +80,34 @@ export const fixtures: Fixtures<
}, },
}; };
function isJsxApi(options: Record<string, unknown>): options is JsxComponent { function isJsxComponent(component: any): component is JsxComponent {
return options?.kind === 'jsx'; return typeof component === 'object' && component && component.__pw_type === 'jsx';
} }
async function innerUpdate(page: Page, jsxOrType: JsxComponent | string, options: Omit<MountOptions, 'hooksConfig'> = {}): Promise<void> { async function innerUpdate(page: Page, componentRef: JsxComponent | ImportRef, options: ObjectComponentOptions = {}): Promise<void> {
const component = createComponent(jsxOrType, options); const component = wrapObject(createComponent(componentRef, options), boundCallbacksForMount);
wrapFunctions(component, page, boundCallbacksForMount);
await page.evaluate(async ({ component }) => { await page.evaluate(async ({ component }) => {
const unwrapFunctions = (object: any) => { component = await window.__pwUnwrapObject(component);
for (const [key, value] of Object.entries(object)) {
if (typeof value === 'string' && (value as string).startsWith('__pw_func_')) {
const ordinal = +value.substring('__pw_func_'.length);
object[key] = (...args: any[]) => {
(window as any)['__ct_dispatch'](ordinal, args);
};
} else if (typeof value === 'object' && value) {
unwrapFunctions(value);
}
}
};
unwrapFunctions(component);
const rootElement = document.getElementById('root')!; const rootElement = document.getElementById('root')!;
return await window.playwrightUpdate(rootElement, component); return await window.playwrightUpdate(rootElement, component);
}, { component }); }, { component });
} }
async function innerMount(page: Page, jsxOrType: JsxComponent | string, options: MountOptions = {}): Promise<string> { async function innerMount(page: Page, componentRef: JsxComponent | ImportRef, options: ObjectComponentOptions & MountOptions = {}): Promise<string> {
const component = createComponent(jsxOrType, options); const component = wrapObject(createComponent(componentRef, options), boundCallbacksForMount);
wrapFunctions(component, page, boundCallbacksForMount);
// WebKit does not wait for deferred scripts. // WebKit does not wait for deferred scripts.
await page.waitForFunction(() => !!window.playwrightMount); await page.waitForFunction(() => !!window.playwrightMount);
const selector = await page.evaluate(async ({ component, hooksConfig }) => { const selector = await page.evaluate(async ({ component, hooksConfig }) => {
const unwrapFunctions = (object: any) => { component = await window.__pwUnwrapObject(component);
for (const [key, value] of Object.entries(object)) {
if (typeof value === 'string' && (value as string).startsWith('__pw_func_')) {
const ordinal = +value.substring('__pw_func_'.length);
object[key] = (...args: any[]) => {
(window as any)['__ct_dispatch'](ordinal, args);
};
} else if (typeof value === 'object' && value) {
unwrapFunctions(value);
}
}
};
unwrapFunctions(component);
let rootElement = document.getElementById('root'); let rootElement = document.getElementById('root');
if (!rootElement) { if (!rootElement) {
rootElement = document.createElement('div'); rootElement = document.createElement('div');
rootElement.id = 'root'; rootElement.id = 'root';
document.body.appendChild(rootElement); document.body.appendChild(rootElement);
} }
await window.playwrightMount(component, rootElement, hooksConfig); await window.playwrightMount(component, rootElement, hooksConfig);
return '#root >> internal:control=component'; return '#root >> internal:control=component';
@ -138,20 +115,12 @@ async function innerMount(page: Page, jsxOrType: JsxComponent | string, options:
return selector; return selector;
} }
function createComponent(jsxOrType: JsxComponent | string, options: Omit<MountOptions, 'hooksConfig'> = {}): Component { function createComponent(component: JsxComponent | ImportRef, options: ObjectComponentOptions = {}): Component {
if (typeof jsxOrType !== 'string') return jsxOrType; if (component.__pw_type === 'jsx')
return { __pw_component_marker: true, kind: 'object', type: jsxOrType, options }; return component;
} return {
__pw_type: 'object-component',
function wrapFunctions(object: any, page: Page, callbacks: Function[]) { type: component,
for (const [key, value] of Object.entries(object)) { ...options,
const type = typeof value; };
if (type === 'function') {
const functionName = '__pw_func_' + callbacks.length;
callbacks.push(value as Function);
object[key] = functionName;
} else if (type === 'object' && value) {
wrapFunctions(value, page, callbacks);
}
}
} }

View file

@ -18,11 +18,11 @@ import path from 'path';
import type { T, BabelAPI, PluginObj } from 'playwright/src/transform/babelBundle'; import type { T, BabelAPI, PluginObj } from 'playwright/src/transform/babelBundle';
import { types, declare, traverse } from 'playwright/lib/transform/babelBundle'; import { types, declare, traverse } from 'playwright/lib/transform/babelBundle';
import { resolveImportSpecifierExtension } from 'playwright/lib/util'; import { resolveImportSpecifierExtension } from 'playwright/lib/util';
import { setTransformData } from 'playwright/lib/transform/transform';
const t: typeof T = types; const t: typeof T = types;
const fullNames = new Map<string, string | undefined>(); let jsxComponentNames: Set<string>;
let componentNames: Set<string>; let importInfos: Map<string, ImportInfo>;
let componentIdentifiers: Set<T.Identifier>;
export default declare((api: BabelAPI) => { export default declare((api: BabelAPI) => {
api.assertVersion(7); api.assertVersion(7);
@ -30,11 +30,42 @@ export default declare((api: BabelAPI) => {
const result: PluginObj = { const result: PluginObj = {
name: 'playwright-debug-transform', name: 'playwright-debug-transform',
visitor: { visitor: {
Program(path) { Program: {
fullNames.clear(); enter(path) {
const result = collectComponentUsages(path.node); jsxComponentNames = collectJsxComponentUsages(path.node);
componentNames = result.names; importInfos = new Map();
componentIdentifiers = result.identifiers; },
exit(path) {
let firstDeclaration: any;
let lastImportDeclaration: any;
path.get('body').forEach(p => {
if (p.isImportDeclaration())
lastImportDeclaration = p;
else if (!firstDeclaration)
firstDeclaration = p;
});
const insertionPath = lastImportDeclaration || firstDeclaration;
if (!insertionPath)
return;
for (const [localName, componentImport] of [...importInfos.entries()].reverse()) {
insertionPath.insertAfter(
t.variableDeclaration(
'const',
[
t.variableDeclarator(
t.identifier(localName),
t.objectExpression([
t.objectProperty(t.identifier('__pw_type'), t.stringLiteral('importRef')),
t.objectProperty(t.identifier('id'), t.stringLiteral(componentImport.id)),
]),
)
]
)
);
}
setTransformData('playwright-ct-core', [...importInfos.values()]);
}
}, },
ImportDeclaration(p) { ImportDeclaration(p) {
@ -42,176 +73,109 @@ export default declare((api: BabelAPI) => {
if (!t.isStringLiteral(importNode.source)) if (!t.isStringLiteral(importNode.source))
return; return;
let components = 0; const ext = path.extname(importNode.source.value);
// Convert all non-JS imports into refs.
if (!allJsExtensions.has(ext)) {
for (const specifier of importNode.specifiers) { for (const specifier of importNode.specifiers) {
const specifierName = specifier.local.name;
const componentName = componentNames.has(specifierName) ? specifierName : [...componentNames].find(c => c.startsWith(specifierName + '.'));
if (!componentName)
continue;
if (t.isImportNamespaceSpecifier(specifier)) if (t.isImportNamespaceSpecifier(specifier))
continue; continue;
const { fullName } = componentInfo(specifier, importNode.source.value, this.filename!, componentName); const { localName, info } = importInfo(importNode, specifier, this.filename!);
fullNames.set(componentName, fullName); importInfos.set(localName, info);
++components; }
p.skip();
p.remove();
return;
} }
// All the imports were components => delete. // Convert JS imports that are used as components in JSX expressions into refs.
if (components && components === importNode.specifiers.length) { let importCount = 0;
for (const specifier of importNode.specifiers) {
if (t.isImportNamespaceSpecifier(specifier))
continue;
const { localName, info } = importInfo(importNode, specifier, this.filename!);
if (jsxComponentNames.has(localName)) {
importInfos.set(localName, info);
++importCount;
}
}
// All the imports were from JSX => delete.
if (importCount && importCount === importNode.specifiers.length) {
p.skip(); p.skip();
p.remove(); p.remove();
} }
}, },
Identifier(p) { MemberExpression(path) {
if (componentIdentifiers.has(p.node)) { if (!t.isIdentifier(path.node.object))
const componentName = fullNames.get(p.node.name) || p.node.name;
p.replaceWith(t.stringLiteral(componentName));
}
},
JSXElement(path) {
const jsxElement = path.node;
const jsxName = jsxElement.openingElement.name;
let nameOrExpression: string = '';
if (t.isJSXIdentifier(jsxName))
nameOrExpression = jsxName.name;
else if (t.isJSXMemberExpression(jsxName) && t.isJSXIdentifier(jsxName.object) && t.isJSXIdentifier(jsxName.property))
nameOrExpression = jsxName.object.name + '.' + jsxName.property.name;
if (!nameOrExpression)
return; return;
const componentName = fullNames.get(nameOrExpression) || nameOrExpression; if (!importInfos.has(path.node.object.name))
return;
const props: (T.ObjectProperty | T.SpreadElement)[] = []; if (!t.isIdentifier(path.node.property))
return;
for (const jsxAttribute of jsxElement.openingElement.attributes) { path.replaceWith(
if (t.isJSXAttribute(jsxAttribute)) { t.objectExpression([
let namespace: T.JSXIdentifier | undefined; t.spreadElement(t.identifier(path.node.object.name)),
let name: T.JSXIdentifier | undefined; t.objectProperty(t.identifier('property'), t.stringLiteral(path.node.property.name)),
if (t.isJSXNamespacedName(jsxAttribute.name)) { ])
namespace = jsxAttribute.name.namespace; );
name = jsxAttribute.name.name; },
} else if (t.isJSXIdentifier(jsxAttribute.name)) {
name = jsxAttribute.name;
}
if (!name)
continue;
const attrName = (namespace ? namespace.name + ':' : '') + name.name;
if (t.isStringLiteral(jsxAttribute.value))
props.push(t.objectProperty(t.stringLiteral(attrName), jsxAttribute.value));
else if (t.isJSXExpressionContainer(jsxAttribute.value) && t.isExpression(jsxAttribute.value.expression))
props.push(t.objectProperty(t.stringLiteral(attrName), jsxAttribute.value.expression));
else if (jsxAttribute.value === null)
props.push(t.objectProperty(t.stringLiteral(attrName), t.booleanLiteral(true)));
else
props.push(t.objectProperty(t.stringLiteral(attrName), t.nullLiteral()));
} else if (t.isJSXSpreadAttribute(jsxAttribute)) {
props.push(t.spreadElement(jsxAttribute.argument));
}
}
const children: (T.Expression | T.SpreadElement)[] = [];
for (const child of jsxElement.children) {
if (t.isJSXText(child))
children.push(t.stringLiteral(child.value));
else if (t.isJSXElement(child))
children.push(child);
else if (t.isJSXExpressionContainer(child) && !t.isJSXEmptyExpression(child.expression))
children.push(child.expression);
else if (t.isJSXSpreadChild(child))
children.push(t.spreadElement(child.expression));
}
const component: T.ObjectProperty[] = [
t.objectProperty(t.identifier('__pw_component_marker'), t.booleanLiteral(true)),
t.objectProperty(t.identifier('kind'), t.stringLiteral('jsx')),
t.objectProperty(t.identifier('type'), t.stringLiteral(componentName)),
t.objectProperty(t.identifier('props'), t.objectExpression(props)),
];
if (children.length)
component.push(t.objectProperty(t.identifier('children'), t.arrayExpression(children)));
path.replaceWith(t.objectExpression(component));
}
} }
}; };
return result; return result;
}); });
export function collectComponentUsages(node: T.Node) { function collectJsxComponentUsages(node: T.Node): Set<string> {
const importedLocalNames = new Set<string>();
const names = new Set<string>(); const names = new Set<string>();
const identifiers = new Set<T.Identifier>();
traverse(node, { traverse(node, {
enter: p => { enter: p => {
// First look at all the imports.
if (t.isImportDeclaration(p.node)) {
const importNode = p.node;
if (!t.isStringLiteral(importNode.source))
return;
for (const specifier of importNode.specifiers) {
if (t.isImportNamespaceSpecifier(specifier))
continue;
importedLocalNames.add(specifier.local.name);
}
}
// Treat JSX-everything as component usages. // Treat JSX-everything as component usages.
if (t.isJSXElement(p.node)) { if (t.isJSXElement(p.node)) {
if (t.isJSXIdentifier(p.node.openingElement.name)) if (t.isJSXIdentifier(p.node.openingElement.name))
names.add(p.node.openingElement.name.name); names.add(p.node.openingElement.name.name);
if (t.isJSXMemberExpression(p.node.openingElement.name) && t.isJSXIdentifier(p.node.openingElement.name.object) && t.isJSXIdentifier(p.node.openingElement.name.property)) if (t.isJSXMemberExpression(p.node.openingElement.name) && t.isJSXIdentifier(p.node.openingElement.name.object) && t.isJSXIdentifier(p.node.openingElement.name.property))
names.add(p.node.openingElement.name.object.name + '.' + p.node.openingElement.name.property.name); names.add(p.node.openingElement.name.object.name);
}
// Treat mount(identifier, ...) as component usage if it is in the importedLocalNames list.
if (t.isAwaitExpression(p.node) && t.isCallExpression(p.node.argument) && t.isIdentifier(p.node.argument.callee) && p.node.argument.callee.name === 'mount') {
const callExpression = p.node.argument;
const arg = callExpression.arguments[0];
if (!t.isIdentifier(arg) || !importedLocalNames.has(arg.name))
return;
names.add(arg.name);
identifiers.add(arg);
} }
} }
}); });
return { names, identifiers }; return names;
} }
export type ComponentInfo = { export type ImportInfo = {
fullName: string; id: string;
importPath: string;
isModuleOrAlias: boolean; isModuleOrAlias: boolean;
importedName?: string; importPath: string;
importedNameProperty?: string; remoteName: string | undefined;
deps: string[];
}; };
export function componentInfo(specifier: T.ImportSpecifier | T.ImportDefaultSpecifier, importSource: string, filename: string, componentName: string): ComponentInfo { export function importInfo(importNode: T.ImportDeclaration, specifier: T.ImportSpecifier | T.ImportDefaultSpecifier, filename: string): { localName: string, info: ImportInfo } {
const importSource = importNode.source.value;
const isModuleOrAlias = !importSource.startsWith('.'); const isModuleOrAlias = !importSource.startsWith('.');
const unresolvedImportPath = path.resolve(path.dirname(filename), importSource); const unresolvedImportPath = path.resolve(path.dirname(filename), importSource);
// Support following notations for Button.tsx: // Support following notations for Button.tsx:
// - import { Button } from './Button.js' - via resolveImportSpecifierExtension // - import { Button } from './Button.js' - via resolveImportSpecifierExtension
// - import { Button } from './Button' - via require.resolve // - import { Button } from './Button' - via require.resolve
const importPath = isModuleOrAlias ? importSource : resolveImportSpecifierExtension(unresolvedImportPath) || require.resolve(unresolvedImportPath); const importPath = isModuleOrAlias ? importSource : resolveImportSpecifierExtension(unresolvedImportPath) || require.resolve(unresolvedImportPath);
const prefix = importPath.replace(/[^\w_\d]/g, '_'); const idPrefix = importPath.replace(/[^\w_\d]/g, '_');
const pathInfo = { importPath, isModuleOrAlias };
const specifierName = specifier.local.name; const result: ImportInfo = {
let fullNameSuffix = ''; id: idPrefix,
let importedNameProperty = ''; importPath,
if (componentName !== specifierName) { isModuleOrAlias,
const suffix = componentName.substring(specifierName.length + 1); remoteName: undefined,
fullNameSuffix = '_' + suffix; };
importedNameProperty = '.' + suffix;
if (t.isImportDefaultSpecifier(specifier)) {
} else if (t.isIdentifier(specifier.imported)) {
result.remoteName = specifier.imported.name;
} else {
result.remoteName = specifier.imported.value;
} }
if (t.isImportDefaultSpecifier(specifier)) if (result.remoteName)
return { fullName: prefix + fullNameSuffix, importedNameProperty, deps: [], ...pathInfo }; result.id += '_' + result.remoteName;
return { localName: specifier.local.name, info: result };
if (t.isIdentifier(specifier.imported))
return { fullName: prefix + '_' + specifier.imported.name + fullNameSuffix, importedName: specifier.imported.name, importedNameProperty, deps: [], ...pathInfo };
return { fullName: prefix + '_' + specifier.imported.value + fullNameSuffix, importedName: specifier.imported.value, importedNameProperty, deps: [], ...pathInfo };
} }
const allJsExtensions = new Set(['.js', '.jsx', '.cjs', '.mjs', '.ts', '.tsx', '.cts', '.mts', '']);

View file

@ -19,20 +19,19 @@ import type { PlaywrightTestConfig as BasePlaywrightTestConfig, FullConfig } fro
import type { InlineConfig, Plugin, ResolveFn, ResolvedConfig, UserConfig } from 'vite'; import type { InlineConfig, Plugin, ResolveFn, ResolvedConfig, UserConfig } from 'vite';
import type { TestRunnerPlugin } from '../../playwright/src/plugins'; import type { TestRunnerPlugin } from '../../playwright/src/plugins';
import type { ComponentInfo } from './tsxTransform';
import type { AddressInfo } from 'net'; import type { AddressInfo } from 'net';
import type { PluginContext } from 'rollup'; import type { PluginContext } from 'rollup';
import { debug } from 'playwright-core/lib/utilsBundle'; import { debug } from 'playwright-core/lib/utilsBundle';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { parse, traverse, types as t } from 'playwright/lib/transform/babelBundle';
import { stoppable } from 'playwright/lib/utilsBundle'; import { stoppable } from 'playwright/lib/utilsBundle';
import { assert, calculateSha1 } from 'playwright-core/lib/utils'; import { assert, calculateSha1 } from 'playwright-core/lib/utils';
import { getPlaywrightVersion } from 'playwright-core/lib/utils'; import { getPlaywrightVersion } from 'playwright-core/lib/utils';
import { setExternalDependencies } from 'playwright/lib/transform/compilationCache'; import { getUserData, internalDependenciesForTestFile, setExternalDependencies } from 'playwright/lib/transform/compilationCache';
import { collectComponentUsages, componentInfo } from './tsxTransform';
import { version as viteVersion, build, preview, mergeConfig } from 'vite'; import { version as viteVersion, build, preview, mergeConfig } from 'vite';
import { source as injectedSource } from './generated/indexSource';
import type { ImportInfo } from './tsxTransform';
const log = debug('pw:vite'); const log = debug('pw:vite');
@ -107,7 +106,7 @@ export function createPlugin(
let buildExists = false; let buildExists = false;
let buildInfo: BuildInfo; let buildInfo: BuildInfo;
const registerSource = await fs.promises.readFile(registerSourceFile, 'utf-8'); const registerSource = injectedSource + '\n' + await fs.promises.readFile(registerSourceFile, 'utf-8');
const registerSourceHash = calculateSha1(registerSource); const registerSourceHash = calculateSha1(registerSource);
try { try {
@ -122,16 +121,16 @@ export function createPlugin(
viteVersion, viteVersion,
registerSourceHash, registerSourceHash,
components: [], components: [],
tests: {},
sources: {}, sources: {},
deps: {},
}; };
} }
log('build exists:', buildExists); log('build exists:', buildExists);
const componentRegistry: ComponentRegistry = new Map(); const componentRegistry: ComponentRegistry = new Map();
// 1. Re-parse changed tests and collect required components. const componentsByImportingFile = new Map<string, string[]>();
const hasNewTests = await checkNewTests(suite, buildInfo, componentRegistry); // 1. Populate component registry based on tests' component imports.
log('has new tests:', hasNewTests); await populateComponentsFromTests(componentRegistry, componentsByImportingFile);
// 2. Check if the set of required components has changed. // 2. Check if the set of required components has changed.
const hasNewComponents = await checkNewComponents(buildInfo, componentRegistry); const hasNewComponents = await checkNewComponents(buildInfo, componentRegistry);
@ -168,8 +167,9 @@ export function createPlugin(
frameworkOverrides.plugins = [await frameworkPluginFactory()]; frameworkOverrides.plugins = [await frameworkPluginFactory()];
// But only add out own plugin when we actually build / transform. // But only add out own plugin when we actually build / transform.
const depsCollector = new Map<string, string[]>();
if (sourcesDirty) if (sourcesDirty)
frameworkOverrides.plugins!.push(vitePlugin(registerSource, templateDir, buildInfo, componentRegistry)); frameworkOverrides.plugins!.push(vitePlugin(registerSource, templateDir, buildInfo, componentRegistry, depsCollector));
frameworkOverrides.build = { frameworkOverrides.build = {
target: 'esnext', target: 'esnext',
@ -189,20 +189,31 @@ export function createPlugin(
log('build'); log('build');
await build(finalConfig); await build(finalConfig);
await fs.promises.rename(`${finalConfig.build.outDir}/${relativeTemplateDir}/index.html`, `${finalConfig.build.outDir}/index.html`); await fs.promises.rename(`${finalConfig.build.outDir}/${relativeTemplateDir}/index.html`, `${finalConfig.build.outDir}/index.html`);
buildInfo.deps = Object.fromEntries(depsCollector.entries());
} }
if (hasNewTests || hasNewComponents || sourcesDirty) { if (hasNewComponents || sourcesDirty) {
log('write manifest'); log('write manifest');
await fs.promises.writeFile(buildInfoFile, JSON.stringify(buildInfo, undefined, 2)); await fs.promises.writeFile(buildInfoFile, JSON.stringify(buildInfo, undefined, 2));
} }
for (const [filename, testInfo] of Object.entries(buildInfo.tests)) { for (const projectSuite of suite.suites) {
for (const fileSuite of projectSuite.suites) {
// For every test file...
const testFile = fileSuite.location!.file;
const deps = new Set<string>(); const deps = new Set<string>();
for (const componentName of testInfo.components) { // Collect its JS dependencies (helpers).
const component = componentRegistry.get(componentName); for (const file of [testFile, ...(internalDependenciesForTestFile(testFile) || [])]) {
component?.deps.forEach(d => deps.add(d)); // For each helper, get all the imported components.
for (const componentFile of componentsByImportingFile.get(file) || []) {
// For each component, get all the dependencies.
for (const d of depsCollector.get(componentFile) || [])
deps.add(d);
}
}
// Now we have test file => all components along with dependencies.
setExternalDependencies(testFile, [...deps]);
} }
setExternalDependencies(filename, [...deps]);
} }
const previewServer = await preview(finalConfig); const previewServer = await preview(finalConfig);
@ -231,16 +242,13 @@ type BuildInfo = {
timestamp: number; timestamp: number;
} }
}; };
components: ComponentInfo[]; components: ImportInfo[];
tests: { deps: {
[key: string]: { [key: string]: string[];
timestamp: number;
components: string[];
} }
}; };
};
type ComponentRegistry = Map<string, ComponentInfo>; type ComponentRegistry = Map<string, ImportInfo>;
async function checkSources(buildInfo: BuildInfo): Promise<boolean> { async function checkSources(buildInfo: BuildInfo): Promise<boolean> {
for (const [source, sourceInfo] of Object.entries(buildInfo.sources)) { for (const [source, sourceInfo] of Object.entries(buildInfo.sources)) {
@ -258,32 +266,18 @@ async function checkSources(buildInfo: BuildInfo): Promise<boolean> {
return false; return false;
} }
async function checkNewTests(suite: Suite, buildInfo: BuildInfo, componentRegistry: ComponentRegistry): Promise<boolean> { async function populateComponentsFromTests(componentRegistry: ComponentRegistry, componentsByImportingFile: Map<string, string[]>) {
const testFiles = new Set<string>(); const importInfos: Map<string, ImportInfo[]> = await getUserData('playwright-ct-core');
for (const project of suite.suites) { for (const [file, importList] of importInfos) {
for (const file of project.suites) for (const importInfo of importList)
testFiles.add(file.location!.file); componentRegistry.set(importInfo.id, importInfo);
componentsByImportingFile.set(file, importList.filter(i => !i.isModuleOrAlias).map(i => i.importPath));
} }
let hasNewTests = false;
for (const testFile of testFiles) {
const timestamp = (await fs.promises.stat(testFile)).mtimeMs;
if (buildInfo.tests[testFile]?.timestamp !== timestamp) {
const components = await parseTestFile(testFile);
log('changed test:', testFile);
for (const component of components)
componentRegistry.set(component.fullName, component);
buildInfo.tests[testFile] = { timestamp, components: components.map(c => c.fullName) };
hasNewTests = true;
}
}
return hasNewTests;
} }
async function checkNewComponents(buildInfo: BuildInfo, componentRegistry: ComponentRegistry): Promise<boolean> { async function checkNewComponents(buildInfo: BuildInfo, componentRegistry: ComponentRegistry): Promise<boolean> {
const newComponents = [...componentRegistry.keys()]; const newComponents = [...componentRegistry.keys()];
const oldComponents = new Map(buildInfo.components.map(c => [c.fullName, c])); const oldComponents = new Map(buildInfo.components.map(c => [c.id, c]));
let hasNewComponents = false; let hasNewComponents = false;
for (const c of newComponents) { for (const c of newComponents) {
@ -293,42 +287,12 @@ async function checkNewComponents(buildInfo: BuildInfo, componentRegistry: Compo
} }
} }
for (const c of oldComponents.values()) for (const c of oldComponents.values())
componentRegistry.set(c.fullName, c); componentRegistry.set(c.id, c);
return hasNewComponents; return hasNewComponents;
} }
async function parseTestFile(testFile: string): Promise<ComponentInfo[]> { function vitePlugin(registerSource: string, templateDir: string, buildInfo: BuildInfo, importInfos: Map<string, ImportInfo>, depsCollector: Map<string, string[]>): Plugin {
const text = await fs.promises.readFile(testFile, 'utf-8');
const ast = parse(text, { errorRecovery: true, plugins: ['typescript', 'jsx'], sourceType: 'module' });
const componentUsages = collectComponentUsages(ast);
const componentNames = componentUsages.names;
const result: ComponentInfo[] = [];
traverse(ast, {
enter: p => {
if (t.isImportDeclaration(p.node)) {
const importNode = p.node;
if (!t.isStringLiteral(importNode.source))
return;
for (const specifier of importNode.specifiers) {
const specifierName = specifier.local.name;
const componentName = componentNames.has(specifierName) ? specifierName : [...componentNames].find(c => c.startsWith(specifierName + '.'));
if (!componentName)
continue;
if (t.isImportNamespaceSpecifier(specifier))
continue;
result.push(componentInfo(specifier, importNode.source.value, testFile, componentName));
}
}
}
});
return result;
}
function vitePlugin(registerSource: string, templateDir: string, buildInfo: BuildInfo, componentRegistry: ComponentRegistry): Plugin {
buildInfo.sources = {}; buildInfo.sources = {};
let moduleResolver: ResolveFn; let moduleResolver: ResolveFn;
return { return {
@ -368,15 +332,12 @@ function vitePlugin(registerSource: string, templateDir: string, buildInfo: Buil
const lines = [content, '']; const lines = [content, ''];
lines.push(registerSource); lines.push(registerSource);
for (const [alias, value] of componentRegistry) { for (const value of importInfos.values()) {
const importPath = value.isModuleOrAlias ? value.importPath : './' + path.relative(folder, value.importPath).replace(/\\/g, '/'); const importPath = value.isModuleOrAlias ? value.importPath : './' + path.relative(folder, value.importPath).replace(/\\/g, '/');
if (value.importedName) lines.push(`const ${value.id} = () => import('${importPath}').then((mod) => mod.${value.remoteName || 'default'});`);
lines.push(`const ${alias} = () => import('${importPath}').then((mod) => mod.${value.importedName + (value.importedNameProperty || '')});`);
else
lines.push(`const ${alias} = () => import('${importPath}').then((mod) => mod.default${value.importedNameProperty || ''});`);
} }
lines.push(`pwRegister({ ${[...componentRegistry.keys()].join(',\n ')} });`); lines.push(`__pwRegistry.initialize({ ${[...importInfos.keys()].join(',\n ')} });`);
return { return {
code: lines.join('\n'), code: lines.join('\n'),
map: { mappings: '' } map: { mappings: '' }
@ -384,13 +345,13 @@ function vitePlugin(registerSource: string, templateDir: string, buildInfo: Buil
}, },
async writeBundle(this: PluginContext) { async writeBundle(this: PluginContext) {
for (const component of componentRegistry.values()) { for (const importInfo of importInfos.values()) {
const id = (await moduleResolver(component.importPath)); const deps = new Set<string>();
const id = await moduleResolver(importInfo.importPath);
if (!id) if (!id)
continue; continue;
const deps = new Set<string>();
collectViteModuleDependencies(this, id, deps); collectViteModuleDependencies(this, id, deps);
component.deps = [...deps]; depsCollector.set(importInfo.importPath, [...deps]);
} }
}, },
}; };
@ -410,7 +371,7 @@ function collectViteModuleDependencies(context: PluginContext, id: string, deps:
collectViteModuleDependencies(context, importedId, deps); collectViteModuleDependencies(context, importedId, deps);
} }
function hasJSComponents(components: ComponentInfo[]): boolean { function hasJSComponents(components: ImportInfo[]): boolean {
for (const component of components) { for (const component of components) {
const extname = path.extname(component.importPath); const extname = path.extname(component.importPath);
if (extname === '.js' || !extname && fs.existsSync(component.importPath + '.js')) if (extname === '.js' || !extname && fs.existsSync(component.importPath + '.js'))

View file

@ -14,33 +14,32 @@
* limitations under the License. * limitations under the License.
*/ */
import type { ImportRegistry } from '../src/injected/importRegistry';
type JsonPrimitive = string | number | boolean | null; type JsonPrimitive = string | number | boolean | null;
type JsonValue = JsonPrimitive | JsonObject | JsonArray; type JsonValue = JsonPrimitive | JsonObject | JsonArray;
type JsonArray = JsonValue[]; type JsonArray = JsonValue[];
export type JsonObject = { [Key in string]?: JsonValue }; export type JsonObject = { [Key in string]?: JsonValue };
// JsxComponentChild can be anything, consider cases like: <>{1}</>, <>{null}</>
export type JsxComponentChild = JsxComponent | string | number | boolean | null;
export type JsxComponent = { export type JsxComponent = {
__pw_component_marker: true, __pw_type: 'jsx',
kind: 'jsx', type: any,
type: string,
props: Record<string, any>, props: Record<string, any>,
children?: JsxComponentChild[],
}; };
export type MountOptions = { export type MountOptions = {
props?: Record<string, any>,
slots?: Record<string, string | string[]>,
on?: Record<string, Function>,
hooksConfig?: any, hooksConfig?: any,
}; };
export type ObjectComponent = { export type ObjectComponentOptions = {
__pw_component_marker: true, props?: Record<string, any>;
kind: 'object', slots?: Record<string, string | string[]>;
type: string, on?: Record<string, Function>;
options?: MountOptions };
export type ObjectComponent = ObjectComponentOptions & {
__pw_type: 'object-component',
type: any,
}; };
export type Component = JsxComponent | ObjectComponent; export type Component = JsxComponent | ObjectComponent;
@ -56,5 +55,10 @@ declare global {
__pw_hooks_after_mount?: (<HooksConfig extends JsonObject = JsonObject>( __pw_hooks_after_mount?: (<HooksConfig extends JsonObject = JsonObject>(
params: { hooksConfig?: HooksConfig; [key: string]: any } params: { hooksConfig?: HooksConfig; [key: string]: any }
) => Promise<void>)[]; ) => Promise<void>)[];
__pwRegistry: ImportRegistry;
// Can't start with __pw due to core reuse bindings logic for __pw*.
__ctDispatchFunction: (ordinal: number, args: any[]) => void;
__pwUnwrapObject: (value: any) => Promise<any>;
__pwTransformObject: (value: any, mapping: (v: any) => { result: any } | undefined) => any;
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-react", "name": "@playwright/experimental-ct-react",
"version": "1.41.0-next", "version": "1.41.2",
"description": "Playwright Component Testing for React", "description": "Playwright Component Testing for React",
"repository": { "repository": {
"type": "git", "type": "git",
@ -29,7 +29,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.41.0-next", "@playwright/experimental-ct-core": "1.41.2",
"@vitejs/plugin-react": "^4.0.0" "@vitejs/plugin-react": "^4.0.0"
}, },
"bin": { "bin": {

View file

@ -17,130 +17,38 @@
// @ts-check // @ts-check
// This file is injected into the registry as text, no dependencies are allowed. // This file is injected into the registry as text, no dependencies are allowed.
import * as __pwReact from 'react'; import __pwReact from 'react';
import { createRoot as __pwCreateRoot } from 'react-dom/client'; import { createRoot as __pwCreateRoot } from 'react-dom/client';
/** @typedef {import('../playwright-ct-core/types/component').JsxComponentChild} JsxComponentChild */
/** @typedef {import('../playwright-ct-core/types/component').JsxComponent} JsxComponent */ /** @typedef {import('../playwright-ct-core/types/component').JsxComponent} JsxComponent */
/** @typedef {import('react').FunctionComponent} FrameworkComponent */
/** @type {Map<string, () => Promise<FrameworkComponent>>} */
const __pwLoaderRegistry = new Map();
/** @type {Map<string, FrameworkComponent>} */
const __pwRegistry = new Map();
/** @type {Map<Element, import('react-dom/client').Root>} */ /** @type {Map<Element, import('react-dom/client').Root>} */
const __pwRootRegistry = new Map(); const __pwRootRegistry = new Map();
/**
* @param {Record<string, () => Promise<FrameworkComponent>>} components
*/
export function pwRegister(components) {
for (const [name, value] of Object.entries(components))
__pwLoaderRegistry.set(name, value);
}
/** /**
* @param {any} component * @param {any} component
* @returns {component is JsxComponent} * @returns {component is JsxComponent}
*/ */
function isComponent(component) { function isJsxComponent(component) {
return component.__pw_component_marker === true && component.kind === 'jsx'; return typeof component === 'object' && component && component.__pw_type === 'jsx';
} }
/** /**
* @param {JsxComponent | JsxComponentChild} component * @param {any} value
*/ */
async function __pwResolveComponent(component) { function __pwRender(value) {
if (!isComponent(component)) return window.__pwTransformObject(value, v => {
return; if (isJsxComponent(v)) {
const component = v;
let componentFactory = __pwLoaderRegistry.get(component.type); const props = component.props ? __pwRender(component.props) : {};
if (!componentFactory) { return { result: __pwReact.createElement(/** @type { any } */ (component.type), { ...props, children: undefined }, props.children) };
// Lookup by shorthand.
for (const [name, value] of __pwLoaderRegistry) {
if (component.type.endsWith(`_${name}`)) {
componentFactory = value;
break;
} }
}
}
if (!componentFactory && component.type[0].toUpperCase() === component.type[0])
throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...__pwRegistry.keys()]}`);
if (componentFactory)
__pwRegistry.set(component.type, await componentFactory());
if (component.children?.length)
await Promise.all(component.children.map(child => __pwResolveComponent(child)));
if (component.props)
await __resolveProps(component.props);
}
/**
* @param {Record<string, any>} props
*/
async function __resolveProps(props) {
for (const prop of Object.values(props)) {
if (Array.isArray(prop))
await Promise.all(prop.map(child => __pwResolveComponent(child)));
else if (isComponent(prop))
await __pwResolveComponent(prop);
else if (typeof prop === 'object' && prop !== null)
await __resolveProps(prop);
}
}
/**
* @param {JsxComponentChild} child
*/
function __renderChild(child) {
if (Array.isArray(child))
return child.map(grandChild => __renderChild(grandChild));
if (isComponent(child))
return __pwRender(child);
return child;
}
/**
* @param {Record<string, any>} props
*/
function __renderProps(props) {
const newProps = {};
for (const [key, prop] of Object.entries(props)) {
if (Array.isArray(prop))
newProps[key] = prop.map(child => __renderChild(child));
else if (isComponent(prop))
newProps[key] = __renderChild(prop);
else if (typeof prop === 'object' && prop !== null)
newProps[key] = __renderProps(prop);
else
newProps[key] = prop;
}
return newProps;
}
/**
* @param {JsxComponent} component
*/
function __pwRender(component) {
const componentFunc = __pwRegistry.get(component.type);
const props = __renderProps(component.props || {});
const children = component.children?.map(child => __renderChild(child)).filter(child => {
if (typeof child === 'string')
return !!child.trim();
return true;
}); });
const reactChildren = Array.isArray(children) && children.length === 1 ? children[0] : children;
return __pwReact.createElement(componentFunc || component.type, props, reactChildren);
} }
window.playwrightMount = async (component, rootElement, hooksConfig) => { window.playwrightMount = async (component, rootElement, hooksConfig) => {
if (component.kind !== 'jsx') if (!isJsxComponent(component))
throw new Error('Object mount notation is not supported'); throw new Error('Object mount notation is not supported');
await __pwResolveComponent(component);
let App = () => __pwRender(component); let App = () => __pwRender(component);
for (const hook of window.__pw_hooks_before_mount || []) { for (const hook of window.__pw_hooks_before_mount || []) {
const wrapper = await hook({ App, hooksConfig }); const wrapper = await hook({ App, hooksConfig });
@ -171,10 +79,9 @@ window.playwrightUnmount = async rootElement => {
}; };
window.playwrightUpdate = async (rootElement, component) => { window.playwrightUpdate = async (rootElement, component) => {
if (component.kind !== 'jsx') if (!isJsxComponent(component))
throw new Error('Object mount notation is not supported'); throw new Error('Object mount notation is not supported');
await __pwResolveComponent(component);
const root = __pwRootRegistry.get(rootElement); const root = __pwRootRegistry.get(rootElement);
if (root === undefined) if (root === undefined)
throw new Error('Component was not mounted'); throw new Error('Component was not mounted');

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-react17", "name": "@playwright/experimental-ct-react17",
"version": "1.41.0-next", "version": "1.41.2",
"description": "Playwright Component Testing for React", "description": "Playwright Component Testing for React",
"repository": { "repository": {
"type": "git", "type": "git",
@ -29,7 +29,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.41.0-next", "@playwright/experimental-ct-core": "1.41.2",
"@vitejs/plugin-react": "^4.0.0" "@vitejs/plugin-react": "^4.0.0"
}, },
"bin": { "bin": {

View file

@ -17,93 +17,35 @@
// @ts-check // @ts-check
// This file is injected into the registry as text, no dependencies are allowed. // This file is injected into the registry as text, no dependencies are allowed.
// Don't clash with the user land.
import __pwReact from 'react'; import __pwReact from 'react';
import __pwReactDOM from 'react-dom'; import __pwReactDOM from 'react-dom';
/** @typedef {import('../playwright-ct-core/types/component').JsxComponentChild} JsxComponentChild */
/** @typedef {import('../playwright-ct-core/types/component').JsxComponent} JsxComponent */ /** @typedef {import('../playwright-ct-core/types/component').JsxComponent} JsxComponent */
/** @typedef {import('react').FunctionComponent} FrameworkComponent */
/** @type {Map<string, () => Promise<FrameworkComponent>>} */
const __pwLoaderRegistry = new Map();
/** @type {Map<string, FrameworkComponent>} */
const __pwRegistry = new Map();
/**
* @param {{[key: string]: () => Promise<FrameworkComponent>}} components
*/
export function pwRegister(components) {
for (const [name, value] of Object.entries(components))
__pwLoaderRegistry.set(name, value);
}
/** /**
* @param {any} component * @param {any} component
* @returns {component is JsxComponent} * @returns {component is JsxComponent}
*/ */
function isComponent(component) { function isJsxComponent(component) {
return !(typeof component !== 'object' || Array.isArray(component)); return typeof component === 'object' && component && component.__pw_type === 'jsx';
} }
/** /**
* @param {JsxComponent | JsxComponentChild} component * @param {any} value
*/ */
async function __pwResolveComponent(component) { function __pwRender(value) {
if (!isComponent(component)) return window.__pwTransformObject(value, v => {
return; if (isJsxComponent(v)) {
const component = v;
let componentFactory = __pwLoaderRegistry.get(component.type); const props = component.props ? __pwRender(component.props) : {};
if (!componentFactory) { return { result: __pwReact.createElement(/** @type { any } */ (component.type), { ...props, children: undefined }, props.children) };
// Lookup by shorthand.
for (const [name, value] of __pwLoaderRegistry) {
if (component.type.endsWith(`_${name}`)) {
componentFactory = value;
break;
} }
}
}
if (!componentFactory && component.type[0].toUpperCase() === component.type[0])
throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...__pwRegistry.keys()]}`);
if (componentFactory)
__pwRegistry.set(component.type, await componentFactory());
if (component.children?.length)
await Promise.all(component.children.map(child => __pwResolveComponent(child)));
}
/**
* @param {JsxComponentChild} child
*/
function __renderChild(child) {
if (Array.isArray(child))
return child.map(grandChild => __renderChild(grandChild));
if (isComponent(child))
return __pwRender(child);
return child;
}
/**
* @param {JsxComponent} component
*/
function __pwRender(component) {
const componentFunc = __pwRegistry.get(component.type);
const children = component.children?.map(child => __renderChild(child)).filter(child => {
if (typeof child === 'string')
return !!child.trim();
return true;
}); });
const reactChildren = Array.isArray(children) && children.length === 1 ? children[0] : children;
return __pwReact.createElement(componentFunc || component.type, component.props, reactChildren);
} }
window.playwrightMount = async (component, rootElement, hooksConfig) => { window.playwrightMount = async (component, rootElement, hooksConfig) => {
if (component.kind !== 'jsx') if (!isJsxComponent(component))
throw new Error('Object mount notation is not supported'); throw new Error('Object mount notation is not supported');
await __pwResolveComponent(component);
let App = () => __pwRender(component); let App = () => __pwRender(component);
for (const hook of window.__pw_hooks_before_mount || []) { for (const hook of window.__pw_hooks_before_mount || []) {
const wrapper = await hook({ App, hooksConfig }); const wrapper = await hook({ App, hooksConfig });
@ -123,9 +65,8 @@ window.playwrightUnmount = async rootElement => {
}; };
window.playwrightUpdate = async (rootElement, component) => { window.playwrightUpdate = async (rootElement, component) => {
if (component.kind !== 'jsx') if (!isJsxComponent(component))
throw new Error('Object mount notation is not supported'); throw new Error('Object mount notation is not supported');
await __pwResolveComponent(component);
__pwReactDOM.render(__pwRender(component), rootElement); __pwReactDOM.render(__pwRender(component), rootElement);
}; };

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-solid", "name": "@playwright/experimental-ct-solid",
"version": "1.41.0-next", "version": "1.41.2",
"description": "Playwright Component Testing for Solid", "description": "Playwright Component Testing for Solid",
"repository": { "repository": {
"type": "git", "type": "git",
@ -29,7 +29,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.41.0-next", "@playwright/experimental-ct-core": "1.41.2",
"vite-plugin-solid": "^2.7.0" "vite-plugin-solid": "^2.7.0"
}, },
"devDependencies": { "devDependencies": {

View file

@ -20,94 +20,62 @@
import { render as __pwSolidRender, createComponent as __pwSolidCreateComponent } from 'solid-js/web'; import { render as __pwSolidRender, createComponent as __pwSolidCreateComponent } from 'solid-js/web';
import __pwH from 'solid-js/h'; import __pwH from 'solid-js/h';
/** @typedef {import('../playwright-ct-core/types/component').JsxComponentChild} JsxComponentChild */
/** @typedef {import('../playwright-ct-core/types/component').JsxComponent} JsxComponent */ /** @typedef {import('../playwright-ct-core/types/component').JsxComponent} JsxComponent */
/** @typedef {() => import('solid-js').JSX.Element} FrameworkComponent */ /** @typedef {() => import('solid-js').JSX.Element} FrameworkComponent */
/** @type {Map<string, () => Promise<FrameworkComponent>>} */
const __pwLoaderRegistry = new Map();
/** @type {Map<string, FrameworkComponent>} */
const __pwRegistry = new Map();
/**
* @param {{[key: string]: () => Promise<FrameworkComponent>}} components
*/
export function pwRegister(components) {
for (const [name, value] of Object.entries(components))
__pwLoaderRegistry.set(name, value);
}
/** /**
* @param {any} component * @param {any} component
* @returns {component is JsxComponent} * @returns {component is JsxComponent}
*/ */
function isComponent(component) { function isJsxComponent(component) {
return !(typeof component !== 'object' || Array.isArray(component)); return typeof component === 'object' && component && component.__pw_type === 'jsx';
} }
/** /**
* @param {JsxComponent | JsxComponentChild} component * @param {any} child
*/
async function __pwResolveComponent(component) {
if (!isComponent(component))
return;
let componentFactory = __pwLoaderRegistry.get(component.type);
if (!componentFactory) {
// Lookup by shorthand.
for (const [name, value] of __pwLoaderRegistry) {
if (component.type.endsWith(`_${name}`)) {
componentFactory = value;
break;
}
}
}
if (!componentFactory && component.type[0].toUpperCase() === component.type[0])
throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...__pwRegistry.keys()]}`);
if (componentFactory)
__pwRegistry.set(component.type, await componentFactory());
if (component.children?.length)
await Promise.all(component.children.map(child => __pwResolveComponent(child)));
}
/**
* @param {JsxComponentChild} child
*/ */
function __pwCreateChild(child) { function __pwCreateChild(child) {
if (Array.isArray(child)) if (Array.isArray(child))
return child.map(grandChild => __pwCreateChild(grandChild)); return child.map(grandChild => __pwCreateChild(grandChild));
if (isComponent(child)) if (isJsxComponent(child))
return __pwCreateComponent(child); return __pwCreateComponent(child);
return child; return child;
} }
/**
* @param {JsxComponent} component
* @returns {any[] | undefined}
*/
function __pwJsxChildArray(component) {
if (!component.props.children)
return;
if (Array.isArray(component.props.children))
return component.props.children;
return [component.props.children];
}
/** /**
* @param {JsxComponent} component * @param {JsxComponent} component
*/ */
function __pwCreateComponent(component) { function __pwCreateComponent(component) {
const componentFunc = __pwRegistry.get(component.type); const children = __pwJsxChildArray(component)?.map(child => __pwCreateChild(child)).filter(child => {
const children = component.children?.map(child => __pwCreateChild(child)).filter(child => {
if (typeof child === 'string') if (typeof child === 'string')
return !!child.trim(); return !!child.trim();
return true; return true;
}); });
if (!componentFunc) if (typeof component.type === 'string')
return __pwH(component.type, component.props, children); return __pwH(component.type, component.props, children);
return __pwSolidCreateComponent(componentFunc, { ...component.props, children }); return __pwSolidCreateComponent(component.type, { ...component.props, children });
} }
const __pwUnmountKey = Symbol('unmountKey'); const __pwUnmountKey = Symbol('unmountKey');
window.playwrightMount = async (component, rootElement, hooksConfig) => { window.playwrightMount = async (component, rootElement, hooksConfig) => {
if (component.kind !== 'jsx') if (!isJsxComponent(component))
throw new Error('Object mount notation is not supported'); throw new Error('Object mount notation is not supported');
await __pwResolveComponent(component);
let App = () => __pwCreateComponent(component); let App = () => __pwCreateComponent(component);
for (const hook of window.__pw_hooks_before_mount || []) { for (const hook of window.__pw_hooks_before_mount || []) {
const wrapper = await hook({ App, hooksConfig }); const wrapper = await hook({ App, hooksConfig });
@ -131,7 +99,7 @@ window.playwrightUnmount = async rootElement => {
}; };
window.playwrightUpdate = async (rootElement, component) => { window.playwrightUpdate = async (rootElement, component) => {
if (component.kind !== 'jsx') if (!isJsxComponent(component))
throw new Error('Object mount notation is not supported'); throw new Error('Object mount notation is not supported');
window.playwrightUnmount(rootElement); window.playwrightUnmount(rootElement);

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-svelte", "name": "@playwright/experimental-ct-svelte",
"version": "1.41.0-next", "version": "1.41.2",
"description": "Playwright Component Testing for Svelte", "description": "Playwright Component Testing for Svelte",
"repository": { "repository": {
"type": "git", "type": "git",
@ -29,7 +29,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.41.0-next", "@playwright/experimental-ct-core": "1.41.2",
"@sveltejs/vite-plugin-svelte": "^3.0.1" "@sveltejs/vite-plugin-svelte": "^3.0.1"
}, },
"devDependencies": { "devDependencies": {

View file

@ -25,50 +25,12 @@ import { detach as __pwDetach, insert as __pwInsert, noop as __pwNoop } from 'sv
/** @typedef {any} FrameworkComponent */ /** @typedef {any} FrameworkComponent */
/** @typedef {import('svelte').SvelteComponent} SvelteComponent */ /** @typedef {import('svelte').SvelteComponent} SvelteComponent */
/** @type {Map<string, () => Promise<FrameworkComponent>>} */
const __pwLoaderRegistry = new Map();
/** @type {Map<string, FrameworkComponent>} */
const __pwRegistry = new Map();
/**
* @param {{[key: string]: () => Promise<FrameworkComponent>}} components
*/
export function pwRegister(components) {
for (const [name, value] of Object.entries(components))
__pwLoaderRegistry.set(name, value);
}
/** /**
* @param {any} component * @param {any} component
* @returns {component is ObjectComponent} * @returns {component is ObjectComponent}
*/ */
function isComponent(component) { function isObjectComponent(component) {
return !(typeof component !== 'object' || Array.isArray(component)); return typeof component === 'object' && component && component.__pw_type === 'object-component';
}
/**
* @param {ObjectComponent} component
*/
async function __pwResolveComponent(component) {
if (!isComponent(component))
return;
let componentFactory = __pwLoaderRegistry.get(component.type);
if (!componentFactory) {
// Lookup by shorthand.
for (const [name, value] of __pwLoaderRegistry) {
if (component.type.endsWith(`_${name}_svelte`)) {
componentFactory = value;
break;
}
}
}
if (!componentFactory)
throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...__pwRegistry.keys()]}`);
if (componentFactory)
__pwRegistry.set(component.type, await componentFactory());
} }
/** /**
@ -105,19 +67,19 @@ function __pwCreateSlots(slots) {
const __pwSvelteComponentKey = Symbol('svelteComponent'); const __pwSvelteComponentKey = Symbol('svelteComponent');
window.playwrightMount = async (component, rootElement, hooksConfig) => { window.playwrightMount = async (component, rootElement, hooksConfig) => {
if (component.kind !== 'object') if (!isObjectComponent(component))
throw new Error('JSX mount notation is not supported'); throw new Error('JSX mount notation is not supported');
await __pwResolveComponent(component); const objectComponent = component;
const componentCtor = __pwRegistry.get(component.type); const componentCtor = component.type;
class App extends componentCtor { class App extends componentCtor {
constructor(options = {}) { constructor(options = {}) {
super({ super({
target: rootElement, target: rootElement,
props: { props: {
...component.options?.props, ...objectComponent.props,
$$slots: __pwCreateSlots(component.options?.slots), $$slots: __pwCreateSlots(objectComponent.slots),
$$scope: {}, $$scope: {},
}, },
...options ...options
@ -134,7 +96,7 @@ window.playwrightMount = async (component, rootElement, hooksConfig) => {
rootElement[__pwSvelteComponentKey] = svelteComponent; rootElement[__pwSvelteComponentKey] = svelteComponent;
for (const [key, listener] of Object.entries(component.options?.on || {})) for (const [key, listener] of Object.entries(objectComponent.on || {}))
svelteComponent.$on(key, event => listener(event.detail)); svelteComponent.$on(key, event => listener(event.detail));
for (const hook of window.__pw_hooks_after_mount || []) for (const hook of window.__pw_hooks_after_mount || [])
@ -149,17 +111,16 @@ window.playwrightUnmount = async rootElement => {
}; };
window.playwrightUpdate = async (rootElement, component) => { window.playwrightUpdate = async (rootElement, component) => {
if (component.kind !== 'object') if (!isObjectComponent(component))
throw new Error('JSX mount notation is not supported'); throw new Error('JSX mount notation is not supported');
await __pwResolveComponent(component);
const svelteComponent = /** @type {SvelteComponent} */ (rootElement[__pwSvelteComponentKey]); const svelteComponent = /** @type {SvelteComponent} */ (rootElement[__pwSvelteComponentKey]);
if (!svelteComponent) if (!svelteComponent)
throw new Error('Component was not mounted'); throw new Error('Component was not mounted');
for (const [key, listener] of Object.entries(component.options?.on || {})) for (const [key, listener] of Object.entries(component.on || {}))
svelteComponent.$on(key, event => listener(event.detail)); svelteComponent.$on(key, event => listener(event.detail));
if (component.options?.props) if (component.props)
svelteComponent.$set(component.options.props); svelteComponent.$set(component.props);
}; };

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-vue", "name": "@playwright/experimental-ct-vue",
"version": "1.41.0-next", "version": "1.41.2",
"description": "Playwright Component Testing for Vue", "description": "Playwright Component Testing for Vue",
"repository": { "repository": {
"type": "git", "type": "git",
@ -29,7 +29,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.41.0-next", "@playwright/experimental-ct-core": "1.41.2",
"@vitejs/plugin-vue": "^4.2.1" "@vitejs/plugin-vue": "^4.2.1"
}, },
"bin": { "bin": {

View file

@ -22,69 +22,35 @@ import { compile as __pwCompile } from '@vue/compiler-dom';
import * as __pwVue from 'vue'; import * as __pwVue from 'vue';
/** @typedef {import('../playwright-ct-core/types/component').Component} Component */ /** @typedef {import('../playwright-ct-core/types/component').Component} Component */
/** @typedef {import('../playwright-ct-core/types/component').JsxComponentChild} JsxComponentChild */
/** @typedef {import('../playwright-ct-core/types/component').JsxComponent} JsxComponent */ /** @typedef {import('../playwright-ct-core/types/component').JsxComponent} JsxComponent */
/** @typedef {import('../playwright-ct-core/types/component').ObjectComponent} ObjectComponent */ /** @typedef {import('../playwright-ct-core/types/component').ObjectComponent} ObjectComponent */
/** @typedef {import('vue').Component} FrameworkComponent */ /** @typedef {import('vue').Component} FrameworkComponent */
/** @type {Map<string, () => Promise<FrameworkComponent>>} */ const __pwAllListeners = new Map();
const __pwLoaderRegistry = new Map();
/** @type {Map<string, FrameworkComponent>} */
const __pwRegistry = new Map();
/** /**
* @param {{[key: string]: () => Promise<FrameworkComponent>}} components * @param {any} component
* @returns {component is ObjectComponent}
*/ */
export function pwRegister(components) { function isObjectComponent(component) {
for (const [name, value] of Object.entries(components)) return typeof component === 'object' && component && component.__pw_type === 'object-component';
__pwLoaderRegistry.set(name, value);
} }
/** /**
* @param {any} component * @param {any} component
* @returns {component is Component} * @returns {component is JsxComponent}
*/ */
function isComponent(component) { function isJsxComponent(component) {
return !(typeof component !== 'object' || Array.isArray(component)); return typeof component === 'object' && component && component.__pw_type === 'jsx';
} }
/** /**
* @param {Component | JsxComponentChild} component * @param {any} child
*/
async function __pwResolveComponent(component) {
if (!isComponent(component))
return;
let componentFactory = __pwLoaderRegistry.get(component.type);
if (!componentFactory) {
// Lookup by shorthand.
for (const [name, value] of __pwLoaderRegistry) {
if (component.type.endsWith(`_${name}_vue`)) {
componentFactory = value;
break;
}
}
}
if (!componentFactory && component.type[0].toUpperCase() === component.type[0])
throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...__pwRegistry.keys()]}`);
if (componentFactory)
__pwRegistry.set(component.type, await componentFactory());
if ('children' in component && component.children?.length)
await Promise.all(component.children.map(child => __pwResolveComponent(child)));
}
const __pwAllListeners = new Map();
/**
* @param {JsxComponentChild} child
*/ */
function __pwCreateChild(child) { function __pwCreateChild(child) {
if (Array.isArray(child)) if (Array.isArray(child))
return child.map(grandChild => __pwCreateChild(grandChild)); return child.map(grandChild => __pwCreateChild(grandChild));
if (isComponent(child)) if (isJsxComponent(child) || isObjectComponent(child))
return __pwCreateWrapper(child); return __pwCreateWrapper(child);
return child; return child;
} }
@ -132,14 +98,23 @@ function __pwSlotToFunction(slot) {
throw Error(`Invalid slot received.`); throw Error(`Invalid slot received.`);
} }
/**
* @param {JsxComponent} component
* @returns {any[] | undefined}
*/
function __pwJsxChildArray(component) {
if (!component.props.children)
return;
if (Array.isArray(component.props.children))
return component.props.children;
return [component.props.children];
}
/** /**
* @param {Component} component * @param {Component} component
*/ */
function __pwCreateComponent(component) { function __pwCreateComponent(component) {
let componentFunc = __pwRegistry.get(component.type); const isVueComponent = typeof component.type !== 'string';
componentFunc = componentFunc || component.type;
const isVueComponent = componentFunc !== component.type;
/** /**
* @type {(import('vue').VNode | string)[]} * @type {(import('vue').VNode | string)[]}
@ -151,12 +126,12 @@ function __pwCreateComponent(component) {
/** @type {{[key: string]: any}} */ /** @type {{[key: string]: any}} */
let props = {}; let props = {};
if (component.kind === 'jsx') { if (component.__pw_type === 'jsx') {
for (const child of component.children || []) { for (const child of __pwJsxChildArray(component) || []) {
if (typeof child !== 'string' && child.type === 'template' && child.kind === 'jsx') { if (isJsxComponent(child) && child.type === 'template') {
const slotProperty = Object.keys(child.props).find(k => k.startsWith('v-slot:')); const slotProperty = Object.keys(child.props).find(k => k.startsWith('v-slot:'));
const slot = slotProperty ? slotProperty.substring('v-slot:'.length) : 'default'; const slot = slotProperty ? slotProperty.substring('v-slot:'.length) : 'default';
slots[slot] = child.children?.map(__pwCreateChild); slots[slot] = __pwJsxChildArray(child)?.map(__pwCreateChild);
} else { } else {
children.push(__pwCreateChild(child)); children.push(__pwCreateChild(child));
} }
@ -175,16 +150,16 @@ function __pwCreateComponent(component) {
} }
} }
if (component.kind === 'object') { if (component.__pw_type === 'object-component') {
// Vue test util syntax. // Vue test util syntax.
for (const [key, value] of Object.entries(component.options?.slots || {})) { for (const [key, value] of Object.entries(component.slots || {})) {
if (key === 'default') if (key === 'default')
children.push(__pwSlotToFunction(value)); children.push(__pwSlotToFunction(value));
else else
slots[key] = __pwSlotToFunction(value); slots[key] = __pwSlotToFunction(value);
} }
props = component.options?.props || {}; props = component.props || {};
for (const [key, value] of Object.entries(component.options?.on || {})) for (const [key, value] of Object.entries(component.on || {}))
listeners[key] = value; listeners[key] = value;
} }
@ -197,7 +172,7 @@ function __pwCreateComponent(component) {
lastArg = children; lastArg = children;
} }
return { Component: componentFunc, props, slots: lastArg, listeners }; return { Component: component.type, props, slots: lastArg, listeners };
} }
function __pwWrapFunctions(slots) { function __pwWrapFunctions(slots) {
@ -248,7 +223,6 @@ const __pwAppKey = Symbol('appKey');
const __pwWrapperKey = Symbol('wrapperKey'); const __pwWrapperKey = Symbol('wrapperKey');
window.playwrightMount = async (component, rootElement, hooksConfig) => { window.playwrightMount = async (component, rootElement, hooksConfig) => {
await __pwResolveComponent(component);
const app = __pwCreateApp({ const app = __pwCreateApp({
render: () => { render: () => {
const wrapper = __pwCreateWrapper(component); const wrapper = __pwCreateWrapper(component);
@ -275,7 +249,6 @@ window.playwrightUnmount = async rootElement => {
}; };
window.playwrightUpdate = async (rootElement, component) => { window.playwrightUpdate = async (rootElement, component) => {
await __pwResolveComponent(component);
const wrapper = rootElement[__pwWrapperKey]; const wrapper = rootElement[__pwWrapperKey];
if (!wrapper) if (!wrapper)
throw new Error('Component was not mounted'); throw new Error('Component was not mounted');

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-vue2", "name": "@playwright/experimental-ct-vue2",
"version": "1.41.0-next", "version": "1.41.2",
"description": "Playwright Component Testing for Vue2", "description": "Playwright Component Testing for Vue2",
"repository": { "repository": {
"type": "git", "type": "git",
@ -29,7 +29,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.41.0-next", "@playwright/experimental-ct-core": "1.41.2",
"@vitejs/plugin-vue2": "^2.2.0" "@vitejs/plugin-vue2": "^2.2.0"
}, },
"devDependencies": { "devDependencies": {

View file

@ -21,67 +21,33 @@
import __pwVue, { h as __pwH } from 'vue'; import __pwVue, { h as __pwH } from 'vue';
/** @typedef {import('../playwright-ct-core/types/component').Component} Component */ /** @typedef {import('../playwright-ct-core/types/component').Component} Component */
/** @typedef {import('../playwright-ct-core/types/component').JsxComponentChild} JsxComponentChild */
/** @typedef {import('../playwright-ct-core/types/component').JsxComponent} JsxComponent */ /** @typedef {import('../playwright-ct-core/types/component').JsxComponent} JsxComponent */
/** @typedef {import('../playwright-ct-core/types/component').ObjectComponent} ObjectComponent */ /** @typedef {import('../playwright-ct-core/types/component').ObjectComponent} ObjectComponent */
/** @typedef {import('vue').Component} FrameworkComponent */ /** @typedef {import('vue').Component} FrameworkComponent */
/** @type {Map<string, () => Promise<FrameworkComponent>>} */
const __pwLoaderRegistry = new Map();
/** @type {Map<string, FrameworkComponent>} */
const __pwRegistry = new Map();
/** /**
* @param {{[key: string]: () => Promise<FrameworkComponent>}} components * @param {any} component
* @returns {component is ObjectComponent}
*/ */
export function pwRegister(components) { function isObjectComponent(component) {
for (const [name, value] of Object.entries(components)) return typeof component === 'object' && component && component.__pw_type === 'object-component';
__pwLoaderRegistry.set(name, value);
} }
/** /**
* @param {any} component * @param {any} component
* @returns {component is Component} * @returns {component is JsxComponent}
*/ */
function isComponent(component) { function isJsxComponent(component) {
return !(typeof component !== 'object' || Array.isArray(component)); return typeof component === 'object' && component && component.__pw_type === 'jsx';
} }
/** /**
* @param {Component | JsxComponentChild} component * @param {any} child
*/
async function __pwResolveComponent(component) {
if (!isComponent(component))
return;
let componentFactory = __pwLoaderRegistry.get(component.type);
if (!componentFactory) {
// Lookup by shorthand.
for (const [name, value] of __pwLoaderRegistry) {
if (component.type.endsWith(`_${name}_vue`)) {
componentFactory = value;
break;
}
}
}
if (!componentFactory && component.type[0].toUpperCase() === component.type[0])
throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...__pwRegistry.keys()]}`);
if (componentFactory)
__pwRegistry.set(component.type, await componentFactory());
if ('children' in component && component.children?.length)
await Promise.all(component.children.map(child => __pwResolveComponent(child)));
}
/**
* @param {Component | JsxComponentChild} child
*/ */
function __pwCreateChild(child) { function __pwCreateChild(child) {
if (Array.isArray(child)) if (Array.isArray(child))
return child.map(grandChild => __pwCreateChild(grandChild)); return child.map(grandChild => __pwCreateChild(grandChild));
if (isComponent(child)) if (isJsxComponent(child) || isObjectComponent(child))
return __pwCreateWrapper(child); return __pwCreateWrapper(child);
return child; return child;
} }
@ -94,18 +60,26 @@ function __pwCreateChild(child) {
* @return {boolean} * @return {boolean}
*/ */
function __pwComponentHasKeyInProps(Component, key) { function __pwComponentHasKeyInProps(Component, key) {
if (Array.isArray(Component.props)) return typeof Component.props === 'object' && Component.props && key in Component.props;
return Component.props.includes(key); }
return Object.entries(Component.props).flat().includes(key); /**
* @param {JsxComponent} component
* @returns {any[] | undefined}
*/
function __pwJsxChildArray(component) {
if (!component.props.children)
return;
if (Array.isArray(component.props.children))
return component.props.children;
return [component.props.children];
} }
/** /**
* @param {Component} component * @param {Component} component
*/ */
function __pwCreateComponent(component) { function __pwCreateComponent(component) {
const componentFunc = __pwRegistry.get(component.type) || component.type; const isVueComponent = typeof component.type !== 'string';
const isVueComponent = componentFunc !== component.type;
/** /**
* @type {(import('vue').VNode | string)[]} * @type {(import('vue').VNode | string)[]}
@ -119,12 +93,12 @@ function __pwCreateComponent(component) {
nodeData.scopedSlots = {}; nodeData.scopedSlots = {};
nodeData.on = {}; nodeData.on = {};
if (component.kind === 'jsx') { if (component.__pw_type === 'jsx') {
for (const child of component.children || []) { for (const child of __pwJsxChildArray(component) || []) {
if (typeof child !== 'string' && child.type === 'template' && child.kind === 'jsx') { if (isJsxComponent(child) && child.type === 'template') {
const slotProperty = Object.keys(child.props).find(k => k.startsWith('v-slot:')); const slotProperty = Object.keys(child.props).find(k => k.startsWith('v-slot:'));
const slot = slotProperty ? slotProperty.substring('v-slot:'.length) : 'default'; const slot = slotProperty ? slotProperty.substring('v-slot:'.length) : 'default';
nodeData.scopedSlots[slot] = () => child.children?.map(c => __pwCreateChild(c)); nodeData.scopedSlots[slot] = () => __pwJsxChildArray(child)?.map(c => __pwCreateChild(c));
} else { } else {
children.push(__pwCreateChild(child)); children.push(__pwCreateChild(child));
} }
@ -135,7 +109,7 @@ function __pwCreateComponent(component) {
const event = key.substring('v-on:'.length); const event = key.substring('v-on:'.length);
nodeData.on[event] = value; nodeData.on[event] = value;
} else { } else {
if (isVueComponent && __pwComponentHasKeyInProps(componentFunc, key)) if (isVueComponent && __pwComponentHasKeyInProps(component.type, key))
nodeData.props[key] = value; nodeData.props[key] = value;
else else
nodeData.attrs[key] = value; nodeData.attrs[key] = value;
@ -143,18 +117,17 @@ function __pwCreateComponent(component) {
} }
} }
if (component.kind === 'object') { if (component.__pw_type === 'object-component') {
// Vue test util syntax. // Vue test util syntax.
const options = component.options || {}; for (const [key, value] of Object.entries(component.slots || {})) {
for (const [key, value] of Object.entries(options.slots || {})) {
const list = (Array.isArray(value) ? value : [value]).map(v => __pwCreateChild(v)); const list = (Array.isArray(value) ? value : [value]).map(v => __pwCreateChild(v));
if (key === 'default') if (key === 'default')
children.push(...list); children.push(...list);
else else
nodeData.scopedSlots[key] = () => list; nodeData.scopedSlots[key] = () => list;
} }
nodeData.props = options.props || {}; nodeData.props = component.props || {};
for (const [key, value] of Object.entries(options.on || {})) for (const [key, value] of Object.entries(component.on || {}))
nodeData.on[key] = value; nodeData.on[key] = value;
} }
@ -167,7 +140,7 @@ function __pwCreateComponent(component) {
lastArg = children; lastArg = children;
} }
return { Component: componentFunc, nodeData, slots: lastArg }; return { Component: component.type, nodeData, slots: lastArg };
} }
/** /**
@ -184,7 +157,6 @@ const instanceKey = Symbol('instanceKey');
const wrapperKey = Symbol('wrapperKey'); const wrapperKey = Symbol('wrapperKey');
window.playwrightMount = async (component, rootElement, hooksConfig) => { window.playwrightMount = async (component, rootElement, hooksConfig) => {
await __pwResolveComponent(component);
let options = {}; let options = {};
for (const hook of window.__pw_hooks_before_mount || []) for (const hook of window.__pw_hooks_before_mount || [])
options = await hook({ hooksConfig, Vue: __pwVue }); options = await hook({ hooksConfig, Vue: __pwVue });
@ -213,7 +185,6 @@ window.playwrightUnmount = async rootElement => {
}; };
window.playwrightUpdate = async (element, options) => { window.playwrightUpdate = async (element, options) => {
await __pwResolveComponent(options);
const wrapper = /** @type {any} */(element)[wrapperKey]; const wrapper = /** @type {any} */(element)[wrapperKey];
if (!wrapper) if (!wrapper)
throw new Error('Component was not mounted'); throw new Error('Component was not mounted');

View file

@ -1,6 +1,6 @@
{ {
"name": "playwright-firefox", "name": "playwright-firefox",
"version": "1.41.0-next", "version": "1.41.2",
"description": "A high-level API to automate Firefox", "description": "A high-level API to automate Firefox",
"repository": { "repository": {
"type": "git", "type": "git",
@ -30,6 +30,6 @@
"install": "node install.js" "install": "node install.js"
}, },
"dependencies": { "dependencies": {
"playwright-core": "1.41.0-next" "playwright-core": "1.41.2"
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/test", "name": "@playwright/test",
"version": "1.41.0-next", "version": "1.41.2",
"description": "A high-level API to automate web browsers", "description": "A high-level API to automate web browsers",
"repository": { "repository": {
"type": "git", "type": "git",
@ -30,6 +30,6 @@
}, },
"scripts": {}, "scripts": {},
"dependencies": { "dependencies": {
"playwright": "1.41.0-next" "playwright": "1.41.2"
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "playwright-webkit", "name": "playwright-webkit",
"version": "1.41.0-next", "version": "1.41.2",
"description": "A high-level API to automate WebKit", "description": "A high-level API to automate WebKit",
"repository": { "repository": {
"type": "git", "type": "git",
@ -30,6 +30,6 @@
"install": "node install.js" "install": "node install.js"
}, },
"dependencies": { "dependencies": {
"playwright-core": "1.41.0-next" "playwright-core": "1.41.2"
} }
} }

View file

@ -66,6 +66,7 @@ function babelTransformOptions(isTypeScript: boolean, isModule: boolean, plugins
// Support JSX/TSX at all times, regardless of the file extension. // Support JSX/TSX at all times, regardless of the file extension.
plugins.push([require('@babel/plugin-transform-react-jsx'), { plugins.push([require('@babel/plugin-transform-react-jsx'), {
throwIfNamespace: false,
runtime: 'automatic', runtime: 'automatic',
importSource: path.dirname(require.resolve('playwright')), importSource: path.dirname(require.resolve('playwright')),
}]); }]);

View file

@ -16,6 +16,7 @@
function jsx(type, props) { function jsx(type, props) {
return { return {
__pw_type: 'jsx',
type, type,
props, props,
}; };
@ -23,6 +24,7 @@ function jsx(type, props) {
function jsxs(type, props) { function jsxs(type, props) {
return { return {
__pw_type: 'jsx',
type, type,
props, props,
}; };

View file

@ -1,6 +1,6 @@
{ {
"name": "playwright", "name": "playwright",
"version": "1.41.0-next", "version": "1.41.2",
"description": "A high-level API to automate web browsers", "description": "A high-level API to automate web browsers",
"repository": { "repository": {
"type": "git", "type": "git",
@ -24,6 +24,7 @@
"./lib/transform/babelBundle": "./lib/transform/babelBundle.js", "./lib/transform/babelBundle": "./lib/transform/babelBundle.js",
"./lib/transform/compilationCache": "./lib/transform/compilationCache.js", "./lib/transform/compilationCache": "./lib/transform/compilationCache.js",
"./lib/transform/esmLoader": "./lib/transform/esmLoader.js", "./lib/transform/esmLoader": "./lib/transform/esmLoader.js",
"./lib/transform/transform": "./lib/transform/transform.js",
"./lib/internalsForTest": "./lib/internalsForTest.js", "./lib/internalsForTest": "./lib/internalsForTest.js",
"./lib/plugins": "./lib/plugins/index.js", "./lib/plugins": "./lib/plugins/index.js",
"./jsx-runtime": { "./jsx-runtime": {
@ -54,7 +55,7 @@
}, },
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.41.0-next" "playwright-core": "1.41.2"
}, },
"optionalDependencies": { "optionalDependencies": {
"fsevents": "2.3.2" "fsevents": "2.3.2"

View file

@ -23,9 +23,17 @@ import { isWorkerProcess } from '../common/globals';
export type MemoryCache = { export type MemoryCache = {
codePath: string; codePath: string;
sourceMapPath: string; sourceMapPath: string;
dataPath: string;
moduleUrl?: string; moduleUrl?: string;
}; };
type SerializedCompilationCache = {
sourceMaps: [string, string][],
memoryCache: [string, MemoryCache][],
fileDependencies: [string, string[]][],
externalDependencies: [string, string[]][],
};
// Assumptions for the compilation cache: // Assumptions for the compilation cache:
// - Files in the temp directory we work with can disappear at any moment, either some of them or all together. // - Files in the temp directory we work with can disappear at any moment, either some of them or all together.
// - Multiple workers can be trying to read from the compilation cache at the same time. // - Multiple workers can be trying to read from the compilation cache at the same time.
@ -84,12 +92,12 @@ export function installSourceMapSupportIfNeeded() {
}); });
} }
function _innerAddToCompilationCache(filename: string, options: { codePath: string, sourceMapPath: string, moduleUrl?: string }) { function _innerAddToCompilationCache(filename: string, entry: MemoryCache) {
sourceMaps.set(options.moduleUrl || filename, options.sourceMapPath); sourceMaps.set(entry.moduleUrl || filename, entry.sourceMapPath);
memoryCache.set(filename, options); memoryCache.set(filename, entry);
} }
export function getFromCompilationCache(filename: string, hash: string, moduleUrl?: string): { cachedCode?: string, addToCache?: (code: string, map?: any) => void } { export function getFromCompilationCache(filename: string, hash: string, moduleUrl?: string): { cachedCode?: string, addToCache?: (code: string, map: any | undefined | null, data: Map<string, any>) => void } {
// First check the memory cache by filename, this cache will always work in the worker, // First check the memory cache by filename, this cache will always work in the worker,
// because we just compiled this file in the loader. // because we just compiled this file in the loader.
const cache = memoryCache.get(filename); const cache = memoryCache.get(filename);
@ -105,27 +113,30 @@ export function getFromCompilationCache(filename: string, hash: string, moduleUr
const cachePath = calculateCachePath(filename, hash); const cachePath = calculateCachePath(filename, hash);
const codePath = cachePath + '.js'; const codePath = cachePath + '.js';
const sourceMapPath = cachePath + '.map'; const sourceMapPath = cachePath + '.map';
const dataPath = cachePath + '.data';
try { try {
const cachedCode = fs.readFileSync(codePath, 'utf8'); const cachedCode = fs.readFileSync(codePath, 'utf8');
_innerAddToCompilationCache(filename, { codePath, sourceMapPath, moduleUrl }); _innerAddToCompilationCache(filename, { codePath, sourceMapPath, dataPath, moduleUrl });
return { cachedCode }; return { cachedCode };
} catch { } catch {
} }
return { return {
addToCache: (code: string, map: any) => { addToCache: (code: string, map: any | undefined | null, data: Map<string, any>) => {
if (isWorkerProcess()) if (isWorkerProcess())
return; return;
fs.mkdirSync(path.dirname(cachePath), { recursive: true }); fs.mkdirSync(path.dirname(cachePath), { recursive: true });
if (map) if (map)
fs.writeFileSync(sourceMapPath, JSON.stringify(map), 'utf8'); fs.writeFileSync(sourceMapPath, JSON.stringify(map), 'utf8');
if (data.size)
fs.writeFileSync(dataPath, JSON.stringify(Object.fromEntries(data.entries()), undefined, 2), 'utf8');
fs.writeFileSync(codePath, code, 'utf8'); fs.writeFileSync(codePath, code, 'utf8');
_innerAddToCompilationCache(filename, { codePath, sourceMapPath, moduleUrl }); _innerAddToCompilationCache(filename, { codePath, sourceMapPath, dataPath, moduleUrl });
} }
}; };
} }
export function serializeCompilationCache(): any { export function serializeCompilationCache(): SerializedCompilationCache {
return { return {
sourceMaps: [...sourceMaps.entries()], sourceMaps: [...sourceMaps.entries()],
memoryCache: [...memoryCache.entries()], memoryCache: [...memoryCache.entries()],
@ -200,6 +211,10 @@ export function collectAffectedTestFiles(dependency: string, testFileCollector:
} }
} }
export function internalDependenciesForTestFile(filename: string): Set<string> | undefined{
return fileDependencies.get(filename);
}
export function dependenciesForTestFile(filename: string): Set<string> { export function dependenciesForTestFile(filename: string): Set<string> {
const result = new Set<string>(); const result = new Set<string>();
for (const dep of fileDependencies.get(filename) || []) for (const dep of fileDependencies.get(filename) || [])
@ -224,3 +239,17 @@ export function belongsToNodeModules(file: string) {
return true; return true;
return false; return false;
} }
export async function getUserData(pluginName: string): Promise<Map<string, any>> {
const result = new Map<string, any>();
for (const [fileName, cache] of memoryCache) {
if (!cache.dataPath)
continue;
if (!fs.existsSync(cache.dataPath))
continue;
const data = JSON.parse(await fs.promises.readFile(cache.dataPath, 'utf8'));
if (data[pluginName])
result.set(fileName, data[pluginName]);
}
return result;
}

View file

@ -16,8 +16,8 @@
import crypto from 'crypto'; import crypto from 'crypto';
import path from 'path'; import path from 'path';
import { sourceMapSupport, pirates } from '../utilsBundle';
import url from 'url'; import url from 'url';
import { sourceMapSupport, pirates } from '../utilsBundle';
import type { Location } from '../../types/testReporter'; import type { Location } from '../../types/testReporter';
import type { TsConfigLoaderResult } from '../third_party/tsconfig-loader'; import type { TsConfigLoaderResult } from '../third_party/tsconfig-loader';
import { tsConfigLoader } from '../third_party/tsconfig-loader'; import { tsConfigLoader } from '../third_party/tsconfig-loader';
@ -159,6 +159,12 @@ export function shouldTransform(filename: string): boolean {
return !belongsToNodeModules(filename); return !belongsToNodeModules(filename);
} }
let transformData: Map<string, any>;
export function setTransformData(pluginName: string, value: any) {
transformData.set(pluginName, value);
}
export function transformHook(originalCode: string, filename: string, moduleUrl?: string): string { export function transformHook(originalCode: string, filename: string, moduleUrl?: string): string {
const isTypeScript = filename.endsWith('.ts') || filename.endsWith('.tsx') || filename.endsWith('.mts') || filename.endsWith('.cts'); const isTypeScript = filename.endsWith('.ts') || filename.endsWith('.tsx') || filename.endsWith('.mts') || filename.endsWith('.cts');
const hasPreprocessor = const hasPreprocessor =
@ -177,9 +183,10 @@ export function transformHook(originalCode: string, filename: string, moduleUrl?
process.env.BROWSERSLIST_IGNORE_OLD_DATA = 'true'; process.env.BROWSERSLIST_IGNORE_OLD_DATA = 'true';
const { babelTransform }: { babelTransform: BabelTransformFunction } = require('./babelBundle'); const { babelTransform }: { babelTransform: BabelTransformFunction } = require('./babelBundle');
transformData = new Map<string, any>();
const { code, map } = babelTransform(originalCode, filename, isTypeScript, !!moduleUrl, pluginsPrologue, pluginsEpilogue); const { code, map } = babelTransform(originalCode, filename, isTypeScript, !!moduleUrl, pluginsPrologue, pluginsEpilogue);
if (code) if (code)
addToCache!(code, map); addToCache!(code, map, transformData);
return code || ''; return code || '';
} }

View file

@ -32,7 +32,7 @@ export class ZipTraceModelBackend implements TraceModelBackend {
this._traceURL = traceURL; this._traceURL = traceURL;
zipjs.configure({ baseURL: self.location.href } as any); zipjs.configure({ baseURL: self.location.href } as any);
this._zipReader = new zipjs.ZipReader( this._zipReader = new zipjs.ZipReader(
new zipjs.HttpReader(formatUrl(traceURL), { mode: 'cors', credentials: 'include', preventHeadRequest: true } as any), new zipjs.HttpReader(formatUrl(traceURL), { mode: 'cors', preventHeadRequest: true } as any),
{ useWebWorkers: false }); { useWebWorkers: false });
this._entriesPromise = this._zipReader.getEntries({ onprogress: progress }).then(entries => { this._entriesPromise = this._zipReader.getEntries({ onprogress: progress }).then(entries => {
const map = new Map<string, zip.Entry>(); const map = new Map<string, zip.Entry>();

View file

@ -76,3 +76,4 @@ test('drag resize', async ({ page, mount }) => {
expect.soft(mainBox).toEqual({ x: 0, y: 0, width: 500, height: 100 }); expect.soft(mainBox).toEqual({ x: 0, y: 0, width: 500, height: 100 });
expect.soft(sidebarBox).toEqual({ x: 0, y: 101, width: 500, height: 399 }); expect.soft(sidebarBox).toEqual({ x: 0, y: 101, width: 500, height: 399 });
}); });

View file

@ -1,6 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "ES2015",
"lib": [ "lib": [
"dom", "dom",
"dom.iterable", "dom.iterable",

View file

@ -746,32 +746,4 @@ await page.GetByText("Click me").ClickAsync(new LocatorClickOptions
Button = MouseButton.Middle, Button = MouseButton.Middle,
});`); });`);
}); });
test('should record slider', async ({ page, openRecorder }) => {
const recorder = await openRecorder();
await recorder.setContentAndWait(`<input type="range" min="0" max="10" value="5">`);
const dragSlider = async () => {
await page.locator('input').focus();
const { x, y, width, height } = await page.locator('input').boundingBox();
await page.mouse.move(x + width / 2, y + height / 2);
await page.mouse.down();
await page.mouse.move(x + width, y + height / 2);
await page.mouse.up();
};
const [sources] = await Promise.all([
recorder.waitForOutput('JavaScript', 'fill'),
dragSlider(),
]);
await expect(page.locator('input')).toHaveValue('10');
expect(sources.get('JavaScript')!.text).toContain(`
await page.getByRole('slider').fill('10');`);
expect(sources.get('JavaScript')!.text).not.toContain(`
await page.getByRole('slider').click();`);
});
}); });

View file

@ -523,11 +523,13 @@ test('should load jsx with top-level component', async ({ runInlineTest }) => {
const component = <div>Hello <span>world</span></div>; const component = <div>Hello <span>world</span></div>;
test('succeeds', () => { test('succeeds', () => {
expect(component).toEqual({ expect(component).toEqual({
__pw_type: 'jsx',
type: 'div', type: 'div',
props: { props: {
children: [ children: [
'Hello ', 'Hello ',
{ {
__pw_type: 'jsx',
type: 'span', type: 'span',
props: { props: {
children: 'world' children: 'world'

View file

@ -76,6 +76,8 @@ export async function writeFiles(testInfo: TestInfo, files: Files, initial: bool
await Promise.all(Object.keys(files).map(async name => { await Promise.all(Object.keys(files).map(async name => {
const fullName = path.join(baseDir, name); const fullName = path.join(baseDir, name);
if (files[name] === undefined)
return;
await fs.promises.mkdir(path.dirname(fullName), { recursive: true }); await fs.promises.mkdir(path.dirname(fullName), { recursive: true });
await fs.promises.writeFile(fullName, files[name]); await fs.promises.writeFile(fullName, files[name]);
})); }));

View file

@ -44,7 +44,7 @@ test('should work with the empty component list', async ({ runInlineTest }, test
const metainfo = JSON.parse(fs.readFileSync(testInfo.outputPath('playwright/.cache/metainfo.json'), 'utf-8')); const metainfo = JSON.parse(fs.readFileSync(testInfo.outputPath('playwright/.cache/metainfo.json'), 'utf-8'));
expect(metainfo.version).toEqual(require('playwright-core/package.json').version); expect(metainfo.version).toEqual(require('playwright-core/package.json').version);
expect(metainfo.viteVersion).toEqual(require('vite/package.json').version); expect(metainfo.viteVersion).toEqual(require('vite/package.json').version);
expect(Object.entries(metainfo.tests)).toHaveLength(1); expect(Object.entries(metainfo.deps)).toHaveLength(0);
expect(Object.entries(metainfo.sources)).toHaveLength(9); expect(Object.entries(metainfo.sources)).toHaveLength(9);
}); });
@ -135,102 +135,61 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
}); });
expect(metainfo.components).toEqual([{ expect(metainfo.components).toEqual([{
fullName: expect.stringContaining('playwright_test_src_button_tsx_Button'), id: expect.stringContaining('playwright_test_src_button_tsx_Button'),
importedName: 'Button', remoteName: 'Button',
importedNameProperty: '',
importPath: expect.stringContaining('button.tsx'), importPath: expect.stringContaining('button.tsx'),
isModuleOrAlias: false, isModuleOrAlias: false,
deps: [
expect.stringContaining('button.tsx'),
expect.stringContaining('jsx-runtime.js'),
]
}, { }, {
fullName: expect.stringContaining('playwright_test_src_clashingNames1_tsx_ClashingName'), id: expect.stringContaining('playwright_test_src_clashingNames1_tsx_ClashingName'),
importedName: 'ClashingName', remoteName: 'ClashingName',
importedNameProperty: '',
importPath: expect.stringContaining('clashingNames1.tsx'), importPath: expect.stringContaining('clashingNames1.tsx'),
isModuleOrAlias: false, isModuleOrAlias: false,
deps: [
expect.stringContaining('clashingNames1.tsx'),
expect.stringContaining('jsx-runtime.js'),
]
}, { }, {
fullName: expect.stringContaining('playwright_test_src_clashingNames2_tsx_ClashingName'), id: expect.stringContaining('playwright_test_src_clashingNames2_tsx_ClashingName'),
importedName: 'ClashingName', remoteName: 'ClashingName',
importedNameProperty: '',
importPath: expect.stringContaining('clashingNames2.tsx'), importPath: expect.stringContaining('clashingNames2.tsx'),
isModuleOrAlias: false, isModuleOrAlias: false,
deps: [
expect.stringContaining('clashingNames2.tsx'),
expect.stringContaining('jsx-runtime.js'),
]
}, { }, {
fullName: expect.stringContaining('playwright_test_src_components_tsx_Component1'), id: expect.stringContaining('playwright_test_src_components_tsx_Component1'),
importedName: 'Component1', remoteName: 'Component1',
importedNameProperty: '',
importPath: expect.stringContaining('components.tsx'), importPath: expect.stringContaining('components.tsx'),
isModuleOrAlias: false, isModuleOrAlias: false,
deps: [
expect.stringContaining('components.tsx'),
expect.stringContaining('jsx-runtime.js'),
]
}, { }, {
fullName: expect.stringContaining('playwright_test_src_components_tsx_Component2'), id: expect.stringContaining('playwright_test_src_components_tsx_Component2'),
importedName: 'Component2', remoteName: 'Component2',
importedNameProperty: '',
importPath: expect.stringContaining('components.tsx'), importPath: expect.stringContaining('components.tsx'),
isModuleOrAlias: false, isModuleOrAlias: false,
deps: [
expect.stringContaining('components.tsx'),
expect.stringContaining('jsx-runtime.js'),
]
}, { }, {
fullName: expect.stringContaining('playwright_test_src_defaultExport_tsx'), id: expect.stringContaining('playwright_test_src_defaultExport_tsx'),
importPath: expect.stringContaining('defaultExport.tsx'), importPath: expect.stringContaining('defaultExport.tsx'),
importedNameProperty: '',
isModuleOrAlias: false, isModuleOrAlias: false,
deps: [
expect.stringContaining('defaultExport.tsx'),
expect.stringContaining('jsx-runtime.js'),
]
}]); }]);
for (const [file, test] of Object.entries(metainfo.tests)) { for (const [, value] of Object.entries(metainfo.deps))
if (file.endsWith('clashing-imports.spec.tsx')) { (value as string[]).sort();
expect(test).toEqual({
timestamp: expect.any(Number), expect(Object.entries(metainfo.deps)).toEqual([
components: [ [expect.stringContaining('clashingNames1.tsx'), [
expect.stringContaining('clashingNames1_tsx_ClashingName'), expect.stringContaining('jsx-runtime.js'),
expect.stringContaining('clashingNames2_tsx_ClashingName'), expect.stringContaining('clashingNames1.tsx'),
], ]],
}); [expect.stringContaining('clashingNames2.tsx'), [
} expect.stringContaining('jsx-runtime.js'),
if (file.endsWith('default-import.spec.tsx')) { expect.stringContaining('clashingNames2.tsx'),
expect(test).toEqual({ ]],
timestamp: expect.any(Number), [expect.stringContaining('defaultExport.tsx'), [
components: [ expect.stringContaining('jsx-runtime.js'),
expect.stringContaining('defaultExport_tsx'), expect.stringContaining('defaultExport.tsx'),
], ]],
}); [expect.stringContaining('components.tsx'), [
} expect.stringContaining('jsx-runtime.js'),
if (file.endsWith('named-imports.spec.tsx')) { expect.stringContaining('components.tsx'),
expect(test).toEqual({ ]],
timestamp: expect.any(Number), [expect.stringContaining('button.tsx'), [
components: [ expect.stringContaining('jsx-runtime.js'),
expect.stringContaining('components_tsx_Component1'), expect.stringContaining('button.tsx'),
expect.stringContaining('components_tsx_Component2'), ]],
], ]);
});
}
if (file.endsWith('one-import.spec.tsx')) {
expect(test).toEqual({
timestamp: expect.any(Number),
components: [
expect.stringContaining('button_tsx_Button'),
],
});
}
}
}); });
test('should cache build', async ({ runInlineTest }, testInfo) => { test('should cache build', async ({ runInlineTest }, testInfo) => {
@ -497,26 +456,160 @@ test('should retain deps when test changes', async ({ runInlineTest }, testInfo)
const metainfo = JSON.parse(fs.readFileSync(testInfo.outputPath('playwright/.cache/metainfo.json'), 'utf-8')); const metainfo = JSON.parse(fs.readFileSync(testInfo.outputPath('playwright/.cache/metainfo.json'), 'utf-8'));
expect(metainfo.components).toEqual([{ expect(metainfo.components).toEqual([{
fullName: expect.stringContaining('playwright_test_src_button_tsx_Button'), id: expect.stringContaining('playwright_test_src_button_tsx_Button'),
importedName: 'Button', remoteName: 'Button',
importedNameProperty: '',
importPath: expect.stringContaining('button.tsx'), importPath: expect.stringContaining('button.tsx'),
isModuleOrAlias: false, isModuleOrAlias: false,
deps: [
expect.stringContaining('button.tsx'),
expect.stringContaining('jsx-runtime.js'),
]
}]); }]);
expect(Object.entries(metainfo.tests)).toEqual([ for (const [, value] of Object.entries(metainfo.deps))
(value as string[]).sort();
expect(Object.entries(metainfo.deps)).toEqual([
[ [
expect.stringContaining('button.test.tsx'), expect.stringContaining('button.tsx'),
{ [
components: [ expect.stringContaining('jsx-runtime.js'),
expect.stringContaining('src_button_tsx_Button'), expect.stringContaining('button.tsx'),
], ],
timestamp: expect.any(Number)
}
] ]
]); ]);
}); });
test('should render component via re-export', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'playwright.config.ts': playwrightConfig,
'playwright/index.html': `<script type="module" src="./index.ts"></script>`,
'playwright/index.ts': ``,
'src/button.tsx': `
export const Button = () => <button>Button</button>;
`,
'src/buttonHelper.ts': `
import { Button } from './button.tsx';
export { Button };
`,
'src/button.test.tsx': `
import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from './buttonHelper';
test('pass', async ({ mount }) => {
const component = await mount(<Button></Button>);
await expect(component).toHaveText('Button');
});
`,
}, { workers: 1 });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
});
test('should render component exported via fixture', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'playwright.config.ts': playwrightConfig,
'playwright/index.html': `<script type="module" src="./index.ts"></script>`,
'playwright/index.ts': ``,
'src/button.tsx': `
export const Button = () => <button>Button</button>;
`,
'src/buttonFixture.tsx': `
import { Button } from './button';
import { test as baseTest } from '@playwright/experimental-ct-react';
export { expect } from '@playwright/experimental-ct-react';
export const test = baseTest.extend({
button: async ({ mount }, use) => {
await use(await mount(<Button></Button>));
}
});
`,
'src/button.test.tsx': `
import { test, expect } from './buttonFixture';
test('pass', async ({ button }) => {
await expect(button).toHaveText('Button');
});
`,
}, { workers: 1 });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
});
test('should pass imported images from test to component', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'playwright.config.ts': playwrightConfig,
'playwright/index.html': `<script type="module" src="./index.ts"></script>`,
'playwright/index.ts': ``,
'src/image.png': Buffer.from('iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAQAAAD9CzEMAAACMElEQVRYw+1XT0tCQRD/9Qci0Cw7mp1C6BMYnt5niMhPEEFCh07evNk54XnuGkhFehA/QxHkqYMEFWXpscMTipri7fqeu+vbfY+EoBkQ3Zn5zTo7MzsL/NNfoClkUUQNN3jCJ/ETfavRSpYkkSmFQzz8wMr4gaSp8OBJ2HCU4Iwd0kqGgd9GPxCccZ+0jWgWVW1wxlWy0qR51I3hv7lOllq7b4SC/+aGzr+QBadjEKgAykvzJGXwr/Lj4JfRk5hUSLKIa00HPUJRki0xeMWSWxVXmi5sddXKymqTyxdwquXAUVV3WREeLx3gTcNFWQY/jXtB8QIzgt4qTvAR4OCe0ATKCmrnmFMEM0Pp2BvrIisaFUdUjgKKZgYWSjjDLR5J+x13lATHuHSti6JBzQP+gq2QHXjfRaiJojbPgYqbmGFow0VpiyIW0/VIF9QKLzeBWA2MHmwCu8QJQV++Ps/joHQQH4HpuO0uobUeVztgIcr4Vnf4we9orWfUIWKHbEVyYKkPmaVpIVKICuo0ZYXWjHTITXWhsVYxkIDpUoKsla1i2Oz2QjvYG9fshu36GbFQ8DGyHNOuvRdOKZSDUtCFM7wyHeSM4XN8e7bOpd9F2gg+TRYal753bGkbuEjzMg0YW/yDV1czUDm+e43Byz86OnRwsYDMKXlmkYbeAOwffrtU/nGpXpwkXfPhVza+D9AiMAtrtOMYfVr0q8Wr1nh8n8ADZCJPqAk8AifyjP2n36cvkA6/Wln9MokAAAAASUVORK5CYII=', 'base64'),
'src/image.test.tsx': `
import { test, expect } from '@playwright/experimental-ct-react';
import imageSrc from './image.png';
test('pass', async ({ mount }) => {
const component = await mount(<img src={imageSrc}></img>);
await expect(component).toHaveJSProperty('naturalWidth', 48);
});
`,
}, { workers: 1 });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
});
test('should pass dates, regex, urls and bigints', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': playwrightConfig,
'playwright/index.html': `<script type="module" src="./index.ts"></script>`,
'playwright/index.ts': ``,
'src/button.tsx': `
export const Button = ({ props }: any) => {
const { date, url, bigint, regex } = props;
const types = [
date instanceof Date,
url instanceof URL,
typeof bigint === 'bigint',
regex instanceof RegExp,
];
return <div>{types.join(' ')}</div>;
};
`,
'src/component.spec.tsx': `
import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from './button';
test('renders props with builtin types', async ({ mount, page }) => {
const component = await mount(<Button props={{
date: new Date(),
url: new URL('https://example.com'),
bigint: BigInt(42),
regex: /foo/,
}} />);
await expect(component).toHaveText('true true true true');
});
`,
}, { workers: 1 });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
});
test('should pass undefined value as param', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': playwrightConfig,
'playwright/index.html': `<script type="module" src="./index.ts"></script>`,
'playwright/index.ts': ``,
'src/component.tsx': `
export const Component = ({ value }: { value?: number }) => {
return <div>{typeof value}</div>;
};
`,
'src/component.spec.tsx': `
import { test, expect } from '@playwright/experimental-ct-react';
import { Component } from './component';
test('renders props with undefined type', async ({ mount, page }) => {
const component = await mount(<Component value={undefined} />);
await expect(component).toHaveText('undefined');
});
`,
}, { workers: 1 });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
});

View file

@ -196,7 +196,7 @@ test('should work with stray JSX import', async ({ runInlineTest }) => {
expect(result.passed).toBe(1); expect(result.passed).toBe(1);
}); });
test.fixme('should work with stray JS import', async ({ runInlineTest }) => { test('should work with stray JS import', async ({ runInlineTest }) => {
const result = await runInlineTest({ const result = await runInlineTest({
'playwright.config.ts': playwrightConfig, 'playwright.config.ts': playwrightConfig,
'playwright/index.html': `<script type="module" src="./index.js"></script>`, 'playwright/index.html': `<script type="module" src="./index.js"></script>`,
@ -495,3 +495,23 @@ test('should normalize children', async ({ runInlineTest }) => {
expect(result.exitCode).toBe(0); expect(result.exitCode).toBe(0);
expect(result.passed).toBe(2); expect(result.passed).toBe(2);
}); });
test('should allow props children', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': playwrightConfig,
'playwright/index.html': `<script type="module" src="./index.ts"></script>`,
'playwright/index.ts': ``,
'src/component.spec.tsx': `
import { test, expect } from '@playwright/experimental-ct-react';
test("renders children from props object", async ({ mount, page }) => {
const props = { children: 'test' };
await mount(<button {...props} />);
await expect(page.getByText('test')).toBeVisible();
});
`,
}, { workers: 1 });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
});

View file

@ -207,3 +207,81 @@ test('should watch component', async ({ runUITest, writeFiles }) => {
pass <= pass <=
`); `);
}); });
test('should watch component via util', async ({ runUITest, writeFiles }) => {
const { page } = await runUITest({
...basicTestTree,
'src/button.tsx': undefined,
'src/button.ts': `
import { Button } from './buttonComponent';
export { Button };
`,
'src/buttonComponent.tsx': `
export const Button = () => <button>Button</button>;
`,
});
await expect.poll(dumpTestTree(page)).toBe(`
button.test.tsx
pass
`);
await page.getByTitle('Watch all').click();
await page.getByTitle('Run all').click();
await expect.poll(dumpTestTree(page)).toBe(`
button.test.tsx
pass
`);
await writeFiles({
'src/buttonComponent.tsx': `
export const Button = () => <button>Button2</button>;
`
});
await expect.poll(dumpTestTree(page)).toBe(`
button.test.tsx
pass <=
`);
});
test('should watch component when editing util', async ({ runUITest, writeFiles }) => {
const { page } = await runUITest({
...basicTestTree,
'src/button.tsx': undefined,
'src/button.ts': `
import { Button } from './buttonComponent';
export { Button };
`,
'src/buttonComponent.tsx': `
export const Button = () => <button>Button</button>;
`,
'src/buttonComponent2.tsx': `
export const Button = () => <button>Button2</button>;
`,
});
await expect.poll(dumpTestTree(page)).toBe(`
button.test.tsx
pass
`);
await page.getByTitle('Watch all').click();
await page.getByTitle('Run all').click();
await expect.poll(dumpTestTree(page)).toBe(`
button.test.tsx
pass
`);
await writeFiles({
'src/button.ts': `
import { Button } from './buttonComponent2';
export { Button };
`,
});
await expect.poll(dumpTestTree(page)).toBe(`
button.test.tsx
pass <=
`);
});

View file

@ -304,6 +304,7 @@ steps.push({
onChanges.push({ onChanges.push({
inputs: [ inputs: [
'packages/playwright-core/src/server/injected/**', 'packages/playwright-core/src/server/injected/**',
'packages/playwright-ct-core/src/injected/**',
'packages/playwright-core/src/utils/isomorphic/**', 'packages/playwright-core/src/utils/isomorphic/**',
'utils/generate_injected.js', 'utils/generate_injected.js',
], ],

View file

@ -22,11 +22,40 @@ const path = require('path');
const ROOT = path.join(__dirname, '..'); const ROOT = path.join(__dirname, '..');
const esbuild = require('esbuild'); const esbuild = require('esbuild');
/**
* @type {[string, string, string, boolean][]}
*/
const injectedScripts = [ const injectedScripts = [
[
path.join(ROOT, 'packages', 'playwright-core', 'src', 'server', 'injected', 'utilityScript.ts'), path.join(ROOT, 'packages', 'playwright-core', 'src', 'server', 'injected', 'utilityScript.ts'),
path.join(ROOT, 'packages', 'playwright-core', 'lib', 'server', 'injected', 'packed'),
path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'),
true,
],
[
path.join(ROOT, 'packages', 'playwright-core', 'src', 'server', 'injected', 'injectedScript.ts'), path.join(ROOT, 'packages', 'playwright-core', 'src', 'server', 'injected', 'injectedScript.ts'),
path.join(ROOT, 'packages', 'playwright-core', 'lib', 'server', 'injected', 'packed'),
path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'),
true,
],
[
path.join(ROOT, 'packages', 'playwright-core', 'src', 'server', 'injected', 'consoleApi.ts'), path.join(ROOT, 'packages', 'playwright-core', 'src', 'server', 'injected', 'consoleApi.ts'),
path.join(ROOT, 'packages', 'playwright-core', 'lib', 'server', 'injected', 'packed'),
path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'),
true,
],
[
path.join(ROOT, 'packages', 'playwright-core', 'src', 'server', 'injected', 'recorder.ts'), path.join(ROOT, 'packages', 'playwright-core', 'src', 'server', 'injected', 'recorder.ts'),
path.join(ROOT, 'packages', 'playwright-core', 'lib', 'server', 'injected', 'packed'),
path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'),
true,
],
[
path.join(ROOT, 'packages', 'playwright-ct-core', 'src', 'injected', 'index.ts'),
path.join(ROOT, 'packages', 'playwright-ct-core', 'lib', 'injected', 'packed'),
path.join(ROOT, 'packages', 'playwright-ct-core', 'src', 'generated'),
false,
]
]; ];
const modulePrefix = ` const modulePrefix = `
@ -79,10 +108,8 @@ const inlineCSSPlugin = {
}; };
(async () => { (async () => {
const generatedFolder = path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'); for (const [injected, outdir, generatedFolder, hasExports] of injectedScripts) {
await fs.promises.mkdir(generatedFolder, { recursive: true }); await fs.promises.mkdir(generatedFolder, { recursive: true });
for (const injected of injectedScripts) {
const outdir = path.join(ROOT, 'packages', 'playwright-core', 'lib', 'server', 'injected', 'packed');
const buildOutput = await esbuild.build({ const buildOutput = await esbuild.build({
entryPoints: [injected], entryPoints: [injected],
bundle: true, bundle: true,
@ -97,6 +124,7 @@ const inlineCSSPlugin = {
const baseName = path.basename(injected); const baseName = path.basename(injected);
const outFileJs = path.join(outdir, baseName.replace('.ts', '.js')); const outFileJs = path.join(outdir, baseName.replace('.ts', '.js'));
let content = await fs.promises.readFile(outFileJs, 'utf-8'); let content = await fs.promises.readFile(outFileJs, 'utf-8');
if (hasExports)
content = await replaceEsbuildHeader(content, outFileJs); content = await replaceEsbuildHeader(content, outFileJs);
const newContent = `export const source = ${JSON.stringify(content)};`; const newContent = `export const source = ${JSON.stringify(content)};`;
await fs.promises.writeFile(path.join(generatedFolder, baseName.replace('.ts', 'Source.ts')), newContent); await fs.promises.writeFile(path.join(generatedFolder, baseName.replace('.ts', 'Source.ts')), newContent);