vitest-browser-angular 0.2.0 → 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 +287 -25
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/pure-dwpG9XN0.mjs +93 -0
- package/dist/pure-iXV3gO_T.d.mts +190 -0
- package/dist/pure.d.mts +2 -2
- package/dist/pure.mjs +1 -1
- package/dist/{setup-N1YCbpUn.mjs → setup-DWV3P7kE.mjs} +450 -3316
- package/dist/setup-zones.mjs +1009 -3890
- package/dist/setup.mjs +1 -1
- package/package.json +12 -10
- package/dist/pure-B-Ur9eBk.mjs +0 -42
- package/dist/pure-WnJuXVlP.d.mts +0 -31
package/README.md
CHANGED
|
@@ -1,38 +1,188 @@
|
|
|
1
1
|
# vitest-browser-angular
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This community package renders Angular components in [Vitest Browser Mode](https://vitest.dev/guide/browser).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
```ts
|
|
6
|
+
import { Component, input } from '@angular/core';
|
|
7
|
+
import { expect, test } from 'vitest';
|
|
8
|
+
import { render } from 'vitest-browser-angular';
|
|
9
|
+
|
|
10
|
+
@Component({
|
|
11
|
+
selector: 'app-hello-world',
|
|
12
|
+
template: '<h1>Hello, {{ name() }}!</h1>',
|
|
13
|
+
})
|
|
14
|
+
export class HelloWorld {
|
|
15
|
+
name = input.required<string>();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
test('renders name', async () => {
|
|
19
|
+
const { locator } = await render(HelloWorld, {
|
|
20
|
+
inputs: {
|
|
21
|
+
name: 'World',
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
await expect.element(locator).toHaveTextContent('Hello, World!');
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Setup
|
|
30
|
+
|
|
31
|
+
There are currently two ways to set up Vitest for Angular:
|
|
32
|
+
|
|
33
|
+
- Analog's [`vitest-angular` plugin](https://analogjs.org/docs/features/testing/vitest) _(community)_.
|
|
34
|
+
- Angular CLI's [`unit-test` builder](https://angular.dev/guide/testing#configuration) _(official)_.
|
|
35
|
+
|
|
36
|
+
While Angular CLI's `unit-test` builder is the official way to set up Vitest for Angular, it has some [limitations](https://analogjs.org/docs/features/testing/overview#angular-support-for-vitest). Analog's `vitest-angular` plugin provides more Vitest features and greater flexibility.
|
|
37
|
+
|
|
38
|
+
### Setup with Analog Plugin
|
|
39
|
+
|
|
40
|
+
1. Set up Vitest
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
npm add -D @analogjs/platform vitest-browser-angular
|
|
44
|
+
|
|
45
|
+
ng g @analogjs/platform:setup-vitest
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
2. Activate browser mode in the generated Vitest configuration by following the [browser mode configuration instructions](https://vitest.dev/guide/browser/#configuration).
|
|
49
|
+
|
|
50
|
+
### Setup with Angular CLI
|
|
51
|
+
|
|
52
|
+
1. Configure your Angular project to use the `@angular/build:unit-test` builder, and add the browsers of your choice.
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
...,
|
|
57
|
+
"projects": {
|
|
58
|
+
"my-app": {
|
|
59
|
+
...,
|
|
60
|
+
"architect": {
|
|
61
|
+
"test": {
|
|
62
|
+
"builder": "@angular/build:unit-test",
|
|
63
|
+
"options": {
|
|
64
|
+
"browsers": ["Chromium", "Firefox", "Webkit"]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
_Since Angular v21, Vitest is the default runner so you don't need to set the `runner` option._
|
|
74
|
+
|
|
75
|
+
2. Install the browser provider of your choice using `ng add`
|
|
6
76
|
|
|
7
77
|
```sh
|
|
8
|
-
|
|
78
|
+
# With Playwright
|
|
79
|
+
ng add @vitest/browser-playwright
|
|
80
|
+
|
|
81
|
+
# or with WebdriverIO
|
|
82
|
+
ng add @vitest/browser-webdriverio
|
|
9
83
|
```
|
|
10
84
|
|
|
11
|
-
|
|
85
|
+
3. Add the `vitest-browser-angular` package to your project.
|
|
12
86
|
|
|
13
|
-
|
|
87
|
+
```sh
|
|
88
|
+
npm add -D vitest-browser-angular
|
|
89
|
+
```
|
|
14
90
|
|
|
15
|
-
|
|
91
|
+
## Zone.js VS Zoneless Setup
|
|
92
|
+
|
|
93
|
+
Angular CLI will automatically set up the test environment for you depending on the presence of `zone.js` in your project's polyfills.
|
|
94
|
+
|
|
95
|
+
When using the Analog plugin, you can control the behavior using the `zoneless` option of `setupTestBed()` in `test-setup.ts`:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import { setupTestBed } from '@analogjs/vitest-angular/setup-testbed';
|
|
99
|
+
|
|
100
|
+
setupTestBed({
|
|
101
|
+
zoneless: true,
|
|
102
|
+
});
|
|
103
|
+
```
|
|
16
104
|
|
|
17
105
|
For detailed setup instructions for both Zone.js and Zoneless configurations, please refer to the [Analog Vitest documentation](https://analogjs.org/docs/features/testing/vitest).
|
|
18
106
|
|
|
107
|
+
## Component Preview
|
|
108
|
+
|
|
109
|
+
To preview, debug and interact with a component in the browser after the test, you can prevent Angular from destroying it.
|
|
110
|
+
|
|
111
|
+
In Angular CLI, enable this using the `--debug` option.
|
|
112
|
+
|
|
113
|
+
With the Analog plugin, enable this using the `browserMode` option of `setupTestBed()` in `test-setup.ts`:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { setupTestBed } from '@analogjs/vitest-angular/setup-testbed';
|
|
117
|
+
|
|
118
|
+
setupTestBed({
|
|
119
|
+
browserMode: true,
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
19
123
|
## Usage
|
|
20
124
|
|
|
125
|
+
### Basic Example
|
|
126
|
+
|
|
127
|
+
The `render` function supports two query patterns:
|
|
128
|
+
|
|
21
129
|
```ts
|
|
22
130
|
import { test, expect } from 'vitest';
|
|
23
131
|
import { render } from 'vitest-browser-angular';
|
|
24
132
|
|
|
25
133
|
@Component({
|
|
26
|
-
template:
|
|
134
|
+
template: ` <h1>Welcome</h1> `,
|
|
27
135
|
})
|
|
28
|
-
export class
|
|
29
|
-
|
|
30
|
-
|
|
136
|
+
export class MyComponent {}
|
|
137
|
+
|
|
138
|
+
test('query elements', async () => {
|
|
139
|
+
// Pattern 1: Use locator to query within the component element
|
|
140
|
+
const { locator } = await render(MyComponent);
|
|
141
|
+
await expect.element(locator.getByText('Welcome')).toBeVisible();
|
|
142
|
+
|
|
143
|
+
// Pattern 2: Use screen to query from document.body (useful for portals/overlays)
|
|
144
|
+
const screen = await render(MyComponent);
|
|
145
|
+
await expect.element(screen.getByText('Welcome')).toBeVisible();
|
|
146
|
+
await expect.element(screen.getByText('Some Popover Content')).toBeVisible();
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Query Methods
|
|
151
|
+
|
|
152
|
+
Both `locator` and `screen` provide the following query methods:
|
|
153
|
+
|
|
154
|
+
- `getByRole` - Locate by ARIA role and accessible name
|
|
155
|
+
- `getByText` - Locate by text content
|
|
156
|
+
- `getByLabelText` - Locate by associated label text
|
|
157
|
+
- `getByPlaceholder` - Locate by placeholder text
|
|
158
|
+
- `getByAltText` - Locate by alt text (images)
|
|
159
|
+
- `getByTitle` - Locate by title attribute
|
|
160
|
+
- `getByTestId` - Locate by data-testid attribute
|
|
161
|
+
|
|
162
|
+
**When to use which pattern:**
|
|
163
|
+
|
|
164
|
+
- **`locator`**: (full name: "Component Locator") - queries are scoped to the component's host element. Best for most component tests.
|
|
165
|
+
- **`screen`**: Queries start from `baseElement` (defaults to `document.body`). Use when testing components that render content outside their host element (modals, tooltips, portals).
|
|
31
166
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
167
|
+
### Container Element
|
|
168
|
+
|
|
169
|
+
Access the component's host element directly via `container` (shortcut for `fixture.nativeElement`):
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
const { container, locator } = await render(MyComponent);
|
|
173
|
+
expect(container).toBe(locator.element());
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Base Element
|
|
177
|
+
|
|
178
|
+
Customize the root element for screen queries (useful for portal/overlay testing):
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
const customContainer = document.querySelector('#modal-root');
|
|
182
|
+
const screen = await render(ModalComponent, {
|
|
183
|
+
baseElement: customContainer,
|
|
35
184
|
});
|
|
185
|
+
// screen queries now start from customContainer instead of document.body
|
|
36
186
|
```
|
|
37
187
|
|
|
38
188
|
## Inputs
|
|
@@ -52,15 +202,15 @@ export class ProductComponent {
|
|
|
52
202
|
}
|
|
53
203
|
|
|
54
204
|
test('render with inputs', async () => {
|
|
55
|
-
const
|
|
205
|
+
const screen = await render(ProductComponent, {
|
|
56
206
|
inputs: {
|
|
57
207
|
name: 'Laptop',
|
|
58
208
|
price: 1299.99,
|
|
59
209
|
},
|
|
60
210
|
});
|
|
61
211
|
|
|
62
|
-
await expect.element(
|
|
63
|
-
await expect.element(
|
|
212
|
+
await expect.element(screen.getByText('Laptop')).toBeVisible();
|
|
213
|
+
await expect.element(screen.getByText(/Price: \$1299\.99/)).toBeVisible();
|
|
64
214
|
});
|
|
65
215
|
```
|
|
66
216
|
|
|
@@ -91,12 +241,12 @@ import { RouterLink, RouterOutlet } from '@angular/router';
|
|
|
91
241
|
export class RoutedComponent {}
|
|
92
242
|
|
|
93
243
|
test('render with simple routing', async () => {
|
|
94
|
-
const
|
|
244
|
+
const screen = await render(RoutedComponent, {
|
|
95
245
|
withRouting: true,
|
|
96
246
|
});
|
|
97
247
|
|
|
98
|
-
await expect.element(
|
|
99
|
-
await expect.element(
|
|
248
|
+
await expect.element(screen.getByText('Home')).toBeVisible();
|
|
249
|
+
await expect.element(screen.getByText('About')).toBeVisible();
|
|
100
250
|
});
|
|
101
251
|
```
|
|
102
252
|
|
|
@@ -143,18 +293,128 @@ const routes: Routes = [
|
|
|
143
293
|
];
|
|
144
294
|
|
|
145
295
|
test('render with route configuration', async () => {
|
|
146
|
-
const {
|
|
296
|
+
const { locator, routerHarness, router } = await render(AppComponent, {
|
|
147
297
|
withRouting: {
|
|
148
298
|
routes,
|
|
149
299
|
initialRoute: '/home',
|
|
150
300
|
},
|
|
151
301
|
});
|
|
152
302
|
|
|
153
|
-
await expect.element(
|
|
303
|
+
await expect.element(locator).toHaveTextContent('Home Page');
|
|
304
|
+
|
|
305
|
+
// Navigate programmatically (prefer routerHarness over router)
|
|
306
|
+
await routerHarness.navigateByUrl('/about');
|
|
307
|
+
await expect.element(locator).toHaveTextContent('About Page');
|
|
308
|
+
|
|
309
|
+
// Use router to inspect state
|
|
310
|
+
expect(router.url).toBe('/about');
|
|
311
|
+
});
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Route Params
|
|
315
|
+
|
|
316
|
+
When rendering a routed component, `componentClassInstance` provides access to the actual component instance with full routing context:
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
import { Component, inject } from '@angular/core';
|
|
320
|
+
import { ActivatedRoute, Routes } from '@angular/router';
|
|
321
|
+
|
|
322
|
+
@Component({
|
|
323
|
+
template: '<h1>User: {{ userId }}</h1>',
|
|
324
|
+
})
|
|
325
|
+
export class UserComponent {
|
|
326
|
+
private route = inject(ActivatedRoute);
|
|
327
|
+
userId = this.route.snapshot.params['id'];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
test('access route params', async () => {
|
|
331
|
+
const routes: Routes = [{ path: 'user/:id', component: UserComponent }];
|
|
332
|
+
|
|
333
|
+
const { componentClassInstance } = await render(UserComponent, {
|
|
334
|
+
withRouting: {
|
|
335
|
+
routes,
|
|
336
|
+
initialRoute: '/user/42',
|
|
337
|
+
},
|
|
338
|
+
});
|
|
154
339
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
340
|
+
expect(componentClassInstance.userId).toBe('42');
|
|
341
|
+
});
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Passing Inputs via Route Data
|
|
345
|
+
|
|
346
|
+
By default, `withComponentInputBinding()` is enabled, which automatically binds route `data`, route params, and query params to matching component inputs. This works with both signal inputs (`input()`) and `@Input()` decorators:
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
import { Component, input } from '@angular/core';
|
|
350
|
+
import { Routes } from '@angular/router';
|
|
351
|
+
|
|
352
|
+
@Component({
|
|
353
|
+
template: `
|
|
354
|
+
<h2>{{ name() }}</h2>
|
|
355
|
+
<p>Age: {{ age() }}</p>
|
|
356
|
+
<p>Role: {{ role() }}</p>
|
|
357
|
+
`,
|
|
358
|
+
})
|
|
359
|
+
export class ProfileComponent {
|
|
360
|
+
name = input('Guest');
|
|
361
|
+
age = input(0);
|
|
362
|
+
role = input('user');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
test('pass inputs via route data', async () => {
|
|
366
|
+
const routes: Routes = [
|
|
367
|
+
{
|
|
368
|
+
path: 'profile',
|
|
369
|
+
component: ProfileComponent,
|
|
370
|
+
data: {
|
|
371
|
+
name: 'Jane Doe',
|
|
372
|
+
age: 30,
|
|
373
|
+
role: 'admin',
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
];
|
|
377
|
+
|
|
378
|
+
const { locator, componentClassInstance } = await render(ProfileComponent, {
|
|
379
|
+
withRouting: {
|
|
380
|
+
routes,
|
|
381
|
+
initialRoute: '/profile',
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// Inputs are automatically bound from route data
|
|
386
|
+
expect(componentClassInstance.name()).toBe('Jane Doe');
|
|
387
|
+
expect(componentClassInstance.age()).toBe(30);
|
|
388
|
+
expect(componentClassInstance.role()).toBe('admin');
|
|
389
|
+
|
|
390
|
+
await expect.element(locator.getByText('Jane Doe')).toBeVisible();
|
|
391
|
+
});
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Disabling Input Binding
|
|
395
|
+
|
|
396
|
+
If you need to manually handle route data via `ActivatedRoute` instead of automatic input binding, use `disableInputBinding`:
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
test('disable automatic input binding', async () => {
|
|
400
|
+
const routes: Routes = [
|
|
401
|
+
{
|
|
402
|
+
path: 'profile',
|
|
403
|
+
component: ProfileComponent,
|
|
404
|
+
data: { name: 'Jane Doe' },
|
|
405
|
+
},
|
|
406
|
+
];
|
|
407
|
+
|
|
408
|
+
const { componentClassInstance } = await render(ProfileComponent, {
|
|
409
|
+
withRouting: {
|
|
410
|
+
routes,
|
|
411
|
+
initialRoute: '/profile',
|
|
412
|
+
disableInputBinding: true, // Inputs will NOT be bound from route data
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// Inputs retain their default values
|
|
417
|
+
expect(componentClassInstance.name()).toBe('Guest');
|
|
158
418
|
});
|
|
159
419
|
```
|
|
160
420
|
|
|
@@ -172,11 +432,13 @@ export class HelloWorldComponent {
|
|
|
172
432
|
}
|
|
173
433
|
|
|
174
434
|
test('renders component with service provider', async () => {
|
|
175
|
-
const
|
|
435
|
+
const screen = await render(ServiceConsumerComponent, {
|
|
176
436
|
componentProviders: [
|
|
177
437
|
{ provide: GreetingService, useClass: FakeGreetingService },
|
|
178
438
|
],
|
|
179
439
|
});
|
|
440
|
+
|
|
441
|
+
await expect.element(screen.getByText('Fake Greeting')).toBeVisible();
|
|
180
442
|
});
|
|
181
443
|
```
|
|
182
444
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as RenderResult, c as render, i as RenderFn, n as Inputs, r as RenderConfig, s as cleanup } from "./pure-iXV3gO_T.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/index.d.ts
|
|
4
4
|
declare module "vitest/browser" {
|
package/dist/index.mjs
CHANGED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { page, utils } from "vitest/browser";
|
|
2
|
+
import { inputBinding } from "@angular/core";
|
|
3
|
+
import { TestBed, ɵgetCleanupHook } from "@angular/core/testing";
|
|
4
|
+
import { Router, provideRouter, withComponentInputBinding } from "@angular/router";
|
|
5
|
+
import { RouterTestingHarness } from "@angular/router/testing";
|
|
6
|
+
|
|
7
|
+
//#region src/pure.ts
|
|
8
|
+
const { debug, getElementLocatorSelectors } = utils;
|
|
9
|
+
/**
|
|
10
|
+
* Renders an Angular component for testing with Vitest Browser Mode.
|
|
11
|
+
*
|
|
12
|
+
* @param componentClass - The component class to render
|
|
13
|
+
* @param options - Configuration options for rendering
|
|
14
|
+
* @returns A promise that resolves to the render result with locators and component access
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* // Basic render
|
|
19
|
+
* const { locator } = await render(MyComponent);
|
|
20
|
+
* await expect.element(locator.getByText('Hello')).toBeVisible();
|
|
21
|
+
*
|
|
22
|
+
* // With inputs
|
|
23
|
+
* const { componentClassInstance } = await render(UserComponent, {
|
|
24
|
+
* inputs: { name: 'John', age: 30 },
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* // With routing and route data as inputs
|
|
28
|
+
* const { router } = await render(ProfileComponent, {
|
|
29
|
+
* withRouting: {
|
|
30
|
+
* routes: [{ path: 'profile', component: ProfileComponent, data: { userId: '42' } }],
|
|
31
|
+
* initialRoute: '/profile',
|
|
32
|
+
* },
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
async function render(componentClass, options) {
|
|
37
|
+
const imports = [componentClass, ...options?.imports || []];
|
|
38
|
+
const providers = [...options?.providers || []];
|
|
39
|
+
const baseElement = options?.baseElement || document.body;
|
|
40
|
+
if (options?.withRouting && options?.inputs) console.warn("[vitest-browser-angular] Using `inputs` with `withRouting` is not supported. Inputs cannot be passed directly to routed components. Consider passing data via route params, query params, or route data instead.");
|
|
41
|
+
const routingConfig = options?.withRouting ? typeof options.withRouting === "boolean" ? {
|
|
42
|
+
routes: [{
|
|
43
|
+
path: "**",
|
|
44
|
+
component: componentClass
|
|
45
|
+
}],
|
|
46
|
+
initialRoute: "/"
|
|
47
|
+
} : options.withRouting : void 0;
|
|
48
|
+
if (routingConfig) if (routingConfig.disableInputBinding) providers.push(provideRouter(routingConfig.routes));
|
|
49
|
+
else providers.push(provideRouter(routingConfig.routes, withComponentInputBinding()));
|
|
50
|
+
TestBed.configureTestingModule({
|
|
51
|
+
imports,
|
|
52
|
+
providers
|
|
53
|
+
});
|
|
54
|
+
if (options?.componentProviders) TestBed.overrideComponent(componentClass, { add: { providers: options.componentProviders } });
|
|
55
|
+
let fixture;
|
|
56
|
+
let container;
|
|
57
|
+
let componentClassInstance;
|
|
58
|
+
let routerHarness;
|
|
59
|
+
let router;
|
|
60
|
+
if (routingConfig) {
|
|
61
|
+
routerHarness = await RouterTestingHarness.create(routingConfig.initialRoute);
|
|
62
|
+
router = TestBed.inject(Router);
|
|
63
|
+
fixture = routerHarness.fixture;
|
|
64
|
+
container = routerHarness.routeNativeElement;
|
|
65
|
+
componentClassInstance = routerHarness.routeDebugElement?.componentInstance;
|
|
66
|
+
} else {
|
|
67
|
+
const bindings = Object.entries(options?.inputs ?? {}).map(([key, value]) => inputBinding(key, () => value));
|
|
68
|
+
fixture = TestBed.createComponent(componentClass, { bindings });
|
|
69
|
+
container = fixture.nativeElement;
|
|
70
|
+
componentClassInstance = fixture.componentInstance;
|
|
71
|
+
}
|
|
72
|
+
fixture.autoDetectChanges();
|
|
73
|
+
await fixture.whenStable();
|
|
74
|
+
const locator = page.elementLocator(container);
|
|
75
|
+
return {
|
|
76
|
+
baseElement,
|
|
77
|
+
container,
|
|
78
|
+
fixture,
|
|
79
|
+
debug: (el = baseElement, maxLength, opts) => debug(el, maxLength, opts),
|
|
80
|
+
componentClassInstance,
|
|
81
|
+
component: locator,
|
|
82
|
+
locator,
|
|
83
|
+
routerHarness,
|
|
84
|
+
router,
|
|
85
|
+
...getElementLocatorSelectors(baseElement)
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function cleanup(shouldTeardown = false) {
|
|
89
|
+
return ɵgetCleanupHook(shouldTeardown)();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
//#endregion
|
|
93
|
+
export { render as n, cleanup as t };
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { Locator, LocatorSelectors, PrettyDOMOptions } from "vitest/browser";
|
|
2
|
+
import { EnvironmentProviders, InputSignal, Provider, Type } from "@angular/core";
|
|
3
|
+
import { ComponentFixture } from "@angular/core/testing";
|
|
4
|
+
import { Router, Routes } from "@angular/router";
|
|
5
|
+
import { RouterTestingHarness } from "@angular/router/testing";
|
|
6
|
+
|
|
7
|
+
//#region src/pure.d.ts
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Configuration options for rendering components with Angular Router support.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* // Basic routing with route params
|
|
15
|
+
* await render(UserComponent, {
|
|
16
|
+
* withRouting: {
|
|
17
|
+
* routes: [{ path: 'user/:id', component: UserComponent }],
|
|
18
|
+
* initialRoute: '/user/42',
|
|
19
|
+
* },
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* // Passing inputs via route data (uses withComponentInputBinding)
|
|
23
|
+
* await render(ProfileComponent, {
|
|
24
|
+
* withRouting: {
|
|
25
|
+
* routes: [{
|
|
26
|
+
* path: 'profile',
|
|
27
|
+
* component: ProfileComponent,
|
|
28
|
+
* data: { name: 'John', age: 30 },
|
|
29
|
+
* }],
|
|
30
|
+
* initialRoute: '/profile',
|
|
31
|
+
* },
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
interface RoutingConfig {
|
|
36
|
+
/**
|
|
37
|
+
* The route configuration to use. These routes are passed to `provideRouter()`.
|
|
38
|
+
*/
|
|
39
|
+
routes: Routes;
|
|
40
|
+
/**
|
|
41
|
+
* The initial route to navigate to after setting up the router.
|
|
42
|
+
* This triggers navigation and activates the matching route's component.
|
|
43
|
+
*
|
|
44
|
+
* @example '/user/42' or '/profile?tab=settings'
|
|
45
|
+
*/
|
|
46
|
+
initialRoute?: string;
|
|
47
|
+
/**
|
|
48
|
+
* When `true`, disables Angular's `withComponentInputBinding()` feature.
|
|
49
|
+
*
|
|
50
|
+
* By default, `withComponentInputBinding()` is enabled, which automatically
|
|
51
|
+
* binds route params, query params, and route data to matching component inputs.
|
|
52
|
+
*
|
|
53
|
+
* Set this to `true` if you want to manually handle route data via `ActivatedRoute`.
|
|
54
|
+
*
|
|
55
|
+
* @default false
|
|
56
|
+
*/
|
|
57
|
+
disableInputBinding?: boolean;
|
|
58
|
+
}
|
|
59
|
+
type Inputs<CMP_TYPE extends Type<unknown>> = Partial<{ [PROP in keyof InstanceType<CMP_TYPE> as InstanceType<CMP_TYPE>[PROP] extends InputSignal<unknown> ? PROP : never]: InstanceType<CMP_TYPE>[PROP] extends InputSignal<infer VALUE> ? VALUE : never }>;
|
|
60
|
+
/**
|
|
61
|
+
* Options for rendering a component with `render()`.
|
|
62
|
+
*/
|
|
63
|
+
interface ComponentRenderOptions<CMP_TYPE extends Type<unknown> = Type<unknown>> {
|
|
64
|
+
/** The base element to render into. Defaults to `document.body`. */
|
|
65
|
+
baseElement?: HTMLElement;
|
|
66
|
+
/**
|
|
67
|
+
* Input values to pass to the component.
|
|
68
|
+
*
|
|
69
|
+
* Note: When using `withRouting`, inputs cannot be passed directly.
|
|
70
|
+
* Use route `data` instead.
|
|
71
|
+
*/
|
|
72
|
+
inputs?: Inputs<CMP_TYPE>;
|
|
73
|
+
/**
|
|
74
|
+
* Enable Angular Router support for the component.
|
|
75
|
+
*
|
|
76
|
+
* - When `true`: Creates a wildcard route for the component and navigates to `/`.
|
|
77
|
+
* - When `RoutingConfig`: Uses the provided routes and initial route.
|
|
78
|
+
*
|
|
79
|
+
* By default, `withComponentInputBinding()` is enabled, allowing you to pass
|
|
80
|
+
* inputs via route `data`, route params, or query params.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* // Simple routing (component matches any route)
|
|
85
|
+
* await render(MyComponent, { withRouting: true });
|
|
86
|
+
*
|
|
87
|
+
* // Full routing configuration
|
|
88
|
+
* await render(UserComponent, {
|
|
89
|
+
* withRouting: {
|
|
90
|
+
* routes: [
|
|
91
|
+
* { path: 'user/:id', component: UserComponent, data: { role: 'admin' } }
|
|
92
|
+
* ],
|
|
93
|
+
* initialRoute: '/user/42',
|
|
94
|
+
* },
|
|
95
|
+
* });
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
withRouting?: RoutingConfig | boolean;
|
|
99
|
+
/** Additional providers to configure in the testing module. */
|
|
100
|
+
providers?: Array<Provider | EnvironmentProviders>;
|
|
101
|
+
/** Providers to add specifically to the component being rendered. */
|
|
102
|
+
componentProviders?: Array<Provider>;
|
|
103
|
+
/** Additional imports for the testing module. */
|
|
104
|
+
imports?: unknown[];
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* @deprecated Use ComponentRenderOptions instead
|
|
108
|
+
*/
|
|
109
|
+
type RenderConfig<CMP_TYPE extends Type<unknown> = Type<unknown>> = ComponentRenderOptions<CMP_TYPE>;
|
|
110
|
+
interface RenderResult<T> extends LocatorSelectors {
|
|
111
|
+
baseElement: HTMLElement;
|
|
112
|
+
container: HTMLElement;
|
|
113
|
+
/**
|
|
114
|
+
* The ComponentFixture for the rendered component.
|
|
115
|
+
* When using `withRouting`, this is the RouterTestingHarness's internal fixture
|
|
116
|
+
* not a fixture of `T` directly.
|
|
117
|
+
*/
|
|
118
|
+
fixture: ComponentFixture<T> | InstanceType<typeof RouterTestingHarness>["fixture"];
|
|
119
|
+
debug(el?: HTMLElement | HTMLElement[] | Locator | Locator[], maxLength?: number, options?: PrettyDOMOptions): void;
|
|
120
|
+
/**
|
|
121
|
+
* @deprecated Use locator instead
|
|
122
|
+
*/
|
|
123
|
+
component: Locator;
|
|
124
|
+
/** Vitest browser locator scoped to the rendered component's container. */
|
|
125
|
+
locator: Locator;
|
|
126
|
+
/** The instance of the rendered component's class. */
|
|
127
|
+
componentClassInstance: T;
|
|
128
|
+
/**
|
|
129
|
+
* The RouterTestingHarness instance. Only available when `withRouting` is used.
|
|
130
|
+
*
|
|
131
|
+
* **Preferred for navigation in tests.** Use `navigateByUrl()` which:
|
|
132
|
+
* - Waits for all redirects to complete
|
|
133
|
+
* - Automatically runs change detection
|
|
134
|
+
* - Returns the activated component instance
|
|
135
|
+
* - Handles guard rejections gracefully
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```typescript
|
|
139
|
+
* // Navigate and get the activated component
|
|
140
|
+
* const userComponent = await routerHarness.navigateByUrl('/user/42', UserComponent);
|
|
141
|
+
*
|
|
142
|
+
* // Simple navigation
|
|
143
|
+
* await routerHarness.navigateByUrl('/about');
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
routerHarness?: RouterTestingHarness;
|
|
147
|
+
/**
|
|
148
|
+
* The Angular Router instance. Only available when `withRouting` is used.
|
|
149
|
+
*
|
|
150
|
+
* Useful for inspecting router state. For navigation, prefer `routerHarness.navigateByUrl()`.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```typescript
|
|
154
|
+
* expect(router.url).toBe('/user/42');
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
router?: Router;
|
|
158
|
+
}
|
|
159
|
+
type RenderFn = <T>(component: Type<T>, options?: ComponentRenderOptions<Type<T>>) => Promise<RenderResult<T>>;
|
|
160
|
+
/**
|
|
161
|
+
* Renders an Angular component for testing with Vitest Browser Mode.
|
|
162
|
+
*
|
|
163
|
+
* @param componentClass - The component class to render
|
|
164
|
+
* @param options - Configuration options for rendering
|
|
165
|
+
* @returns A promise that resolves to the render result with locators and component access
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* ```typescript
|
|
169
|
+
* // Basic render
|
|
170
|
+
* const { locator } = await render(MyComponent);
|
|
171
|
+
* await expect.element(locator.getByText('Hello')).toBeVisible();
|
|
172
|
+
*
|
|
173
|
+
* // With inputs
|
|
174
|
+
* const { componentClassInstance } = await render(UserComponent, {
|
|
175
|
+
* inputs: { name: 'John', age: 30 },
|
|
176
|
+
* });
|
|
177
|
+
*
|
|
178
|
+
* // With routing and route data as inputs
|
|
179
|
+
* const { router } = await render(ProfileComponent, {
|
|
180
|
+
* withRouting: {
|
|
181
|
+
* routes: [{ path: 'profile', component: ProfileComponent, data: { userId: '42' } }],
|
|
182
|
+
* initialRoute: '/profile',
|
|
183
|
+
* },
|
|
184
|
+
* });
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
declare function render<T>(componentClass: Type<T>, options?: ComponentRenderOptions<Type<T>>): Promise<RenderResult<T>>;
|
|
188
|
+
declare function cleanup(shouldTeardown?: boolean): void;
|
|
189
|
+
//#endregion
|
|
190
|
+
export { RenderResult as a, render as c, RenderFn as i, Inputs as n, RoutingConfig as o, RenderConfig as r, cleanup as s, ComponentRenderOptions as t };
|
package/dist/pure.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export { Inputs, RenderConfig, RenderFn, RenderResult, RoutingConfig, cleanup, render };
|
|
1
|
+
import { a as RenderResult, c as render, i as RenderFn, n as Inputs, o as RoutingConfig, r as RenderConfig, s as cleanup, t as ComponentRenderOptions } from "./pure-iXV3gO_T.mjs";
|
|
2
|
+
export { ComponentRenderOptions, Inputs, RenderConfig, RenderFn, RenderResult, RoutingConfig, cleanup, render };
|
package/dist/pure.mjs
CHANGED