Compare commits
15 commits
main
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4096676ab | ||
|
|
414affaed4 | ||
|
|
7a72adc33c | ||
|
|
8f0163f3f4 | ||
|
|
98a661824d | ||
|
|
50f1f08e9c | ||
|
|
e5d201b459 | ||
|
|
8ee2d81143 | ||
|
|
50a8e4f52a | ||
|
|
cb6c64cc33 | ||
|
|
06518b2091 | ||
|
|
d47ed6a076 | ||
|
|
4d9f923dfe | ||
|
|
ece2a97702 | ||
|
|
38d699f64f |
|
|
@ -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
3
.gitignore
vendored
|
|
@ -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/
|
||||||
|
|
|
||||||
|
|
@ -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
100
package-lock.json
generated
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
[vitePlugin.ts]
|
||||||
|
generated/indexSource.ts
|
||||||
|
|
||||||
|
[mount.ts]
|
||||||
|
generated/serializers.ts
|
||||||
|
injected/**
|
||||||
49
packages/playwright-ct-core/src/injected/importRegistry.ts
Normal file
49
packages/playwright-ct-core/src/injected/importRegistry.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
packages/playwright-ct-core/src/injected/index.ts
Normal file
22
packages/playwright-ct-core/src/injected/index.ts
Normal 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;
|
||||||
93
packages/playwright-ct-core/src/injected/serializers.ts
Normal file
93
packages/playwright-ct-core/src/injected/serializers.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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,74 +69,45 @@ 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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
boundCallbacksForMount = [];
|
boundCallbacksForMount = [];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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', '']);
|
||||||
|
|
|
||||||
|
|
@ -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'))
|
||||||
|
|
|
||||||
32
packages/playwright-ct-core/types/component.d.ts
vendored
32
packages/playwright-ct-core/types/component.d.ts
vendored
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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')),
|
||||||
}]);
|
}]);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>();
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "ES2015",
|
||||||
"lib": [
|
"lib": [
|
||||||
"dom",
|
"dom",
|
||||||
"dom.iterable",
|
"dom.iterable",
|
||||||
|
|
|
||||||
|
|
@ -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();`);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -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>`,
|
||||||
|
|
@ -481,7 +481,7 @@ test('should normalize children', async ({ runInlineTest }) => {
|
||||||
import { OneChild, OtherComponent } from './component';
|
import { OneChild, OtherComponent } from './component';
|
||||||
|
|
||||||
test("can pass an HTML element to OneChild", async ({ mount }) => {
|
test("can pass an HTML element to OneChild", async ({ mount }) => {
|
||||||
const component = await mount(<OneChild><p>child</p> </OneChild>);
|
const component = await mount(<OneChild><p>child</p></OneChild>);
|
||||||
await expect(component).toHaveText("child");
|
await expect(component).toHaveText("child");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -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 <=
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue