cherry-pick(#28975): chore: refactor import processing in ct
This commit is contained in:
parent
4d9f923dfe
commit
d47ed6a076
|
|
@ -0,0 +1,2 @@
|
|||
[importRegistry.ts]
|
||||
../types/**
|
||||
59
packages/playwright-ct-core/src/importRegistry.ts
Normal file
59
packages/playwright-ct-core/src/importRegistry.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* 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 { ImportRef } from '../types/component';
|
||||
|
||||
export class ImportRegistry {
|
||||
private _registry = new Map<string, () => Promise<any>>();
|
||||
|
||||
initialize(components: Record<string, () => Promise<any>>) {
|
||||
for (const [name, value] of Object.entries(components))
|
||||
this._registry.set(name, value);
|
||||
}
|
||||
|
||||
async resolveImports(value: any): Promise<any> {
|
||||
if (value === null || typeof value !== 'object')
|
||||
return value;
|
||||
if (this._isImportRef(value)) {
|
||||
const importFunction = this._registry.get(value.id);
|
||||
if (!importFunction)
|
||||
throw new Error(`Unregistered component: ${value.id}. Following components are registered: ${[...this._registry.keys()]}`);
|
||||
let importedObject = await importFunction();
|
||||
if (!importedObject)
|
||||
throw new Error(`Could not resolve component: ${value.id}.`);
|
||||
if (value.property) {
|
||||
importedObject = importedObject[value.property];
|
||||
if (!importedObject)
|
||||
throw new Error(`Could not instantiate component: ${value.id}.${value.property}.`);
|
||||
}
|
||||
return importedObject;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
const result = [];
|
||||
for (const item of value)
|
||||
result.push(await this.resolveImports(item));
|
||||
return result;
|
||||
}
|
||||
const result: any = {};
|
||||
for (const [key, prop] of Object.entries(value))
|
||||
result[key] = await this.resolveImports(prop);
|
||||
return result;
|
||||
}
|
||||
|
||||
private _isImportRef(value: any): value is ImportRef {
|
||||
return typeof value === 'object' && value && value.__pw_type === 'importRef';
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
|
||||
import type { Fixtures, Locator, Page, BrowserContextOptions, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, BrowserContext } from 'playwright/test';
|
||||
import type { Component, JsxComponent, MountOptions } from '../types/component';
|
||||
import type { Component, ImportRef, JsxComponent, MountOptions, ObjectComponentOptions } from '../types/component';
|
||||
import type { ContextReuseMode, FullConfigInternal } from '../../playwright/src/common/config';
|
||||
|
||||
let boundCallbacksForMount: Function[] = [];
|
||||
|
|
@ -25,12 +25,16 @@ interface MountResult extends Locator {
|
|||
update(options: Omit<MountOptions, 'hooksConfig'> | string | JsxComponent): Promise<void>;
|
||||
}
|
||||
|
||||
export const fixtures: Fixtures<
|
||||
PlaywrightTestArgs & PlaywrightTestOptions & {
|
||||
type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & {
|
||||
mount: (component: any, options: any) => Promise<MountResult>;
|
||||
},
|
||||
PlaywrightWorkerArgs & PlaywrightWorkerOptions & { _ctWorker: { context: BrowserContext | undefined, hash: string } },
|
||||
{ _contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>, _contextReuseMode: ContextReuseMode }> = {
|
||||
};
|
||||
type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & { _ctWorker: { context: BrowserContext | undefined, hash: string } };
|
||||
type BaseTestFixtures = {
|
||||
_contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>,
|
||||
_contextReuseMode: ContextReuseMode
|
||||
};
|
||||
|
||||
export const fixtures: Fixtures<TestFixtures, WorkerFixtures, BaseTestFixtures> = {
|
||||
|
||||
_contextReuseMode: 'when-possible',
|
||||
|
||||
|
|
@ -51,9 +55,9 @@ export const fixtures: Fixtures<
|
|||
},
|
||||
|
||||
mount: async ({ page }, use) => {
|
||||
await use(async (component: JsxComponent | string, options?: MountOptions) => {
|
||||
await use(async (componentRef: JsxComponent | ImportRef, options?: ObjectComponentOptions & MountOptions) => {
|
||||
const selector = await (page as any)._wrapApiCall(async () => {
|
||||
return await innerMount(page, component, options);
|
||||
return await innerMount(page, componentRef, options);
|
||||
}, true);
|
||||
const locator = page.locator(selector);
|
||||
return Object.assign(locator, {
|
||||
|
|
@ -63,23 +67,23 @@ export const fixtures: Fixtures<
|
|||
await window.playwrightUnmount(rootElement);
|
||||
});
|
||||
},
|
||||
update: async (options: JsxComponent | Omit<MountOptions, 'hooksConfig'>) => {
|
||||
if (isJsxApi(options))
|
||||
update: async (options: JsxComponent | ObjectComponentOptions) => {
|
||||
if (isJsxComponent(options))
|
||||
return await innerUpdate(page, options);
|
||||
await innerUpdate(page, component, options);
|
||||
await innerUpdate(page, componentRef, options);
|
||||
}
|
||||
});
|
||||
});
|
||||
boundCallbacksForMount = [];
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
function isJsxApi(options: Record<string, unknown>): options is JsxComponent {
|
||||
return options?.kind === 'jsx';
|
||||
function isJsxComponent(component: any): component is JsxComponent {
|
||||
return typeof component === 'object' && component && component.__pw_type === 'jsx';
|
||||
}
|
||||
|
||||
async function innerUpdate(page: Page, jsxOrType: JsxComponent | string, options: Omit<MountOptions, 'hooksConfig'> = {}): Promise<void> {
|
||||
const component = createComponent(jsxOrType, options);
|
||||
async function innerUpdate(page: Page, componentRef: JsxComponent | ImportRef, options: ObjectComponentOptions = {}): Promise<void> {
|
||||
const component = createComponent(componentRef, options);
|
||||
wrapFunctions(component, page, boundCallbacksForMount);
|
||||
|
||||
await page.evaluate(async ({ component }) => {
|
||||
|
|
@ -97,13 +101,14 @@ async function innerUpdate(page: Page, jsxOrType: JsxComponent | string, options
|
|||
};
|
||||
|
||||
unwrapFunctions(component);
|
||||
component = await window.__pwRegistry.resolveImports(component);
|
||||
const rootElement = document.getElementById('root')!;
|
||||
return await window.playwrightUpdate(rootElement, component);
|
||||
}, { component });
|
||||
}
|
||||
|
||||
async function innerMount(page: Page, jsxOrType: JsxComponent | string, options: MountOptions = {}): Promise<string> {
|
||||
const component = createComponent(jsxOrType, options);
|
||||
async function innerMount(page: Page, componentRef: JsxComponent | ImportRef, options: ObjectComponentOptions & MountOptions = {}): Promise<string> {
|
||||
const component = createComponent(componentRef, options);
|
||||
wrapFunctions(component, page, boundCallbacksForMount);
|
||||
|
||||
// WebKit does not wait for deferred scripts.
|
||||
|
|
@ -130,7 +135,7 @@ async function innerMount(page: Page, jsxOrType: JsxComponent | string, options:
|
|||
rootElement.id = 'root';
|
||||
document.body.appendChild(rootElement);
|
||||
}
|
||||
|
||||
component = await window.__pwRegistry.resolveImports(component);
|
||||
await window.playwrightMount(component, rootElement, hooksConfig);
|
||||
|
||||
return '#root >> internal:control=component';
|
||||
|
|
@ -138,9 +143,14 @@ async function innerMount(page: Page, jsxOrType: JsxComponent | string, options:
|
|||
return selector;
|
||||
}
|
||||
|
||||
function createComponent(jsxOrType: JsxComponent | string, options: Omit<MountOptions, 'hooksConfig'> = {}): Component {
|
||||
if (typeof jsxOrType !== 'string') return jsxOrType;
|
||||
return { __pw_component_marker: true, kind: 'object', type: jsxOrType, options };
|
||||
function createComponent(component: JsxComponent | ImportRef, options: ObjectComponentOptions = {}): Component {
|
||||
if (component.__pw_type === 'jsx')
|
||||
return component;
|
||||
return {
|
||||
__pw_type: 'object-component',
|
||||
type: component,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
function wrapFunctions(object: any, page: Page, callbacks: Function[]) {
|
||||
|
|
|
|||
|
|
@ -20,9 +20,8 @@ import { types, declare, traverse } from 'playwright/lib/transform/babelBundle';
|
|||
import { resolveImportSpecifierExtension } from 'playwright/lib/util';
|
||||
const t: typeof T = types;
|
||||
|
||||
const fullNames = new Map<string, string | undefined>();
|
||||
let componentNames: Set<string>;
|
||||
let componentIdentifiers: Set<T.Identifier>;
|
||||
let componentImports: Map<string, ImportInfo>;
|
||||
|
||||
export default declare((api: BabelAPI) => {
|
||||
api.assertVersion(7);
|
||||
|
|
@ -30,11 +29,41 @@ export default declare((api: BabelAPI) => {
|
|||
const result: PluginObj = {
|
||||
name: 'playwright-debug-transform',
|
||||
visitor: {
|
||||
Program(path) {
|
||||
fullNames.clear();
|
||||
Program: {
|
||||
enter(path) {
|
||||
const result = collectComponentUsages(path.node);
|
||||
componentNames = result.names;
|
||||
componentIdentifiers = result.identifiers;
|
||||
componentImports = new Map();
|
||||
},
|
||||
exit(path) {
|
||||
let firstDeclaration: any;
|
||||
let lastImportDeclaration: any;
|
||||
path.get('body').forEach(p => {
|
||||
if (p.isImportDeclaration())
|
||||
lastImportDeclaration = p;
|
||||
else if (!firstDeclaration)
|
||||
firstDeclaration = p;
|
||||
});
|
||||
const insertionPath = lastImportDeclaration || firstDeclaration;
|
||||
if (!insertionPath)
|
||||
return;
|
||||
for (const componentImport of [...componentImports.values()].reverse()) {
|
||||
insertionPath.insertAfter(
|
||||
t.variableDeclaration(
|
||||
'const',
|
||||
[
|
||||
t.variableDeclarator(
|
||||
t.identifier(componentImport.localName),
|
||||
t.objectExpression([
|
||||
t.objectProperty(t.identifier('__pw_type'), t.stringLiteral('importRef')),
|
||||
t.objectProperty(t.identifier('id'), t.stringLiteral(componentImport.id)),
|
||||
]),
|
||||
)
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
ImportDeclaration(p) {
|
||||
|
|
@ -44,14 +73,12 @@ export default declare((api: BabelAPI) => {
|
|||
|
||||
let components = 0;
|
||||
for (const specifier of importNode.specifiers) {
|
||||
const specifierName = specifier.local.name;
|
||||
const componentName = componentNames.has(specifierName) ? specifierName : [...componentNames].find(c => c.startsWith(specifierName + '.'));
|
||||
if (!componentName)
|
||||
continue;
|
||||
if (t.isImportNamespaceSpecifier(specifier))
|
||||
continue;
|
||||
const { fullName } = componentInfo(specifier, importNode.source.value, this.filename!, componentName);
|
||||
fullNames.set(componentName, fullName);
|
||||
const info = importInfo(importNode, specifier, this.filename!);
|
||||
if (!componentNames.has(info.localName))
|
||||
continue;
|
||||
componentImports.set(info.localName, info);
|
||||
++components;
|
||||
}
|
||||
|
||||
|
|
@ -62,76 +89,20 @@ export default declare((api: BabelAPI) => {
|
|||
}
|
||||
},
|
||||
|
||||
Identifier(p) {
|
||||
if (componentIdentifiers.has(p.node)) {
|
||||
const componentName = fullNames.get(p.node.name) || p.node.name;
|
||||
p.replaceWith(t.stringLiteral(componentName));
|
||||
}
|
||||
},
|
||||
|
||||
JSXElement(path) {
|
||||
const jsxElement = path.node;
|
||||
const jsxName = jsxElement.openingElement.name;
|
||||
let nameOrExpression: string = '';
|
||||
if (t.isJSXIdentifier(jsxName))
|
||||
nameOrExpression = jsxName.name;
|
||||
else if (t.isJSXMemberExpression(jsxName) && t.isJSXIdentifier(jsxName.object) && t.isJSXIdentifier(jsxName.property))
|
||||
nameOrExpression = jsxName.object.name + '.' + jsxName.property.name;
|
||||
if (!nameOrExpression)
|
||||
MemberExpression(path) {
|
||||
if (!t.isIdentifier(path.node.object))
|
||||
return;
|
||||
const componentName = fullNames.get(nameOrExpression) || nameOrExpression;
|
||||
|
||||
const props: (T.ObjectProperty | T.SpreadElement)[] = [];
|
||||
|
||||
for (const jsxAttribute of jsxElement.openingElement.attributes) {
|
||||
if (t.isJSXAttribute(jsxAttribute)) {
|
||||
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 = (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))
|
||||
props.push(t.objectProperty(t.stringLiteral(attrName), jsxAttribute.value.expression));
|
||||
else if (jsxAttribute.value === null)
|
||||
props.push(t.objectProperty(t.stringLiteral(attrName), t.booleanLiteral(true)));
|
||||
else
|
||||
props.push(t.objectProperty(t.stringLiteral(attrName), t.nullLiteral()));
|
||||
} else if (t.isJSXSpreadAttribute(jsxAttribute)) {
|
||||
props.push(t.spreadElement(jsxAttribute.argument));
|
||||
}
|
||||
}
|
||||
|
||||
const children: (T.Expression | T.SpreadElement)[] = [];
|
||||
for (const child of jsxElement.children) {
|
||||
if (t.isJSXText(child))
|
||||
children.push(t.stringLiteral(child.value));
|
||||
else if (t.isJSXElement(child))
|
||||
children.push(child);
|
||||
else if (t.isJSXExpressionContainer(child) && !t.isJSXEmptyExpression(child.expression))
|
||||
children.push(child.expression);
|
||||
else if (t.isJSXSpreadChild(child))
|
||||
children.push(t.spreadElement(child.expression));
|
||||
}
|
||||
|
||||
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)),
|
||||
];
|
||||
if (children.length)
|
||||
component.push(t.objectProperty(t.identifier('children'), t.arrayExpression(children)));
|
||||
|
||||
path.replaceWith(t.objectExpression(component));
|
||||
}
|
||||
if (!componentImports.has(path.node.object.name))
|
||||
return;
|
||||
if (!t.isIdentifier(path.node.property))
|
||||
return;
|
||||
path.replaceWith(
|
||||
t.objectExpression([
|
||||
t.spreadElement(t.identifier(path.node.object.name)),
|
||||
t.objectProperty(t.identifier('property'), t.stringLiteral(path.node.property.name)),
|
||||
])
|
||||
);
|
||||
},
|
||||
}
|
||||
};
|
||||
return result;
|
||||
|
|
@ -140,7 +111,6 @@ export default declare((api: BabelAPI) => {
|
|||
export function collectComponentUsages(node: T.Node) {
|
||||
const importedLocalNames = new Set<string>();
|
||||
const names = new Set<string>();
|
||||
const identifiers = new Set<T.Identifier>();
|
||||
traverse(node, {
|
||||
enter: p => {
|
||||
|
||||
|
|
@ -162,7 +132,7 @@ export function collectComponentUsages(node: T.Node) {
|
|||
if (t.isJSXIdentifier(p.node.openingElement.name))
|
||||
names.add(p.node.openingElement.name.name);
|
||||
if (t.isJSXMemberExpression(p.node.openingElement.name) && t.isJSXIdentifier(p.node.openingElement.name.object) && t.isJSXIdentifier(p.node.openingElement.name.property))
|
||||
names.add(p.node.openingElement.name.object.name + '.' + p.node.openingElement.name.property.name);
|
||||
names.add(p.node.openingElement.name.object.name);
|
||||
}
|
||||
|
||||
// Treat mount(identifier, ...) as component usage if it is in the importedLocalNames list.
|
||||
|
|
@ -173,45 +143,46 @@ export function collectComponentUsages(node: T.Node) {
|
|||
return;
|
||||
|
||||
names.add(arg.name);
|
||||
identifiers.add(arg);
|
||||
}
|
||||
}
|
||||
});
|
||||
return { names, identifiers };
|
||||
return { names };
|
||||
}
|
||||
|
||||
export type ComponentInfo = {
|
||||
fullName: string;
|
||||
importPath: string;
|
||||
export type ImportInfo = {
|
||||
id: string;
|
||||
isModuleOrAlias: boolean;
|
||||
importedName?: string;
|
||||
importedNameProperty?: string;
|
||||
deps: string[];
|
||||
importPath: string;
|
||||
localName: string;
|
||||
remoteName: string | undefined;
|
||||
};
|
||||
|
||||
export function componentInfo(specifier: T.ImportSpecifier | T.ImportDefaultSpecifier, importSource: string, filename: string, componentName: string): ComponentInfo {
|
||||
export function importInfo(importNode: T.ImportDeclaration, specifier: T.ImportSpecifier | T.ImportDefaultSpecifier, filename: string): ImportInfo {
|
||||
const importSource = importNode.source.value;
|
||||
const isModuleOrAlias = !importSource.startsWith('.');
|
||||
const unresolvedImportPath = path.resolve(path.dirname(filename), importSource);
|
||||
// Support following notations for Button.tsx:
|
||||
// - import { Button } from './Button.js' - via resolveImportSpecifierExtension
|
||||
// - import { Button } from './Button' - via require.resolve
|
||||
const importPath = isModuleOrAlias ? importSource : resolveImportSpecifierExtension(unresolvedImportPath) || require.resolve(unresolvedImportPath);
|
||||
const prefix = importPath.replace(/[^\w_\d]/g, '_');
|
||||
const pathInfo = { importPath, isModuleOrAlias };
|
||||
const idPrefix = importPath.replace(/[^\w_\d]/g, '_');
|
||||
|
||||
const specifierName = specifier.local.name;
|
||||
let fullNameSuffix = '';
|
||||
let importedNameProperty = '';
|
||||
if (componentName !== specifierName) {
|
||||
const suffix = componentName.substring(specifierName.length + 1);
|
||||
fullNameSuffix = '_' + suffix;
|
||||
importedNameProperty = '.' + suffix;
|
||||
const result: ImportInfo = {
|
||||
id: idPrefix,
|
||||
importPath,
|
||||
isModuleOrAlias,
|
||||
localName: specifier.local.name,
|
||||
remoteName: undefined,
|
||||
};
|
||||
|
||||
if (t.isImportDefaultSpecifier(specifier)) {
|
||||
} else if (t.isIdentifier(specifier.imported)) {
|
||||
result.remoteName = specifier.imported.name;
|
||||
} else {
|
||||
result.remoteName = specifier.imported.value;
|
||||
}
|
||||
|
||||
if (t.isImportDefaultSpecifier(specifier))
|
||||
return { fullName: prefix + fullNameSuffix, importedNameProperty, deps: [], ...pathInfo };
|
||||
|
||||
if (t.isIdentifier(specifier.imported))
|
||||
return { fullName: prefix + '_' + specifier.imported.name + fullNameSuffix, importedName: specifier.imported.name, importedNameProperty, deps: [], ...pathInfo };
|
||||
return { fullName: prefix + '_' + specifier.imported.value + fullNameSuffix, importedName: specifier.imported.value, importedNameProperty, deps: [], ...pathInfo };
|
||||
if (result.remoteName)
|
||||
result.id += '_' + result.remoteName;
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import type { PlaywrightTestConfig as BasePlaywrightTestConfig, FullConfig } fro
|
|||
|
||||
import type { InlineConfig, Plugin, ResolveFn, ResolvedConfig, UserConfig } from 'vite';
|
||||
import type { TestRunnerPlugin } from '../../playwright/src/plugins';
|
||||
import type { ComponentInfo } from './tsxTransform';
|
||||
import type { AddressInfo } from 'net';
|
||||
import type { PluginContext } from 'rollup';
|
||||
import { debug } from 'playwright-core/lib/utilsBundle';
|
||||
|
|
@ -31,14 +30,23 @@ import { stoppable } from 'playwright/lib/utilsBundle';
|
|||
import { assert, calculateSha1 } from 'playwright-core/lib/utils';
|
||||
import { getPlaywrightVersion } from 'playwright-core/lib/utils';
|
||||
import { setExternalDependencies } from 'playwright/lib/transform/compilationCache';
|
||||
import { collectComponentUsages, componentInfo } from './tsxTransform';
|
||||
import { collectComponentUsages, importInfo } from './tsxTransform';
|
||||
import { version as viteVersion, build, preview, mergeConfig } from 'vite';
|
||||
import type { ImportInfo } from './tsxTransform';
|
||||
|
||||
const log = debug('pw:vite');
|
||||
|
||||
let stoppableServer: any;
|
||||
const playwrightVersion = getPlaywrightVersion();
|
||||
|
||||
type ComponentInfo = {
|
||||
id: string;
|
||||
importPath: string;
|
||||
isModuleOrAlias: boolean;
|
||||
remoteName: string | undefined;
|
||||
deps: string[];
|
||||
};
|
||||
|
||||
type CtConfig = BasePlaywrightTestConfig['use'] & {
|
||||
ctPort?: number;
|
||||
ctTemplateDir?: string;
|
||||
|
|
@ -107,7 +115,13 @@ export function createPlugin(
|
|||
let buildExists = false;
|
||||
let buildInfo: BuildInfo;
|
||||
|
||||
const registerSource = await fs.promises.readFile(registerSourceFile, 'utf-8');
|
||||
const importRegistryFile = await fs.promises.readFile(path.resolve(__dirname, 'importRegistry.js'), 'utf-8');
|
||||
assert(importRegistryFile.includes(importRegistryPrefix));
|
||||
assert(importRegistryFile.includes(importRegistrySuffix));
|
||||
const importRegistrySource = importRegistryFile.replace(importRegistryPrefix, '').replace(importRegistrySuffix, '') + `
|
||||
window.__pwRegistry = new ImportRegistry();
|
||||
`;
|
||||
const registerSource = importRegistrySource + await fs.promises.readFile(registerSourceFile, 'utf-8');
|
||||
const registerSourceHash = calculateSha1(registerSource);
|
||||
|
||||
try {
|
||||
|
|
@ -269,11 +283,19 @@ async function checkNewTests(suite: Suite, buildInfo: BuildInfo, componentRegist
|
|||
for (const testFile of testFiles) {
|
||||
const timestamp = (await fs.promises.stat(testFile)).mtimeMs;
|
||||
if (buildInfo.tests[testFile]?.timestamp !== timestamp) {
|
||||
const components = await parseTestFile(testFile);
|
||||
const componentImports = await parseTestFile(testFile);
|
||||
log('changed test:', testFile);
|
||||
for (const component of components)
|
||||
componentRegistry.set(component.fullName, component);
|
||||
buildInfo.tests[testFile] = { timestamp, components: components.map(c => c.fullName) };
|
||||
for (const componentImport of componentImports) {
|
||||
const ci: ComponentInfo = {
|
||||
id: componentImport.id,
|
||||
isModuleOrAlias: componentImport.isModuleOrAlias,
|
||||
importPath: componentImport.importPath,
|
||||
remoteName: componentImport.remoteName,
|
||||
deps: [],
|
||||
};
|
||||
componentRegistry.set(componentImport.id, { ...ci, deps: [] });
|
||||
}
|
||||
buildInfo.tests[testFile] = { timestamp, components: componentImports.map(c => c.id) };
|
||||
hasNewTests = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -283,7 +305,7 @@ async function checkNewTests(suite: Suite, buildInfo: BuildInfo, componentRegist
|
|||
|
||||
async function checkNewComponents(buildInfo: BuildInfo, componentRegistry: ComponentRegistry): Promise<boolean> {
|
||||
const newComponents = [...componentRegistry.keys()];
|
||||
const oldComponents = new Map(buildInfo.components.map(c => [c.fullName, c]));
|
||||
const oldComponents = new Map(buildInfo.components.map(c => [c.id, c]));
|
||||
|
||||
let hasNewComponents = false;
|
||||
for (const c of newComponents) {
|
||||
|
|
@ -293,17 +315,17 @@ async function checkNewComponents(buildInfo: BuildInfo, componentRegistry: Compo
|
|||
}
|
||||
}
|
||||
for (const c of oldComponents.values())
|
||||
componentRegistry.set(c.fullName, c);
|
||||
componentRegistry.set(c.id, c);
|
||||
|
||||
return hasNewComponents;
|
||||
}
|
||||
|
||||
async function parseTestFile(testFile: string): Promise<ComponentInfo[]> {
|
||||
async function parseTestFile(testFile: string): Promise<ImportInfo[]> {
|
||||
const text = await fs.promises.readFile(testFile, 'utf-8');
|
||||
const ast = parse(text, { errorRecovery: true, plugins: ['typescript', 'jsx'], sourceType: 'module' });
|
||||
const componentUsages = collectComponentUsages(ast);
|
||||
const componentNames = componentUsages.names;
|
||||
const result: ComponentInfo[] = [];
|
||||
const result: ImportInfo[] = [];
|
||||
|
||||
traverse(ast, {
|
||||
enter: p => {
|
||||
|
|
@ -313,13 +335,12 @@ async function parseTestFile(testFile: string): Promise<ComponentInfo[]> {
|
|||
return;
|
||||
|
||||
for (const specifier of importNode.specifiers) {
|
||||
const specifierName = specifier.local.name;
|
||||
const componentName = componentNames.has(specifierName) ? specifierName : [...componentNames].find(c => c.startsWith(specifierName + '.'));
|
||||
if (!componentName)
|
||||
continue;
|
||||
if (t.isImportNamespaceSpecifier(specifier))
|
||||
continue;
|
||||
result.push(componentInfo(specifier, importNode.source.value, testFile, componentName));
|
||||
const info = importInfo(importNode, specifier, testFile);
|
||||
if (!componentNames.has(info.localName))
|
||||
continue;
|
||||
result.push(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -370,13 +391,10 @@ function vitePlugin(registerSource: string, templateDir: string, buildInfo: Buil
|
|||
|
||||
for (const [alias, value] of componentRegistry) {
|
||||
const importPath = value.isModuleOrAlias ? value.importPath : './' + path.relative(folder, value.importPath).replace(/\\/g, '/');
|
||||
if (value.importedName)
|
||||
lines.push(`const ${alias} = () => import('${importPath}').then((mod) => mod.${value.importedName + (value.importedNameProperty || '')});`);
|
||||
else
|
||||
lines.push(`const ${alias} = () => import('${importPath}').then((mod) => mod.default${value.importedNameProperty || ''});`);
|
||||
lines.push(`const ${alias} = () => import('${importPath}').then((mod) => mod.${value.remoteName || 'default'});`);
|
||||
}
|
||||
|
||||
lines.push(`pwRegister({ ${[...componentRegistry.keys()].join(',\n ')} });`);
|
||||
lines.push(`__pwRegistry.initialize({ ${[...componentRegistry.keys()].join(',\n ')} });`);
|
||||
return {
|
||||
code: lines.join('\n'),
|
||||
map: { mappings: '' }
|
||||
|
|
@ -418,3 +436,13 @@ function hasJSComponents(components: ComponentInfo[]): boolean {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
const importRegistryPrefix = `"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.ImportRegistry = void 0;`;
|
||||
|
||||
const importRegistrySuffix = `exports.ImportRegistry = ImportRegistry;`;
|
||||
|
|
|
|||
34
packages/playwright-ct-core/types/component.d.ts
vendored
34
packages/playwright-ct-core/types/component.d.ts
vendored
|
|
@ -14,33 +14,38 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ImportRegistry } from '../src/importRegistry';
|
||||
|
||||
type JsonPrimitive = string | number | boolean | null;
|
||||
type JsonValue = JsonPrimitive | JsonObject | JsonArray;
|
||||
type JsonArray = JsonValue[];
|
||||
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 ImportRef = {
|
||||
__pw_type: 'importRef',
|
||||
id: string,
|
||||
property?: string,
|
||||
};
|
||||
|
||||
export type JsxComponent = {
|
||||
__pw_component_marker: true,
|
||||
kind: 'jsx',
|
||||
type: string,
|
||||
__pw_type: 'jsx',
|
||||
type: any,
|
||||
props: Record<string, any>,
|
||||
children?: JsxComponentChild[],
|
||||
};
|
||||
|
||||
export type MountOptions = {
|
||||
props?: Record<string, any>,
|
||||
slots?: Record<string, string | string[]>,
|
||||
on?: Record<string, Function>,
|
||||
hooksConfig?: any,
|
||||
};
|
||||
|
||||
export type ObjectComponent = {
|
||||
__pw_component_marker: true,
|
||||
kind: 'object',
|
||||
type: string,
|
||||
options?: MountOptions
|
||||
export type ObjectComponentOptions = {
|
||||
props?: Record<string, any>;
|
||||
slots?: Record<string, string | string[]>;
|
||||
on?: Record<string, Function>;
|
||||
};
|
||||
|
||||
export type ObjectComponent = ObjectComponentOptions & {
|
||||
__pw_type: 'object-component',
|
||||
type: any,
|
||||
};
|
||||
|
||||
export type Component = JsxComponent | ObjectComponent;
|
||||
|
|
@ -56,5 +61,6 @@ declare global {
|
|||
__pw_hooks_after_mount?: (<HooksConfig extends JsonObject = JsonObject>(
|
||||
params: { hooksConfig?: HooksConfig; [key: string]: any }
|
||||
) => Promise<void>)[];
|
||||
__pwRegistry: ImportRegistry;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
packages/playwright-ct-react/index.d.ts
vendored
2
packages/playwright-ct-react/index.d.ts
vendored
|
|
@ -22,7 +22,7 @@ export interface MountOptions<HooksConfig extends JsonObject> {
|
|||
hooksConfig?: HooksConfig;
|
||||
}
|
||||
|
||||
interface MountResult extends Locator {
|
||||
export interface MountResult extends Locator {
|
||||
unmount(): Promise<void>;
|
||||
update(component: JSX.Element): Promise<void>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,130 +17,48 @@
|
|||
// @ts-check
|
||||
// This file is injected into the registry as text, no dependencies are allowed.
|
||||
|
||||
import * as __pwReact from 'react';
|
||||
import __pwReact from 'react';
|
||||
import { createRoot as __pwCreateRoot } from 'react-dom/client';
|
||||
|
||||
/** @typedef {import('../playwright-ct-core/types/component').JsxComponentChild} JsxComponentChild */
|
||||
/** @typedef {import('../playwright-ct-core/types/component').JsxComponent} JsxComponent */
|
||||
/** @typedef {import('react').FunctionComponent} FrameworkComponent */
|
||||
|
||||
/** @type {Map<string, () => Promise<FrameworkComponent>>} */
|
||||
const __pwLoaderRegistry = new Map();
|
||||
/** @type {Map<string, FrameworkComponent>} */
|
||||
const __pwRegistry = new Map();
|
||||
/** @type {Map<Element, import('react-dom/client').Root>} */
|
||||
const __pwRootRegistry = new Map();
|
||||
|
||||
/**
|
||||
* @param {Record<string, () => Promise<FrameworkComponent>>} components
|
||||
*/
|
||||
export function pwRegister(components) {
|
||||
for (const [name, value] of Object.entries(components))
|
||||
__pwLoaderRegistry.set(name, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} component
|
||||
* @returns {component is JsxComponent}
|
||||
*/
|
||||
function isComponent(component) {
|
||||
return component.__pw_component_marker === true && component.kind === 'jsx';
|
||||
function isJsxComponent(component) {
|
||||
return typeof component === 'object' && component && component.__pw_type === 'jsx';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {JsxComponent | JsxComponentChild} component
|
||||
* @param {any} value
|
||||
*/
|
||||
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;
|
||||
function __pwRender(value) {
|
||||
if (value === null || typeof value !== 'object')
|
||||
return value;
|
||||
if (isJsxComponent(value)) {
|
||||
const component = value;
|
||||
const props = component.props ? __pwRender(component.props) : {};
|
||||
return __pwReact.createElement(/** @type { any } */ (component.type), { ...props, children: undefined }, props.children);
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
const result = [];
|
||||
for (const item of value)
|
||||
result.push(__pwRender(item));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
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 (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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {JsxComponentChild} child
|
||||
*/
|
||||
function __renderChild(child) {
|
||||
if (Array.isArray(child))
|
||||
return child.map(grandChild => __renderChild(grandChild));
|
||||
if (isComponent(child))
|
||||
return __pwRender(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, props, reactChildren);
|
||||
const result = {};
|
||||
for (const [key, prop] of Object.entries(value))
|
||||
result[key] = __pwRender(prop);
|
||||
return result;
|
||||
}
|
||||
|
||||
window.playwrightMount = async (component, rootElement, hooksConfig) => {
|
||||
if (component.kind !== 'jsx')
|
||||
if (!isJsxComponent(component))
|
||||
throw new Error('Object mount notation is not supported');
|
||||
|
||||
await __pwResolveComponent(component);
|
||||
let App = () => __pwRender(component);
|
||||
for (const hook of window.__pw_hooks_before_mount || []) {
|
||||
const wrapper = await hook({ App, hooksConfig });
|
||||
|
|
@ -171,10 +89,9 @@ window.playwrightUnmount = async rootElement => {
|
|||
};
|
||||
|
||||
window.playwrightUpdate = async (rootElement, component) => {
|
||||
if (component.kind !== 'jsx')
|
||||
if (!isJsxComponent(component))
|
||||
throw new Error('Object mount notation is not supported');
|
||||
|
||||
await __pwResolveComponent(component);
|
||||
const root = __pwRootRegistry.get(rootElement);
|
||||
if (root === undefined)
|
||||
throw new Error('Component was not mounted');
|
||||
|
|
|
|||
|
|
@ -17,93 +17,45 @@
|
|||
// @ts-check
|
||||
// This file is injected into the registry as text, no dependencies are allowed.
|
||||
|
||||
// Don't clash with the user land.
|
||||
import __pwReact from 'react';
|
||||
import __pwReactDOM from 'react-dom';
|
||||
|
||||
/** @typedef {import('../playwright-ct-core/types/component').JsxComponentChild} JsxComponentChild */
|
||||
/** @typedef {import('../playwright-ct-core/types/component').JsxComponent} JsxComponent */
|
||||
/** @typedef {import('react').FunctionComponent} FrameworkComponent */
|
||||
|
||||
/** @type {Map<string, () => Promise<FrameworkComponent>>} */
|
||||
const __pwLoaderRegistry = new Map();
|
||||
/** @type {Map<string, FrameworkComponent>} */
|
||||
const __pwRegistry = new Map();
|
||||
|
||||
/**
|
||||
* @param {{[key: string]: () => Promise<FrameworkComponent>}} components
|
||||
*/
|
||||
export function pwRegister(components) {
|
||||
for (const [name, value] of Object.entries(components))
|
||||
__pwLoaderRegistry.set(name, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} component
|
||||
* @returns {component is JsxComponent}
|
||||
*/
|
||||
function isComponent(component) {
|
||||
return !(typeof component !== 'object' || Array.isArray(component));
|
||||
function isJsxComponent(component) {
|
||||
return typeof component === 'object' && component && component.__pw_type === 'jsx';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {JsxComponent | JsxComponentChild} component
|
||||
* @param {any} value
|
||||
*/
|
||||
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;
|
||||
function __pwRender(value) {
|
||||
if (value === null || typeof value !== 'object')
|
||||
return value;
|
||||
if (isJsxComponent(value)) {
|
||||
const component = value;
|
||||
const props = component.props ? __pwRender(component.props) : {};
|
||||
return __pwReact.createElement(/** @type { any } */ (component.type), { ...props, children: undefined }, props.children);
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
const result = [];
|
||||
for (const item of value)
|
||||
result.push(__pwRender(item));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
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 (component.children?.length)
|
||||
await Promise.all(component.children.map(child => __pwResolveComponent(child)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {JsxComponentChild} child
|
||||
*/
|
||||
function __renderChild(child) {
|
||||
if (Array.isArray(child))
|
||||
return child.map(grandChild => __renderChild(grandChild));
|
||||
if (isComponent(child))
|
||||
return __pwRender(child);
|
||||
return child;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {JsxComponent} component
|
||||
*/
|
||||
function __pwRender(component) {
|
||||
const componentFunc = __pwRegistry.get(component.type);
|
||||
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);
|
||||
const result = {};
|
||||
for (const [key, prop] of Object.entries(value))
|
||||
result[key] = __pwRender(prop);
|
||||
return result;
|
||||
}
|
||||
|
||||
window.playwrightMount = async (component, rootElement, hooksConfig) => {
|
||||
if (component.kind !== 'jsx')
|
||||
if (!isJsxComponent(component))
|
||||
throw new Error('Object mount notation is not supported');
|
||||
|
||||
await __pwResolveComponent(component);
|
||||
let App = () => __pwRender(component);
|
||||
for (const hook of window.__pw_hooks_before_mount || []) {
|
||||
const wrapper = await hook({ App, hooksConfig });
|
||||
|
|
@ -123,9 +75,8 @@ window.playwrightUnmount = async rootElement => {
|
|||
};
|
||||
|
||||
window.playwrightUpdate = async (rootElement, component) => {
|
||||
if (component.kind !== 'jsx')
|
||||
if (!isJsxComponent(component))
|
||||
throw new Error('Object mount notation is not supported');
|
||||
|
||||
await __pwResolveComponent(component);
|
||||
__pwReactDOM.render(__pwRender(component), rootElement);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,94 +20,62 @@
|
|||
import { render as __pwSolidRender, createComponent as __pwSolidCreateComponent } from 'solid-js/web';
|
||||
import __pwH from 'solid-js/h';
|
||||
|
||||
/** @typedef {import('../playwright-ct-core/types/component').JsxComponentChild} JsxComponentChild */
|
||||
/** @typedef {import('../playwright-ct-core/types/component').JsxComponent} JsxComponent */
|
||||
/** @typedef {() => import('solid-js').JSX.Element} FrameworkComponent */
|
||||
|
||||
/** @type {Map<string, () => Promise<FrameworkComponent>>} */
|
||||
const __pwLoaderRegistry = new Map();
|
||||
/** @type {Map<string, FrameworkComponent>} */
|
||||
const __pwRegistry = new Map();
|
||||
|
||||
/**
|
||||
* @param {{[key: string]: () => Promise<FrameworkComponent>}} components
|
||||
*/
|
||||
export function pwRegister(components) {
|
||||
for (const [name, value] of Object.entries(components))
|
||||
__pwLoaderRegistry.set(name, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} component
|
||||
* @returns {component is JsxComponent}
|
||||
*/
|
||||
function isComponent(component) {
|
||||
return !(typeof component !== 'object' || Array.isArray(component));
|
||||
function isJsxComponent(component) {
|
||||
return typeof component === 'object' && component && component.__pw_type === 'jsx';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {JsxComponent | JsxComponentChild} 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 (component.children?.length)
|
||||
await Promise.all(component.children.map(child => __pwResolveComponent(child)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {JsxComponentChild} child
|
||||
* @param {any} child
|
||||
*/
|
||||
function __pwCreateChild(child) {
|
||||
if (Array.isArray(child))
|
||||
return child.map(grandChild => __pwCreateChild(grandChild));
|
||||
if (isComponent(child))
|
||||
if (isJsxComponent(child))
|
||||
return __pwCreateComponent(child);
|
||||
return child;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {JsxComponent} component
|
||||
* @returns {any[] | undefined}
|
||||
*/
|
||||
function __pwJsxChildArray(component) {
|
||||
if (!component.props.children)
|
||||
return;
|
||||
if (Array.isArray(component.props.children))
|
||||
return component.props.children;
|
||||
return [component.props.children];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {JsxComponent} component
|
||||
*/
|
||||
function __pwCreateComponent(component) {
|
||||
const componentFunc = __pwRegistry.get(component.type);
|
||||
const children = component.children?.map(child => __pwCreateChild(child)).filter(child => {
|
||||
const children = __pwJsxChildArray(component)?.map(child => __pwCreateChild(child)).filter(child => {
|
||||
if (typeof child === 'string')
|
||||
return !!child.trim();
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!componentFunc)
|
||||
if (typeof component.type === 'string')
|
||||
return __pwH(component.type, component.props, children);
|
||||
|
||||
return __pwSolidCreateComponent(componentFunc, { ...component.props, children });
|
||||
return __pwSolidCreateComponent(component.type, { ...component.props, children });
|
||||
}
|
||||
|
||||
const __pwUnmountKey = Symbol('unmountKey');
|
||||
|
||||
window.playwrightMount = async (component, rootElement, hooksConfig) => {
|
||||
if (component.kind !== 'jsx')
|
||||
if (!isJsxComponent(component))
|
||||
throw new Error('Object mount notation is not supported');
|
||||
|
||||
await __pwResolveComponent(component);
|
||||
let App = () => __pwCreateComponent(component);
|
||||
for (const hook of window.__pw_hooks_before_mount || []) {
|
||||
const wrapper = await hook({ App, hooksConfig });
|
||||
|
|
@ -131,7 +99,7 @@ window.playwrightUnmount = async rootElement => {
|
|||
};
|
||||
|
||||
window.playwrightUpdate = async (rootElement, component) => {
|
||||
if (component.kind !== 'jsx')
|
||||
if (!isJsxComponent(component))
|
||||
throw new Error('Object mount notation is not supported');
|
||||
|
||||
window.playwrightUnmount(rootElement);
|
||||
|
|
|
|||
|
|
@ -25,50 +25,12 @@ import { detach as __pwDetach, insert as __pwInsert, noop as __pwNoop } from 'sv
|
|||
/** @typedef {any} FrameworkComponent */
|
||||
/** @typedef {import('svelte').SvelteComponent} SvelteComponent */
|
||||
|
||||
/** @type {Map<string, () => Promise<FrameworkComponent>>} */
|
||||
const __pwLoaderRegistry = new Map();
|
||||
/** @type {Map<string, FrameworkComponent>} */
|
||||
const __pwRegistry = new Map();
|
||||
|
||||
/**
|
||||
* @param {{[key: string]: () => Promise<FrameworkComponent>}} components
|
||||
*/
|
||||
export function pwRegister(components) {
|
||||
for (const [name, value] of Object.entries(components))
|
||||
__pwLoaderRegistry.set(name, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} component
|
||||
* @returns {component is ObjectComponent}
|
||||
*/
|
||||
function isComponent(component) {
|
||||
return !(typeof component !== 'object' || Array.isArray(component));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ObjectComponent} 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());
|
||||
function isObjectComponent(component) {
|
||||
return typeof component === 'object' && component && component.__pw_type === 'object-component';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -105,19 +67,19 @@ function __pwCreateSlots(slots) {
|
|||
const __pwSvelteComponentKey = Symbol('svelteComponent');
|
||||
|
||||
window.playwrightMount = async (component, rootElement, hooksConfig) => {
|
||||
if (component.kind !== 'object')
|
||||
if (!isObjectComponent(component))
|
||||
throw new Error('JSX mount notation is not supported');
|
||||
|
||||
await __pwResolveComponent(component);
|
||||
const componentCtor = __pwRegistry.get(component.type);
|
||||
const objectComponent = component;
|
||||
const componentCtor = component.type;
|
||||
|
||||
class App extends componentCtor {
|
||||
constructor(options = {}) {
|
||||
super({
|
||||
target: rootElement,
|
||||
props: {
|
||||
...component.options?.props,
|
||||
$$slots: __pwCreateSlots(component.options?.slots),
|
||||
...objectComponent.props,
|
||||
$$slots: __pwCreateSlots(objectComponent.slots),
|
||||
$$scope: {},
|
||||
},
|
||||
...options
|
||||
|
|
@ -134,7 +96,7 @@ window.playwrightMount = async (component, rootElement, hooksConfig) => {
|
|||
|
||||
rootElement[__pwSvelteComponentKey] = svelteComponent;
|
||||
|
||||
for (const [key, listener] of Object.entries(component.options?.on || {}))
|
||||
for (const [key, listener] of Object.entries(objectComponent.on || {}))
|
||||
svelteComponent.$on(key, event => listener(event.detail));
|
||||
|
||||
for (const hook of window.__pw_hooks_after_mount || [])
|
||||
|
|
@ -149,17 +111,16 @@ window.playwrightUnmount = async rootElement => {
|
|||
};
|
||||
|
||||
window.playwrightUpdate = async (rootElement, component) => {
|
||||
if (component.kind !== 'object')
|
||||
if (!isObjectComponent(component))
|
||||
throw new Error('JSX mount notation is not supported');
|
||||
|
||||
await __pwResolveComponent(component);
|
||||
const svelteComponent = /** @type {SvelteComponent} */ (rootElement[__pwSvelteComponentKey]);
|
||||
if (!svelteComponent)
|
||||
throw new Error('Component was not mounted');
|
||||
|
||||
for (const [key, listener] of Object.entries(component.options?.on || {}))
|
||||
for (const [key, listener] of Object.entries(component.on || {}))
|
||||
svelteComponent.$on(key, event => listener(event.detail));
|
||||
|
||||
if (component.options?.props)
|
||||
svelteComponent.$set(component.options.props);
|
||||
if (component.props)
|
||||
svelteComponent.$set(component.props);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -22,69 +22,35 @@ import { compile as __pwCompile } from '@vue/compiler-dom';
|
|||
import * as __pwVue from 'vue';
|
||||
|
||||
/** @typedef {import('../playwright-ct-core/types/component').Component} Component */
|
||||
/** @typedef {import('../playwright-ct-core/types/component').JsxComponentChild} JsxComponentChild */
|
||||
/** @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<string, () => Promise<FrameworkComponent>>} */
|
||||
const __pwLoaderRegistry = new Map();
|
||||
/** @type {Map<string, FrameworkComponent>} */
|
||||
const __pwRegistry = new Map();
|
||||
const __pwAllListeners = new Map();
|
||||
|
||||
/**
|
||||
* @param {{[key: string]: () => Promise<FrameworkComponent>}} components
|
||||
* @param {any} component
|
||||
* @returns {component is ObjectComponent}
|
||||
*/
|
||||
export function pwRegister(components) {
|
||||
for (const [name, value] of Object.entries(components))
|
||||
__pwLoaderRegistry.set(name, value);
|
||||
function isObjectComponent(component) {
|
||||
return typeof component === 'object' && component && component.__pw_type === 'object-component';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} component
|
||||
* @returns {component is Component}
|
||||
* @returns {component is JsxComponent}
|
||||
*/
|
||||
function isComponent(component) {
|
||||
return !(typeof component !== 'object' || Array.isArray(component));
|
||||
function isJsxComponent(component) {
|
||||
return typeof component === 'object' && component && component.__pw_type === 'jsx';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Component | JsxComponentChild} 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 && component.children?.length)
|
||||
await Promise.all(component.children.map(child => __pwResolveComponent(child)));
|
||||
}
|
||||
|
||||
const __pwAllListeners = new Map();
|
||||
|
||||
/**
|
||||
* @param {JsxComponentChild} child
|
||||
* @param {any} child
|
||||
*/
|
||||
function __pwCreateChild(child) {
|
||||
if (Array.isArray(child))
|
||||
return child.map(grandChild => __pwCreateChild(grandChild));
|
||||
if (isComponent(child))
|
||||
if (isJsxComponent(child) || isObjectComponent(child))
|
||||
return __pwCreateWrapper(child);
|
||||
return child;
|
||||
}
|
||||
|
|
@ -132,14 +98,23 @@ function __pwSlotToFunction(slot) {
|
|||
throw Error(`Invalid slot received.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {JsxComponent} component
|
||||
* @returns {any[] | undefined}
|
||||
*/
|
||||
function __pwJsxChildArray(component) {
|
||||
if (!component.props.children)
|
||||
return;
|
||||
if (Array.isArray(component.props.children))
|
||||
return component.props.children;
|
||||
return [component.props.children];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Component} component
|
||||
*/
|
||||
function __pwCreateComponent(component) {
|
||||
let componentFunc = __pwRegistry.get(component.type);
|
||||
componentFunc = componentFunc || component.type;
|
||||
|
||||
const isVueComponent = componentFunc !== component.type;
|
||||
const isVueComponent = typeof component.type !== 'string';
|
||||
|
||||
/**
|
||||
* @type {(import('vue').VNode | string)[]}
|
||||
|
|
@ -151,12 +126,12 @@ function __pwCreateComponent(component) {
|
|||
/** @type {{[key: string]: any}} */
|
||||
let props = {};
|
||||
|
||||
if (component.kind === 'jsx') {
|
||||
for (const child of component.children || []) {
|
||||
if (typeof child !== 'string' && child.type === 'template' && child.kind === 'jsx') {
|
||||
if (component.__pw_type === 'jsx') {
|
||||
for (const child of __pwJsxChildArray(component) || []) {
|
||||
if (isJsxComponent(child) && 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(__pwCreateChild);
|
||||
slots[slot] = __pwJsxChildArray(child)?.map(__pwCreateChild);
|
||||
} else {
|
||||
children.push(__pwCreateChild(child));
|
||||
}
|
||||
|
|
@ -175,16 +150,16 @@ function __pwCreateComponent(component) {
|
|||
}
|
||||
}
|
||||
|
||||
if (component.kind === 'object') {
|
||||
if (component.__pw_type === 'object-component') {
|
||||
// Vue test util syntax.
|
||||
for (const [key, value] of Object.entries(component.options?.slots || {})) {
|
||||
for (const [key, value] of Object.entries(component.slots || {})) {
|
||||
if (key === 'default')
|
||||
children.push(__pwSlotToFunction(value));
|
||||
else
|
||||
slots[key] = __pwSlotToFunction(value);
|
||||
}
|
||||
props = component.options?.props || {};
|
||||
for (const [key, value] of Object.entries(component.options?.on || {}))
|
||||
props = component.props || {};
|
||||
for (const [key, value] of Object.entries(component.on || {}))
|
||||
listeners[key] = value;
|
||||
}
|
||||
|
||||
|
|
@ -197,7 +172,7 @@ function __pwCreateComponent(component) {
|
|||
lastArg = children;
|
||||
}
|
||||
|
||||
return { Component: componentFunc, props, slots: lastArg, listeners };
|
||||
return { Component: component.type, props, slots: lastArg, listeners };
|
||||
}
|
||||
|
||||
function __pwWrapFunctions(slots) {
|
||||
|
|
@ -248,7 +223,6 @@ 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);
|
||||
|
|
@ -275,7 +249,6 @@ window.playwrightUnmount = async rootElement => {
|
|||
};
|
||||
|
||||
window.playwrightUpdate = async (rootElement, component) => {
|
||||
await __pwResolveComponent(component);
|
||||
const wrapper = rootElement[__pwWrapperKey];
|
||||
if (!wrapper)
|
||||
throw new Error('Component was not mounted');
|
||||
|
|
|
|||
|
|
@ -21,67 +21,33 @@
|
|||
import __pwVue, { h as __pwH } from 'vue';
|
||||
|
||||
/** @typedef {import('../playwright-ct-core/types/component').Component} Component */
|
||||
/** @typedef {import('../playwright-ct-core/types/component').JsxComponentChild} JsxComponentChild */
|
||||
/** @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<string, () => Promise<FrameworkComponent>>} */
|
||||
const __pwLoaderRegistry = new Map();
|
||||
/** @type {Map<string, FrameworkComponent>} */
|
||||
const __pwRegistry = new Map();
|
||||
|
||||
/**
|
||||
* @param {{[key: string]: () => Promise<FrameworkComponent>}} components
|
||||
* @param {any} component
|
||||
* @returns {component is ObjectComponent}
|
||||
*/
|
||||
export function pwRegister(components) {
|
||||
for (const [name, value] of Object.entries(components))
|
||||
__pwLoaderRegistry.set(name, value);
|
||||
function isObjectComponent(component) {
|
||||
return typeof component === 'object' && component && component.__pw_type === 'object-component';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} component
|
||||
* @returns {component is Component}
|
||||
* @returns {component is JsxComponent}
|
||||
*/
|
||||
function isComponent(component) {
|
||||
return !(typeof component !== 'object' || Array.isArray(component));
|
||||
function isJsxComponent(component) {
|
||||
return typeof component === 'object' && component && component.__pw_type === 'jsx';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Component | JsxComponentChild} 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 && component.children?.length)
|
||||
await Promise.all(component.children.map(child => __pwResolveComponent(child)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Component | JsxComponentChild} child
|
||||
* @param {any} child
|
||||
*/
|
||||
function __pwCreateChild(child) {
|
||||
if (Array.isArray(child))
|
||||
return child.map(grandChild => __pwCreateChild(grandChild));
|
||||
if (isComponent(child))
|
||||
if (isJsxComponent(child) || isObjectComponent(child))
|
||||
return __pwCreateWrapper(child);
|
||||
return child;
|
||||
}
|
||||
|
|
@ -94,18 +60,26 @@ function __pwCreateChild(child) {
|
|||
* @return {boolean}
|
||||
*/
|
||||
function __pwComponentHasKeyInProps(Component, key) {
|
||||
if (Array.isArray(Component.props))
|
||||
return Component.props.includes(key);
|
||||
return typeof Component.props === 'object' && Component.props && key in Component.props;
|
||||
}
|
||||
|
||||
return Object.entries(Component.props).flat().includes(key);
|
||||
/**
|
||||
* @param {JsxComponent} component
|
||||
* @returns {any[] | undefined}
|
||||
*/
|
||||
function __pwJsxChildArray(component) {
|
||||
if (!component.props.children)
|
||||
return;
|
||||
if (Array.isArray(component.props.children))
|
||||
return component.props.children;
|
||||
return [component.props.children];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Component} component
|
||||
*/
|
||||
function __pwCreateComponent(component) {
|
||||
const componentFunc = __pwRegistry.get(component.type) || component.type;
|
||||
const isVueComponent = componentFunc !== component.type;
|
||||
const isVueComponent = typeof component.type !== 'string';
|
||||
|
||||
/**
|
||||
* @type {(import('vue').VNode | string)[]}
|
||||
|
|
@ -119,12 +93,12 @@ function __pwCreateComponent(component) {
|
|||
nodeData.scopedSlots = {};
|
||||
nodeData.on = {};
|
||||
|
||||
if (component.kind === 'jsx') {
|
||||
for (const child of component.children || []) {
|
||||
if (typeof child !== 'string' && child.type === 'template' && child.kind === 'jsx') {
|
||||
if (component.__pw_type === 'jsx') {
|
||||
for (const child of __pwJsxChildArray(component) || []) {
|
||||
if (isJsxComponent(child) && child.type === 'template') {
|
||||
const slotProperty = Object.keys(child.props).find(k => k.startsWith('v-slot:'));
|
||||
const slot = slotProperty ? slotProperty.substring('v-slot:'.length) : 'default';
|
||||
nodeData.scopedSlots[slot] = () => child.children?.map(c => __pwCreateChild(c));
|
||||
nodeData.scopedSlots[slot] = () => __pwJsxChildArray(child)?.map(c => __pwCreateChild(c));
|
||||
} else {
|
||||
children.push(__pwCreateChild(child));
|
||||
}
|
||||
|
|
@ -135,7 +109,7 @@ function __pwCreateComponent(component) {
|
|||
const event = key.substring('v-on:'.length);
|
||||
nodeData.on[event] = value;
|
||||
} else {
|
||||
if (isVueComponent && __pwComponentHasKeyInProps(componentFunc, key))
|
||||
if (isVueComponent && __pwComponentHasKeyInProps(component.type, key))
|
||||
nodeData.props[key] = value;
|
||||
else
|
||||
nodeData.attrs[key] = value;
|
||||
|
|
@ -143,18 +117,17 @@ function __pwCreateComponent(component) {
|
|||
}
|
||||
}
|
||||
|
||||
if (component.kind === 'object') {
|
||||
if (component.__pw_type === 'object-component') {
|
||||
// Vue test util syntax.
|
||||
const options = component.options || {};
|
||||
for (const [key, value] of Object.entries(options.slots || {})) {
|
||||
for (const [key, value] of Object.entries(component.slots || {})) {
|
||||
const list = (Array.isArray(value) ? value : [value]).map(v => __pwCreateChild(v));
|
||||
if (key === 'default')
|
||||
children.push(...list);
|
||||
else
|
||||
nodeData.scopedSlots[key] = () => list;
|
||||
}
|
||||
nodeData.props = options.props || {};
|
||||
for (const [key, value] of Object.entries(options.on || {}))
|
||||
nodeData.props = component.props || {};
|
||||
for (const [key, value] of Object.entries(component.on || {}))
|
||||
nodeData.on[key] = value;
|
||||
}
|
||||
|
||||
|
|
@ -167,7 +140,7 @@ function __pwCreateComponent(component) {
|
|||
lastArg = children;
|
||||
}
|
||||
|
||||
return { Component: componentFunc, nodeData, slots: lastArg };
|
||||
return { Component: component.type, nodeData, slots: lastArg };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -184,7 +157,6 @@ 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 });
|
||||
|
|
@ -213,7 +185,6 @@ 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');
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ function babelTransformOptions(isTypeScript: boolean, isModule: boolean, plugins
|
|||
|
||||
// Support JSX/TSX at all times, regardless of the file extension.
|
||||
plugins.push([require('@babel/plugin-transform-react-jsx'), {
|
||||
throwIfNamespace: false,
|
||||
runtime: 'automatic',
|
||||
importSource: path.dirname(require.resolve('playwright')),
|
||||
}]);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
function jsx(type, props) {
|
||||
return {
|
||||
__pw_type: 'jsx',
|
||||
type,
|
||||
props,
|
||||
};
|
||||
|
|
@ -23,6 +24,7 @@ function jsx(type, props) {
|
|||
|
||||
function jsxs(type, props) {
|
||||
return {
|
||||
__pw_type: 'jsx',
|
||||
type,
|
||||
props,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"target": "ES2015",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
|
|
|
|||
|
|
@ -523,11 +523,13 @@ test('should load jsx with top-level component', async ({ runInlineTest }) => {
|
|||
const component = <div>Hello <span>world</span></div>;
|
||||
test('succeeds', () => {
|
||||
expect(component).toEqual({
|
||||
__pw_type: 'jsx',
|
||||
type: 'div',
|
||||
props: {
|
||||
children: [
|
||||
'Hello ',
|
||||
{
|
||||
__pw_type: 'jsx',
|
||||
type: 'span',
|
||||
props: {
|
||||
children: 'world'
|
||||
|
|
|
|||
|
|
@ -135,9 +135,8 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
|
|||
});
|
||||
|
||||
expect(metainfo.components).toEqual([{
|
||||
fullName: expect.stringContaining('playwright_test_src_button_tsx_Button'),
|
||||
importedName: 'Button',
|
||||
importedNameProperty: '',
|
||||
id: expect.stringContaining('playwright_test_src_button_tsx_Button'),
|
||||
remoteName: 'Button',
|
||||
importPath: expect.stringContaining('button.tsx'),
|
||||
isModuleOrAlias: false,
|
||||
deps: [
|
||||
|
|
@ -145,9 +144,8 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
|
|||
expect.stringContaining('jsx-runtime.js'),
|
||||
]
|
||||
}, {
|
||||
fullName: expect.stringContaining('playwright_test_src_clashingNames1_tsx_ClashingName'),
|
||||
importedName: 'ClashingName',
|
||||
importedNameProperty: '',
|
||||
id: expect.stringContaining('playwright_test_src_clashingNames1_tsx_ClashingName'),
|
||||
remoteName: 'ClashingName',
|
||||
importPath: expect.stringContaining('clashingNames1.tsx'),
|
||||
isModuleOrAlias: false,
|
||||
deps: [
|
||||
|
|
@ -155,9 +153,8 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
|
|||
expect.stringContaining('jsx-runtime.js'),
|
||||
]
|
||||
}, {
|
||||
fullName: expect.stringContaining('playwright_test_src_clashingNames2_tsx_ClashingName'),
|
||||
importedName: 'ClashingName',
|
||||
importedNameProperty: '',
|
||||
id: expect.stringContaining('playwright_test_src_clashingNames2_tsx_ClashingName'),
|
||||
remoteName: 'ClashingName',
|
||||
importPath: expect.stringContaining('clashingNames2.tsx'),
|
||||
isModuleOrAlias: false,
|
||||
deps: [
|
||||
|
|
@ -165,9 +162,8 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
|
|||
expect.stringContaining('jsx-runtime.js'),
|
||||
]
|
||||
}, {
|
||||
fullName: expect.stringContaining('playwright_test_src_components_tsx_Component1'),
|
||||
importedName: 'Component1',
|
||||
importedNameProperty: '',
|
||||
id: expect.stringContaining('playwright_test_src_components_tsx_Component1'),
|
||||
remoteName: 'Component1',
|
||||
importPath: expect.stringContaining('components.tsx'),
|
||||
isModuleOrAlias: false,
|
||||
deps: [
|
||||
|
|
@ -175,9 +171,8 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
|
|||
expect.stringContaining('jsx-runtime.js'),
|
||||
]
|
||||
}, {
|
||||
fullName: expect.stringContaining('playwright_test_src_components_tsx_Component2'),
|
||||
importedName: 'Component2',
|
||||
importedNameProperty: '',
|
||||
id: expect.stringContaining('playwright_test_src_components_tsx_Component2'),
|
||||
remoteName: 'Component2',
|
||||
importPath: expect.stringContaining('components.tsx'),
|
||||
isModuleOrAlias: false,
|
||||
deps: [
|
||||
|
|
@ -185,9 +180,8 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
|
|||
expect.stringContaining('jsx-runtime.js'),
|
||||
]
|
||||
}, {
|
||||
fullName: expect.stringContaining('playwright_test_src_defaultExport_tsx'),
|
||||
id: expect.stringContaining('playwright_test_src_defaultExport_tsx'),
|
||||
importPath: expect.stringContaining('defaultExport.tsx'),
|
||||
importedNameProperty: '',
|
||||
isModuleOrAlias: false,
|
||||
deps: [
|
||||
expect.stringContaining('defaultExport.tsx'),
|
||||
|
|
@ -497,9 +491,8 @@ test('should retain deps when test changes', async ({ runInlineTest }, testInfo)
|
|||
const metainfo = JSON.parse(fs.readFileSync(testInfo.outputPath('playwright/.cache/metainfo.json'), 'utf-8'));
|
||||
|
||||
expect(metainfo.components).toEqual([{
|
||||
fullName: expect.stringContaining('playwright_test_src_button_tsx_Button'),
|
||||
importedName: 'Button',
|
||||
importedNameProperty: '',
|
||||
id: expect.stringContaining('playwright_test_src_button_tsx_Button'),
|
||||
remoteName: 'Button',
|
||||
importPath: expect.stringContaining('button.tsx'),
|
||||
isModuleOrAlias: false,
|
||||
deps: [
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ test('should work with stray JSX import', async ({ runInlineTest }) => {
|
|||
expect(result.passed).toBe(1);
|
||||
});
|
||||
|
||||
test.fixme('should work with stray JS import', async ({ runInlineTest }) => {
|
||||
test('should work with stray JS import', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'playwright.config.ts': playwrightConfig,
|
||||
'playwright/index.html': `<script type="module" src="./index.js"></script>`,
|
||||
|
|
@ -481,7 +481,7 @@ test('should normalize children', async ({ runInlineTest }) => {
|
|||
import { OneChild, OtherComponent } from './component';
|
||||
|
||||
test("can pass an HTML element to OneChild", async ({ mount }) => {
|
||||
const component = await mount(<OneChild><p>child</p> </OneChild>);
|
||||
const component = await mount(<OneChild><p>child</p></OneChild>);
|
||||
await expect(component).toHaveText("child");
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue