From 0e5dbecf7ec8b1aa2bef3237baa964fa7ca04b10 Mon Sep 17 00:00:00 2001 From: Younes Jaaidi Date: Fri, 23 Feb 2024 17:44:58 +0100 Subject: [PATCH] feat(ct-angular): render component with signal inputs --- packages/playwright-ct-angular/index.d.ts | 2 +- .../playwright-ct-angular/registerSource.mjs | 118 ++++++------------ .../components/button-signals.component.ts | 12 ++ .../ct-angular/tests/render.spec.ts | 12 +- .../components/ct-angular/tests/slots.spec.ts | 47 ------- 5 files changed, 64 insertions(+), 127 deletions(-) create mode 100644 tests/components/ct-angular/src/components/button-signals.component.ts delete mode 100644 tests/components/ct-angular/tests/slots.spec.ts diff --git a/packages/playwright-ct-angular/index.d.ts b/packages/playwright-ct-angular/index.d.ts index b97bb64af2..92e774ee6b 100644 --- a/packages/playwright-ct-angular/index.d.ts +++ b/packages/playwright-ct-angular/index.d.ts @@ -42,7 +42,7 @@ type ComponentSlots = Record & { default?: ComponentSlot type ComponentEvents = Record; export interface MountOptions { - props?: Partial, // TODO: filter props + props?: Partial | Record, // TODO: filter props and handle signals slots?: ComponentSlots; on?: ComponentEvents; hooksConfig?: HooksConfig; diff --git a/packages/playwright-ct-angular/registerSource.mjs b/packages/playwright-ct-angular/registerSource.mjs index 55f5446627..546bc8851c 100644 --- a/packages/playwright-ct-angular/registerSource.mjs +++ b/packages/playwright-ct-angular/registerSource.mjs @@ -1,18 +1,18 @@ -// /** -// * 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. @@ -75,7 +75,7 @@ window.playwrightUpdate = async (rootElement, component) => { if (!fixture) throw new Error('Component was not mounted'); - __pwUpdateProps(fixture, component.props); + __pwUpdateProps(fixture, component); __pwUpdateEvents(fixture, component.on); fixture.detectChanges(); @@ -91,40 +91,35 @@ const __pwFixtureRegistry = new Map(); * @param {ComponentInfo} component */ async function __pwRenderComponent(component) { - let componentClass = component.type; + /** @type {import('@angular/core').Type} */ + let componentClass; - if (typeof componentClass === 'string') { + if (__pwIsTemplate(component)) { const templateInfo = /** @type {TemplateInfo} */(component); componentClass = defineComponent({ imports: templateInfo.imports, selector: 'pw-template-component', standalone: true, - template: componentClass + template: templateInfo.type, })(class {}); + } else { + componentClass = /** @type {import('@angular/core').Type} */(component.type); } const componentMetadata = reflectComponentType(componentClass); if (!componentMetadata?.isStandalone) throw new Error('Only standalone components are supported'); - const WrapperComponent = defineComponent({ - selector: 'pw-wrapper-component', - template: ``, - })(class {}); - TestBed.configureTestingModule({ imports: [componentClass], - declarations: [WrapperComponent] }); await TestBed.compileComponents(); - __pwUpdateSlots(WrapperComponent, component.slots, componentMetadata.selector); - - const fixture = TestBed.createComponent(WrapperComponent); + const fixture = TestBed.createComponent(componentClass); fixture.nativeElement.id = 'root'; - __pwUpdateProps(fixture, component.props); + __pwUpdateProps(fixture, component); __pwUpdateEvents(fixture, component.on); fixture.autoDetectChanges(); @@ -134,10 +129,18 @@ async function __pwRenderComponent(component) { /** * @param {import('@angular/core/testing').ComponentFixture} fixture + * @param {ComponentInfo} componentInfo */ -function __pwUpdateProps(fixture, props = {}) { - for (const [name, value] of Object.entries(props)) - fixture.debugElement.children[0].context[name] = value; +function __pwUpdateProps(fixture, componentInfo) { + if (!componentInfo.props) return; + + if (__pwIsTemplate(componentInfo)) { + Object.assign(fixture.componentInstance, componentInfo.props); + } else { + for (const [name, value] of Object.entries(componentInfo.props)) + fixture.componentRef.setInput(name, value); + } + } /** @@ -150,7 +153,7 @@ function __pwUpdateEvents(fixture, events = {}) { /* Unsubscribe previous listener. */ outputSubscriptionRecord[name]?.unsubscribe(); - const subscription = fixture.debugElement.children[0].componentInstance[ + const subscription = fixture.componentInstance[ name ].subscribe((/** @type {unknown} */ event) => listener(event)); @@ -162,51 +165,10 @@ function __pwUpdateEvents(fixture, events = {}) { __pwOutputSubscriptionRegistry.set(fixture, outputSubscriptionRecord); } + /** - * @param {any} value - * @return {?HTMLElement} + * @param {ComponentInfo} component */ -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)) { - let slotElements; - if (typeof value !== 'object') - slotElements = [__pwCreateSlot(value)]; - - if (Array.isArray(value)) - slotElements = value.map(__pwCreateSlot); - - if (!slotElements) - throw new Error(`Invalid slot with name: \`${key}\` supplied to \`mount()\``); - - for (const slotElement of slotElements) { - if (!slotElement) - throw new Error(`Invalid slot with name: \`${key}\` supplied to \`mount()\``); - - if (key === 'default') { - wrapper.appendChild(slotElement); - continue; - } - - if (slotElement.nodeName === '#text') { - throw new Error( - `Invalid slot with name: \`${key}\` supplied to \`mount()\`, expected \`HTMLElement\` but received \`TextNode\`.` - ); - } - - slotElement.setAttribute(key, ''); - wrapper.appendChild(slotElement); - } - } - - TestBed.overrideTemplate(Component, wrapper.outerHTML); +function __pwIsTemplate(component) { + return typeof component.type === 'string'; } diff --git a/tests/components/ct-angular/src/components/button-signals.component.ts b/tests/components/ct-angular/src/components/button-signals.component.ts new file mode 100644 index 0000000000..18115e4a12 --- /dev/null +++ b/tests/components/ct-angular/src/components/button-signals.component.ts @@ -0,0 +1,12 @@ +import { Component, input } from '@angular/core'; + +@Component({ + standalone: true, + selector: 'app-button-signals', + template: ` + + ` +}) +export class ButtonSignalsComponent { + title = input.required(); +} diff --git a/tests/components/ct-angular/tests/render.spec.ts b/tests/components/ct-angular/tests/render.spec.ts index 5676b99cb3..6937ae4d6c 100644 --- a/tests/components/ct-angular/tests/render.spec.ts +++ b/tests/components/ct-angular/tests/render.spec.ts @@ -3,8 +3,9 @@ import { ButtonComponent } from '@/components/button.component'; import { EmptyComponent } from '@/components/empty.component'; import { ComponentComponent } from '@/components/component.component'; import { NotInlinedComponent } from '@/components/not-inlined.component'; +import { ButtonSignalsComponent } from '@/components/button-signals.component'; -test('render props', async ({ mount }) => { +test('render inputs', async ({ mount }) => { const component = await mount(ButtonComponent, { props: { title: 'Submit', @@ -13,6 +14,15 @@ test('render props', async ({ mount }) => { await expect(component).toContainText('Submit'); }); +test('render signal-based inputs', async ({ mount }) => { + const component = await mount(ButtonSignalsComponent, { + props: { + title: 'Submit', + }, + }); + await expect(component).toContainText('Submit'); +}); + test('get textContent of the empty component', async ({ mount }) => { const component = await mount(EmptyComponent); expect(await component.allTextContents()).toEqual(['']); diff --git a/tests/components/ct-angular/tests/slots.spec.ts b/tests/components/ct-angular/tests/slots.spec.ts deleted file mode 100644 index 36ba30ffdb..0000000000 --- a/tests/components/ct-angular/tests/slots.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { test, expect } from '@playwright/experimental-ct-angular'; -import { DefaultSlotComponent } from '@/components/default-slot.component'; -import { NamedSlotsComponent } from '@/components/named-slots.component'; - -test('render a default slot', async ({ mount }) => { - const component = await mount(DefaultSlotComponent, { - slots: { - default: 'Main Content', - }, - }); - await expect(component.getByRole('strong')).toContainText('Main Content'); -}); - -test('render a component as slot', async ({ mount }) => { - const component = await mount(DefaultSlotComponent, { - slots: { - default: '', // component is registered globally in /playwright/index.ts - }, - }); - await expect(component).toContainText('Submit'); -}); - -test('render a component with multiple slots', async ({ mount }) => { - const component = await mount(DefaultSlotComponent, { - slots: { - default: [ - '
One
', - '
Two
', - ], - }, - }); - await expect(component.getByTestId('one')).toContainText('One'); - await expect(component.getByTestId('two')).toContainText('Two'); -}); - -test('render a component with a named slots', async ({ mount }) => { - const component = await mount(NamedSlotsComponent, { - slots: { - header: '
Header
', //
Main Content
', - footer: '
Footer
', - }, - }); - await expect(component).toContainText('Header'); - await expect(component).toContainText('Main Content'); - await expect(component).toContainText('Footer'); -});