Skip to content
This repository has been archived by the owner on Jul 1, 2024. It is now read-only.

Commit

Permalink
feat(ct): support non-event-emitter outputs (#43)
Browse files Browse the repository at this point in the history
  • Loading branch information
sand4rt authored Jan 31, 2024
1 parent ba7a089 commit 2021495
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 6 deletions.
17 changes: 17 additions & 0 deletions ct-angular/src/components/output.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { DOCUMENT } from "@angular/common";
import { Component, Output, inject } from "@angular/core";
import { Subject, finalize } from "rxjs";

@Component({
standalone: true,
template: `OutputComponent`,
})
export class OutputComponent {
@Output() answerChange = new Subject().pipe(
/* Detect when observable is unsubscribed from,
* and set a global variable `hasUnsubscribed` to true. */
finalize(() => ((this._window as any).hasUnsubscribed = true))
);

private _window = inject(DOCUMENT).defaultView;
}
14 changes: 14 additions & 0 deletions ct-angular/tests/unmount.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { test, expect } from '@sand4rt/experimental-ct-angular';
import { ButtonComponent } from '@/components/button.component';
import { MultiRootComponent } from '@/components/multi-root.component';
import { OutputComponent } from '@/components/output.component';

test('unmount', async ({ page, mount }) => {
const component = await mount(ButtonComponent, {
Expand All @@ -21,3 +22,16 @@ test('unmount a multi root component', async ({ mount, page }) => {
await expect(page.locator('#root')).not.toContainText('root 1');
await expect(page.locator('#root')).not.toContainText('root 2');
});

test('unsubscribe from events when the component is unmounted', async ({ mount, page }) => {
const component = await mount(OutputComponent, {
on: {
answerChange() {},
},
});
await component.unmount();
/* Check that the output observable had been unsubscribed from
* as it sets a global variable `hasUnusbscribed` to true
* when it detects unsubscription. Cf. OutputComponent. */
expect(await page.evaluate(() => (window as any).hasUnsubscribed)).toBe(true);
});
26 changes: 26 additions & 0 deletions ct-angular/tests/update.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { test, expect } from '@sand4rt/experimental-ct-angular';
import { CounterComponent } from '@/components/counter.component';
import { ButtonComponent } from '@/components/button.component';

test('update props without remounting', async ({ mount }) => {
const component = await mount(CounterComponent, {
Expand Down Expand Up @@ -30,3 +31,28 @@ test('update event listeners without remounting', async ({ mount }) => {

await expect(component.getByTestId('remount-count')).toContainText('1');
});

test('replace existing listener when new listener is set', async ({ mount }) => {
let count = 0;

const component = await mount(ButtonComponent, {
props: {
title: 'Submit',
},
on: {
submit() {
count++;
},
},
});
component.update({
on: {
submit() {
count++;
},
},
});
await component.click();
expect(count).toBe(1);
});

29 changes: 23 additions & 6 deletions playwright-ct-angular/registerSource.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import 'zone.js';
import { getTestBed, TestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
import { EventEmitter, reflectComponentType, Component as defineComponent } from '@angular/core';
import { reflectComponentType, Component as defineComponent } from '@angular/core';
import { Router } from '@angular/router';

/** @typedef {import('@playwright/experimental-ct-core/types/component').Component} Component */
Expand All @@ -30,6 +30,8 @@ import { Router } from '@angular/router';

/** @type {Map<string, import('@angular/core/testing').ComponentFixture>} */
const __pwFixtureRegistry = new Map();
/** @type {WeakMap<import('@angular/core/testing').ComponentFixture, Record<string, import('rxjs').Subscription>>} */
const __pwOutputSubscriptionRegistry = new WeakMap();

getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
Expand All @@ -48,12 +50,22 @@ function __pwUpdateProps(fixture, props = {}) {
* @param {import('@angular/core/testing').ComponentFixture} fixture
*/
function __pwUpdateEvents(fixture, events = {}) {
for (const [name, value] of Object.entries(events)) {
fixture.debugElement.children[0].componentInstance[name] = {
...new EventEmitter(),
emit: event => value(event)
};
const outputSubscriptionRecord =
__pwOutputSubscriptionRegistry.get(fixture) ?? {};
for (const [name, listener] of Object.entries(events)) {
/* Unsubscribe previous listener. */
outputSubscriptionRecord[name]?.unsubscribe();

const subscription = fixture.debugElement.children[0].componentInstance[
name
].subscribe((event) => listener(event));

/* Store new subscription. */
outputSubscriptionRecord[name] = subscription;
}

/* Update output subscription registry. */
__pwOutputSubscriptionRegistry.set(fixture, outputSubscriptionRecord);
}

function __pwUpdateSlots(Component, slots = {}, tagName) {
Expand Down Expand Up @@ -161,6 +173,11 @@ window.playwrightUnmount = async rootElement => {
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();
};
Expand Down

0 comments on commit 2021495

Please sign in to comment.