@jscutlery/playwright-ct-angular 0.2.2 → 0.4.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
@@ -1,13 +1,14 @@
1
1
  # Playwright Component Testing for Angular _(experimental)_
2
2
 
3
- This library brings **Angular support** to [Playwright's **experimental** Component Testing](https://playwright.dev/docs/test-components).
3
+ This library brings **Angular support** to [Playwright's **experimental
4
+ ** Component Testing](https://playwright.dev/docs/test-components).
4
5
 
5
6
  This will allow us to test our Angular components with Playwright without building the whole app and with more control.
6
7
 
7
8
  `@jscutlery/playwright-ct-angular` currently supports:
8
9
 
9
10
  - ✅ **Testing [Versatile Angular Components](https://marmicode.io/blog/versatile-angular)**
10
- - 🎛 **Passing type-safe inputs to the tested components**
11
+ - 🎛 **Passing type-safe inputs to the tested components**
11
12
  - 🎭 **Spying on component outputs in a type-safe fashion**
12
13
 
13
14
  https://user-images.githubusercontent.com/2674658/206226065-ba856329-dda7-43b1-9c28-4416b190f4d4.mp4
@@ -39,37 +40,19 @@ import { expect, test } from '@jscutlery/playwright-ct-angular';
39
40
  import { GreetingsComponent } from './greetings.component';
40
41
 
41
42
  test('<jc-greetings> should be polite', async ({ mount }) => {
42
- const locator = await mount(GreetingsComponent, {inputs: {name: 'Edouard'}});
43
+ const locator = await mount(GreetingsComponent, { props: { name: 'Edouard' } });
43
44
  expect(locator).toHaveText('👋 Hello Edouard!');
44
45
  });
45
46
  ```
46
47
 
47
- ## 🎭 Spying on outputs
48
-
49
- `mount()` will automatically create a `jest-mock`'s spy, subscribe to the outputs given through the `spyOutputs`
50
- option and return them in the `spies` property, in a type-safe way showing only the outputs that are spied on.
51
-
52
- Note that the `spyOutputs` is type-safe and will only allow properties that exist on the component.
53
-
54
- ```ts
55
- import { expect, test } from '@jscutlery/playwright-ct-angular';
56
- import { NameEditorComponent } from './name-editor.component';
57
-
58
- test('<jc-name-editor> should be polite', async ({ mount }) => {
59
- const locator = await mount(NameEditorComponent, {spyOutputs: ['nameChange']});
60
-
61
- await locator.getByLabel('Name').type('Edouard');
62
-
63
- expect(locator.spies.nameChange).lastCalledWith('Edouard');
64
- });
65
- ```
66
-
67
48
  ### Passing output callbacks
68
49
 
69
- We can also pass custom output callback functions for some extreme cases or if we want to use a custom spy implementation for example or just debug.
50
+ We can also pass custom output callback functions for some extreme cases or if we want to use a custom spy
51
+ implementation for example or just debug.
52
+
70
53
  ```ts
71
54
  await mount(NameEditorComponent, {
72
- outputs: {
55
+ on: {
73
56
  nameChange(name) {
74
57
  console.log(name);
75
58
  }
@@ -89,7 +72,7 @@ import { RecipeSearchTestContainer } from './recipe-search.test-container';
89
72
 
90
73
  test('...', async ({ mount }) => {
91
74
  await mount(RecipeSearchTestContainer, {
92
- inputs: {
75
+ props: {
93
76
  recipes: [
94
77
  beer,
95
78
  burger
@@ -140,24 +123,30 @@ We can also customize the shared `playwright/index.html` nearby.
140
123
  ### Specific Styles
141
124
 
142
125
  If we want to load some specific styles for a single test, we might prefer using a test container component:
126
+
143
127
  ```ts
144
128
  import styles from './some-styles.css';
129
+
145
130
  @Component({
146
131
  template: '<jc-greetings></jc-greetings>',
147
132
  encapsulation: ViewEncapsulation.None,
148
133
  styles: [styles]
149
134
  })
150
- class GreetingsTestContainer {}
135
+ class GreetingsTestContainer {
136
+ }
151
137
  ```
152
138
 
153
139
  ### Angular Material & Angular Libraries with styles
154
140
 
155
141
  As mentioned in [Versatile Angular Style Blog Post](https://marmicode.io/blog/versatile-angular),
156
- Angular Material and other Angular libraries might use a [Conditional "style" Export](https://nodejs.org/api/packages.html#conditional-exports)
142
+ Angular Material and other Angular libraries might use
143
+ a [Conditional "style" Export](https://nodejs.org/api/packages.html#conditional-exports)
157
144
  that allows us to import prebuilt styles
158
- _(Cf. [Angular Package Format](https://angular.io/guide/angular-package-format) [managing assets in a library](https://angular.io/guide/creating-libraries#managing-assets-in-a-library))_.
145
+ _(
146
+ Cf. [Angular Package Format](https://angular.io/guide/angular-package-format) [managing assets in a library](https://angular.io/guide/creating-libraries#managing-assets-in-a-library))_.
159
147
 
160
148
  In that case, we can add the following configuration to our `playwright-ct.config.ts`:
149
+
161
150
  ```ts
162
151
  const config: PlaywrightTestConfig = {
163
152
  // ...
@@ -174,11 +163,13 @@ const config: PlaywrightTestConfig = {
174
163
  ```
175
164
 
176
165
  ## More examples
166
+
177
167
  Cf. [/tests/playwright-ct-angular-wide/src](https://github.com/jscutlery/devkit/tree/main/tests/playwright-ct-angular-wide/src)
178
168
 
179
169
  # ⚠️ Known Limitations
180
170
 
181
- The way Playwright Component Testing works is different from the way things work with Karma, Jest, Vitest, Cypress etc...
171
+ The way Playwright Component Testing works is different from the way things work with Karma, Jest, Vitest, Cypress
172
+ etc...
182
173
  Playwright Component Testing tests run in a Node.js environment while the component is rendered in a browser.
183
174
 
184
175
  This causes a couple of limitations as we can't directly access the `TestBed's` or the component's internals,
@@ -187,13 +178,15 @@ and we can only exchange serializable data with the component.
187
178
  ## 🪄 The Magic Behind the Scenes
188
179
 
189
180
  The magical workaround behind the scenes is that at build time:
181
+
190
182
  1. Playwright analyses all the calls to `mount()`,
191
183
  2. it grabs the first parameter (the component class),
192
184
  3. replaces the component class with a unique string (constructed from the component class name and es-module),
193
185
  4. adds the component's es-module to Vite entrypoints,
194
186
  5. and finally creates a map matching each unique string to the right es-module.
195
187
 
196
- This way, when calling `mount()`, Playwright with communicate the unique string to the browser who will know which es-module to load.
188
+ This way, when calling `mount()`, Playwright with communicate the unique string to the browser who will know which
189
+ es-module to load.
197
190
 
198
191
  Cf. https://youtu.be/y3YxX4sFJbM
199
192
  Cf. https://github.com/microsoft/playwright/blob/cac67fb94f2c8a0ee82878054c39790e660f17ca/packages/playwright-test/src/tsxTransform.ts#L153
@@ -212,15 +205,17 @@ await mount(cmp);
212
205
 
213
206
  ```ts
214
207
  // 🛑 this won't work
215
- test(MyComponent.name, async ({ mount }) => {});
208
+ test(MyComponent.name, async ({ mount }) => {
209
+ });
216
210
  ```
217
211
 
218
212
  ### ...declare components in the same file.
219
213
 
220
214
  ```ts
221
215
  // 🛑 this won't work
222
- @Component({...})
223
- class GreetingsComponent {}
216
+ @Component({ ... })
217
+ class GreetingsComponent {
218
+ }
224
219
 
225
220
  test('<jc-greetings>', async ({ mount }) => {
226
221
  await mount(GreetingsComponent);
@@ -228,20 +223,25 @@ test('<jc-greetings>', async ({ mount }) => {
228
223
  ```
229
224
 
230
225
  ### ...pass any symbol other than a component class
226
+
231
227
  This makes the following impossible:
228
+
232
229
  - passing providers to the `mount()` function
233
230
  - passing modules to the `mount()` function (this is what currently makes the usage of mounting templates impossible)
234
231
  - use non-standalone components
235
232
 
236
233
  ### ...use non-[Versatile Angular Style](https://marmicode.io/blog/versatile-angular)
234
+
237
235
  We'll need a vite plugin to support traditional DI, external templates and stylesheets etc...
238
236
 
239
237
  ## 🔮 Future workarounds
240
238
 
241
239
  If you really can't make it to [Versatile Angular Style](https://marmicode.io/blog/versatile-angular),
242
- we can think of a couple of workarounds like using a vite plugin like **Brandon Roberts' [vite-plugin-angular](https://github.com/analogjs/analog/tree/main/packages/vite-plugin-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)**
243
242
 
244
- We could also implement some custom transform that registers the providers and import modules, like Playwright does for the component class.
243
+ We could also implement some custom transform that registers the providers and import modules, like Playwright does for
244
+ the component class.
245
245
 
246
246
  # Setup
247
247
 
@@ -265,6 +265,7 @@ yarn add -D @jscutlery/playwright-ct-angular @playwright/test # or npm install -
265
265
  ## 🛠 Configure
266
266
 
267
267
  Update `playwright-ct-config.ts` and replace:
268
+
268
269
  ```ts
269
270
  import type { PlaywrightTestConfig } from '@playwright/experimental-ct-react';
270
271
  import { devices } from '@playwright/experimental-ct-react';
package/hooks.esm.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./src/hooks";
package/index.cjs.js CHANGED
@@ -2,11 +2,9 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var mount = require('@playwright/experimental-ct-core/lib/mount');
6
- var playwright = require('@playwright/test');
7
- var jestMock = require('jest-mock');
8
- var experimentalCtCore = require('@playwright/experimental-ct-core');
9
- var path = require('path');
5
+ var path = require('node:path');
6
+ var playwrightCtCore = require('@playwright/experimental-ct-core');
7
+ var test$1 = require('@playwright/test');
10
8
 
11
9
  function _interopNamespace(e) {
12
10
  if (e && e.__esModule) return e;
@@ -26,87 +24,44 @@ function _interopNamespace(e) {
26
24
  return Object.freeze(n);
27
25
  }
28
26
 
29
- var playwright__namespace = /*#__PURE__*/_interopNamespace(playwright);
30
27
  var path__namespace = /*#__PURE__*/_interopNamespace(path);
28
+ var playwrightCtCore__namespace = /*#__PURE__*/_interopNamespace(playwrightCtCore);
31
29
 
32
- const mountTest = playwright__namespace.test.extend(mount.fixtures);
33
- const test = mountTest.extend({
34
- /**
35
- * Override mount to return output spies and enjoy type inference.
36
- * This allows this:
37
- *
38
- * const { spies } = mount(..., {outputs: {citySelect: jest.fn()}})
39
- *
40
- * instead of this:
41
- *
42
- * const citySelectSpy: jest.MockedFunction<(city: string) => void> = jest.fn();
43
- * mount(..., {outputs: {citySelect: citySelectSpy}});
44
- *
45
- * @hack we are willingly not using `async mount(...)` syntax here
46
- * because it would be transpiled to `_async(function mount() { ... }`
47
- * by Nx's default rollout config.
48
- * Playwright dynamically parses the given function so converting
49
- * async / await to a wrapped function would simply break the library causing
50
- * a `TypeError: mount is not a function` error.
51
- */
52
- mount({ mount, page }, use) {
53
- return use(async (component, options = {}) => {
54
- /* 1. Create a spy for each output in `spyOutputs`... */
55
- const spies = options.spyOutputs?.reduce((acc, key) => {
56
- return {
57
- ...acc,
58
- [key]: jestMock.fn(),
59
- };
60
- }, {}) ?? {};
61
- /* 2. ...then use it as an output... */
62
- await mount(component, {
63
- ...options,
64
- outputs: {
65
- ...options.outputs,
66
- ...spies,
67
- },
68
- });
69
- /* Grab a locator to the TestBed's root element.
70
- * We are loading a fresh new page for each test
71
- * so the id should always be "root0".
72
- * Otherwise, we'd have to hack our way and somehow grab all
73
- * the `#root*`, sort them and grab the last one.
74
- * As a matter of fact, we can't grab a reference to the fixture. */
75
- const locator = page.locator('#root0');
76
- /* Add spies to the returned locator. */
77
- return Object.assign(locator, { spies });
78
- });
79
- },
80
- });
81
- const expect = playwright__namespace.expect;
82
- const devices = playwright__namespace.devices;
83
-
84
- /**
85
- * Copyright (c) Microsoft Corporation.
86
- *
87
- * Licensed under the Apache License, Version 2.0 (the "License");
88
- * you may not use this file except in compliance with the License.
89
- * You may obtain a copy of the License at
90
- *
91
- * http://www.apache.org/licenses/LICENSE-2.0
92
- *
93
- * Unless required by applicable law or agreed to in writing, software
94
- * distributed under the License is distributed on an "AS IS" BASIS,
95
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
96
- * See the License for the specific language governing permissions and
97
- * limitations under the License.
98
- */
99
- function plugin() {
100
- /* Only fetch upon request to avoid resolution in workers. */
101
- const { createPlugin,
102
- // eslint-disable-next-line @typescript-eslint/no-var-requires
103
- } = require('@playwright/experimental-ct-core/lib/vitePlugin');
104
- // eslint-disable-next-line @typescript-eslint/no-empty-function
105
- return createPlugin(path__namespace.join(__dirname, 'register-source.mjs'), () => { });
106
- }
107
- const defineConfig = (config) => experimentalCtCore.defineConfig({ ...config, _plugins: [plugin] });
30
+ /* @hack `test` is not exported in the type definition of `@playwright/experimental-ct-core`. */
31
+ // @eslint-disable-next-line @typescript-eslint/no-explicit-any
32
+ const test = playwrightCtCore__namespace.test;
33
+ const defineConfig = (config, ...configs) => {
34
+ const original = playwrightCtCore__namespace.defineConfig({
35
+ ...config,
36
+ '@playwright/test': {
37
+ packageJSON: require.resolve('./package.json'),
38
+ },
39
+ '@playwright/experimental-ct-core': {
40
+ registerSourceFile: path__namespace.join(__dirname, 'register-source.mjs'),
41
+ },
42
+ }, ...configs);
43
+ /* @hack for some weird reason, if we do not require the babel plugin here,
44
+ * babel loads an empty object instead of the default exported function. */
45
+ require('@jscutlery/playwright-ct-angular/transform-angular');
46
+ return {
47
+ ...original,
48
+ '@playwright/test': {
49
+ ...original['@playwright/test'],
50
+ babelPlugins: [
51
+ ...original['@playwright/test'].babelPlugins,
52
+ [require.resolve('@jscutlery/playwright-ct-angular/transform-angular')],
53
+ ],
54
+ },
55
+ };
56
+ };
108
57
 
58
+ Object.defineProperty(exports, 'devices', {
59
+ enumerable: true,
60
+ get: function () { return test$1.devices; }
61
+ });
62
+ Object.defineProperty(exports, 'expect', {
63
+ enumerable: true,
64
+ get: function () { return test$1.expect; }
65
+ });
109
66
  exports.defineConfig = defineConfig;
110
- exports.devices = devices;
111
- exports.expect = expect;
112
67
  exports.test = test;
package/index.esm.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./src/index";
package/index.esm.js CHANGED
@@ -1,84 +1,33 @@
1
- import { fixtures } from '@playwright/experimental-ct-core/lib/mount';
2
- import * as playwright from '@playwright/test';
3
- import { fn } from 'jest-mock';
4
- import { defineConfig as defineConfig$1 } from '@playwright/experimental-ct-core';
5
- import * as path from 'path';
1
+ import * as path from 'node:path';
2
+ import * as playwrightCtCore from '@playwright/experimental-ct-core';
3
+ export { devices, expect } from '@playwright/test';
6
4
 
7
- const mountTest = playwright.test.extend(fixtures);
8
- const test = mountTest.extend({
9
- /**
10
- * Override mount to return output spies and enjoy type inference.
11
- * This allows this:
12
- *
13
- * const { spies } = mount(..., {outputs: {citySelect: jest.fn()}})
14
- *
15
- * instead of this:
16
- *
17
- * const citySelectSpy: jest.MockedFunction<(city: string) => void> = jest.fn();
18
- * mount(..., {outputs: {citySelect: citySelectSpy}});
19
- *
20
- * @hack we are willingly not using `async mount(...)` syntax here
21
- * because it would be transpiled to `_async(function mount() { ... }`
22
- * by Nx's default rollout config.
23
- * Playwright dynamically parses the given function so converting
24
- * async / await to a wrapped function would simply break the library causing
25
- * a `TypeError: mount is not a function` error.
26
- */
27
- mount({ mount, page }, use) {
28
- return use(async (component, options = {}) => {
29
- /* 1. Create a spy for each output in `spyOutputs`... */
30
- const spies = options.spyOutputs?.reduce((acc, key) => {
31
- return {
32
- ...acc,
33
- [key]: fn(),
34
- };
35
- }, {}) ?? {};
36
- /* 2. ...then use it as an output... */
37
- await mount(component, {
38
- ...options,
39
- outputs: {
40
- ...options.outputs,
41
- ...spies,
42
- },
43
- });
44
- /* Grab a locator to the TestBed's root element.
45
- * We are loading a fresh new page for each test
46
- * so the id should always be "root0".
47
- * Otherwise, we'd have to hack our way and somehow grab all
48
- * the `#root*`, sort them and grab the last one.
49
- * As a matter of fact, we can't grab a reference to the fixture. */
50
- const locator = page.locator('#root0');
51
- /* Add spies to the returned locator. */
52
- return Object.assign(locator, { spies });
53
- });
54
- },
55
- });
56
- const expect = playwright.expect;
57
- const devices = playwright.devices;
5
+ /* @hack `test` is not exported in the type definition of `@playwright/experimental-ct-core`. */
6
+ // @eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ const test = playwrightCtCore.test;
8
+ const defineConfig = (config, ...configs) => {
9
+ const original = playwrightCtCore.defineConfig({
10
+ ...config,
11
+ '@playwright/test': {
12
+ packageJSON: require.resolve('./package.json'),
13
+ },
14
+ '@playwright/experimental-ct-core': {
15
+ registerSourceFile: path.join(__dirname, 'register-source.mjs'),
16
+ },
17
+ }, ...configs);
18
+ /* @hack for some weird reason, if we do not require the babel plugin here,
19
+ * babel loads an empty object instead of the default exported function. */
20
+ require('@jscutlery/playwright-ct-angular/transform-angular');
21
+ return {
22
+ ...original,
23
+ '@playwright/test': {
24
+ ...original['@playwright/test'],
25
+ babelPlugins: [
26
+ ...original['@playwright/test'].babelPlugins,
27
+ [require.resolve('@jscutlery/playwright-ct-angular/transform-angular')],
28
+ ],
29
+ },
30
+ };
31
+ };
58
32
 
59
- /**
60
- * Copyright (c) Microsoft Corporation.
61
- *
62
- * Licensed under the Apache License, Version 2.0 (the "License");
63
- * you may not use this file except in compliance with the License.
64
- * You may obtain a copy of the License at
65
- *
66
- * http://www.apache.org/licenses/LICENSE-2.0
67
- *
68
- * Unless required by applicable law or agreed to in writing, software
69
- * distributed under the License is distributed on an "AS IS" BASIS,
70
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
71
- * See the License for the specific language governing permissions and
72
- * limitations under the License.
73
- */
74
- function plugin() {
75
- /* Only fetch upon request to avoid resolution in workers. */
76
- const { createPlugin,
77
- // eslint-disable-next-line @typescript-eslint/no-var-requires
78
- } = require('@playwright/experimental-ct-core/lib/vitePlugin');
79
- // eslint-disable-next-line @typescript-eslint/no-empty-function
80
- return createPlugin(path.join(__dirname, 'register-source.mjs'), () => { });
81
- }
82
- const defineConfig = (config) => defineConfig$1({ ...config, _plugins: [plugin] });
83
-
84
- export { defineConfig, devices, expect, test };
33
+ export { defineConfig, test };
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@jscutlery/playwright-ct-angular",
3
- "version": "0.2.2",
3
+ "version": "0.4.0",
4
4
  "dependencies": {
5
+ "@babel/core": "~7.23.0",
5
6
  "jest-mock": "^28.1.3",
6
7
  "rxjs": "^7.5.7",
7
8
  "vite": "^3.2.1"
@@ -33,6 +34,11 @@
33
34
  "module": "./register.esm.js",
34
35
  "import": "./register.cjs.mjs",
35
36
  "default": "./register.cjs.js"
37
+ },
38
+ "./transform-angular": {
39
+ "module": "./transform-angular.esm.js",
40
+ "import": "./transform-angular.cjs.mjs",
41
+ "default": "./transform-angular.cjs.js"
36
42
  }
37
43
  },
38
44
  "module": "./index.esm.js",
@@ -3,7 +3,7 @@ import 'zone.js';
3
3
  import { getTestBed, TestBed } from '@angular/core/testing';
4
4
  import {
5
5
  BrowserDynamicTestingModule,
6
- platformBrowserDynamicTesting,
6
+ platformBrowserDynamicTesting
7
7
  } from '@angular/platform-browser-dynamic/testing';
8
8
  import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
9
9
  import { Subscription } from 'rxjs';
@@ -32,29 +32,30 @@ export function pwRegister(components) {
32
32
  let subscription;
33
33
 
34
34
  globalThis.playwrightMount = async (component, rootElement, hooksConfig) => {
35
- const loader = _componentLoaderMap.get(component.type);
36
- const cmpType = await loader();
35
+ const cmpType = component.type;
37
36
 
38
37
  subscription = new Subscription();
39
38
 
40
39
  TestBed.configureTestingModule({
41
- imports: [BrowserAnimationsModule],
40
+ imports: [BrowserAnimationsModule]
42
41
  });
43
42
  const fixture = TestBed.createComponent(cmpType);
43
+ fixture.nativeElement.id = 'root';
44
44
 
45
45
  /* Set inputs. */
46
- for (const [name, value] of Object.entries(component.options.inputs ?? {})) {
46
+ for (const [name, value] of Object.entries(component.props ?? {})) {
47
47
  fixture.componentInstance[name] = value;
48
48
  }
49
49
 
50
50
  /* Subscribe to outputs. */
51
51
  for (const [name, callback] of Object.entries(
52
- component.options.outputs ?? {}
52
+ component.on ?? {}
53
53
  )) {
54
54
  subscription.add(fixture.componentInstance[name].subscribe(callback));
55
55
  }
56
56
 
57
57
  fixture.autoDetectChanges();
58
+ await fixture.whenStable();
58
59
  };
59
60
 
60
61
  globalThis.playwrightUnmount = async (rootElement) => {
package/register.cjs.js CHANGED
@@ -31,29 +31,30 @@ function pwRegister(components) {
31
31
  let subscription;
32
32
 
33
33
  globalThis.playwrightMount = async (component, rootElement, hooksConfig) => {
34
- const loader = _componentLoaderMap.get(component.type);
35
- const cmpType = await loader();
34
+ const cmpType = component.type;
36
35
 
37
36
  subscription = new rxjs.Subscription();
38
37
 
39
38
  testing.TestBed.configureTestingModule({
40
- imports: [animations.BrowserAnimationsModule],
39
+ imports: [animations.BrowserAnimationsModule]
41
40
  });
42
41
  const fixture = testing.TestBed.createComponent(cmpType);
42
+ fixture.nativeElement.id = 'root';
43
43
 
44
44
  /* Set inputs. */
45
- for (const [name, value] of Object.entries(component.options.inputs ?? {})) {
45
+ for (const [name, value] of Object.entries(component.props ?? {})) {
46
46
  fixture.componentInstance[name] = value;
47
47
  }
48
48
 
49
49
  /* Subscribe to outputs. */
50
50
  for (const [name, callback] of Object.entries(
51
- component.options.outputs ?? {}
51
+ component.on ?? {}
52
52
  )) {
53
53
  subscription.add(fixture.componentInstance[name].subscribe(callback));
54
54
  }
55
55
 
56
56
  fixture.autoDetectChanges();
57
+ await fixture.whenStable();
57
58
  };
58
59
 
59
60
  globalThis.playwrightUnmount = async (rootElement) => {
@@ -0,0 +1,2 @@
1
+ export * from "./src/register";
2
+ export { default } from "./src/register";
package/register.esm.js CHANGED
@@ -29,29 +29,30 @@ function pwRegister(components) {
29
29
  let subscription;
30
30
 
31
31
  globalThis.playwrightMount = async (component, rootElement, hooksConfig) => {
32
- const loader = _componentLoaderMap.get(component.type);
33
- const cmpType = await loader();
32
+ const cmpType = component.type;
34
33
 
35
34
  subscription = new Subscription();
36
35
 
37
36
  TestBed.configureTestingModule({
38
- imports: [BrowserAnimationsModule],
37
+ imports: [BrowserAnimationsModule]
39
38
  });
40
39
  const fixture = TestBed.createComponent(cmpType);
40
+ fixture.nativeElement.id = 'root';
41
41
 
42
42
  /* Set inputs. */
43
- for (const [name, value] of Object.entries(component.options.inputs ?? {})) {
43
+ for (const [name, value] of Object.entries(component.props ?? {})) {
44
44
  fixture.componentInstance[name] = value;
45
45
  }
46
46
 
47
47
  /* Subscribe to outputs. */
48
48
  for (const [name, callback] of Object.entries(
49
- component.options.outputs ?? {}
49
+ component.on ?? {}
50
50
  )) {
51
51
  subscription.add(fixture.componentInstance[name].subscribe(callback));
52
52
  }
53
53
 
54
54
  fixture.autoDetectChanges();
55
+ await fixture.whenStable();
55
56
  };
56
57
 
57
58
  globalThis.playwrightUnmount = async (rootElement) => {
package/src/index.d.ts CHANGED
@@ -1,19 +1,26 @@
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
- export { devices, expect, test } from './lib/playwright-ct-angular';
17
- export type { ComponentFixtures } from './lib/playwright-ct-angular';
18
- import { defineConfig as originalDefineConfig } from '@playwright/experimental-ct-core';
19
- export declare const defineConfig: typeof originalDefineConfig;
1
+ import { Type } from '@angular/core';
2
+ import type { PlaywrightTestConfig, TestType } from '@playwright/experimental-ct-core';
3
+ import * as playwright from '@playwright/test';
4
+ import type { Observable } from 'rxjs';
5
+ export type { PlaywrightTestConfig };
6
+ export { expect, devices } from '@playwright/test';
7
+ export interface ComponentFixtures {
8
+ mount<COMPONENT>(component: Type<COMPONENT>, options?: MountOptions<COMPONENT>): Promise<MountResult<COMPONENT>>;
9
+ }
10
+ export interface MountOptions<COMPONENT> {
11
+ props?: Partial<COMPONENT>;
12
+ on?: Outputs<COMPONENT>;
13
+ }
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
+ export interface MountResult<COMPONENT> extends playwright.Locator {
18
+ unmount(): Promise<void>;
19
+ update(options: {
20
+ props?: Partial<COMPONENT>;
21
+ on?: Outputs<COMPONENT>;
22
+ }): Promise<void>;
23
+ }
24
+ type Emitted<OBSERVABLE> = OBSERVABLE extends Observable<infer EMITTED> ? EMITTED : OBSERVABLE;
25
+ export declare const test: TestType<ComponentFixtures>;
26
+ export declare const defineConfig: (config: any, ...configs: any[]) => PlaywrightTestConfig;