diff --git a/packages/playwright-ct-core/src/vitePlugin.ts b/packages/playwright-ct-core/src/vitePlugin.ts index a04d1a27ad..b262439c59 100644 --- a/packages/playwright-ct-core/src/vitePlugin.ts +++ b/packages/playwright-ct-core/src/vitePlugin.ts @@ -322,9 +322,9 @@ function vitePlugin(registerSource: string, relativeTemplateDir: string, buildIn for (const [alias, value] of componentRegistry) { const importPath = value.isModuleOrAlias ? value.importPath : './' + path.relative(folder, value.importPath).replace(/\\/g, '/'); if (value.importedName) - lines.push(`import { ${value.importedName} as ${alias} } from '${importPath}';`); + lines.push(`const ${alias} = () => import('${importPath}').then((mod) => mod.${value.importedName});`); else - lines.push(`import ${alias} from '${importPath}';`); + lines.push(`const ${alias} = () => import('${importPath}').then((mod) => mod.default);`); } lines.push(`pwRegister({ ${[...componentRegistry.keys()].join(',\n ')} });`); diff --git a/packages/playwright-ct-react/registerSource.mjs b/packages/playwright-ct-react/registerSource.mjs index d3c7cda8fe..400924f1ed 100644 --- a/packages/playwright-ct-react/registerSource.mjs +++ b/packages/playwright-ct-react/registerSource.mjs @@ -21,48 +21,74 @@ import * as __pwReact from 'react'; import { createRoot as __pwCreateRoot } from 'react-dom/client'; /** @typedef {import('../playwright-ct-core/types/component').Component} Component */ +/** @typedef {import('../playwright-ct-core/types/component').JsxComponent} JsxComponent */ +/** @typedef {import('../playwright-ct-core/types/component').ObjectComponent} ObjectComponent */ /** @typedef {import('react').FunctionComponent} FrameworkComponent */ +/** @type {Map Promise>} */ +const __pwLoaderRegistry = new Map(); /** @type {Map} */ const __pwRegistry = new Map(); /** @type {Map} */ const __pwRootRegistry = new Map(); /** - * @param {{[key: string]: FrameworkComponent}} components + * @param {{[key: string]: () => Promise}} components */ export function pwRegister(components) { for (const [name, value] of Object.entries(components)) - __pwRegistry.set(name, value); + __pwLoaderRegistry.set(name, value); +} + +/** + * @param {Component} component + * @returns {component is JsxComponent | ObjectComponent} + */ +function isComponent(component) { + return !(typeof component !== 'object' || Array.isArray(component)); +} + +/** + * @param {Component} component + */ +async function __pwResolveComponent(component) { + if (!isComponent(component)) + return + + let componentFactory = __pwLoaderRegistry.get(component.type); + if (!componentFactory) { + // Lookup by shorthand. + for (const [name, value] of __pwLoaderRegistry) { + if (component.type.endsWith(`_${name}`)) { + componentFactory = value; + break; + } + } + } + + if (!componentFactory && component.type[0].toUpperCase() === component.type[0]) + throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...__pwRegistry.keys()]}`); + + if(componentFactory) + __pwRegistry.set(component.type, await componentFactory()) + + if ('children' in component) + await Promise.all(component.children.map(child => __pwResolveComponent(child))) } /** * @param {Component} component */ function __pwRender(component) { - if (typeof component !== 'object' || Array.isArray(component)) + if (!isComponent(component)) return component; - let componentFunc = __pwRegistry.get(component.type); - if (!componentFunc) { - // Lookup by shorthand. - for (const [name, value] of __pwRegistry) { - if (component.type.endsWith(`_${name}`)) { - componentFunc = value; - break; - } - } - } - - if (!componentFunc && component.type[0].toUpperCase() === component.type[0]) - throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...__pwRegistry.keys()]}`); - - const componentFuncOrString = componentFunc || component.type; + const componentFunc = __pwRegistry.get(component.type); if (component.kind !== 'jsx') throw new Error('Object mount notation is not supported'); - return __pwReact.createElement(componentFuncOrString, component.props, ...component.children.map(child => { + return __pwReact.createElement(componentFunc || component.type, component.props, ...component.children.map(child => { if (typeof child === 'string') return child; return __pwRender(child); @@ -74,6 +100,7 @@ function __pwRender(component) { } window.playwrightMount = async (component, rootElement, hooksConfig) => { + await __pwResolveComponent(component); let App = () => __pwRender(component); for (const hook of window.__pw_hooks_before_mount || []) { const wrapper = await hook({ App, hooksConfig }); @@ -105,6 +132,7 @@ window.playwrightUnmount = async rootElement => { }; window.playwrightUpdate = async (rootElement, component) => { + await __pwResolveComponent(component); const root = __pwRootRegistry.get(rootElement); if (root === undefined) throw new Error('Component was not mounted'); diff --git a/packages/playwright-ct-react17/registerSource.mjs b/packages/playwright-ct-react17/registerSource.mjs index e77a546038..06a0ec10b1 100644 --- a/packages/playwright-ct-react17/registerSource.mjs +++ b/packages/playwright-ct-react17/registerSource.mjs @@ -22,46 +22,72 @@ import __pwReact from 'react'; import __pwReactDOM from 'react-dom'; /** @typedef {import('../playwright-ct-core/types/component').Component} Component */ +/** @typedef {import('../playwright-ct-core/types/component').JsxComponent} JsxComponent */ +/** @typedef {import('../playwright-ct-core/types/component').ObjectComponent} ObjectComponent */ /** @typedef {import('react').FunctionComponent} FrameworkComponent */ -/** @type {Map} */ +/** @type {Map Promise>} */ +const __pwLoaderRegistry = new Map(); +/** @type {Map Promise}} components */ export function pwRegister(components) { for (const [name, value] of Object.entries(components)) - __pwRegistry.set(name, value); + __pwLoaderRegistry.set(name, value); +} + +/** + * @param {Component} component + * @returns {component is JsxComponent | ObjectComponent} + */ +function isComponent(component) { + return !(typeof component !== 'object' || Array.isArray(component)); +} + +/** + * @param {Component} component + */ +async function __pwResolveComponent(component) { + if (!isComponent(component)) + return + + let componentFactory = __pwLoaderRegistry.get(component.type); + if (!componentFactory) { + // Lookup by shorthand. + for (const [name, value] of __pwLoaderRegistry) { + if (component.type.endsWith(`_${name}`)) { + componentFactory = value; + break; + } + } + } + + if (!componentFactory && component.type[0].toUpperCase() === component.type[0]) + throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...__pwRegistry.keys()]}`); + + if(componentFactory) + __pwRegistry.set(component.type, await componentFactory()) + + if ('children' in component) + await Promise.all(component.children.map(child => __pwResolveComponent(child))) } /** * @param {Component} component */ function __pwRender(component) { - if (typeof component !== 'object' || Array.isArray(component)) + if (!isComponent(component)) return component; - let componentFunc = __pwRegistry.get(component.type); - if (!componentFunc) { - // Lookup by shorthand. - for (const [name, value] of __pwRegistry) { - if (component.type.endsWith(`_${name}`)) { - componentFunc = value; - break; - } - } - } - - if (!componentFunc && component.type[0].toUpperCase() === component.type[0]) - throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...__pwRegistry.keys()]}`); - - const componentFuncOrString = componentFunc || component.type; - + const componentFunc = __pwRegistry.get(component.type); + if (component.kind !== 'jsx') throw new Error('Object mount notation is not supported'); - return __pwReact.createElement(componentFuncOrString, component.props, ...component.children.map(child => { + return __pwReact.createElement(componentFunc || component.type, component.props, ...component.children.map(child => { if (typeof child === 'string') return child; return __pwRender(child); @@ -73,6 +99,7 @@ function __pwRender(component) { } window.playwrightMount = async (component, rootElement, hooksConfig) => { + await __pwResolveComponent(component); let App = () => __pwRender(component); for (const hook of window.__pw_hooks_before_mount || []) { const wrapper = await hook({ App, hooksConfig }); @@ -92,5 +119,6 @@ window.playwrightUnmount = async rootElement => { }; window.playwrightUpdate = async (rootElement, component) => { + await __pwResolveComponent(component); __pwReactDOM.render(__pwRender(/** @type {Component} */(component)), rootElement); }; diff --git a/packages/playwright-ct-solid/registerSource.mjs b/packages/playwright-ct-solid/registerSource.mjs index 426c414038..de401f6e4e 100644 --- a/packages/playwright-ct-solid/registerSource.mjs +++ b/packages/playwright-ct-solid/registerSource.mjs @@ -21,17 +21,57 @@ import { render as __pwSolidRender, createComponent as __pwSolidCreateComponent import __pwH from 'solid-js/h'; /** @typedef {import('../playwright-ct-core/types/component').Component} Component */ +/** @typedef {import('../playwright-ct-core/types/component').JsxComponent} JsxComponent */ +/** @typedef {import('../playwright-ct-core/types/component').ObjectComponent} ObjectComponent */ /** @typedef {() => import('solid-js').JSX.Element} FrameworkComponent */ +/** @type {Map Promise>} */ +const __pwLoaderRegistry = new Map(); /** @type {Map} */ const __pwRegistry = new Map(); /** - * @param {{[key: string]: FrameworkComponent}} components + * @param {{[key: string]: () => Promise}} components */ export function pwRegister(components) { for (const [name, value] of Object.entries(components)) - __pwRegistry.set(name, value); + __pwLoaderRegistry.set(name, value); +} + +/** + * @param {Component} component + * @returns {component is JsxComponent | ObjectComponent} + */ +function isComponent(component) { + return !(typeof component !== 'object' || Array.isArray(component)); +} + +/** + * @param {Component} component + */ +async function __pwResolveComponent(component) { + if (!isComponent(component)) + return + + let componentFactory = __pwLoaderRegistry.get(component.type); + if (!componentFactory) { + // Lookup by shorthand. + for (const [name, value] of __pwLoaderRegistry) { + if (component.type.endsWith(`_${name}`)) { + componentFactory = value; + break; + } + } + } + + if (!componentFactory && component.type[0].toUpperCase() === component.type[0]) + throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...__pwRegistry.keys()]}`); + + if(componentFactory) + __pwRegistry.set(component.type, await componentFactory()) + + if ('children' in component) + await Promise.all(component.children.map(child => __pwResolveComponent(child))) } function __pwCreateChild(child) { @@ -45,19 +85,7 @@ function __pwCreateComponent(component) { if (typeof component !== 'object' || Array.isArray(component)) return component; - let Component = __pwRegistry.get(component.type); - if (!Component) { - // Lookup by shorthand. - for (const [name, value] of __pwRegistry) { - if (component.type.endsWith(`_${name}`)) { - Component = value; - break; - } - } - } - - if (!Component && component.type[0].toUpperCase() === component.type[0]) - throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...__pwRegistry.keys()]}`); + const componentFunc = __pwRegistry.get(component.type); if (component.kind !== 'jsx') throw new Error('Object mount notation is not supported'); @@ -69,15 +97,16 @@ function __pwCreateComponent(component) { return children; }, []); - if (!Component) + if (!componentFunc) return __pwH(component.type, component.props, children); - return __pwSolidCreateComponent(Component, { ...component.props, children }); + return __pwSolidCreateComponent(componentFunc, { ...component.props, children }); } const __pwUnmountKey = Symbol('unmountKey'); window.playwrightMount = async (component, rootElement, hooksConfig) => { + await __pwResolveComponent(component); let App = () => __pwCreateComponent(component); for (const hook of window.__pw_hooks_before_mount || []) { const wrapper = await hook({ App, hooksConfig }); diff --git a/packages/playwright-ct-svelte/registerSource.mjs b/packages/playwright-ct-svelte/registerSource.mjs index 99f6dcf1c7..676b7cf193 100644 --- a/packages/playwright-ct-svelte/registerSource.mjs +++ b/packages/playwright-ct-svelte/registerSource.mjs @@ -21,18 +21,58 @@ import { detach as __pwDetach, insert as __pwInsert, noop as __pwNoop } from 'svelte/internal'; /** @typedef {import('../playwright-ct-core/types/component').Component} Component */ +/** @typedef {import('../playwright-ct-core/types/component').JsxComponent} JsxComponent */ +/** @typedef {import('../playwright-ct-core/types/component').ObjectComponent} ObjectComponent */ /** @typedef {any} FrameworkComponent */ /** @typedef {import('svelte').SvelteComponent} SvelteComponent */ +/** @type {Map Promise>} */ +const __pwLoaderRegistry = new Map(); /** @type {Map} */ const __pwRegistry = new Map(); /** - * @param {{[key: string]: FrameworkComponent}} components + * @param {{[key: string]: () => Promise}} components */ export function pwRegister(components) { for (const [name, value] of Object.entries(components)) - __pwRegistry.set(name, value); + __pwLoaderRegistry.set(name, value); +} + +/** + * @param {Component} component + * @returns {component is JsxComponent | ObjectComponent} + */ +function isComponent(component) { + return !(typeof component !== 'object' || Array.isArray(component)); +} + +/** + * @param {Component} component + */ +async function __pwResolveComponent(component) { + if (!isComponent(component)) + return + + let componentFactory = __pwLoaderRegistry.get(component.type); + if (!componentFactory) { + // Lookup by shorthand. + for (const [name, value] of __pwLoaderRegistry) { + if (component.type.endsWith(`_${name}_svelte`)) { + componentFactory = value; + break; + } + } + } + + if (!componentFactory) + throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...__pwRegistry.keys()]}`); + + if(componentFactory) + __pwRegistry.set(component.type, await componentFactory()) + + if ('children' in component) + await Promise.all(component.children.map(child => __pwResolveComponent(child))) } /** @@ -69,20 +109,9 @@ function __pwCreateSlots(slots) { const __pwSvelteComponentKey = Symbol('svelteComponent'); window.playwrightMount = async (component, rootElement, hooksConfig) => { - let componentCtor = __pwRegistry.get(component.type); - if (!componentCtor) { - // Lookup by shorthand. - for (const [name, value] of __pwRegistry) { - if (component.type.endsWith(`_${name}_svelte`)) { - componentCtor = value; - break; - } - } - } - - if (!componentCtor) - throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...__pwRegistry.keys()]}`); - + await __pwResolveComponent(component); + const componentCtor = __pwRegistry.get(component.type); + if (component.kind !== 'object') throw new Error('JSX mount notation is not supported'); @@ -115,6 +144,7 @@ window.playwrightUnmount = async rootElement => { }; window.playwrightUpdate = async (rootElement, component) => { + await __pwResolveComponent(component); const svelteComponent = /** @type {SvelteComponent} */ (rootElement[__pwSvelteComponentKey]); if (!svelteComponent) throw new Error('Component was not mounted'); diff --git a/packages/playwright-ct-vue/registerSource.mjs b/packages/playwright-ct-vue/registerSource.mjs index ae2e00b419..7d743a1092 100644 --- a/packages/playwright-ct-vue/registerSource.mjs +++ b/packages/playwright-ct-vue/registerSource.mjs @@ -21,18 +21,58 @@ import { createApp as __pwCreateApp, setDevtoolsHook as __pwSetDevtoolsHook, h a import { compile as __pwCompile } from '@vue/compiler-dom'; import * as __pwVue from 'vue'; -/** @typedef {import('@playwright/test/types/experimentalComponent').Component} Component */ +/** @typedef {import('../playwright-ct-core/types/component').Component} Component */ +/** @typedef {import('../playwright-ct-core/types/component').JsxComponent} JsxComponent */ +/** @typedef {import('../playwright-ct-core/types/component').ObjectComponent} ObjectComponent */ /** @typedef {import('vue').Component} FrameworkComponent */ +/** @type {Map Promise>} */ +const __pwLoaderRegistry = new Map(); /** @type {Map} */ const __pwRegistry = new Map(); /** - * @param {{[key: string]: FrameworkComponent}} components + * @param {{[key: string]: () => Promise}} components */ export function pwRegister(components) { for (const [name, value] of Object.entries(components)) - __pwRegistry.set(name, value); + __pwLoaderRegistry.set(name, value); +} + +/** + * @param {Component} component + * @returns {component is JsxComponent | ObjectComponent} + */ +function isComponent(component) { + return !(typeof component !== 'object' || Array.isArray(component)); +} + +/** + * @param {Component} component + */ +async function __pwResolveComponent(component) { + if (!isComponent(component)) + return + + let componentFactory = __pwLoaderRegistry.get(component.type); + if (!componentFactory) { + // Lookup by shorthand. + for (const [name, value] of __pwLoaderRegistry) { + if (component.type.endsWith(`_${name}_vue`)) { + componentFactory = value; + break; + } + } + } + + if (!componentFactory && component.type[0].toUpperCase() === component.type[0]) + throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...__pwRegistry.keys()]}`); + + if(componentFactory) + __pwRegistry.set(component.type, await componentFactory()) + + if ('children' in component) + await Promise.all(component.children.map(child => __pwResolveComponent(child))) } const __pwAllListeners = new Map(); @@ -95,23 +135,7 @@ function __pwCreateComponent(component) { if (typeof component === 'string') return component; - /** - * @type {import('vue').Component | string | undefined} - */ let componentFunc = __pwRegistry.get(component.type); - if (!componentFunc) { - // Lookup by shorthand. - for (const [name, value] of __pwRegistry) { - if (component.type.endsWith(`_${name}_vue`)) { - componentFunc = value; - break; - } - } - } - - if (!componentFunc && component.type[0].toUpperCase() === component.type[0]) - throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...__pwRegistry.keys()]}`); - componentFunc = componentFunc || component.type; const isVueComponent = componentFunc !== component.type; @@ -223,6 +247,7 @@ const __pwAppKey = Symbol('appKey'); const __pwWrapperKey = Symbol('wrapperKey'); window.playwrightMount = async (component, rootElement, hooksConfig) => { + await __pwResolveComponent(component); const app = __pwCreateApp({ render: () => { const wrapper = __pwCreateWrapper(component); @@ -248,7 +273,8 @@ window.playwrightUnmount = async rootElement => { app.unmount(); }; -window.playwrightUpdate = async (rootElement, options) => { +window.playwrightUpdate = async (rootElement, component) => { + await __pwResolveComponent(component); const wrapper = rootElement[__pwWrapperKey]; if (!wrapper) throw new Error('Component was not mounted'); @@ -256,7 +282,7 @@ window.playwrightUpdate = async (rootElement, options) => { if (!wrapper.component) throw new Error('Updating a native HTML element is not supported'); - const { slots, listeners, props } = __pwCreateComponent(options); + const { slots, listeners, props } = __pwCreateComponent(component); wrapper.component.slots = __pwWrapFunctions(slots); __pwAllListeners.set(wrapper, listeners); diff --git a/packages/playwright-ct-vue2/registerSource.mjs b/packages/playwright-ct-vue2/registerSource.mjs index 54bd466090..4c2741907c 100644 --- a/packages/playwright-ct-vue2/registerSource.mjs +++ b/packages/playwright-ct-vue2/registerSource.mjs @@ -21,17 +21,57 @@ import __pwVue, { h as __pwH } from 'vue'; /** @typedef {import('../playwright-ct-core/types/component').Component} Component */ +/** @typedef {import('../playwright-ct-core/types/component').JsxComponent} JsxComponent */ +/** @typedef {import('../playwright-ct-core/types/component').ObjectComponent} ObjectComponent */ /** @typedef {import('vue').Component} FrameworkComponent */ +/** @type {Map Promise>} */ +const __pwLoaderRegistry = new Map(); /** @type {Map} */ const __pwRegistry = new Map(); /** - * @param {{[key: string]: FrameworkComponent}} components + * @param {{[key: string]: () => Promise}} components */ export function pwRegister(components) { for (const [name, value] of Object.entries(components)) - __pwRegistry.set(name, value); + __pwLoaderRegistry.set(name, value); +} + +/** + * @param {Component} component + * @returns {component is JsxComponent | ObjectComponent} + */ +function isComponent(component) { + return !(typeof component !== 'object' || Array.isArray(component)); +} + +/** + * @param {Component} component + */ +async function __pwResolveComponent(component) { + if (!isComponent(component)) + return + + let componentFactory = __pwLoaderRegistry.get(component.type); + if (!componentFactory) { + // Lookup by shorthand. + for (const [name, value] of __pwLoaderRegistry) { + if (component.type.endsWith(`_${name}_vue`)) { + componentFactory = value; + break; + } + } + } + + if (!componentFactory && component.type[0].toUpperCase() === component.type[0]) + throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...__pwRegistry.keys()]}`); + + if(componentFactory) + __pwRegistry.set(component.type, await componentFactory()) + + if ('children' in component) + await Promise.all(component.children.map(child => __pwResolveComponent(child))) } /** @@ -59,25 +99,7 @@ function __pwComponentHasKeyInProps(Component, key) { * @param {Component} component */ function __pwCreateComponent(component) { - /** - * @type {import('vue').Component | string | undefined} - */ - let componentFunc = __pwRegistry.get(component.type); - if (!componentFunc) { - // Lookup by shorthand. - for (const [name, value] of __pwRegistry) { - if (component.type.endsWith(`_${name}_vue`)) { - componentFunc = value; - break; - } - } - } - - if (!componentFunc && component.type[0].toUpperCase() === component.type[0]) - throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...__pwRegistry.keys()]}`); - - componentFunc = componentFunc || component.type; - + const componentFunc = __pwRegistry.get(component.type) || component.type; const isVueComponent = componentFunc !== component.type; /** @@ -157,6 +179,7 @@ const instanceKey = Symbol('instanceKey'); const wrapperKey = Symbol('wrapperKey'); window.playwrightMount = async (component, rootElement, hooksConfig) => { + await __pwResolveComponent(component); let options = {}; for (const hook of window.__pw_hooks_before_mount || []) options = await hook({ hooksConfig, Vue: __pwVue }); @@ -185,6 +208,7 @@ window.playwrightUnmount = async rootElement => { }; window.playwrightUpdate = async (element, options) => { + await __pwResolveComponent(options); const wrapper = /** @type {any} */(element)[wrapperKey]; if (!wrapper) throw new Error('Component was not mounted'); diff --git a/packages/web/playwright/index.ts b/packages/web/playwright/index.ts index 1c6abfbafa..0bff9a7ef4 100644 --- a/packages/web/playwright/index.ts +++ b/packages/web/playwright/index.ts @@ -17,3 +17,4 @@ import '../src/common.css'; import '../src/theme.ts'; import '../src/third_party/vscode/codicon.css'; +import '../src/third_party/vscode/colors.css';