feat(ct): react component as props (#28382)

closes: https://github.com/microsoft/playwright/issues/28367#issuecomment-1830298864
This commit is contained in:
Sander 2023-12-23 05:51:59 +01:00 committed by GitHub
parent afb2582eaa
commit 4d62784eeb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 65 additions and 6 deletions

View File

@ -140,7 +140,7 @@ async function innerMount(page: Page, jsxOrType: JsxComponent | string, options:
function createComponent(jsxOrType: JsxComponent | string, options: Omit<MountOptions, 'hooksConfig'> = {}): Component {
if (typeof jsxOrType !== 'string') return jsxOrType;
return { kind: 'object', type: jsxOrType, options };
return { __pw_component_marker: true, kind: 'object', type: jsxOrType, options };
}
function wrapFunctions(object: any, page: Page, callbacks: Function[]) {

View File

@ -121,7 +121,8 @@ export default declare((api: BabelAPI) => {
children.push(t.spreadElement(child.expression));
}
const component = [
const component: T.ObjectProperty[] = [
t.objectProperty(t.identifier('__pw_component_marker'), t.booleanLiteral(true)),
t.objectProperty(t.identifier('kind'), t.stringLiteral('jsx')),
t.objectProperty(t.identifier('type'), t.stringLiteral(componentName)),
t.objectProperty(t.identifier('props'), t.objectExpression(props)),

View File

@ -22,6 +22,7 @@ export type JsonObject = { [Key in string]?: JsonValue };
// JsxComponentChild can be anything, consider cases like: <>{1}</>, <>{null}</>
export type JsxComponentChild = JsxComponent | string | number | boolean | null;
export type JsxComponent = {
__pw_component_marker: true,
kind: 'jsx',
type: string,
props: Record<string, any>,
@ -36,6 +37,7 @@ export type MountOptions = {
};
export type ObjectComponent = {
__pw_component_marker: true,
kind: 'object',
type: string,
options?: MountOptions

View File

@ -32,7 +32,7 @@ const __pwRegistry = new Map();
const __pwRootRegistry = new Map();
/**
* @param {{[key: string]: () => Promise<FrameworkComponent>}} components
* @param {Record<string, () => Promise<FrameworkComponent>>} components
*/
export function pwRegister(components) {
for (const [name, value] of Object.entries(components))
@ -44,7 +44,7 @@ export function pwRegister(components) {
* @returns {component is JsxComponent}
*/
function isComponent(component) {
return !(typeof component !== 'object' || Array.isArray(component));
return component.__pw_component_marker === true && component.kind === 'jsx';
}
/**
@ -73,6 +73,23 @@ async function __pwResolveComponent(component) {
if (component.children?.length)
await Promise.all(component.children.map(child => __pwResolveComponent(child)));
if (component.props)
await __resolveProps(component.props);
}
/**
* @param {Record<string, any>} props
*/
async function __resolveProps(props) {
for (const prop of Object.values(props)) {
if (Array.isArray(prop))
await Promise.all(prop.map(child => __pwResolveComponent(child)));
else if (isComponent(prop))
await __pwResolveComponent(prop);
else if (typeof prop === 'object' && prop !== null)
await __resolveProps(prop);
}
}
/**
@ -86,18 +103,37 @@ function __renderChild(child) {
return child;
}
/**
* @param {Record<string, any>} props
*/
function __renderProps(props) {
const newProps = {};
for (const [key, prop] of Object.entries(props)) {
if (Array.isArray(prop))
newProps[key] = prop.map(child => __renderChild(child));
else if (isComponent(prop))
newProps[key] = __renderChild(prop);
else if (typeof prop === 'object' && prop !== null)
newProps[key] = __renderProps(prop);
else
newProps[key] = prop;
}
return newProps;
}
/**
* @param {JsxComponent} component
*/
function __pwRender(component) {
const componentFunc = __pwRegistry.get(component.type);
const props = __renderProps(component.props || {});
const children = component.children?.map(child => __renderChild(child)).filter(child => {
if (typeof child === 'string')
return !!child.trim();
return true;
});
const reactChildren = Array.isArray(children) && children.length === 1 ? children[0] : children;
return __pwReact.createElement(componentFunc || component.type, component.props, reactChildren);
return __pwReact.createElement(componentFunc || component.type, props, reactChildren);
}
window.playwrightMount = async (component, rootElement, hooksConfig) => {
@ -117,7 +153,6 @@ window.playwrightMount = async (component, rootElement, hooksConfig) => {
'Attempting to mount a component into an container that already has a React root'
);
}
const root = __pwCreateRoot(rootElement);
__pwRootRegistry.set(rootElement, root);
root.render(App());

View File

@ -0,0 +1,9 @@
import { ReactNode } from "react";
type ComponentAsProp = {
component: ReactNode[] | ReactNode;
};
export function ComponentAsProp({ component }: ComponentAsProp) {
return <div>{component}</div>
}

View File

@ -1,12 +1,24 @@
import { test, expect } from '@playwright/experimental-ct-react';
import Button from '@/components/Button';
import EmptyFragment from '@/components/EmptyFragment';
import { ComponentAsProp } from '@/components/ComponentAsProp';
test('render props', async ({ mount }) => {
const component = await mount(<Button title="Submit" />);
await expect(component).toContainText('Submit');
});
test('render component as props', async ({ mount }) => {
const component = await mount(<ComponentAsProp component={<Button title="Submit" />} />);
await expect(component.getByRole('button', { name: 'submit' })).toBeVisible();
});
test('render jsx array as props', async ({ mount }) => {
const component = await mount(<ComponentAsProp component={[<h4>{[4]}</h4>,[[<p>[2,3]</p>]]]} />);
await expect(component.getByRole('heading', { level: 4 })).toHaveText('4');
await expect(component.getByRole('paragraph')).toHaveText('[2,3]');
});
test('render attributes', async ({ mount }) => {
const component = await mount(<Button className="primary" title="Submit" />);
await expect(component).toHaveClass('primary');