@jscutlery/playwright-ct-angular 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,31 +3,62 @@
3
3
  This library brings **Angular support** to [Playwright's **experimental
4
4
  ** Component Testing](https://playwright.dev/docs/test-components).
5
5
 
6
- This will allow us to test our Angular components with Playwright without building the whole app and with more control.
6
+ This will allow you to test your Angular components with Playwright without building the whole app and with more
7
+ control.
7
8
 
8
9
  `@jscutlery/playwright-ct-angular` currently supports:
9
10
 
10
- - ✅ **Testing [Versatile Angular Components](https://marmicode.io/blog/versatile-angular)**
11
- - 🎛 **Passing type-safe inputs to the tested components**
12
- - 🎭 **Spying on component outputs in a type-safe fashion**
11
+ - ✅ **Testing Angular components/directives/pipes**
12
+ - 🎛 **Controlling inputs/outputs in a type-safe fashion**
13
+ - 🥸 **Overriding providers**
14
+ - 🍳 **Testing with templates**
13
15
 
14
16
  https://user-images.githubusercontent.com/2674658/206226065-ba856329-dda7-43b1-9c28-4416b190f4d4.mp4
15
17
 
16
- # 🚀 Writing our first
17
-
18
- First, we will have to set up Playwright Component Testing as [mentioned below](#setup).
18
+ # Table of Contents
19
+
20
+ <!-- TOC -->
21
+
22
+ * [Playwright Component Testing for Angular _(experimental)_](#playwright-component-testing-for-angular-_experimental_)
23
+ * [Table of Contents](#table-of-contents)
24
+ * [🚀 Writing your first test](#-writing-your-first-test)
25
+ * [✅ Basic Test](#-basic-test)
26
+ * [🎛 Testing Inputs](#-testing-inputs)
27
+ * [Passing output callbacks](#passing-output-callbacks)
28
+ * [🥸 Providing Test Doubles & Importing Additional Modules](#-providing-test-doubles--importing-additional-modules)
29
+ * [🎨 Using Styles](#-using-styles)
30
+ * [Shared Styles](#shared-styles)
31
+ * [Specific Styles](#specific-styles)
32
+ * [Angular Material & Angular Libraries with styles](#angular-material--angular-libraries-with-styles)
33
+ * [More examples](#more-examples)
34
+ * [⚠️ Known Limitations](#-known-limitations)
35
+ * [🪄 The Magic Behind the Scenes](#-the-magic-behind-the-scenes)
36
+ * [It is currently impossible to...](#it-is-currently-impossible-to)
37
+ * [...hold the component type in a variable](#hold-the-component-type-in-a-variable)
38
+ * [...use the component type elsewhere in the file.](#use-the-component-type-elsewhere-in-the-file)
39
+ * [...declare components in the same file.](#declare-components-in-the-same-file)
40
+ * [📦 Setup](#-setup)
41
+ * [1. Install](#1-install)
42
+ * [2. Configure](#2-configure)
43
+ * [3. Change tests extension](#3-change-tests-extension)
44
+
45
+ <!-- TOC -->
46
+
47
+ # 🚀 Writing your first test
48
+
49
+ First, you will have to set up Playwright Component Testing as [mentioned below](#setup).
19
50
 
20
51
  ⚠️ Make sure to check the [known limitations](#-known-limitations) before writing more tests.
21
52
 
22
53
  ## ✅ Basic Test
23
54
 
24
- Then, we can write our first test in `.../src/greetings.component.pw.ts`:
55
+ Then, you can write your first test in `.../src/greetings.component.pw.ts`:
25
56
 
26
57
  ```ts
27
58
  import { expect, test } from '@jscutlery/playwright-ct-angular';
28
59
  import { GreetingsComponent } from './greetings.component';
29
60
 
30
- test('<jc-greetings> should be polite', async ({ mount }) => {
61
+ test(`GreetingsComponent should be polite`, async ({ mount }) => {
31
62
  const locator = await mount(GreetingsComponent);
32
63
  expect(locator).toHaveText('👋 Hello!');
33
64
  });
@@ -39,7 +70,7 @@ test('<jc-greetings> should be polite', async ({ mount }) => {
39
70
  import { expect, test } from '@jscutlery/playwright-ct-angular';
40
71
  import { GreetingsComponent } from './greetings.component';
41
72
 
42
- test('<jc-greetings> should be polite', async ({ mount }) => {
73
+ test(`GreetingsComponent should be polite`, async ({ mount }) => {
43
74
  const locator = await mount(GreetingsComponent, { props: { name: 'Edouard' } });
44
75
  expect(locator).toHaveText('👋 Hello Edouard!');
45
76
  });
@@ -47,7 +78,7 @@ test('<jc-greetings> should be polite', async ({ mount }) => {
47
78
 
48
79
  ### Passing output callbacks
49
80
 
50
- We can also pass custom output callback functions for some extreme cases or if we want to use a custom spy
81
+ You can also pass custom output callback functions for some extreme cases or if you want to use a custom spy
51
82
  implementation for example or just debug.
52
83
 
53
84
  ```ts
@@ -95,20 +126,24 @@ test('...', async ({ mount }) => {
95
126
  ],
96
127
  })
97
128
  export class RecipeSearchTestContainer {
98
- private _repo = inject(RecipeRepositoryFake);
99
-
100
- @Input() set recipes(recipes: Recipe[]) {
101
- this._repo.recipes = recipes;
102
- }
129
+ recipes = input<Recipe[]>([]);
130
+ #repo = inject(RecipeRepositoryFake);
131
+ #syncRecipesWithRepo = effect(() => {
132
+ this.#repo.setRecipes(this.recipes());
133
+ });
103
134
  }
104
135
 
105
136
  /* Cf. https://github.com/jscutlery/devkit/tree/main/tests/playwright-ct-angular-wide/src/testing/recipe-repository.fake.ts
106
137
  * for a better example. */
107
138
  class RecipeRepositoryFake implements RecipeRepositoryDef {
108
- recipes: Recipe[] = [];
139
+ #recipes: Recipe[] = [];
109
140
 
110
141
  searchRecipes() {
111
- return defer(() => of(this.recipes));
142
+ return defer(() => of(this.#recipes));
143
+ }
144
+
145
+ setRecipes(recipes: Recipe[]) {
146
+ this.#recipes = recipes;
112
147
  }
113
148
  }
114
149
  ```
@@ -117,12 +152,12 @@ class RecipeRepositoryFake implements RecipeRepositoryDef {
117
152
 
118
153
  ### Shared Styles
119
154
 
120
- In order to import styles that are shared between our tests, we can do so by importing them in `playwright/index.ts`.
121
- We can also customize the shared `playwright/index.html` nearby.
155
+ In order to import styles that are shared between your tests, you can do so by importing them in `playwright/index.ts`.
156
+ You can also customize the shared `playwright/index.html` nearby.
122
157
 
123
158
  ### Specific Styles
124
159
 
125
- If we want to load some specific styles for a single test, we might prefer using a test container component:
160
+ If you want to load some specific styles for a single test, you might prefer using a test container component:
126
161
 
127
162
  ```ts
128
163
  import styles from './some-styles.css';
@@ -164,31 +199,33 @@ const config: PlaywrightTestConfig = {
164
199
 
165
200
  ## More examples
166
201
 
167
- Cf. [/tests/playwright-ct-angular-wide/src](https://github.com/jscutlery/devkit/tree/main/tests/playwright-ct-angular-wide/src)
202
+ Cf. [/tests/playwright-ct-angular-demo/src](https://github.com/jscutlery/devkit/tree/main/tests/playwright-ct-angular-demo/src)
168
203
 
169
204
  # ⚠️ Known Limitations
170
205
 
171
206
  The way Playwright Component Testing works is different from the way things work with Karma, Jest, Vitest, Cypress
172
207
  etc...
173
- Playwright Component Testing tests run in a Node.js environment while the component is rendered in a browser.
208
+ Playwright Component Testing tests run in a Node.js environment and control the browser through Chrome DevTools
209
+ Protocol, while the component is rendered in a browser.
174
210
 
175
- This causes a couple of limitations as we can't directly access the `TestBed's` or the component's internals,
176
- and we can only exchange serializable data with the component.
211
+ This causes a couple of limitations as we can't directly access the `TestBed`'s or the component's internals,
212
+ and **we can only exchange serializable data with the component**.
177
213
 
178
214
  ## 🪄 The Magic Behind the Scenes
179
215
 
180
216
  The magical workaround behind the scenes is that at build time:
181
217
 
182
218
  1. Playwright analyses all the calls to `mount()`,
183
- 2. it grabs the first parameter (the component class),
219
+ 2. it grabs the arguments (e.g. the component class),
184
220
  3. replaces the component class with a unique string (constructed from the component class name and es-module),
185
- 4. adds the component's es-module to Vite entrypoints,
186
- 5. and finally creates a map matching each unique string to the right es-module.
221
+ 4. adds the component's ES module to Vite entrypoints,
222
+ 5. and finally creates a map matching each unique string to the right ES module.
187
223
 
188
- This way, when calling `mount()`, Playwright with communicate the unique string to the browser who will know which
189
- es-module to load.
224
+ This way, when calling `mount()`, Playwright will communicate the unique string to the browser who will know which
225
+ ES module to load.
190
226
 
191
227
  Cf. https://youtu.be/y3YxX4sFJbM
228
+
192
229
  Cf. https://github.com/microsoft/playwright/blob/cac67fb94f2c8a0ee82878054c39790e660f17ca/packages/playwright-test/src/tsxTransform.ts#L153
193
230
 
194
231
  ## It is currently impossible to...
@@ -217,39 +254,18 @@ test(MyComponent.name, async ({ mount }) => {
217
254
  class GreetingsComponent {
218
255
  }
219
256
 
220
- test('<jc-greetings>', async ({ mount }) => {
257
+ test('should work', async ({ mount }) => {
221
258
  await mount(GreetingsComponent);
222
259
  });
223
260
  ```
224
261
 
225
- ### ...pass any symbol other than a component class
226
-
227
- This makes the following impossible:
228
-
229
- - passing providers to the `mount()` function
230
- - passing modules to the `mount()` function (this is what currently makes the usage of mounting templates impossible)
231
- - use non-standalone components
232
-
233
- ### ...use non-[Versatile Angular Style](https://marmicode.io/blog/versatile-angular)
234
-
235
- We'll need a vite plugin to support traditional DI, external templates and stylesheets etc...
236
-
237
- ## 🔮 Future workarounds
238
-
239
- If you really can't make it to [Versatile Angular Style](https://marmicode.io/blog/versatile-angular),
240
- we can think of a couple of workarounds like using a vite plugin like **Brandon
241
- Roberts' [vite-plugin-angular](https://github.com/analogjs/analog/tree/main/packages/vite-plugin-angular)**
242
-
243
- We could also implement some custom transform that registers the providers and import modules, like Playwright does for
244
- the component class.
262
+ # 📦 Setup
245
263
 
246
- # Setup
247
-
248
- ## 📦 Install
264
+ ### 1. Install
249
265
 
250
266
  ```sh
251
267
  # You can run this command in an existing workspace.
252
- yarn create playwright --ct # or npm init playwright@latest -- --ct
268
+ npm create playwright -- --ct
253
269
 
254
270
  # Choose React
255
271
 
@@ -259,29 +275,46 @@ yarn create playwright --ct # or npm init playwright@latest -- --ct
259
275
  # svelte
260
276
  # solid
261
277
 
262
- yarn add -D @jscutlery/playwright-ct-angular @playwright/test # or npm install -D @jscutlery/playwright-ct-angular @playwright/test
278
+ npm add -D @jscutlery/playwright-ct-angular @jscutlery/swc-angular unplugin-swc
279
+ npm uninstall -D @playwright/experimental-ct-react
263
280
  ```
264
281
 
265
- ## 🛠 Configure
282
+ ### 2. Configure
266
283
 
267
- Update `playwright-ct-config.ts` and replace:
284
+ - Update `playwright-ct.config.ts` and replace:
268
285
 
269
286
  ```ts
270
- import type { PlaywrightTestConfig } from '@playwright/experimental-ct-react';
271
- import { devices } from '@playwright/experimental-ct-react';
287
+ import { defineConfig, devices } from '@playwright/experimental-ct-react';
272
288
  ```
273
289
 
274
290
  with
275
291
 
276
292
  ```ts
277
- import type { PlaywrightTestConfig } from '@jscutlery/playwright-ct-angular';
278
- import { devices } from '@jscutlery/playwright-ct-angular';
293
+ import { defineConfig, devices } from '@jscutlery/playwright-ct-angular';
294
+ import { swcAngularUnpluginOptions } from '@jscutlery/swc-angular'
295
+ import swc from 'unplugin-swc';
296
+ ```
297
+
298
+ - Configure vite plugin:
299
+
300
+ ```ts
301
+ export default defineConfig({
302
+ use: {
303
+ // ...
304
+ ctViteConfig: {
305
+ // ...
306
+ plugins: [
307
+ swc.vite(swcAngularUnpluginOptions())
308
+ ]
309
+ }
310
+ }
311
+ });
279
312
  ```
280
313
 
281
- ### Change tests extension
314
+ ### 3. Change tests extension
282
315
 
283
316
  In order to avoid collisions with other tests (e.g. Jest / Vitest),
284
- We can replace the default matching extension `.spec.ts` with `.pw.ts`:
317
+ You can replace the default matching extension `.spec.ts` with `.pw.ts`:
285
318
 
286
319
  ```ts
287
320
  const config: PlaywrightTestConfig = {
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  /**
2
3
  * Copyright (c) Microsoft Corporation.
3
4
  *
@@ -5,7 +6,7 @@
5
6
  * you may not use this file except in compliance with the License.
6
7
  * You may obtain a copy of the License at
7
8
  *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
+ * http://www.apache.org/licenses/LICENSE-2.0
9
10
  *
10
11
  * Unless required by applicable law or agreed to in writing, software
11
12
  * distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,5 +14,7 @@
13
14
  * See the License for the specific language governing permissions and
14
15
  * limitations under the License.
15
16
  */
16
- declare const _default: (components: any) => void;
17
- export default _default;
17
+
18
+ const { program } = require('@playwright/experimental-ct-core/lib/program');
19
+
20
+ program.parse(process.argv);
package/hooks.cjs.js CHANGED
@@ -2,31 +2,16 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- /**
6
- * Copyright (c) Microsoft Corporation.
7
- *
8
- * Licensed under the Apache License, Version 2.0 (the "License");
9
- * you may not use this file except in compliance with the License.
10
- * You may obtain a copy of the License at
11
- *
12
- * http://www.apache.org/licenses/LICENSE-2.0
13
- *
14
- * Unless required by applicable law or agreed to in writing, software
15
- * distributed under the License is distributed on an "AS IS" BASIS,
16
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
- * See the License for the specific language governing permissions and
18
- * limitations under the License.
19
- */
20
5
  const __pw_hooks_before_mount = [];
21
6
  const __pw_hooks_after_mount = [];
22
7
  window['__pw_hooks_before_mount'] = __pw_hooks_before_mount;
23
8
  window['__pw_hooks_after_mount'] = __pw_hooks_after_mount;
24
- const beforeMount = (callback) => {
9
+ function beforeMount(callback) {
25
10
  __pw_hooks_before_mount.push(callback);
26
- };
27
- const afterMount = (callback) => {
11
+ }
12
+ function afterMount(callback) {
28
13
  __pw_hooks_after_mount.push(callback);
29
- };
14
+ }
30
15
 
31
16
  exports.afterMount = afterMount;
32
17
  exports.beforeMount = beforeMount;
package/hooks.esm.js CHANGED
@@ -1,27 +1,12 @@
1
- /**
2
- * Copyright (c) Microsoft Corporation.
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
1
  const __pw_hooks_before_mount = [];
17
2
  const __pw_hooks_after_mount = [];
18
3
  window['__pw_hooks_before_mount'] = __pw_hooks_before_mount;
19
4
  window['__pw_hooks_after_mount'] = __pw_hooks_after_mount;
20
- const beforeMount = (callback) => {
5
+ function beforeMount(callback) {
21
6
  __pw_hooks_before_mount.push(callback);
22
- };
23
- const afterMount = (callback) => {
7
+ }
8
+ function afterMount(callback) {
24
9
  __pw_hooks_after_mount.push(callback);
25
- };
10
+ }
26
11
 
27
12
  export { afterMount, beforeMount };
package/package.json CHANGED
@@ -1,23 +1,22 @@
1
1
  {
2
2
  "name": "@jscutlery/playwright-ct-angular",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "dependencies": {
5
- "@babel/core": "~7.23.0",
6
- "jest-mock": "^28.1.3",
7
- "rxjs": "^7.5.7",
8
- "vite": "^3.2.1"
5
+ "@babel/core": "7.23.9",
6
+ "@playwright/experimental-ct-core": "1.43.1",
7
+ "@playwright/test": "1.43.1"
9
8
  },
10
9
  "peerDependencies": {
11
10
  "@angular/compiler": "^16.0.0 || ^17.0.0",
12
11
  "@angular/core": "^16.0.0 || ^17.0.0",
13
12
  "@angular/platform-browser": "^16.0.0 || ^17.0.0",
14
13
  "@angular/platform-browser-dynamic": "^16.0.0 || ^17.0.0",
15
- "@playwright/experimental-ct-core": "^1.39.0",
16
- "@playwright/test": "^1.39.0",
17
14
  "rxjs": "^7.0.0",
18
- "tslib": "*",
19
15
  "zone.js": "*"
20
16
  },
17
+ "bin": {
18
+ "playwright": "cli.js"
19
+ },
21
20
  "exports": {
22
21
  "./package.json": "./package.json",
23
22
  ".": {
@@ -30,11 +29,6 @@
30
29
  "import": "./hooks.cjs.mjs",
31
30
  "default": "./hooks.cjs.js"
32
31
  },
33
- "./register": {
34
- "module": "./register.esm.js",
35
- "import": "./register.cjs.mjs",
36
- "default": "./register.cjs.js"
37
- },
38
32
  "./transform-angular": {
39
33
  "module": "./transform-angular.esm.js",
40
34
  "import": "./transform-angular.cjs.mjs",
@@ -1,69 +1,148 @@
1
1
  import '@angular/compiler';
2
2
  import 'zone.js';
3
+
3
4
  import { getTestBed, TestBed } from '@angular/core/testing';
4
- import {
5
- BrowserDynamicTestingModule,
6
- platformBrowserDynamicTesting
7
- } from '@angular/platform-browser-dynamic/testing';
8
- import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
5
+ import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
9
6
  import { Subscription } from 'rxjs';
7
+ import { Component, reflectComponentType } from '@angular/core';
10
8
 
11
- const _componentLoaderMap = new Map();
9
+ /**
10
+ * @typedef {{type: string} & import('./index').MountTemplateOptions} TemplateInfo
11
+ * @typedef {{type: import('@angular/core').Type<unknown>} & import('./index').MountOptions | TemplateInfo} ComponentInfo
12
+ */
12
13
 
13
14
  getTestBed().initTestEnvironment(
14
15
  BrowserDynamicTestingModule,
15
16
  platformBrowserDynamicTesting()
16
17
  );
17
18
 
19
+ window.playwrightMount = async (component, rootElement, hooksConfig) => {
20
+ for (const hook of window.__pw_hooks_before_mount || [])
21
+ await hook({ hooksConfig, TestBed });
22
+
23
+ const fixture = await __pwRenderComponent(component);
24
+
25
+ fixture.autoDetectChanges();
26
+ await fixture.whenStable();
27
+
28
+ for (const hook of window.__pw_hooks_after_mount || [])
29
+ await hook({ hooksConfig });
30
+
31
+ __pwFixtureRegistry.set(rootElement.id, fixture);
32
+ };
33
+
34
+ window.playwrightUnmount = async (rootElement) => {
35
+ const fixture = __pwFixtureRegistry.get(rootElement.id);
36
+ if (!fixture)
37
+ throw new Error('Component was not mounted');
38
+
39
+ /* Unsubscribe from all outputs. */
40
+ for (const subscription of Object.values(__pwOutputSubscriptionRegistry.get(fixture) ?? {}))
41
+ subscription?.unsubscribe();
42
+ __pwOutputSubscriptionRegistry.delete(fixture);
43
+
44
+ fixture.destroy();
45
+ fixture.nativeElement.replaceChildren();
46
+ };
47
+
18
48
  /**
19
- * This is the function used by Playwright CT to map each component to its symbol.
20
- * The key here is a slug generated by Playwright CT based on the component's module path + component's name.
49
+ * @param {{type: import('@angular/core').Type<unknown>} & import('./index').MountOptions | {type: string} & import('./index').MountTemplateOptions} component
21
50
  */
22
- export function pwRegister(components) {
23
- for (const [name, value] of Object.entries(components)) {
24
- _componentLoaderMap.set(name, value);
25
- }
26
- }
51
+ window.playwrightUpdate = async (rootElement, component) => {
52
+ const fixture = __pwFixtureRegistry.get(rootElement.id);
53
+ if (!fixture)
54
+ throw new Error('Component was not mounted');
55
+
56
+ __pwUpdateProps(fixture, component);
57
+ __pwUpdateEvents(fixture, component.on);
58
+
59
+ fixture.autoDetectChanges();
60
+ await fixture.whenStable();
61
+ };
62
+
63
+ /** @type {WeakMap<import('@angular/core/testing').ComponentFixture, Record<string, import('rxjs').Subscription>>} */
64
+ const __pwOutputSubscriptionRegistry = new WeakMap();
65
+
66
+ /** @type {Map<string, import('@angular/core/testing').ComponentFixture>} */
67
+ const __pwFixtureRegistry = new Map();
27
68
 
28
69
  /**
29
- * Container subscription holding all outputs subscription
30
- * in order to unsubscribe when component is unmounted.
70
+ * @param {ComponentInfo} component
31
71
  */
32
- let subscription;
72
+ async function __pwRenderComponent(component) {
73
+ /** @type {import('@angular/core').Type<unknown>} */
74
+ let componentClass;
33
75
 
34
- globalThis.playwrightMount = async (component, rootElement, hooksConfig) => {
35
- const cmpType = component.type;
76
+ if (__pwIsTemplate(component)) {
77
+ const templateInfo = /** @type {TemplateInfo} */(component);
78
+ componentClass = Component({
79
+ standalone: true,
80
+ selector: 'pw-template-component',
81
+ imports: templateInfo.imports,
82
+ template: templateInfo.type
83
+ })(class {
84
+ });
85
+ } else {
86
+ componentClass = /** @type {import('@angular/core').Type<unknown>} */(component.type);
87
+ }
36
88
 
37
- subscription = new Subscription();
89
+ const componentMetadata = reflectComponentType(componentClass);
90
+ if (!componentMetadata?.isStandalone)
91
+ throw new Error('Only standalone components are supported');
38
92
 
39
93
  TestBed.configureTestingModule({
40
- imports: [BrowserAnimationsModule]
94
+ imports: [componentClass],
95
+ providers: component.providers
41
96
  });
42
- const fixture = TestBed.createComponent(cmpType);
97
+
98
+ const fixture = TestBed.createComponent(componentClass);
43
99
  fixture.nativeElement.id = 'root';
44
100
 
45
- /* Set inputs. */
46
- for (const [name, value] of Object.entries(component.props ?? {})) {
47
- fixture.componentInstance[name] = value;
48
- }
101
+ __pwUpdateProps(fixture, component);
102
+ __pwUpdateEvents(fixture, component.on);
103
+
104
+ return fixture;
105
+ }
49
106
 
50
- /* Subscribe to outputs. */
51
- for (const [name, callback] of Object.entries(
52
- component.on ?? {}
53
- )) {
54
- subscription.add(fixture.componentInstance[name].subscribe(callback));
107
+ /**
108
+ * @param {import('@angular/core/testing').ComponentFixture} fixture
109
+ * @param {ComponentInfo} componentInfo
110
+ */
111
+ function __pwUpdateProps(fixture, componentInfo) {
112
+ if (!componentInfo.props)
113
+ return;
114
+
115
+ if (__pwIsTemplate(componentInfo)) {
116
+ Object.assign(fixture.componentInstance, componentInfo.props);
117
+ } else {
118
+ for (const [name, value] of Object.entries(componentInfo.props))
119
+ fixture.componentRef.setInput(name, value);
55
120
  }
121
+ }
56
122
 
57
- fixture.autoDetectChanges();
58
- await fixture.whenStable();
59
- };
123
+ /**
124
+ * @param {import('@angular/core/testing').ComponentFixture} fixture
125
+ */
126
+ function __pwUpdateEvents(fixture, events = {}) {
127
+ const outputSubscriptionRecord =
128
+ __pwOutputSubscriptionRegistry.get(fixture) ?? {};
129
+ for (const [name, listener] of Object.entries(events)) {
130
+ /* Unsubscribe previous listener. */
131
+ outputSubscriptionRecord[name]?.unsubscribe();
60
132
 
61
- globalThis.playwrightUnmount = async (rootElement) => {
62
- subscription.unsubscribe();
63
- subscription = null;
64
- getTestBed().resetTestingModule();
65
- };
133
+ /* Store new subscription. */
134
+ outputSubscriptionRecord[name] = fixture.componentInstance[
135
+ name
136
+ ].subscribe((/** @type {unknown} */ event) => listener(event));
137
+ }
66
138
 
67
- globalThis.playwrightUpdate = async (rootElement, component) => {
68
- rootElement.innerHTML = component;
69
- };
139
+ /* Update output subscription registry. */
140
+ __pwOutputSubscriptionRegistry.set(fixture, outputSubscriptionRecord);
141
+ }
142
+
143
+ /**
144
+ * @param {ComponentInfo} component
145
+ */
146
+ function __pwIsTemplate(component) {
147
+ return typeof component.type === 'string';
148
+ }
package/src/hooks.d.ts CHANGED
@@ -13,5 +13,12 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- export declare const beforeMount: (callback: any) => void;
17
- export declare const afterMount: (callback: any) => void;
16
+ import { TestBedStatic } from '@angular/core/testing';
17
+ import { JsonObject } from '@playwright/experimental-ct-core/types/component';
18
+ export declare function beforeMount<HOOKS extends JsonObject>(callback: (params: {
19
+ hooksConfig?: HOOKS;
20
+ TestBed: TestBedStatic;
21
+ }) => Promise<void>): void;
22
+ export declare function afterMount<HOOKS extends JsonObject>(callback: (params: {
23
+ hooksConfig?: HOOKS;
24
+ }) => Promise<void>): void;
package/src/index.d.ts CHANGED
@@ -1,26 +1,39 @@
1
- import { Type } from '@angular/core';
1
+ import { EnvironmentProviders, InputSignal, Provider, Type } from '@angular/core';
2
2
  import type { PlaywrightTestConfig, TestType } from '@playwright/experimental-ct-core';
3
+ import * as playwrightCtCore from '@playwright/experimental-ct-core';
4
+ import { JsonObject } from '@playwright/experimental-ct-core/types/component';
3
5
  import * as playwright from '@playwright/test';
4
- import type { Observable } from 'rxjs';
5
6
  export type { PlaywrightTestConfig };
6
7
  export { expect, devices } from '@playwright/test';
7
8
  export interface ComponentFixtures {
8
- mount<COMPONENT>(component: Type<COMPONENT>, options?: MountOptions<COMPONENT>): Promise<MountResult<COMPONENT>>;
9
+ mount<COMPONENT, HOOKS extends JsonObject>(template: string, options?: MountTemplateOptions<COMPONENT, HOOKS>): Promise<MountResult<COMPONENT>>;
10
+ mount<COMPONENT, HOOKS extends JsonObject>(component: Type<COMPONENT>, options?: MountOptions<COMPONENT, HOOKS>): Promise<MountResult<COMPONENT>>;
9
11
  }
10
- export interface MountOptions<COMPONENT> {
11
- props?: Partial<COMPONENT>;
12
- on?: Outputs<COMPONENT>;
12
+ export interface MountOptions<COMPONENT, HOOKS> {
13
+ hooksConfig?: HOOKS;
14
+ providers?: Array<Provider | EnvironmentProviders>;
15
+ props?: Inputs<COMPONENT>;
16
+ on?: OutputListeners<COMPONENT>;
17
+ }
18
+ export interface MountTemplateOptions<COMPONENT, HOOKS extends JsonObject> extends MountOptions<COMPONENT, HOOKS> {
19
+ imports?: Type<unknown>[];
13
20
  }
14
- export type Outputs<COMPONENT> = Partial<{
15
- [K in keyof COMPONENT]: COMPONENT[K] extends Observable<unknown> ? (value: Emitted<COMPONENT[K]>) => void : COMPONENT[K];
16
- }>;
17
21
  export interface MountResult<COMPONENT> extends playwright.Locator {
18
22
  unmount(): Promise<void>;
19
23
  update(options: {
20
- props?: Partial<COMPONENT>;
21
- on?: Outputs<COMPONENT>;
24
+ props?: Inputs<COMPONENT>;
25
+ on?: OutputListeners<COMPONENT>;
22
26
  }): Promise<void>;
23
27
  }
24
- type Emitted<OBSERVABLE> = OBSERVABLE extends Observable<infer EMITTED> ? EMITTED : OBSERVABLE;
28
+ export type Inputs<COMPONENT> = Partial<{
29
+ [PROPERTY in keyof COMPONENT]: COMPONENT[PROPERTY] extends InputSignal<infer VALUE> ? VALUE : COMPONENT[PROPERTY];
30
+ }>;
31
+ export type OutputListeners<COMPONENT> = {
32
+ [PROPERTY in keyof COMPONENT as COMPONENT[PROPERTY] extends Subscribable<any> ? PROPERTY : never]: (value: Emitted<COMPONENT[PROPERTY]>) => void;
33
+ };
34
+ interface Subscribable<T> {
35
+ subscribe(subscriber: (value: T) => void): void;
36
+ }
37
+ type Emitted<SUBSCRIBABLE> = SUBSCRIBABLE extends Subscribable<infer EMITTED> ? EMITTED : SUBSCRIBABLE;
25
38
  export declare const test: TestType<ComponentFixtures>;
26
- export declare const defineConfig: (config: any, ...configs: any[]) => PlaywrightTestConfig;
39
+ export declare const defineConfig: typeof playwrightCtCore.defineConfig;
package/register.cjs.d.ts DELETED
@@ -1,2 +0,0 @@
1
- export * from "./src/register";
2
- export { default } from "./src/register";
@@ -1 +0,0 @@
1
- exports._default = require('./register.cjs.js').default;
package/register.cjs.js DELETED
@@ -1,89 +0,0 @@
1
- 'use strict';
2
-
3
- require('@angular/compiler');
4
- require('zone.js');
5
- var testing = require('@angular/core/testing');
6
- var testing$1 = require('@angular/platform-browser-dynamic/testing');
7
- var animations = require('@angular/platform-browser/animations');
8
- var rxjs = require('rxjs');
9
-
10
- const _componentLoaderMap = new Map();
11
-
12
- testing.getTestBed().initTestEnvironment(
13
- testing$1.BrowserDynamicTestingModule,
14
- testing$1.platformBrowserDynamicTesting()
15
- );
16
-
17
- /**
18
- * This is the function used by Playwright CT to map each component to its symbol.
19
- * The key here is a slug generated by Playwright CT based on the component's module path + component's name.
20
- */
21
- function pwRegister(components) {
22
- for (const [name, value] of Object.entries(components)) {
23
- _componentLoaderMap.set(name, value);
24
- }
25
- }
26
-
27
- /**
28
- * Container subscription holding all outputs subscription
29
- * in order to unsubscribe when component is unmounted.
30
- */
31
- let subscription;
32
-
33
- globalThis.playwrightMount = async (component, rootElement, hooksConfig) => {
34
- const cmpType = component.type;
35
-
36
- subscription = new rxjs.Subscription();
37
-
38
- testing.TestBed.configureTestingModule({
39
- imports: [animations.BrowserAnimationsModule]
40
- });
41
- const fixture = testing.TestBed.createComponent(cmpType);
42
- fixture.nativeElement.id = 'root';
43
-
44
- /* Set inputs. */
45
- for (const [name, value] of Object.entries(component.props ?? {})) {
46
- fixture.componentInstance[name] = value;
47
- }
48
-
49
- /* Subscribe to outputs. */
50
- for (const [name, callback] of Object.entries(
51
- component.on ?? {}
52
- )) {
53
- subscription.add(fixture.componentInstance[name].subscribe(callback));
54
- }
55
-
56
- fixture.autoDetectChanges();
57
- await fixture.whenStable();
58
- };
59
-
60
- globalThis.playwrightUnmount = async (rootElement) => {
61
- subscription.unsubscribe();
62
- subscription = null;
63
- testing.getTestBed().resetTestingModule();
64
- };
65
-
66
- globalThis.playwrightUpdate = async (rootElement, component) => {
67
- rootElement.innerHTML = component;
68
- };
69
-
70
- /**
71
- * Copyright (c) Microsoft Corporation.
72
- *
73
- * Licensed under the Apache License, Version 2.0 (the "License");
74
- * you may not use this file except in compliance with the License.
75
- * You may obtain a copy of the License at
76
- *
77
- * http://www.apache.org/licenses/LICENSE-2.0
78
- *
79
- * Unless required by applicable law or agreed to in writing, software
80
- * distributed under the License is distributed on an "AS IS" BASIS,
81
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
82
- * See the License for the specific language governing permissions and
83
- * limitations under the License.
84
- */
85
- var register = (components) => {
86
- pwRegister(components);
87
- };
88
-
89
- module.exports = register;
package/register.cjs.mjs DELETED
@@ -1,2 +0,0 @@
1
- export * from './register.cjs.js';
2
- export { _default as default } from './register.cjs.default.js';
package/register.esm.d.ts DELETED
@@ -1,2 +0,0 @@
1
- export * from "./src/register";
2
- export { default } from "./src/register";
package/register.esm.js DELETED
@@ -1,87 +0,0 @@
1
- import '@angular/compiler';
2
- import 'zone.js';
3
- import { getTestBed, TestBed } from '@angular/core/testing';
4
- import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
5
- import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
6
- import { Subscription } from 'rxjs';
7
-
8
- const _componentLoaderMap = new Map();
9
-
10
- getTestBed().initTestEnvironment(
11
- BrowserDynamicTestingModule,
12
- platformBrowserDynamicTesting()
13
- );
14
-
15
- /**
16
- * This is the function used by Playwright CT to map each component to its symbol.
17
- * The key here is a slug generated by Playwright CT based on the component's module path + component's name.
18
- */
19
- function pwRegister(components) {
20
- for (const [name, value] of Object.entries(components)) {
21
- _componentLoaderMap.set(name, value);
22
- }
23
- }
24
-
25
- /**
26
- * Container subscription holding all outputs subscription
27
- * in order to unsubscribe when component is unmounted.
28
- */
29
- let subscription;
30
-
31
- globalThis.playwrightMount = async (component, rootElement, hooksConfig) => {
32
- const cmpType = component.type;
33
-
34
- subscription = new Subscription();
35
-
36
- TestBed.configureTestingModule({
37
- imports: [BrowserAnimationsModule]
38
- });
39
- const fixture = TestBed.createComponent(cmpType);
40
- fixture.nativeElement.id = 'root';
41
-
42
- /* Set inputs. */
43
- for (const [name, value] of Object.entries(component.props ?? {})) {
44
- fixture.componentInstance[name] = value;
45
- }
46
-
47
- /* Subscribe to outputs. */
48
- for (const [name, callback] of Object.entries(
49
- component.on ?? {}
50
- )) {
51
- subscription.add(fixture.componentInstance[name].subscribe(callback));
52
- }
53
-
54
- fixture.autoDetectChanges();
55
- await fixture.whenStable();
56
- };
57
-
58
- globalThis.playwrightUnmount = async (rootElement) => {
59
- subscription.unsubscribe();
60
- subscription = null;
61
- getTestBed().resetTestingModule();
62
- };
63
-
64
- globalThis.playwrightUpdate = async (rootElement, component) => {
65
- rootElement.innerHTML = component;
66
- };
67
-
68
- /**
69
- * Copyright (c) Microsoft Corporation.
70
- *
71
- * Licensed under the Apache License, Version 2.0 (the "License");
72
- * you may not use this file except in compliance with the License.
73
- * You may obtain a copy of the License at
74
- *
75
- * http://www.apache.org/licenses/LICENSE-2.0
76
- *
77
- * Unless required by applicable law or agreed to in writing, software
78
- * distributed under the License is distributed on an "AS IS" BASIS,
79
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
80
- * See the License for the specific language governing permissions and
81
- * limitations under the License.
82
- */
83
- var register = (components) => {
84
- pwRegister(components);
85
- };
86
-
87
- export { register as default };