chore: add experimental mount (#12657)

This commit is contained in:
Pavel Feldman 2022-03-11 08:00:46 -08:00 committed by GitHub
parent d7c1a57565
commit af55f48754
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 754 additions and 176 deletions

30
package-lock.json generated
View File

@ -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": {

1
packages/html-reporter/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
out-ct/

View File

@ -0,0 +1,28 @@
<!--
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.
-->
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<meta name='color-scheme' content='dark light'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>Playwright CT</title>
</head>
<body>
<div id=root></div>
</body>
</html>

View File

@ -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);

View File

@ -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,
}),
]
};

View File

@ -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: [

View File

@ -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 }) => {

View File

@ -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 }) => {

View File

@ -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 = {

View File

@ -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'));
};

View File

@ -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<Locator>;
webpack: string;
};
export const test = baseTest.extend<TestFixtures>({
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: `<html>
<meta name='color-scheme' content='dark light'>
<style>html, body { padding: 0; margin: 0; background: #aaa; }</style>
<div id='root' style='width: 100%; height: 100%;'></div>
</html>`,
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';

View File

@ -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(),
]

View File

@ -0,0 +1,8 @@
{
"name": "@playwright/ct-react",
"private": true,
"exports": {
"./render": "./render.mjs",
"./test": "./test.js"
}
}

View File

@ -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;

View File

@ -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';
};

34
packages/playwright-ct-react/test.d.ts vendored Normal file
View File

@ -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<Locator>;
}
export const test: TestType<
PlaywrightTestArgs & PlaywrightTestOptions & ComponentFixtures,
PlaywrightWorkerArgs & PlaywrightWorkerOptions>;
export { expect } from '@playwright/test';

View File

@ -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 };

View File

@ -0,0 +1,8 @@
{
"name": "@playwright/ct-svelte",
"private": true,
"exports": {
"./render": "./render.mjs",
"./test": "./test.js"
}
}

View File

@ -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;

View File

@ -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';
};

38
packages/playwright-ct-svelte/test.d.ts vendored Normal file
View File

@ -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<Locator>;
}
export const test: TestType<
PlaywrightTestArgs & PlaywrightTestOptions & ComponentFixtures,
PlaywrightWorkerArgs & PlaywrightWorkerOptions>;
export { expect } from '@playwright/test';

View File

@ -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 };

View File

@ -0,0 +1,8 @@
{
"name": "@playwright/ct-vue",
"private": true,
"exports": {
"./render": "./render.mjs",
"./test": "./test.js"
}
}

18
packages/playwright-ct-vue/render.d.ts vendored Normal file
View File

@ -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;

View File

@ -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';
};

39
packages/playwright-ct-vue/test.d.ts vendored Normal file
View File

@ -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<Locator>;
mount(component: any, options?: {
props?: { [key: string]: any },
slots?: { [key: string]: any },
on?: { [key: string]: Function },
}): Promise<Locator>;
}
export const test: TestType<
PlaywrightTestArgs & PlaywrightTestOptions & ComponentFixtures,
PlaywrightWorkerArgs & PlaywrightWorkerOptions>;
export { expect } from '@playwright/test';

View File

@ -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 };

View File

@ -17,4 +17,3 @@
export * from 'playwright-core';
export * from './types/test';
export { default } from './types/test';

View File

@ -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": {

View File

@ -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<string> {
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);
}
}
}

View File

@ -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<string, string> = new Map();

View File

@ -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<t.ImportDeclaration>, 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));
}

View File

@ -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({

View File

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