From af55f487547addefccd5163f33be684f9008345f Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 11 Mar 2022 08:00:46 -0800 Subject: [PATCH] chore: add experimental mount (#12657) --- package-lock.json | 30 +++++ packages/html-reporter/.gitignore | 1 + .../html-reporter/playwright-ct/index.html | 28 +++++ .../index.ts} | 11 +- .../playwright-ct/webpack.config.js | 62 ++++++++++ packages/html-reporter/playwright.config.ts | 4 +- packages/html-reporter/src/chip.spec.tsx | 3 +- .../html-reporter/src/headerView.spec.tsx | 3 +- .../html-reporter/src/testCaseView.spec.tsx | 3 +- packages/html-reporter/test/component.js | 77 ------------ packages/html-reporter/test/componentTest.ts | 79 ------------ packages/html-reporter/webpack.config.js | 2 - packages/playwright-ct-react/package.json | 8 ++ packages/playwright-ct-react/render.d.ts | 17 +++ packages/playwright-ct-react/render.mjs | 38 ++++++ packages/playwright-ct-react/test.d.ts | 34 ++++++ packages/playwright-ct-react/test.js | 30 +++++ packages/playwright-ct-svelte/package.json | 8 ++ packages/playwright-ct-svelte/render.d.ts | 17 +++ packages/playwright-ct-svelte/render.mjs | 34 ++++++ packages/playwright-ct-svelte/test.d.ts | 38 ++++++ packages/playwright-ct-svelte/test.js | 30 +++++ packages/playwright-ct-vue/package.json | 8 ++ packages/playwright-ct-vue/render.d.ts | 18 +++ packages/playwright-ct-vue/render.mjs | 113 ++++++++++++++++++ packages/playwright-ct-vue/test.d.ts | 39 ++++++ packages/playwright-ct-vue/test.js | 30 +++++ packages/playwright-test/index.d.ts | 1 - packages/playwright-test/package.json | 1 + packages/playwright-test/src/mount.ts | 68 +++++++++++ packages/playwright-test/src/transform.ts | 2 +- packages/playwright-test/src/tsxTransform.ts | 77 +++++++++++- utils/build/build.js | 1 + utils/workspace.js | 15 +++ 34 files changed, 754 insertions(+), 176 deletions(-) create mode 100644 packages/html-reporter/.gitignore create mode 100644 packages/html-reporter/playwright-ct/index.html rename packages/html-reporter/{playwright.components.tsx => playwright-ct/index.ts} (76%) create mode 100644 packages/html-reporter/playwright-ct/webpack.config.js delete mode 100644 packages/html-reporter/test/component.js delete mode 100644 packages/html-reporter/test/componentTest.ts create mode 100644 packages/playwright-ct-react/package.json create mode 100644 packages/playwright-ct-react/render.d.ts create mode 100644 packages/playwright-ct-react/render.mjs create mode 100644 packages/playwright-ct-react/test.d.ts create mode 100644 packages/playwright-ct-react/test.js create mode 100644 packages/playwright-ct-svelte/package.json create mode 100644 packages/playwright-ct-svelte/render.d.ts create mode 100644 packages/playwright-ct-svelte/render.mjs create mode 100644 packages/playwright-ct-svelte/test.d.ts create mode 100644 packages/playwright-ct-svelte/test.js create mode 100644 packages/playwright-ct-vue/package.json create mode 100644 packages/playwright-ct-vue/render.d.ts create mode 100644 packages/playwright-ct-vue/render.mjs create mode 100644 packages/playwright-ct-vue/test.d.ts create mode 100644 packages/playwright-ct-vue/test.js create mode 100644 packages/playwright-test/src/mount.ts diff --git a/package-lock.json b/package-lock.json index e4e68f6dce..206d9b1132 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1117,6 +1117,18 @@ "node": ">= 8" } }, + "node_modules/@playwright/ct-react": { + "resolved": "packages/playwright-ct-react", + "link": true + }, + "node_modules/@playwright/ct-svelte": { + "resolved": "packages/playwright-ct-svelte", + "link": true + }, + "node_modules/@playwright/ct-vue": { + "resolved": "packages/playwright-ct-vue", + "link": true + }, "node_modules/@playwright/test": { "resolved": "packages/playwright-test", "link": true @@ -7364,6 +7376,15 @@ "node": ">= 12" } }, + "packages/playwright-ct-react": { + "name": "@playwright/ct-react" + }, + "packages/playwright-ct-svelte": { + "name": "@playwright/ct-svelte" + }, + "packages/playwright-ct-vue": { + "name": "@playwright/ct-vue" + }, "packages/playwright-firefox": { "version": "1.21.0-next", "hasInstallScript": true, @@ -8195,6 +8216,15 @@ "fastq": "^1.6.0" } }, + "@playwright/ct-react": { + "version": "file:packages/playwright-ct-react" + }, + "@playwright/ct-svelte": { + "version": "file:packages/playwright-ct-svelte" + }, + "@playwright/ct-vue": { + "version": "file:packages/playwright-ct-vue" + }, "@playwright/test": { "version": "file:packages/playwright-test", "requires": { diff --git a/packages/html-reporter/.gitignore b/packages/html-reporter/.gitignore new file mode 100644 index 0000000000..c766654b44 --- /dev/null +++ b/packages/html-reporter/.gitignore @@ -0,0 +1 @@ +out-ct/ \ No newline at end of file diff --git a/packages/html-reporter/playwright-ct/index.html b/packages/html-reporter/playwright-ct/index.html new file mode 100644 index 0000000000..c88687e04d --- /dev/null +++ b/packages/html-reporter/playwright-ct/index.html @@ -0,0 +1,28 @@ + + + + + + + + + Playwright CT + + +
+ + diff --git a/packages/html-reporter/playwright.components.tsx b/packages/html-reporter/playwright-ct/index.ts similarity index 76% rename from packages/html-reporter/playwright.components.tsx rename to packages/html-reporter/playwright-ct/index.ts index 6d120d5ed4..8081aa00d7 100644 --- a/packages/html-reporter/playwright.components.tsx +++ b/packages/html-reporter/playwright-ct/index.ts @@ -14,11 +14,12 @@ * limitations under the License. */ -import { AutoChip, Chip } from './src/chip'; -import { HeaderView } from './src/headerView'; -import { TestCaseView } from './src/testCaseView'; -import './src/theme.css'; -import { registerComponent } from './test/component'; +import { AutoChip, Chip } from '../src/chip'; +import { HeaderView } from '../src/headerView'; +import { TestCaseView } from '../src/testCaseView'; +import '../src/theme.css'; + +import { registerComponent } from '@playwright/ct-react/render'; registerComponent('HeaderView', HeaderView); registerComponent('Chip', Chip); diff --git a/packages/html-reporter/playwright-ct/webpack.config.js b/packages/html-reporter/playwright-ct/webpack.config.js new file mode 100644 index 0000000000..69d380dc6e --- /dev/null +++ b/packages/html-reporter/playwright-ct/webpack.config.js @@ -0,0 +1,62 @@ +/* + 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. +*/ + +const path = require('path'); +const HtmlWebPackPlugin = require('html-webpack-plugin'); + +const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development'; + +module.exports = { + mode, + entry: { + index: path.join(__dirname, 'index.ts'), + }, + resolve: { + extensions: ['.ts', '.js', '.tsx', '.jsx'] + }, + devtool: mode === 'production' ? false : 'source-map', + output: { + globalObject: 'self', + filename: '[name].bundle.js', + path: path.resolve(__dirname, '..', 'out-ct') + }, + module: { + rules: [ + { + test: /\.(j|t)sx?$/, + loader: 'babel-loader', + options: { + presets: [ + "@babel/preset-typescript", + "@babel/preset-react" + ] + }, + exclude: /node_modules/ + }, + { + test: /\.css$/, + use: ['style-loader', 'css-loader'] + }, + ] + }, + plugins: [ + new HtmlWebPackPlugin({ + title: 'Playwright CT', + template: path.join(__dirname, 'index.html'), + inject: true, + }), + ] +}; diff --git a/packages/html-reporter/playwright.config.ts b/packages/html-reporter/playwright.config.ts index cfeb7b0afa..0da003c603 100644 --- a/packages/html-reporter/playwright.config.ts +++ b/packages/html-reporter/playwright.config.ts @@ -15,10 +15,11 @@ */ import { PlaywrightTestConfig, devices } from '@playwright/test'; +import path from 'path'; +import url from 'url'; const config: PlaywrightTestConfig = { testDir: 'src', - snapshotDir: 'snapshots', forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, reporter: process.env.CI ? [ @@ -27,6 +28,7 @@ const config: PlaywrightTestConfig = { ['html', { open: 'on-failure' }] ], use: { + baseURL: url.pathToFileURL(path.join(__dirname, 'out-ct', 'index.html')).toString(), trace: 'on-first-retry', }, projects: [ diff --git a/packages/html-reporter/src/chip.spec.tsx b/packages/html-reporter/src/chip.spec.tsx index d824b9a208..acfeb8fdea 100644 --- a/packages/html-reporter/src/chip.spec.tsx +++ b/packages/html-reporter/src/chip.spec.tsx @@ -15,10 +15,9 @@ */ import React from 'react'; -import { expect, test } from '../test/componentTest'; +import { expect, test } from '@playwright/ct-react/test'; import { AutoChip, Chip } from './chip'; -test.use({ webpack: require.resolve('../webpack.config.js') }); test.use({ viewport: { width: 500, height: 500 } }); test('expand collapse', async ({ mount }) => { diff --git a/packages/html-reporter/src/headerView.spec.tsx b/packages/html-reporter/src/headerView.spec.tsx index b89d6486cf..b13a176b98 100644 --- a/packages/html-reporter/src/headerView.spec.tsx +++ b/packages/html-reporter/src/headerView.spec.tsx @@ -15,10 +15,9 @@ */ import React from 'react'; -import { test, expect } from '../test/componentTest'; +import { test, expect } from '@playwright/ct-react/test'; import { HeaderView } from './headerView'; -test.use({ webpack: require.resolve('../webpack.config.js') }); test.use({ viewport: { width: 720, height: 200 } }); test('should render counters', async ({ mount }) => { diff --git a/packages/html-reporter/src/testCaseView.spec.tsx b/packages/html-reporter/src/testCaseView.spec.tsx index 66235032e9..8037bd97ec 100644 --- a/packages/html-reporter/src/testCaseView.spec.tsx +++ b/packages/html-reporter/src/testCaseView.spec.tsx @@ -15,11 +15,10 @@ */ import React from 'react'; -import { test, expect } from '../test/componentTest'; +import { test, expect } from '@playwright/ct-react/test'; import { TestCaseView } from './testCaseView'; import type { TestCase, TestResult } from '../../playwright-test/src/reporters/html'; -test.use({ webpack: require.resolve('../webpack.config.js') }); test.use({ viewport: { width: 800, height: 600 } }); const result: TestResult = { diff --git a/packages/html-reporter/test/component.js b/packages/html-reporter/test/component.js deleted file mode 100644 index 88dde5d680..0000000000 --- a/packages/html-reporter/test/component.js +++ /dev/null @@ -1,77 +0,0 @@ -/** - * 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. - */ - -const React = require('react'); -const ReactDOM = require('react-dom'); - -const fillStyle = { - position: 'absolute', - top: 0, - right: 0, - bottom: 0, - left: 0, -}; - -const checkerboardCommon = { - ...fillStyle, - backgroundSize: '50px 50px', - backgroundPosition: '0 0, 25px 25px', -}; - -const checkerboardLight = { - ...checkerboardCommon, - backgroundColor: '#FFF', - backgroundImage: `linear-gradient(45deg, #00000008 25%, transparent 25%, transparent 75%, #00000008 75%, #00000008), - linear-gradient(45deg, #00000008 25%, transparent 25%, transparent 75%, #00000008 75%, #00000008)` -}; - -const checkerboardDark = { - ...checkerboardCommon, - backgroundColor: '#000', - backgroundImage: `linear-gradient(45deg, #FFFFFF12 25%, transparent 25%, transparent 75%, #FFFFFF12 75%, #FFFFFF12), - linear-gradient(45deg, #FFFFFF12 25%, transparent 25%, transparent 75%, #FFFFFF12 75%, #FFFFFF12)` -}; - -const Component = ({ style, children }) => { - const checkerboard = window.matchMedia('(prefers-color-scheme: dark)').matches ? checkerboardDark : checkerboardLight; - const bgStyle = { ...checkerboard }; - const fgStyle = { ...fillStyle, ...style }; - return React.createElement( - React.Fragment, null, - React.createElement('div', { style: bgStyle }), - React.createElement('div', { style: fgStyle, id: 'pw-root' }, children)); -}; - -const registry = new Map(); - -export const registerComponent = (name, componentFunc) => { - registry.set(name, componentFunc); -}; - -function render(component) { - const componentFunc = registry.get(component.type) || component.type; - return React.createElement(componentFunc, component.props, ...component.children.map(child => { - if (typeof child === 'string') - return child; - return render(child); - })); -} - -window.__playwright_render = component => { - ReactDOM.render( - React.createElement(Component, null, render(component)), - document.getElementById('root')); -}; diff --git a/packages/html-reporter/test/componentTest.ts b/packages/html-reporter/test/componentTest.ts deleted file mode 100644 index b590f79777..0000000000 --- a/packages/html-reporter/test/componentTest.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * 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 * as path from 'path'; -import { test as baseTest, Locator } from '@playwright/test'; - -type Component = { - type: string; - props: Object; - children: Object[]; -}; - -declare global { - interface Window { - __playwright_render: (component: Component) => void; - } -} - -type TestFixtures = { - mount: (component: any) => Promise; - webpack: string; -}; - -export const test = baseTest.extend({ - webpack: '', - mount: async ({ page, webpack }, use) => { - const webpackConfig = require(webpack); - const outputPath = webpackConfig.output.path; - const filename = webpackConfig.output.filename.replace('[name]', 'playwright'); - await use(async (component: Component) => { - await page.route('http://component/index.html', route => { - route.fulfill({ - body: ` - - -
- `, - contentType: 'text/html' - }); - }); - await page.goto('http://component/index.html'); - - await page.addScriptTag({ path: path.resolve(__dirname, outputPath, filename) }); - - const props = { ...component.props }; - for (const [key, value] of Object.entries(props)) { - if (typeof value === 'function') { - const functionName = '__pw_func_' + key; - await page.exposeFunction(functionName, value); - (props as any)[key] = functionName; - } - } - await page.evaluate(v => { - const props = v.props; - for (const [key, value] of Object.entries(props)) { - if (typeof value === 'string' && (value as string).startsWith('__pw_func_')) - (props as any)[key] = (window as any)[value]; - } - window.__playwright_render({ ...v, props }); - }, { ...component, props }); - return page.locator('#pw-root'); - }); - }, -}); - -export { expect } from '@playwright/test'; diff --git a/packages/html-reporter/webpack.config.js b/packages/html-reporter/webpack.config.js index e8e9cfab92..059d769852 100644 --- a/packages/html-reporter/webpack.config.js +++ b/packages/html-reporter/webpack.config.js @@ -25,7 +25,6 @@ module.exports = { entry: { zip: require.resolve('@zip.js/zip.js/dist/zip-no-worker-inflate.min.js'), app: path.join(__dirname, 'src', 'index.tsx'), - playwright: path.join(__dirname, 'playwright.components.tsx'), }, resolve: { extensions: ['.ts', '.js', '.tsx', '.jsx'] @@ -60,7 +59,6 @@ module.exports = { title: 'Playwright Test Report', template: path.join(__dirname, 'src', 'index.html'), inject: true, - excludeChunks: ['playwright'], }), new BundleJsPlugin(), ] diff --git a/packages/playwright-ct-react/package.json b/packages/playwright-ct-react/package.json new file mode 100644 index 0000000000..f03f4522c7 --- /dev/null +++ b/packages/playwright-ct-react/package.json @@ -0,0 +1,8 @@ +{ + "name": "@playwright/ct-react", + "private": true, + "exports": { + "./render": "./render.mjs", + "./test": "./test.js" + } +} diff --git a/packages/playwright-ct-react/render.d.ts b/packages/playwright-ct-react/render.d.ts new file mode 100644 index 0000000000..9309b7c227 --- /dev/null +++ b/packages/playwright-ct-react/render.d.ts @@ -0,0 +1,17 @@ +/** + * 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 const registerComponent: (name: string, componentFunc: any) => void; diff --git a/packages/playwright-ct-react/render.mjs b/packages/playwright-ct-react/render.mjs new file mode 100644 index 0000000000..5f7a56a0dc --- /dev/null +++ b/packages/playwright-ct-react/render.mjs @@ -0,0 +1,38 @@ +/** + * 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 React from 'react'; +import ReactDOM from 'react-dom'; + +const registry = new Map(); + +export const registerComponent = (name, componentFunc) => { + registry.set(name, componentFunc); +}; + +function render(component) { + const componentFunc = registry.get(component.type) || component.type; + return React.createElement(componentFunc, component.props, ...component.children.map(child => { + if (typeof child === 'string') + return child; + return render(child); + })); +} + +window.playwrightMount = component => { + ReactDOM.render(render(component), document.getElementById('root')); + return '#root'; +}; diff --git a/packages/playwright-ct-react/test.d.ts b/packages/playwright-ct-react/test.d.ts new file mode 100644 index 0000000000..f4a591ce79 --- /dev/null +++ b/packages/playwright-ct-react/test.d.ts @@ -0,0 +1,34 @@ +/** + * 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 type { + TestType, + PlaywrightTestArgs, + PlaywrightTestOptions, + PlaywrightWorkerArgs, + PlaywrightWorkerOptions, + Locator, +} from '@playwright/test'; + +interface ComponentFixtures { + mount(component: JSX.Element): Promise; +} + +export const test: TestType< + PlaywrightTestArgs & PlaywrightTestOptions & ComponentFixtures, + PlaywrightWorkerArgs & PlaywrightWorkerOptions>; + +export { expect } from '@playwright/test'; diff --git a/packages/playwright-ct-react/test.js b/packages/playwright-ct-react/test.js new file mode 100644 index 0000000000..272bc6c25f --- /dev/null +++ b/packages/playwright-ct-react/test.js @@ -0,0 +1,30 @@ +/** + * 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. + */ + +const { test: baseTest, expect } = require('@playwright/test'); +const { mount } = require('@playwright/test/lib/mount'); + +const test = baseTest.extend({ + mount: async ({ page, baseURL }, use) => { + await use(async (component, options) => { + await page.goto(baseURL); + const selector = await mount(page, component, options); + return page.locator(selector); + }); + }, +}); + +module.exports = { test, expect }; diff --git a/packages/playwright-ct-svelte/package.json b/packages/playwright-ct-svelte/package.json new file mode 100644 index 0000000000..4971da76b3 --- /dev/null +++ b/packages/playwright-ct-svelte/package.json @@ -0,0 +1,8 @@ +{ + "name": "@playwright/ct-svelte", + "private": true, + "exports": { + "./render": "./render.mjs", + "./test": "./test.js" + } +} diff --git a/packages/playwright-ct-svelte/render.d.ts b/packages/playwright-ct-svelte/render.d.ts new file mode 100644 index 0000000000..9309b7c227 --- /dev/null +++ b/packages/playwright-ct-svelte/render.d.ts @@ -0,0 +1,17 @@ +/** + * 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 const registerComponent: (name: string, componentFunc: any) => void; diff --git a/packages/playwright-ct-svelte/render.mjs b/packages/playwright-ct-svelte/render.mjs new file mode 100644 index 0000000000..fc3a13b8b0 --- /dev/null +++ b/packages/playwright-ct-svelte/render.mjs @@ -0,0 +1,34 @@ +/** + * 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. + */ + +const registry = new Map(); + +export const registerComponent = (name, componentClass) => { + registry.set(name, componentClass); +}; + +window.playwrightMount = component => { + const componentCtor = registry.get(component.type); + + const wrapper = new componentCtor({ + target: document.getElementById('app'), + props: component.options.props, + }); + + for (const [key, listener] of Object.entries(component.options.on || {})) + wrapper.$on(key, event => listener(event.detail)); + return '#app'; +}; diff --git a/packages/playwright-ct-svelte/test.d.ts b/packages/playwright-ct-svelte/test.d.ts new file mode 100644 index 0000000000..7ba8192589 --- /dev/null +++ b/packages/playwright-ct-svelte/test.d.ts @@ -0,0 +1,38 @@ +/** + * 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 type { + TestType, + PlaywrightTestArgs, + PlaywrightTestOptions, + PlaywrightWorkerArgs, + PlaywrightWorkerOptions, + Locator, +} from '@playwright/test'; + +interface ComponentFixtures { + mount(component: any, options?: { + props?: { [key: string]: any }, + slots?: { [key: string]: any }, + on?: { [key: string]: Function }, + }): Promise; +} + +export const test: TestType< + PlaywrightTestArgs & PlaywrightTestOptions & ComponentFixtures, + PlaywrightWorkerArgs & PlaywrightWorkerOptions>; + +export { expect } from '@playwright/test'; diff --git a/packages/playwright-ct-svelte/test.js b/packages/playwright-ct-svelte/test.js new file mode 100644 index 0000000000..272bc6c25f --- /dev/null +++ b/packages/playwright-ct-svelte/test.js @@ -0,0 +1,30 @@ +/** + * 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. + */ + +const { test: baseTest, expect } = require('@playwright/test'); +const { mount } = require('@playwright/test/lib/mount'); + +const test = baseTest.extend({ + mount: async ({ page, baseURL }, use) => { + await use(async (component, options) => { + await page.goto(baseURL); + const selector = await mount(page, component, options); + return page.locator(selector); + }); + }, +}); + +module.exports = { test, expect }; diff --git a/packages/playwright-ct-vue/package.json b/packages/playwright-ct-vue/package.json new file mode 100644 index 0000000000..2ed049e537 --- /dev/null +++ b/packages/playwright-ct-vue/package.json @@ -0,0 +1,8 @@ +{ + "name": "@playwright/ct-vue", + "private": true, + "exports": { + "./render": "./render.mjs", + "./test": "./test.js" + } +} diff --git a/packages/playwright-ct-vue/render.d.ts b/packages/playwright-ct-vue/render.d.ts new file mode 100644 index 0000000000..e01c25ba2e --- /dev/null +++ b/packages/playwright-ct-vue/render.d.ts @@ -0,0 +1,18 @@ +/** + * 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 const initVueTest: (vue: any) => void; +export const registerComponent: (name: string, componentFunc: any) => void; diff --git a/packages/playwright-ct-vue/render.mjs b/packages/playwright-ct-vue/render.mjs new file mode 100644 index 0000000000..ae60c56cbd --- /dev/null +++ b/packages/playwright-ct-vue/render.mjs @@ -0,0 +1,113 @@ +/** + * 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 { createApp, setDevtoolsHook, h } from 'vue'; + +const registry = new Map(); +let instance = { createApp, setDevtoolsHook, h }; + +export const initVueTest = vue => { + instance = vue; +}; + +export const registerComponent = (name, vueComponent) => { + registry.set(name, vueComponent); +}; + +const allListeners = []; + +function render(component) { + if (typeof component === 'string') + return component; + const componentFunc = registry.get(component.type) || component.type; + + const children = []; + const slots = {}; + const listeners = {}; + let props = {}; + + if (component.kind === 'jsx') { + for (const child of component.children || []) { + if (child.type === 'template') { + const slotProperty = Object.keys(child.props).find(k => k.startsWith('v-slot:')); + const slot = slotProperty ? slotProperty.substring('v-slot:'.length) : 'default'; + slots[slot] = child.children.map(render); + } else { + children.push(render(child)); + } + } + + for (const [key, value] of Object.entries(component.props)) { + if (key.startsWith('v-on:')) + listeners[key.substring('v-on:'.length)] = value; + else + props[key] = value; + } + } + + if (component.kind === 'object') { + // Vue test util syntax. + for (const [key, value] of Object.entries(component.options.slots || {})) { + if (key === 'default') + children.push(value); + else + slots[key] = value; + } + props = component.options.props || {}; + for (const [key, value] of Object.entries(component.options.on || {})) + listeners[key] = value; + } + + let lastArg; + if (Object.entries(slots).length) { + lastArg = slots; + if (children.length) + slots.default = children; + } else if (children.length) { + lastArg = children; + } + + const wrapper = instance.h(componentFunc, props, lastArg); + allListeners.push([wrapper, listeners]); + return wrapper; +} + +function createDevTools() { + return { + emit(eventType, ...payload) { + if (eventType === 'component:emit') { + const [, componentVM, event, eventArgs] = payload; + for (const [wrapper, listeners] of allListeners) { + if (wrapper.component !== componentVM) + continue; + const listener = listeners[event]; + if (!listener) + return; + listener(...eventArgs); + } + } + } + }; +} + +window.playwrightMount = async component => { + const app = instance.createApp({ + render: () => render(component) + }); + instance.setDevtoolsHook(createDevTools(), {}); + app.mount('#app'); + return '#app'; +}; diff --git a/packages/playwright-ct-vue/test.d.ts b/packages/playwright-ct-vue/test.d.ts new file mode 100644 index 0000000000..fa1a14e567 --- /dev/null +++ b/packages/playwright-ct-vue/test.d.ts @@ -0,0 +1,39 @@ +/** + * 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 type { + TestType, + PlaywrightTestArgs, + PlaywrightTestOptions, + PlaywrightWorkerArgs, + PlaywrightWorkerOptions, + Locator, +} from '@playwright/test'; + +interface ComponentFixtures { + mount(component: JSX.Element): Promise; + mount(component: any, options?: { + props?: { [key: string]: any }, + slots?: { [key: string]: any }, + on?: { [key: string]: Function }, + }): Promise; +} + +export const test: TestType< + PlaywrightTestArgs & PlaywrightTestOptions & ComponentFixtures, + PlaywrightWorkerArgs & PlaywrightWorkerOptions>; + +export { expect } from '@playwright/test'; diff --git a/packages/playwright-ct-vue/test.js b/packages/playwright-ct-vue/test.js new file mode 100644 index 0000000000..272bc6c25f --- /dev/null +++ b/packages/playwright-ct-vue/test.js @@ -0,0 +1,30 @@ +/** + * 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. + */ + +const { test: baseTest, expect } = require('@playwright/test'); +const { mount } = require('@playwright/test/lib/mount'); + +const test = baseTest.extend({ + mount: async ({ page, baseURL }, use) => { + await use(async (component, options) => { + await page.goto(baseURL); + const selector = await mount(page, component, options); + return page.locator(selector); + }); + }, +}); + +module.exports = { test, expect }; diff --git a/packages/playwright-test/index.d.ts b/packages/playwright-test/index.d.ts index cd6ece8f42..cc63b3ecde 100644 --- a/packages/playwright-test/index.d.ts +++ b/packages/playwright-test/index.d.ts @@ -17,4 +17,3 @@ export * from 'playwright-core'; export * from './types/test'; export { default } from './types/test'; - \ No newline at end of file diff --git a/packages/playwright-test/package.json b/packages/playwright-test/package.json index 7b80f7320a..257fcc973b 100644 --- a/packages/playwright-test/package.json +++ b/packages/playwright-test/package.json @@ -17,6 +17,7 @@ "./package.json": "./package.json", "./lib/cli": "./lib/cli.js", "./lib/experimentalLoader": "./lib/experimentalLoader.js", + "./lib/mount": "./lib/mount.js", "./reporter": "./reporter.js" }, "bin": { diff --git a/packages/playwright-test/src/mount.ts b/packages/playwright-test/src/mount.ts new file mode 100644 index 0000000000..ce4b0f91c9 --- /dev/null +++ b/packages/playwright-test/src/mount.ts @@ -0,0 +1,68 @@ +/** + * 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 { Page } from '@playwright/test'; +import { createGuid } from 'playwright-core/lib/utils/utils'; + +export async function mount(page: Page, jsxOrType: any, options: any): Promise { + let component; + if (typeof jsxOrType === 'string') + component = { kind: 'object', type: jsxOrType, options }; + else + component = jsxOrType; + + const callbacks: Function[] = []; + wrapFunctions(component, page, callbacks); + + + const dispatchMethod = `__pw_dispatch_${createGuid}`; + + await page.exposeFunction(dispatchMethod, (ordinal: number, args: any[]) => { + callbacks[ordinal](...args); + }); + + const selector = await page.evaluate(async ({ component, dispatchMethod }) => { + const unwrapFunctions = (object: any) => { + 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)[dispatchMethod](ordinal, args); + }; + } else if (typeof value === 'object' && value) { + unwrapFunctions(value); + } + } + }; + + unwrapFunctions(component); + return await (window as any).playwrightMount(component); + }, { component, dispatchMethod }); + return selector; +} + +function wrapFunctions(object: any, page: Page, callbacks: Function[]) { + for (const [key, value] of Object.entries(object)) { + 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); + } + } +} diff --git a/packages/playwright-test/src/transform.ts b/packages/playwright-test/src/transform.ts index 83cb9e8e77..0658c7a8e3 100644 --- a/packages/playwright-test/src/transform.ts +++ b/packages/playwright-test/src/transform.ts @@ -25,7 +25,7 @@ import type { Location } from './types'; import { tsConfigLoader, TsConfigLoaderResult } from './third_party/tsconfig-loader'; import Module from 'module'; -const version = 7; +const version = 8; const cacheDir = process.env.PWTEST_CACHE_DIR || path.join(os.tmpdir(), 'playwright-transform-cache'); const sourceMaps: Map = new Map(); diff --git a/packages/playwright-test/src/tsxTransform.ts b/packages/playwright-test/src/tsxTransform.ts index d2476aa8a7..a883818016 100644 --- a/packages/playwright-test/src/tsxTransform.ts +++ b/packages/playwright-test/src/tsxTransform.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { types as t } from '@babel/core'; +import { types as t, NodePath } from '@babel/core'; import { declare } from '@babel/helper-plugin-utils'; export default declare(api => { @@ -23,6 +23,44 @@ export default declare(api => { return { name: 'playwright-debug-transform', visitor: { + Program(path) { + path.setData('pw-components', new Map()); + }, + + ImportDeclaration(path) { + // Non-JSX transform, replace + // import Button from './ButtonVue.vue' + // import { Card as MyCard } from './Card.vue' + // with + // const Button 'Button', MyCard = 'Card'; + const importNode = path.node; + if (!t.isStringLiteral(importNode.source)) { + flushConst(path, true); + return; + } + + if (!importNode.source.value.endsWith('.vue') && !importNode.source.value.endsWith('.svelte')) { + flushConst(path, true); + return; + } + + const components = path.parentPath.getData('pw-components'); + for (const specifier of importNode.specifiers) { + if (t.isImportDefaultSpecifier(specifier)) { + components.set(specifier.local.name, specifier.local.name); + continue; + } + if (t.isImportSpecifier(specifier)) { + if (t.isIdentifier(specifier.imported)) + components.set(specifier.local.name, specifier.imported.name); + else + components.set(specifier.local.name, specifier.imported.value); + } + } + + flushConst(path, false); + }, + JSXElement(path) { const jsxElement = path.node; const jsxName = jsxElement.openingElement.name; @@ -34,9 +72,17 @@ export default declare(api => { for (const jsxAttribute of jsxElement.openingElement.attributes) { if (t.isJSXAttribute(jsxAttribute)) { - if (!t.isJSXIdentifier(jsxAttribute.name)) + let namespace: t.JSXIdentifier | undefined; + let name: t.JSXIdentifier | undefined; + 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 = jsxAttribute.name.name; + 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)) @@ -58,10 +104,10 @@ export default declare(api => { children.push(child.expression); else if (t.isJSXSpreadChild(child)) children.push(t.spreadElement(child.expression)); - } path.replaceWith(t.objectExpression([ + t.objectProperty(t.identifier('kind'), t.stringLiteral('jsx')), t.objectProperty(t.identifier('type'), t.stringLiteral(name)), t.objectProperty(t.identifier('props'), t.objectExpression(props)), t.objectProperty(t.identifier('children'), t.arrayExpression(children)), @@ -70,3 +116,26 @@ export default declare(api => { } }; }); + +function flushConst(importPath: NodePath, keepPath: boolean) { + const importNode = importPath.node; + const importNodes = (importPath.parentPath.node as t.Program).body.filter(i => t.isImportDeclaration(i)); + const isLast = importNodes.indexOf(importNode) === importNodes.length - 1; + if (!isLast) { + if (!keepPath) + importPath.remove(); + return; + } + + const components = importPath.parentPath.getData('pw-components'); + if (!components.size) + return; + const variables = []; + for (const [key, value] of components) + variables.push(t.variableDeclarator(t.identifier(key), t.stringLiteral(value))); + importPath.skip(); + if (keepPath) + importPath.replaceWithMultiple([importNode, t.variableDeclaration('const', variables)]); + else + importPath.replaceWith(t.variableDeclaration('const', variables)); +} diff --git a/utils/build/build.js b/utils/build/build.js index 8ce72c6b05..26092bd04b 100644 --- a/utils/build/build.js +++ b/utils/build/build.js @@ -176,6 +176,7 @@ const webPackFiles = [ 'packages/playwright-core/src/web/traceViewer/webpack-sw.config.js', 'packages/playwright-core/src/web/recorder/webpack.config.js', 'packages/html-reporter/webpack.config.js', + 'packages/html-reporter/playwright-ct/webpack.config.js', ]; for (const file of webPackFiles) { steps.push({ diff --git a/utils/workspace.js b/utils/workspace.js index e7da1870a1..bc79362de9 100755 --- a/utils/workspace.js +++ b/utils/workspace.js @@ -171,6 +171,21 @@ const workspace = new Workspace(ROOT_PATH, [ path: path.join(ROOT_PATH, 'packages', 'html-reporter'), files: [], }), + new PWPackage({ + name: '@playwright/ct-react', + path: path.join(ROOT_PATH, 'packages', 'playwright-ct-react'), + files: [], + }), + new PWPackage({ + name: '@playwright/ct-svelte', + path: path.join(ROOT_PATH, 'packages', 'playwright-ct-svelte'), + files: [], + }), + new PWPackage({ + name: '@playwright/ct-vue', + path: path.join(ROOT_PATH, 'packages', 'playwright-ct-vue'), + files: [], + }), ]); if (require.main === module) {