fix(ct): return locator to root for fragments (#14639)
fix(fragments): return locator to root for fragments
This commit is contained in:
parent
ee0782b538
commit
2d5572abd8
|
|
@ -14,18 +14,29 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// @ts-check
|
||||||
// This file is injected into the registry as text, no dependencies are allowed.
|
// This file is injected into the registry as text, no dependencies are allowed.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
|
/** @typedef {import('../playwright-test/types/component').Component} Component */
|
||||||
|
/** @typedef {import('react').FunctionComponent} FrameworkComponent */
|
||||||
|
|
||||||
|
/** @type {Map<string, FrameworkComponent>} */
|
||||||
const registry = new Map();
|
const registry = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{[key: string]: FrameworkComponent}} components
|
||||||
|
*/
|
||||||
export function register(components) {
|
export function register(components) {
|
||||||
for (const [name, value] of Object.entries(components))
|
for (const [name, value] of Object.entries(components))
|
||||||
registry.set(name, value);
|
registry.set(name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Component} component
|
||||||
|
*/
|
||||||
function render(component) {
|
function render(component) {
|
||||||
let componentFunc = registry.get(component.type);
|
let componentFunc = registry.get(component.type);
|
||||||
if (!componentFunc) {
|
if (!componentFunc) {
|
||||||
|
|
@ -41,9 +52,12 @@ function render(component) {
|
||||||
if (!componentFunc && component.type[0].toUpperCase() === component.type[0])
|
if (!componentFunc && component.type[0].toUpperCase() === component.type[0])
|
||||||
throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...registry.keys()]}`);
|
throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...registry.keys()]}`);
|
||||||
|
|
||||||
componentFunc = componentFunc || component.type;
|
const componentFuncOrString = componentFunc || component.type;
|
||||||
|
|
||||||
return React.createElement(componentFunc, component.props, ...component.children.map(child => {
|
if (component.kind !== 'jsx')
|
||||||
|
throw new Error('Object mount notation is not supported');
|
||||||
|
|
||||||
|
return React.createElement(componentFuncOrString, component.props, ...component.children.map(child => {
|
||||||
if (typeof child === 'string')
|
if (typeof child === 'string')
|
||||||
return child;
|
return child;
|
||||||
return render(child);
|
return render(child);
|
||||||
|
|
@ -55,11 +69,5 @@ function render(component) {
|
||||||
}
|
}
|
||||||
|
|
||||||
window.playwrightMount = component => {
|
window.playwrightMount = component => {
|
||||||
if (!document.getElementById('root')) {
|
|
||||||
const rootElement = document.createElement('div');
|
|
||||||
rootElement.id = 'root';
|
|
||||||
document.body.append(rootElement);
|
|
||||||
}
|
|
||||||
ReactDOM.render(render(component), document.getElementById('root'));
|
ReactDOM.render(render(component), document.getElementById('root'));
|
||||||
return '#root > *';
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -14,21 +14,25 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
// This file is injected into the registry as text, no dependencies are allowed.
|
// This file is injected into the registry as text, no dependencies are allowed.
|
||||||
|
|
||||||
|
/** @typedef {import('../playwright-test/types/component').Component} Component */
|
||||||
|
/** @typedef {any} FrameworkComponent */
|
||||||
|
|
||||||
|
/** @type {Map<string, FrameworkComponent>} */
|
||||||
const registry = new Map();
|
const registry = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{[key: string]: FrameworkComponent}} components
|
||||||
|
*/
|
||||||
export function register(components) {
|
export function register(components) {
|
||||||
for (const [name, value] of Object.entries(components))
|
for (const [name, value] of Object.entries(components))
|
||||||
registry.set(name, value);
|
registry.set(name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.playwrightMount = component => {
|
window.playwrightMount = (component, rootElement) => {
|
||||||
if (!document.getElementById('root')) {
|
|
||||||
const rootElement = document.createElement('div');
|
|
||||||
rootElement.id = 'root';
|
|
||||||
document.body.append(rootElement);
|
|
||||||
}
|
|
||||||
let componentCtor = registry.get(component.type);
|
let componentCtor = registry.get(component.type);
|
||||||
if (!componentCtor) {
|
if (!componentCtor) {
|
||||||
// Lookup by shorthand.
|
// Lookup by shorthand.
|
||||||
|
|
@ -43,12 +47,13 @@ window.playwrightMount = component => {
|
||||||
if (!componentCtor)
|
if (!componentCtor)
|
||||||
throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...registry.keys()]}`);
|
throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...registry.keys()]}`);
|
||||||
|
|
||||||
|
if (component.kind !== 'object')
|
||||||
|
throw new Error('JSX mount notation is not supported');
|
||||||
|
|
||||||
const wrapper = new componentCtor({
|
const wrapper = new componentCtor({
|
||||||
target: document.getElementById('root'),
|
target: rootElement,
|
||||||
props: component.options?.props,
|
props: component.options?.props,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const [key, listener] of Object.entries(component.options?.on || {}))
|
for (const [key, listener] of Object.entries(component.options?.on || {}))
|
||||||
wrapper.$on(key, event => listener(event.detail));
|
wrapper.$on(key, event => listener(event.detail));
|
||||||
return '#root > *';
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -19,13 +19,14 @@
|
||||||
|
|
||||||
import { createApp, setDevtoolsHook, h } from 'vue';
|
import { createApp, setDevtoolsHook, h } from 'vue';
|
||||||
|
|
||||||
/** @typedef {import('../playwright-test/types/component').Component} Component */
|
/** @typedef {import('@playwright/test/types/component').Component} Component */
|
||||||
|
/** @typedef {import('vue').Component} FrameworkComponent */
|
||||||
|
|
||||||
/** @type { Map<string, import('vue').Component> } */
|
/** @type {Map<string, FrameworkComponent>} */
|
||||||
const registry = new Map();
|
const registry = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{[key: string]: import('vue').Component}} components
|
* @param {{[key: string]: FrameworkComponent}} components
|
||||||
*/
|
*/
|
||||||
export function register(components) {
|
export function register(components) {
|
||||||
for (const [name, value] of Object.entries(components))
|
for (const [name, value] of Object.entries(components))
|
||||||
|
|
@ -35,7 +36,7 @@ export function register(components) {
|
||||||
const allListeners = [];
|
const allListeners = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Component | string} child
|
* @param {Component | string} child
|
||||||
* @returns {import('vue').VNode | string}
|
* @returns {import('vue').VNode | string}
|
||||||
*/
|
*/
|
||||||
function renderChild(child) {
|
function renderChild(child) {
|
||||||
|
|
@ -43,7 +44,7 @@ function renderChild(child) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Component} component
|
* @param {Component} component
|
||||||
* @returns {import('vue').VNode}
|
* @returns {import('vue').VNode}
|
||||||
*/
|
*/
|
||||||
function render(component) {
|
function render(component) {
|
||||||
|
|
@ -70,7 +71,7 @@ function render(component) {
|
||||||
componentFunc = componentFunc || component.type;
|
componentFunc = componentFunc || component.type;
|
||||||
|
|
||||||
const isVueComponent = componentFunc !== component.type;
|
const isVueComponent = componentFunc !== component.type;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {(import('vue').VNode | string)[]}
|
* @type {(import('vue').VNode | string)[]}
|
||||||
*/
|
*/
|
||||||
|
|
@ -154,16 +155,10 @@ function createDevTools() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {any} */ (window).playwrightMount = /** @param {Component} component */ async component => {
|
window.playwrightMount = (component, rootElement) => {
|
||||||
if (!document.getElementById('root')) {
|
|
||||||
const rootElement = document.createElement('div');
|
|
||||||
rootElement.id = 'root';
|
|
||||||
document.body.append(rootElement);
|
|
||||||
}
|
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
render: () => render(component)
|
render: () => render(component)
|
||||||
});
|
});
|
||||||
setDevtoolsHook(createDevTools(), {});
|
setDevtoolsHook(createDevTools(), {});
|
||||||
app.mount('#root');
|
app.mount(rootElement);
|
||||||
return '#root > *';
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,13 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
/** @typedef {import('../playwright-test/types/component').Component} Component */
|
/** @typedef {import('../playwright-test/types/component').Component} Component */
|
||||||
|
/** @typedef {import('vue').Component} FrameworkComponent */
|
||||||
|
|
||||||
/** @type { Map<string, import('vue').Component> } */
|
/** @type {Map<string, FrameworkComponent>} */
|
||||||
const registry = new Map();
|
const registry = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{[key: string]: import('vue').Component}} components
|
* @param {{[key: string]: FrameworkComponent}} components
|
||||||
*/
|
*/
|
||||||
export function register(components) {
|
export function register(components) {
|
||||||
for (const [name, value] of Object.entries(components))
|
for (const [name, value] of Object.entries(components))
|
||||||
|
|
@ -34,8 +35,8 @@ export function register(components) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Component | string} child
|
* @param {Component | string} child
|
||||||
* @param {import('vue').CreateElement} h
|
* @param {import('vue').CreateElement} h
|
||||||
* @returns {import('vue').VNode | string}
|
* @returns {import('vue').VNode | string}
|
||||||
*/
|
*/
|
||||||
function renderChild(child, h) {
|
function renderChild(child, h) {
|
||||||
|
|
@ -43,8 +44,8 @@ function renderChild(child, h) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Component} component
|
* @param {Component} component
|
||||||
* @param {import('vue').CreateElement} h
|
* @param {import('vue').CreateElement} h
|
||||||
* @returns {import('vue').VNode}
|
* @returns {import('vue').VNode}
|
||||||
*/
|
*/
|
||||||
function render(component, h) {
|
function render(component, h) {
|
||||||
|
|
@ -133,16 +134,9 @@ function render(component, h) {
|
||||||
return wrapper;
|
return wrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {any} */ (window).playwrightMount = /** @param {Component} component */ async component => {
|
window.playwrightMount = (component, rootElement) => {
|
||||||
let rootElement = document.getElementById('root');
|
|
||||||
if (!rootElement) {
|
|
||||||
rootElement = document.createElement('div');
|
|
||||||
rootElement.id = 'root';
|
|
||||||
document.body.append(rootElement);
|
|
||||||
}
|
|
||||||
const mounted = new Vue({
|
const mounted = new Vue({
|
||||||
render: h => render(component, h),
|
render: h => render(component, h),
|
||||||
}).$mount();
|
}).$mount();
|
||||||
rootElement.appendChild(mounted.$el);
|
rootElement.appendChild(mounted.$el);
|
||||||
return '#root > *';
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
import { normalizeTraceMode, normalizeVideoMode, shouldCaptureTrace, shouldCaptureVideo } from './index';
|
import { normalizeTraceMode, normalizeVideoMode, shouldCaptureTrace, shouldCaptureVideo } from './index';
|
||||||
import type { Fixtures, Locator, Page, BrowserContextOptions, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, BrowserContext } from './types';
|
import type { Fixtures, Locator, Page, BrowserContextOptions, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, BrowserContext } from './types';
|
||||||
|
import type { Component, JsxComponent, ObjectComponentOptions } from '../types/component';
|
||||||
|
|
||||||
let boundCallbacksForMount: Function[] = [];
|
let boundCallbacksForMount: Function[] = [];
|
||||||
|
|
||||||
|
|
@ -65,7 +66,7 @@ export const fixtures: Fixtures<
|
||||||
},
|
},
|
||||||
|
|
||||||
mount: async ({ page }, use) => {
|
mount: async ({ page }, use) => {
|
||||||
await use(async (component, options) => {
|
await use(async (component: JsxComponent | string, options?: ObjectComponentOptions) => {
|
||||||
const selector = await (page as any)._wrapApiCall(async () => {
|
const selector = await (page as any)._wrapApiCall(async () => {
|
||||||
return await innerMount(page, component, options);
|
return await innerMount(page, component, options);
|
||||||
}, true);
|
}, true);
|
||||||
|
|
@ -75,8 +76,8 @@ export const fixtures: Fixtures<
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
async function innerMount(page: Page, jsxOrType: any, options: any): Promise<string> {
|
async function innerMount(page: Page, jsxOrType: JsxComponent | string, options?: ObjectComponentOptions): Promise<string> {
|
||||||
let component;
|
let component: Component;
|
||||||
if (typeof jsxOrType === 'string')
|
if (typeof jsxOrType === 'string')
|
||||||
component = { kind: 'object', type: jsxOrType, options };
|
component = { kind: 'object', type: jsxOrType, options };
|
||||||
else
|
else
|
||||||
|
|
@ -85,7 +86,7 @@ async function innerMount(page: Page, jsxOrType: any, options: any): Promise<str
|
||||||
wrapFunctions(component, page, boundCallbacksForMount);
|
wrapFunctions(component, page, boundCallbacksForMount);
|
||||||
|
|
||||||
// WebKit does not wait for deferred scripts.
|
// WebKit does not wait for deferred scripts.
|
||||||
await page.waitForFunction(() => !!(window as any).playwrightMount);
|
await page.waitForFunction(() => !!window.playwrightMount);
|
||||||
|
|
||||||
const selector = await page.evaluate(async ({ component }) => {
|
const selector = await page.evaluate(async ({ component }) => {
|
||||||
const unwrapFunctions = (object: any) => {
|
const unwrapFunctions = (object: any) => {
|
||||||
|
|
@ -102,7 +103,17 @@ async function innerMount(page: Page, jsxOrType: any, options: any): Promise<str
|
||||||
};
|
};
|
||||||
|
|
||||||
unwrapFunctions(component);
|
unwrapFunctions(component);
|
||||||
return await (window as any).playwrightMount(component);
|
let rootElement = document.getElementById('root');
|
||||||
|
if (!rootElement) {
|
||||||
|
rootElement = document.createElement('div');
|
||||||
|
rootElement.id = 'root';
|
||||||
|
document.body.appendChild(rootElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.playwrightMount(component, rootElement);
|
||||||
|
|
||||||
|
// When mounting fragments, return selector pointing to the root element.
|
||||||
|
return rootElement.childNodes.length > 1 ? '#root' : '#root > *';
|
||||||
}, { component });
|
}, { component });
|
||||||
return selector;
|
return selector;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
18
packages/playwright-test/types/component.d.ts
vendored
18
packages/playwright-test/types/component.d.ts
vendored
|
|
@ -21,14 +21,22 @@ export type JsxComponent = {
|
||||||
children: (Component | string)[],
|
children: (Component | string)[],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ObjectComponentOptions = {
|
||||||
|
props?: { [key: string]: any },
|
||||||
|
slots?: { [key: string]: any },
|
||||||
|
on?: { [key: string]: Function },
|
||||||
|
};
|
||||||
|
|
||||||
export type ObjectComponent = {
|
export type ObjectComponent = {
|
||||||
kind: 'object',
|
kind: 'object',
|
||||||
type: string,
|
type: string,
|
||||||
options?: {
|
options?: ObjectComponentOptions
|
||||||
props?: { [key: string]: any },
|
|
||||||
slots?: { [key: string]: any },
|
|
||||||
on?: { [key: string]: Function },
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Component = JsxComponent | ObjectComponent;
|
export type Component = JsxComponent | ObjectComponent;
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
playwrightMount(component: Component, rootElement: Element): void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -262,7 +262,34 @@ test('should work with JSX in variable', async ({ runInlineTest }) => {
|
||||||
|
|
||||||
test('pass button', async ({ mount }) => {
|
test('pass button', async ({ mount }) => {
|
||||||
const component = await mount(button);
|
const component = await mount(button);
|
||||||
await expect(component).toHaveText('Button');
|
await expect(component).toHaveText('Button');
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, { workers: 1 });
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.passed).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return root locator for fragments', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright/index.html': `<script type="module" src="/playwright/index.js"></script>`,
|
||||||
|
'playwright/index.js': `//@no-header`,
|
||||||
|
|
||||||
|
'src/button.jsx': `
|
||||||
|
//@no-header
|
||||||
|
export const Button = () => <><h1>Header</h1><button>Button</button></>;
|
||||||
|
`,
|
||||||
|
|
||||||
|
'src/button.test.jsx': `
|
||||||
|
//@no-header
|
||||||
|
import { test, expect } from '@playwright/experimental-ct-react';
|
||||||
|
import { Button } from './button';
|
||||||
|
|
||||||
|
test('pass button', async ({ mount }) => {
|
||||||
|
const component = await mount(<Button></Button>);
|
||||||
|
await expect(component).toContainText('Header');
|
||||||
|
await expect(component).toContainText('Button');
|
||||||
});
|
});
|
||||||
`,
|
`,
|
||||||
}, { workers: 1 });
|
}, { workers: 1 });
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue