diff --git a/packages/playwright-ct-angular/index.js b/packages/playwright-ct-angular/index.js index 691232550f..a6214b6ebf 100644 --- a/packages/playwright-ct-angular/index.js +++ b/packages/playwright-ct-angular/index.js @@ -27,8 +27,7 @@ const defineConfig = (config, ...configs) => { packageJSON: require.resolve('./package.json'), }, '@playwright/experimental-ct-core': { - registerSourceFile: path.join(__dirname, 'registerSource.mjs'), - frameworkPluginFactory: () => {}, + registerSourceFile: path.join(__dirname, 'registerSource.mjs') }, }, ...configs); }; diff --git a/packages/playwright-ct-angular/registerSource.mjs b/packages/playwright-ct-angular/registerSource.mjs index 997be15e4d..7352f38ad3 100644 --- a/packages/playwright-ct-angular/registerSource.mjs +++ b/packages/playwright-ct-angular/registerSource.mjs @@ -1,22 +1,23 @@ -/** - * 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. - */ +// /** +// * 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. +// */ // @ts-check // This file is injected into the registry as text, no dependencies are allowed. +import '@angular/compiler'; import 'zone.js'; import { Component as defineComponent, @@ -29,67 +30,102 @@ import { } from '@angular/platform-browser-dynamic/testing'; import { Router } from '@angular/router'; -/** @typedef {import('@playwright/experimental-ct-core/types/component').Component} Component */ -/** @typedef {import('@playwright/experimental-ct-core/types/component').JsxComponent} JsxComponent */ -/** @typedef {import('@playwright/experimental-ct-core/types/component').ObjectComponent} ObjectComponent */ -/** @typedef {import('@angular/core').Type} FrameworkComponent */ - -/** @type {Map Promise>} */ -const __pwLoaderRegistry = new Map(); -/** @type {Map} */ -const __pwRegistry = new Map(); -/** @type {Map} */ -const __pwFixtureRegistry = new Map(); -/** @type {WeakMap>} */ -const __pwOutputSubscriptionRegistry = new WeakMap(); - getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), ); -/** - * @param {{[key: string]: () => Promise}} components - */ -export function pwRegister(components) { - for (const [name, value] of Object.entries(components)) - __pwLoaderRegistry.set(name, value); -} +window.playwrightMount = async (component, rootElement, hooksConfig) => { + for (const hook of window.__pw_hooks_before_mount || []) + await hook({ hooksConfig, TestBed }); -/** - * @param {Component} component - * @returns {component is JsxComponent | ObjectComponent} - */ -function isComponent(component) { - return !(typeof component !== 'object' || Array.isArray(component)); -} + const fixture = await __pwRenderComponent(component); + for (const hook of window.__pw_hooks_after_mount || []) + await hook({ hooksConfig }); + + __pwFixtureRegistry.set(rootElement.id, fixture); +}; + +window.playwrightUnmount = async rootElement => { + const fixture = __pwFixtureRegistry.get(rootElement.id); + if (!fixture) throw new Error('Component was not mounted'); + + /* Unsubscribe from all outputs. */ + for (const subscription of Object.values(__pwOutputSubscriptionRegistry.get(fixture) ?? {})) + subscription?.unsubscribe(); + __pwOutputSubscriptionRegistry.delete(fixture); + + fixture.destroy(); + fixture.nativeElement.replaceChildren(); +}; + +window.playwrightUpdate = async (rootElement, component) => { + await resolveComponent(component); + if (component.kind === 'jsx') + throw new Error('JSX mount notation is not supported'); + + if (component.options?.slots) + throw new Error('Update slots is not supported yet'); + + const fixture = __pwFixtureRegistry.get(rootElement.id); + if (!fixture) + throw new Error('Component was not mounted'); + + __pwUpdateProps(fixture, component.options?.props); + __pwUpdateEvents(fixture, component.options?.on); + + fixture.detectChanges(); +}; + +/** @type {WeakMap>} */ +const __pwOutputSubscriptionRegistry = new WeakMap(); + +/** @type {Map} */ +const __pwFixtureRegistry = new Map(); /** * @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]) +async function __pwRenderComponent(component) { + const Component = await __pwResolveComponent(component); + if (!Component) 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 componentMetadata = reflectComponentType(Component); + if (!componentMetadata?.isStandalone) + throw new Error('Only standalone components are supported'); + + const WrapperComponent = defineComponent({ + selector: 'pw-wrapper-component', + template: ``, + })(class {}); + + TestBed.configureTestingModule({ + imports: [Component], + declarations: [WrapperComponent] + }); + + await TestBed.compileComponents(); + + __pwUpdateSlots(WrapperComponent, component.options?.slots, componentMetadata.selector); + + // TODO: only inject when router is provided + TestBed.inject(Router).initialNavigation(); + + const fixture = TestBed.createComponent(WrapperComponent); + fixture.nativeElement.id = 'root'; + + __pwUpdateProps(fixture, component.options?.props); + __pwUpdateEvents(fixture, component.options?.on); + + fixture.autoDetectChanges(); + + return fixture; +} + +async function __pwResolveComponent(component) { + return await window.__pwRegistry.resolveImportRef(component.type); } /** @@ -122,6 +158,19 @@ function __pwUpdateEvents(fixture, events = {}) { __pwOutputSubscriptionRegistry.set(fixture, outputSubscriptionRecord); } +/** + * @param {any} value + * @return {?HTMLElement} + */ +function __pwCreateSlot(value) { + return /** @type {?HTMLElement} */ ( + document + .createRange() + .createContextualFragment(value) + .firstChild + ); +} + function __pwUpdateSlots(Component, slots = {}, tagName) { const wrapper = document.createElement(tagName); for (const [key, value] of Object.entries(slots)) { @@ -157,103 +206,3 @@ function __pwUpdateSlots(Component, slots = {}, tagName) { TestBed.overrideTemplate(Component, wrapper.outerHTML); } - -/** - * @param {any} value - * @return {?HTMLElement} - */ -function __pwCreateSlot(value) { - return /** @type {?HTMLElement} */ ( - document - .createRange() - .createContextualFragment(value) - .firstChild - ); -} - -/** - * @param {Component} component - */ -async function __pwRenderComponent(component) { - const Component = __pwRegistry.get(component.type); - if (!Component) - throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...__pwRegistry.keys()]}`); - - if (component.kind !== 'object') - throw new Error('JSX mount notation is not supported'); - - const componentMetadata = reflectComponentType(Component); - if (!componentMetadata?.isStandalone) - throw new Error('Only standalone components are supported'); - - const WrapperComponent = defineComponent({ - selector: 'pw-wrapper-component', - template: ``, - })(class {}); - - TestBed.configureTestingModule({ - imports: [Component], - declarations: [WrapperComponent] - }); - - await TestBed.compileComponents(); - - __pwUpdateSlots(WrapperComponent, component.options?.slots, componentMetadata.selector); - - // TODO: only inject when router is provided - TestBed.inject(Router).initialNavigation(); - - const fixture = TestBed.createComponent(WrapperComponent); - fixture.nativeElement.id = 'root'; - - __pwUpdateProps(fixture, component.options?.props); - __pwUpdateEvents(fixture, component.options?.on); - - fixture.autoDetectChanges(); - - return fixture; -} - -window.playwrightMount = async (component, rootElement, hooksConfig) => { - await __pwResolveComponent(component); - for (const hook of window.__pw_hooks_before_mount || []) - await hook({ hooksConfig, TestBed }); - - const fixture = await __pwRenderComponent(component); - - for (const hook of window.__pw_hooks_after_mount || []) - await hook({ hooksConfig }); - - __pwFixtureRegistry.set(rootElement.id, fixture); -}; - -window.playwrightUnmount = async rootElement => { - const fixture = __pwFixtureRegistry.get(rootElement.id); - if (!fixture) throw new Error('Component was not mounted'); - - /* Unsubscribe from all outputs. */ - for (const subscription of Object.values(__pwOutputSubscriptionRegistry.get(fixture) ?? {})) - subscription?.unsubscribe(); - __pwOutputSubscriptionRegistry.delete(fixture); - - fixture.destroy(); - fixture.nativeElement.replaceChildren(); -}; - -window.playwrightUpdate = async (rootElement, component) => { - await __pwResolveComponent(component); - if (component.kind === 'jsx') - throw new Error('JSX mount notation is not supported'); - - if (component.options?.slots) - throw new Error('Update slots is not supported yet'); - - const fixture = __pwFixtureRegistry.get(rootElement.id); - if (!fixture) - throw new Error('Component was not mounted'); - - __pwUpdateProps(fixture, component.options?.props); - __pwUpdateEvents(fixture, component.options?.on); - - fixture.detectChanges(); -}; diff --git a/tests/components/ct-angular/playwright.config.ts b/tests/components/ct-angular/playwright.config.ts index 1425d31872..78dc18f763 100644 --- a/tests/components/ct-angular/playwright.config.ts +++ b/tests/components/ct-angular/playwright.config.ts @@ -26,9 +26,9 @@ export default defineConfig({ use: { trace: 'on-first-retry', ctViteConfig: { - plugins: [ - ...analogVitePlugin() - ], + // plugins: [ + // ...analogVitePlugin() + // ], resolve: { alias: { '@': resolve('./src'), diff --git a/tests/components/ct-angular/playwright/index.ts b/tests/components/ct-angular/playwright/index.ts index bd7c8a90af..eea37a2089 100644 --- a/tests/components/ct-angular/playwright/index.ts +++ b/tests/components/ct-angular/playwright/index.ts @@ -1,3 +1,4 @@ +import '@angular/compiler'; import { beforeMount, afterMount } from '@playwright/experimental-ct-angular/hooks'; import { provideRouter } from '@angular/router'; import { ButtonComponent } from '@/components/button.component';