@blotch/angular-lynx-testing-library 22.2.1 → 22.2.3

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
@@ -5,27 +5,44 @@ Testing utilities for [AngularLynx](https://github.com/Blotch-Smart-Frames/Angul
5
5
  ## Installation
6
6
 
7
7
  ```sh
8
- npm install --save-dev @blotch/angular-lynx-testing-library \
9
- @lynx-js/testing-environment \
10
- @testing-library/dom \
11
- @testing-library/jest-dom \
12
- vitest jsdom
8
+ ng generate @blotch/angular-lynx:add-testing
13
9
  ```
14
10
 
11
+ It installs this package (and Vitest and jsdom, if the project doesn't have them; `ng new` adds both) and makes sure `ng test` runs through AngularLynx's unit-test builder. To install it by hand: `npm install --save-dev @blotch/angular-lynx-testing-library`.
12
+
15
13
  ## Setup
16
14
 
17
- Add a `vitest.config.ts` to your project:
15
+ None. Tests run with `ng test`, whose builder (`@blotch/angular-lynx-build:unit-test`, which `ng add` sets up) loads this package's setup before the tests whenever the project has it. There is no Vitest config or setup file to write.
16
+
17
+ The setup:
18
+
19
+ - Enables Angular JIT compilation (`@angular/compiler`), for components compiled at runtime
20
+ - Installs `LynxTestingEnv` on the jsdom window
21
+ - Switches to the Lynx main thread (where PAPI functions live)
22
+ - Polyfills PAPI functions the testing environment doesn't implement (`__GetParent`, `__QuerySelector`, etc.)
23
+ - Makes `lynx.reportError` return for a warning, as Lynx does, instead of throwing it, so an `@boundary` shows its fallback with the default error handler
24
+ - Installs `@testing-library/jest-dom` matchers
25
+
26
+ `ng test` compiles the tests ahead of time, like the app, and checks templates strictly, as the Angular Language Service does: a component that uses Lynx elements imports `LYNX_ELEMENTS` from `@blotch/angular-lynx`. The examples below do.
27
+
28
+ ### Without Angular CLI
29
+
30
+ Under plain Vitest, load the setup yourself and define the compile-time flags the AngularLynx runtime reads (the rsbuild plugin defines them in the app, and `ng test` sets them too):
18
31
 
19
32
  ```ts
20
33
  // vitest.config.ts
21
34
  import { defineConfig } from 'vitest/config';
22
- import path from 'node:path';
23
35
 
24
36
  export default defineConfig({
25
37
  define: {
26
38
  __DEV__: JSON.stringify(true),
27
39
  __MAIN_THREAD__: JSON.stringify(true),
40
+ __WEB__: JSON.stringify(false),
41
+ __PROFILE__: JSON.stringify(false),
42
+ __ENABLE_SSR__: JSON.stringify(false),
28
43
  },
44
+ // Angular's JIT compiler reads the metadata of legacy decorators.
45
+ oxc: { decorator: { legacy: true } },
29
46
  test: {
30
47
  environment: 'jsdom',
31
48
  globals: true,
@@ -34,14 +51,6 @@ export default defineConfig({
34
51
  });
35
52
  ```
36
53
 
37
- The setup file:
38
-
39
- - Enables Angular JIT compilation (`@angular/compiler`)
40
- - Installs `LynxTestingEnv` on the jsdom window
41
- - Switches to the Lynx main thread (where PAPI functions live)
42
- - Polyfills PAPI functions the testing environment doesn't implement (`__GetParent`, `__QuerySelector`, etc.)
43
- - Installs `@testing-library/jest-dom` matchers
44
-
45
54
  ## API
46
55
 
47
56
  ### `render(component, options?)`
@@ -144,10 +153,12 @@ console.log(Object.keys(eventMap));
144
153
 
145
154
  ```ts
146
155
  import { Component } from '@angular/core';
156
+ import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
147
157
  import { render } from '@blotch/angular-lynx-testing-library';
148
158
 
149
159
  @Component({
150
160
  selector: 'greeting',
161
+ imports: [LYNX_ELEMENTS],
151
162
  template: `<text>Hello, {{ name }}!</text>`,
152
163
  })
153
164
  class GreetingComponent {
@@ -163,13 +174,15 @@ it('renders the greeting', async () => {
163
174
  ### Injecting providers
164
175
 
165
176
  ```ts
166
- import { InjectionToken, inject } from '@angular/core';
177
+ import { Component, InjectionToken, inject } from '@angular/core';
178
+ import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
167
179
  import { render } from '@blotch/angular-lynx-testing-library';
168
180
 
169
181
  const GREETING = new InjectionToken<string>('GREETING');
170
182
 
171
183
  @Component({
172
184
  selector: 'test-greeting',
185
+ imports: [LYNX_ELEMENTS],
173
186
  template: `<text>{{ greeting() }}</text>`,
174
187
  })
175
188
  class GreetingComponent {
@@ -189,15 +202,16 @@ it('uses the injected value', async () => {
189
202
  Define the signal outside the component so both the component and the test can access it:
190
203
 
191
204
  ```ts
192
- import { Component, signal, ChangeDetectionStrategy } from '@angular/core';
205
+ import { Component, signal } from '@angular/core';
206
+ import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
193
207
  import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
194
208
 
195
209
  const count = signal(0);
196
210
 
197
211
  @Component({
198
212
  selector: 'test-counter',
213
+ imports: [LYNX_ELEMENTS],
199
214
  template: `<text>{{ count() }}</text>`,
200
- changeDetection: ChangeDetectionStrategy.OnPush,
201
215
  })
202
216
  class CounterComponent {
203
217
  count = count;
@@ -219,7 +233,8 @@ it('updates when the signal changes', async () => {
219
233
  AngularLynx event bindings use Lynx prefixes: `(bindtap)`, `(catchtap)`, `(bindinput)`, etc.
220
234
 
221
235
  ```ts
222
- import { Component, signal, ChangeDetectionStrategy } from '@angular/core';
236
+ import { Component, signal } from '@angular/core';
237
+ import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
223
238
  import {
224
239
  render,
225
240
  fireEvent,
@@ -228,12 +243,12 @@ import {
228
243
 
229
244
  @Component({
230
245
  selector: 'test-button',
246
+ imports: [LYNX_ELEMENTS],
231
247
  template: `
232
248
  <view (bindtap)="increment()">
233
249
  <text>{{ count() }}</text>
234
250
  </view>
235
251
  `,
236
- changeDetection: ChangeDetectionStrategy.OnPush,
237
252
  })
238
253
  class ButtonComponent {
239
254
  count = signal(0);
@@ -260,10 +275,10 @@ it('shows content conditionally', async () => {
260
275
 
261
276
  @Component({
262
277
  selector: 'test-if',
278
+ imports: [LYNX_ELEMENTS],
263
279
  template: `@if (visible()) {
264
280
  <text>Hello</text>
265
281
  }`,
266
- changeDetection: ChangeDetectionStrategy.OnPush,
267
282
  })
268
283
  class IfComponent {
269
284
  visible = visible;
@@ -286,10 +301,10 @@ it('renders a dynamic list', async () => {
286
301
 
287
302
  @Component({
288
303
  selector: 'test-for',
304
+ imports: [LYNX_ELEMENTS],
289
305
  template: `@for (item of items(); track item) {
290
306
  <text>{{ item }}</text>
291
307
  }`,
292
- changeDetection: ChangeDetectionStrategy.OnPush,
293
308
  })
294
309
  class ListComponent {
295
310
  items = items;
@@ -361,7 +376,7 @@ Template binding → PAPI registration → DOM listener key:
361
376
  | Auto-cleanup env var | `PTL_SKIP_AUTO_CLEANUP` | — | `ATL_SKIP_AUTO_CLEANUP` |
362
377
  | `renderHook()` | ✓ | — | — |
363
378
  | Snapshot serializer | ✓ (RefProxy) | — | — |
364
- | Vitest plugin | ✓ | — | — |
379
+ | Vitest plugin | ✓ | — | — (`ng test` sets up) |
365
380
  | Named fireEvent helpers | 30+ | 16 | 16 |
366
381
  | Thread-aware fireEvent | ✓ | ✓ | — (single thread) |
367
382
  | Async state helper | `waitSchedule()` | `waitForUpdate()` | `waitForUpdate()` |
package/dist/render.d.ts CHANGED
@@ -48,10 +48,11 @@ export type RenderResult<Q extends Queries = typeof domQueries> = {
48
48
  * - `componentRef.injector.get(Token)` — retrieve a service provided to the app.
49
49
  * - `componentRef.instance.someSignal.set(v)` — drive writable signal state directly.
50
50
  *
51
- * Note: `componentRef.setInput()` requires the full AOT pipeline to register
52
- * signal inputs (input()) or @Input() decorators in component metadata. In the
53
- * Vitest JIT environment this metadata is not available, so the recommended
54
- * pattern is to expose writable signals on the component instance instead.
51
+ * Note: `componentRef.setInput()` needs the input metadata that ahead-of-time
52
+ * compilation registers for signal inputs (input()) and @Input() decorators.
53
+ * `ng test` compiles tests that way, so it works there. Under plain Vitest,
54
+ * which compiles components just in time, that metadata isn't available;
55
+ * expose writable signals on the component instance instead.
55
56
  */
56
57
  componentRef: ComponentRef<unknown>;
57
58
  } & BoundFunctions<typeof domQueries> & BoundFunctions<Q>;
@@ -1 +1 @@
1
- {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,QAAQ,EAEb,KAAK,IAAI,EACV,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,OAAO,EACZ,OAAO,IAAI,UAAU,EAEtB,MAAM,sBAAsB,CAAC;AAG9B,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,UAAU,IAAI;IACjE,SAAS,CAAC,EAAE,CAAC,QAAQ,GAAG,oBAAoB,CAAC,EAAE,CAAC;IAChD;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,UAAU,IAAI;IAChE;;OAEG;IACH,SAAS,EAAE,OAAO,CAAC;IACnB;;OAEG;IACH,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB;;;OAGG;IACH,QAAQ,EAAE,CAAC,CAAC,SAAS,OAAO,GAAG,CAAC,EAC9B,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC,EACxB,OAAO,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,KACvB,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9B;;OAEG;IACH,UAAU,EAAE,MAAM,gBAAgB,CAAC;IACnC;;;;;;;;;;;OAWG;IACH,YAAY,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC;CACrC,GAAG,cAAc,CAAC,OAAO,UAAU,CAAC,GACnC,cAAc,CAAC,CAAC,CAAC,CAAC;AAIpB,eAAO,MAAM,OAAO,QAAO,IAM1B,CAAC;AAEF,eAAO,MAAM,MAAM,GAAU,CAAC,SAAS,OAAO,GAAG,OAAO,UAAU,EAChE,eAAe,IAAI,CAAC,OAAO,CAAC,EAC5B,UAAS,aAAa,CAAC,CAAC,CAA0B,KACjD,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAkDzB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,aAAa,QAAa,OAAO,CAAC,IAAI,CAKlD,CAAC"}
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,QAAQ,EAEb,KAAK,IAAI,EACV,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,OAAO,EACZ,OAAO,IAAI,UAAU,EAEtB,MAAM,sBAAsB,CAAC;AAG9B,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,UAAU,IAAI;IACjE,SAAS,CAAC,EAAE,CAAC,QAAQ,GAAG,oBAAoB,CAAC,EAAE,CAAC;IAChD;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,UAAU,IAAI;IAChE;;OAEG;IACH,SAAS,EAAE,OAAO,CAAC;IACnB;;OAEG;IACH,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB;;;OAGG;IACH,QAAQ,EAAE,CAAC,CAAC,SAAS,OAAO,GAAG,CAAC,EAC9B,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC,EACxB,OAAO,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,KACvB,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9B;;OAEG;IACH,UAAU,EAAE,MAAM,gBAAgB,CAAC;IACnC;;;;;;;;;;;;OAYG;IACH,YAAY,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC;CACrC,GAAG,cAAc,CAAC,OAAO,UAAU,CAAC,GACnC,cAAc,CAAC,CAAC,CAAC,CAAC;AAIpB,eAAO,MAAM,OAAO,QAAO,IAM1B,CAAC;AAEF,eAAO,MAAM,MAAM,GAAU,CAAC,SAAS,OAAO,GAAG,OAAO,UAAU,EAChE,eAAe,IAAI,CAAC,OAAO,CAAC,EAC5B,UAAS,aAAa,CAAC,CAAC,CAA0B,KACjD,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAkDzB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,aAAa,QAAa,OAAO,CAAC,IAAI,CAKlD,CAAC"}
package/dist/setup.js CHANGED
@@ -40,8 +40,79 @@ globalThis.fetch = (url, ...args) => {
40
40
  if (typeof Element.prototype.animate !== 'function') {
41
41
  Element.prototype.animate = () => ({ cancel: () => { }, finished: Promise.resolve() });
42
42
  }
43
+ // ─── lynx.reportError warnings ───────────────────────────────────────────────
44
+ /**
45
+ * Makes `lynx.reportError` return for a warning, as Lynx does, instead of
46
+ * throwing it.
47
+ *
48
+ * The testing environment's `lynx.reportError` throws whatever it's given, so
49
+ * a reported error fails the test the way it would stop the app on a device.
50
+ * On a device, though, a `warning`-level report doesn't interrupt anything,
51
+ * and that's the level `LynxErrorHandler.onViewError()` reports an error that
52
+ * an `@boundary` caught at. Angular calls onViewError() before it swaps in the
53
+ * `@error` block, so the throw stopped the swap, and a test using the default
54
+ * error handler saw neither the content nor its fallback. Other reports still
55
+ * throw.
56
+ *
57
+ * The environment gives each thread a new `lynx` whenever it injects its
58
+ * globals: once when it's created, and again on every reset(), which runs
59
+ * after each test (see index.ts). So this runs from its inject hooks, which
60
+ * must be set before the environment is created below.
61
+ */
62
+ const returnOnWarnings = (threadGlobals) => {
63
+ const { lynx } = threadGlobals;
64
+ const reportError = lynx.reportError;
65
+ lynx.reportError = (error, options) => {
66
+ if (options?.level === 'warning')
67
+ return;
68
+ reportError(error, options);
69
+ };
70
+ };
71
+ globalThis.onInjectMainThreadGlobals = returnOnWarnings;
72
+ globalThis.onInjectBackgroundThreadGlobals = returnOnWarnings;
43
73
  // ─── Install LynxTestingEnv ──────────────────────────────────────────────────
44
- const lynxTestingEnv = new LynxTestingEnv({ window: globalThis.window });
74
+ /**
75
+ * Turns the globals the testing environment is about to write into plain data
76
+ * properties, so the writes stay on the global object.
77
+ *
78
+ * Vitest's jsdom environment defines each jsdom window property on the global
79
+ * as an accessor. Since Vitest 5 its setter also writes the value through to
80
+ * the jsdom window. A thread switch copies every thread global onto the
81
+ * global, and two of those writes break the window: jsdom 30's
82
+ * `window.document` has no setter, so assigning it threw "Cannot set property
83
+ * document of [object Window] which has only a getter", and `_globalProxy` is
84
+ * the object jsdom returns as `dom.window`, so overwriting it made Vitest's
85
+ * teardown fail with "dom.window.close is not a function". The thread
86
+ * globals' values are the window's own (`document`, `Event`) or replace them
87
+ * only for the test (`requestAnimationFrame`), so keeping them on the global
88
+ * matches what Vitest 4 did.
89
+ */
90
+ const keepWritesOffWindow = (keys) => {
91
+ for (const key of keys) {
92
+ const descriptor = Object.getOwnPropertyDescriptor(globalThis, key);
93
+ if (!descriptor?.set)
94
+ continue;
95
+ Object.defineProperty(globalThis, key, {
96
+ value: globalThis[key],
97
+ writable: true,
98
+ configurable: true,
99
+ enumerable: descriptor.enumerable,
100
+ });
101
+ }
102
+ };
103
+ class AngularLynxTestingEnv extends LynxTestingEnv {
104
+ switchToMainThread() {
105
+ keepWritesOffWindow(Object.getOwnPropertyNames(this.mainThread.globalThis));
106
+ super.switchToMainThread();
107
+ }
108
+ switchToBackgroundThread() {
109
+ keepWritesOffWindow(Object.getOwnPropertyNames(this.backgroundThread.globalThis));
110
+ super.switchToBackgroundThread();
111
+ }
112
+ }
113
+ const lynxTestingEnv = new AngularLynxTestingEnv({
114
+ window: globalThis.window,
115
+ });
45
116
  globalThis.lynxTestingEnv = lynxTestingEnv;
46
117
  // Expose Node so @testing-library/dom detects element types correctly.
47
118
  globalThis.Node = globalThis.window.Node;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blotch/angular-lynx-testing-library",
3
- "version": "22.2.1",
3
+ "version": "22.2.3",
4
4
  "description": "Testing utilities for AngularLynx apps",
5
5
  "homepage": "https://github.com/Blotch-Smart-Frames/AngularLynx#readme",
6
6
  "bugs": {
@@ -43,14 +43,14 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@lynx-js/testing-environment": "^0.3.4",
46
- "@testing-library/dom": "^10.4.1"
46
+ "@testing-library/dom": "^10.4.1",
47
+ "@testing-library/jest-dom": "^6.9.1"
47
48
  },
48
49
  "devDependencies": {
49
50
  "@angular/compiler": "^22.2.1",
50
51
  "@angular/core": "^22.2.1",
51
52
  "@angular/platform-browser": "^22.2.1",
52
53
  "@blotch/angular-lynx": "*",
53
- "@testing-library/jest-dom": "^6.9.1",
54
54
  "@types/node": "^22.0.0",
55
55
  "jsdom": "^25.0.0",
56
56
  "typescript": "~6.0.0"
@@ -60,5 +60,14 @@
60
60
  "@angular/core": "^22.2.0",
61
61
  "@angular/platform-browser": "^22.2.0",
62
62
  "@blotch/angular-lynx": "^22.2.0"
63
+ },
64
+ "ng-update": {
65
+ "packageGroup": [
66
+ "@blotch/angular-lynx",
67
+ "@blotch/angular-lynx-build",
68
+ "@blotch/angular-lynx-testing-library",
69
+ "@blotch/rsbuild-plugin-angular-lynx",
70
+ "@blotch/angular-lynx-native"
71
+ ]
63
72
  }
64
73
  }