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) {