@jscutlery/playwright-ct-angular 0.4.0 → 0.5.1
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 +97 -64
- package/{src/register.d.ts → cli.js} +6 -3
- package/hooks.cjs.js +4 -19
- package/hooks.esm.js +4 -19
- package/package.json +7 -13
- package/register-source.mjs +120 -41
- package/src/hooks.d.ts +9 -2
- package/src/index.d.ts +26 -13
- package/register.cjs.d.ts +0 -2
- package/register.cjs.default.js +0 -1
- package/register.cjs.js +0 -89
- package/register.cjs.mjs +0 -2
- package/register.esm.d.ts +0 -2
- package/register.esm.js +0 -87
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
|
|
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
|
|
11
|
-
- 🎛 **
|
|
12
|
-
-
|
|
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
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
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,
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
this.
|
|
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
|
|
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
|
|
121
|
-
|
|
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
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
186
|
-
5. and finally creates a map matching each unique string to the right
|
|
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
|
|
189
|
-
|
|
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('
|
|
257
|
+
test('should work', async ({ mount }) => {
|
|
221
258
|
await mount(GreetingsComponent);
|
|
222
259
|
});
|
|
223
260
|
```
|
|
224
261
|
|
|
225
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
## 📦 Install
|
|
264
|
+
### 1. Install
|
|
249
265
|
|
|
250
266
|
```sh
|
|
251
267
|
# You can run this command in an existing workspace.
|
|
252
|
-
|
|
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
|
-
|
|
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
|
-
|
|
282
|
+
### 2. Configure
|
|
266
283
|
|
|
267
|
-
Update `playwright-ct
|
|
284
|
+
- Update `playwright-ct.config.ts` and replace:
|
|
268
285
|
|
|
269
286
|
```ts
|
|
270
|
-
import
|
|
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
|
|
278
|
-
import {
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
9
|
+
function beforeMount(callback) {
|
|
25
10
|
__pw_hooks_before_mount.push(callback);
|
|
26
|
-
}
|
|
27
|
-
|
|
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
|
-
|
|
5
|
+
function beforeMount(callback) {
|
|
21
6
|
__pw_hooks_before_mount.push(callback);
|
|
22
|
-
}
|
|
23
|
-
|
|
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.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"dependencies": {
|
|
5
|
-
"@babel/core": "
|
|
6
|
-
"
|
|
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",
|
package/register-source.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
*
|
|
30
|
-
* in order to unsubscribe when component is unmounted.
|
|
70
|
+
* @param {ComponentInfo} component
|
|
31
71
|
*/
|
|
32
|
-
|
|
72
|
+
async function __pwRenderComponent(component) {
|
|
73
|
+
/** @type {import('@angular/core').Type<unknown>} */
|
|
74
|
+
let componentClass;
|
|
33
75
|
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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: [
|
|
94
|
+
imports: [componentClass],
|
|
95
|
+
providers: component.providers
|
|
41
96
|
});
|
|
42
|
-
|
|
97
|
+
|
|
98
|
+
const fixture = TestBed.createComponent(componentClass);
|
|
43
99
|
fixture.nativeElement.id = 'root';
|
|
44
100
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
101
|
+
__pwUpdateProps(fixture, component);
|
|
102
|
+
__pwUpdateEvents(fixture, component.on);
|
|
103
|
+
|
|
104
|
+
return fixture;
|
|
105
|
+
}
|
|
49
106
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
133
|
+
/* Store new subscription. */
|
|
134
|
+
outputSubscriptionRecord[name] = fixture.componentInstance[
|
|
135
|
+
name
|
|
136
|
+
].subscribe((/** @type {unknown} */ event) => listener(event));
|
|
137
|
+
}
|
|
66
138
|
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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 type { EnvironmentProviders, EventEmitter, 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>(
|
|
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
|
-
|
|
12
|
-
|
|
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?:
|
|
21
|
-
on?:
|
|
24
|
+
props?: Inputs<COMPONENT>;
|
|
25
|
+
on?: OutputListeners<COMPONENT>;
|
|
22
26
|
}): Promise<void>;
|
|
23
27
|
}
|
|
24
|
-
type
|
|
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<unknown> ? PROPERTY : never]: (value: Emitted<COMPONENT[PROPERTY]>) => void;
|
|
33
|
+
};
|
|
34
|
+
type Subscribable<T> = {
|
|
35
|
+
subscribe(next: (value: T) => void): void;
|
|
36
|
+
} | EventEmitter<T>;
|
|
37
|
+
type Emitted<SUBSCRIBABLE> = SUBSCRIBABLE extends Subscribable<infer EMITTED> ? EMITTED : SUBSCRIBABLE;
|
|
25
38
|
export declare const test: TestType<ComponentFixtures>;
|
|
26
|
-
export declare const defineConfig:
|
|
39
|
+
export declare const defineConfig: typeof playwrightCtCore.defineConfig;
|
package/register.cjs.d.ts
DELETED
package/register.cjs.default.js
DELETED
|
@@ -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
package/register.esm.d.ts
DELETED
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 };
|