fix(ct): return locator to root for fragments (#14639)

fix(fragments): return locator to root for fragments
This commit is contained in:
Pavel Feldman 2022-06-04 14:07:06 -07:00 committed by GitHub
parent ee0782b538
commit 2d5572abd8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 104 additions and 56 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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