@blotch/angular-lynx-testing-library 0.0.0-stage → 22.2.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 CHANGED
@@ -1,3 +1,367 @@
1
- # Temporary Holding Version
1
+ # @blotch/angular-lynx-testing-library
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Testing utilities for [AngularLynx](https://github.com/Blotch-Smart-Frames/AngularLynx) apps. Renders Angular standalone components through the Lynx PAPI pipeline into a JSDOM tree, then exposes the result via [@testing-library/dom](https://testing-library.com/docs/dom-testing-library/intro) queries.
4
+
5
+ ## Installation
6
+
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
13
+ ```
14
+
15
+ ## Setup
16
+
17
+ Add a `vitest.config.ts` to your project:
18
+
19
+ ```ts
20
+ // vitest.config.ts
21
+ import { defineConfig } from 'vitest/config';
22
+ import path from 'node:path';
23
+
24
+ export default defineConfig({
25
+ define: {
26
+ __DEV__: JSON.stringify(true),
27
+ __MAIN_THREAD__: JSON.stringify(true),
28
+ },
29
+ test: {
30
+ environment: 'jsdom',
31
+ globals: true,
32
+ setupFiles: ['@blotch/angular-lynx-testing-library/setup'],
33
+ },
34
+ });
35
+ ```
36
+
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
+ ## API
46
+
47
+ ### `render(component, options?)`
48
+
49
+ Bootstraps a standalone Angular component and returns a JSDOM container with `@testing-library/dom` queries bound to it.
50
+
51
+ ```ts
52
+ import { render } from '@blotch/angular-lynx-testing-library';
53
+
54
+ const { container, getByText, queryByRole } = await render(MyComponent);
55
+ ```
56
+
57
+ `render` is **async** — it `await`s Angular's `bootstrapApplication`. The initial change detection cycle runs before the promise resolves, so the container is ready to query immediately.
58
+
59
+ **Options:**
60
+
61
+ | Option | Type | Description |
62
+ | ----------- | -------------------------------------- | --------------------------------------------------------------------- |
63
+ | `providers` | `(Provider \| EnvironmentProviders)[]` | Additional providers injected into the app (services, tokens, mocks). |
64
+
65
+ `provideZonelessChangeDetection()` and `provideRenderer()` are always included — you do not need to provide them.
66
+
67
+ **Return value:**
68
+
69
+ | Property | Type | Description |
70
+ | ------------ | ------------ | ----------------------------------------------------------------------------------------------------------- |
71
+ | `container` | `Element` | The JSDOM `<page>` root element. All rendered children are descendants. |
72
+ | `unmount` | `() => void` | Destroys the Angular app. |
73
+ | `...queries` | — | All `@testing-library/dom` queries bound to `container` (`getByText`, `queryByRole`, `findByTestId`, etc.). |
74
+
75
+ ### `cleanup()`
76
+
77
+ Destroys the current Angular app and resets the Lynx environment. Called automatically after each test — you only need this for manual control.
78
+
79
+ ```ts
80
+ import { cleanup } from '@blotch/angular-lynx-testing-library';
81
+
82
+ afterEach(cleanup);
83
+ ```
84
+
85
+ Set `ATL_SKIP_AUTO_CLEANUP=true` to disable auto-cleanup.
86
+
87
+ ### `waitForUpdate()`
88
+
89
+ Waits for Angular's zoneless change detection scheduler to flush pending reactive updates, then forces a synchronous CD cycle.
90
+
91
+ ```ts
92
+ import { waitForUpdate } from '@blotch/angular-lynx-testing-library';
93
+
94
+ const count = signal(0);
95
+ count.set(1);
96
+ await waitForUpdate();
97
+ // DOM now reflects count = 1
98
+ ```
99
+
100
+ Call this after any signal mutation that happens outside an event handler (e.g. direct `.set()` in test code).
101
+
102
+ ### `fireEvent`
103
+
104
+ Dispatches Lynx events on JSDOM elements. Use the named helpers for standard Lynx events:
105
+
106
+ ```ts
107
+ import { fireEvent } from '@blotch/angular-lynx-testing-library';
108
+
109
+ fireEvent.tap(element);
110
+ fireEvent.input(element, { value: 'hello' });
111
+ fireEvent.scroll(element, { scrollTop: 100 });
112
+ ```
113
+
114
+ Named helpers dispatch a DOM event with the key `"bindEvent:<name>"` (e.g. `"bindEvent:tap"`), which matches the listener registered by `__AddEvent` when Angular processes a `(bindtap)` template binding.
115
+
116
+ To dispatch a raw DOM event directly:
117
+
118
+ ```ts
119
+ fireEvent(element, new Event('bindEvent:tap'));
120
+ ```
121
+
122
+ **Available named helpers:**
123
+
124
+ `tap` · `longtap` · `touchstart` · `touchmove` · `touchcancel` · `touchend` · `longpress` · `scroll` · `scrollend` · `focus` · `blur` · `input` · `confirm` · `layoutchange` · `transitionend` · `animationend`
125
+
126
+ ### `eventMap`
127
+
128
+ The full Lynx event registry. Each entry has a `defaultInit` object (currently empty for all events, but present for forward-compatibility).
129
+
130
+ ```ts
131
+ import { eventMap } from '@blotch/angular-lynx-testing-library';
132
+
133
+ console.log(Object.keys(eventMap));
134
+ // ['tap', 'longtap', 'touchstart', ...]
135
+ ```
136
+
137
+ ### Re-exports from `@testing-library/dom`
138
+
139
+ `screen` · `within` · `getQueriesForElement`
140
+
141
+ ## Usage
142
+
143
+ ### Rendering a component
144
+
145
+ ```ts
146
+ import { Component } from '@angular/core';
147
+ import { render } from '@blotch/angular-lynx-testing-library';
148
+
149
+ @Component({
150
+ selector: 'greeting',
151
+ template: `<text>Hello, {{ name }}!</text>`,
152
+ })
153
+ class GreetingComponent {
154
+ name = 'Lynx';
155
+ }
156
+
157
+ it('renders the greeting', async () => {
158
+ const { container } = await render(GreetingComponent);
159
+ expect(container.querySelector('text')!.textContent).toBe('Hello, Lynx!');
160
+ });
161
+ ```
162
+
163
+ ### Injecting providers
164
+
165
+ ```ts
166
+ import { InjectionToken, inject } from '@angular/core';
167
+ import { render } from '@blotch/angular-lynx-testing-library';
168
+
169
+ const GREETING = new InjectionToken<string>('GREETING');
170
+
171
+ @Component({
172
+ selector: 'test-greeting',
173
+ template: `<text>{{ greeting() }}</text>`,
174
+ })
175
+ class GreetingComponent {
176
+ greeting = inject(GREETING);
177
+ }
178
+
179
+ it('uses the injected value', async () => {
180
+ const { container } = await render(GreetingComponent, {
181
+ providers: [{ provide: GREETING, useValue: 'Hola' }],
182
+ });
183
+ expect(container.querySelector('text')!.textContent).toBe('Hola');
184
+ });
185
+ ```
186
+
187
+ ### Reactive signals
188
+
189
+ Define the signal outside the component so both the component and the test can access it:
190
+
191
+ ```ts
192
+ import { Component, signal, ChangeDetectionStrategy } from '@angular/core';
193
+ import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
194
+
195
+ const count = signal(0);
196
+
197
+ @Component({
198
+ selector: 'test-counter',
199
+ template: `<text>{{ count() }}</text>`,
200
+ changeDetection: ChangeDetectionStrategy.OnPush,
201
+ })
202
+ class CounterComponent {
203
+ count = count;
204
+ }
205
+
206
+ it('updates when the signal changes', async () => {
207
+ const { container } = await render(CounterComponent);
208
+ expect(container.querySelector('text')!.textContent).toBe('0');
209
+
210
+ count.set(42);
211
+ await waitForUpdate();
212
+
213
+ expect(container.querySelector('text')!.textContent).toBe('42');
214
+ });
215
+ ```
216
+
217
+ ### Handling events
218
+
219
+ AngularLynx event bindings use Lynx prefixes: `(bindtap)`, `(catchtap)`, `(bindinput)`, etc.
220
+
221
+ ```ts
222
+ import { Component, signal, ChangeDetectionStrategy } from '@angular/core';
223
+ import {
224
+ render,
225
+ fireEvent,
226
+ waitForUpdate,
227
+ } from '@blotch/angular-lynx-testing-library';
228
+
229
+ @Component({
230
+ selector: 'test-button',
231
+ template: `
232
+ <view (bindtap)="increment()">
233
+ <text>{{ count() }}</text>
234
+ </view>
235
+ `,
236
+ changeDetection: ChangeDetectionStrategy.OnPush,
237
+ })
238
+ class ButtonComponent {
239
+ count = signal(0);
240
+ increment() {
241
+ this.count.update((n) => n + 1);
242
+ }
243
+ }
244
+
245
+ it('increments on tap', async () => {
246
+ const { container } = await render(ButtonComponent);
247
+
248
+ fireEvent.tap(container.querySelector('view')!);
249
+ await waitForUpdate();
250
+
251
+ expect(container.querySelector('text')!.textContent).toBe('1');
252
+ });
253
+ ```
254
+
255
+ ### Control flow — `@if`
256
+
257
+ ```ts
258
+ it('shows content conditionally', async () => {
259
+ const visible = signal(true);
260
+
261
+ @Component({
262
+ selector: 'test-if',
263
+ template: `@if (visible()) {
264
+ <text>Hello</text>
265
+ }`,
266
+ changeDetection: ChangeDetectionStrategy.OnPush,
267
+ })
268
+ class IfComponent {
269
+ visible = visible;
270
+ }
271
+
272
+ const { container } = await render(IfComponent);
273
+ expect(container.querySelector('text')).not.toBeNull();
274
+
275
+ visible.set(false);
276
+ await waitForUpdate();
277
+ expect(container.querySelector('text')).toBeNull();
278
+ });
279
+ ```
280
+
281
+ ### Control flow — `@for`
282
+
283
+ ```ts
284
+ it('renders a dynamic list', async () => {
285
+ const items = signal(['A', 'B', 'C']);
286
+
287
+ @Component({
288
+ selector: 'test-for',
289
+ template: `@for (item of items(); track item) {
290
+ <text>{{ item }}</text>
291
+ }`,
292
+ changeDetection: ChangeDetectionStrategy.OnPush,
293
+ })
294
+ class ListComponent {
295
+ items = items;
296
+ }
297
+
298
+ const { container } = await render(ListComponent);
299
+ expect(container.querySelectorAll('text').length).toBe(3);
300
+
301
+ items.set(['A', 'B', 'C', 'D']);
302
+ await waitForUpdate();
303
+ expect(container.querySelectorAll('text').length).toBe(4);
304
+ });
305
+ ```
306
+
307
+ ## Architecture notes
308
+
309
+ ### Single-thread model
310
+
311
+ AngularLynx runs **entirely on the Lynx main thread** (`__MAIN_THREAD__ = true`). This differs from React Lynx and Vue Lynx, which split rendering across two threads:
312
+
313
+ | Library | Thread model |
314
+ | --------------- | ---------------------------------------------------------------- |
315
+ | React Lynx | Dual: background thread runs React, main thread applies PAPI ops |
316
+ | Vue Lynx | Dual: background thread runs Vue, main thread applies PAPI ops |
317
+ | **AngularLynx** | **Single: Angular renderer calls PAPI directly on main thread** |
318
+
319
+ In tests, this means there is no thread switching — `__MAIN_THREAD__` stays `true` throughout, and PAPI functions (`__CreatePage`, `__CreateView`, etc.) are always available.
320
+
321
+ ### The render pipeline
322
+
323
+ ```
324
+ render(Component)
325
+ └─ bootstrapApplication(Component, {
326
+ provideZonelessChangeDetection(),
327
+ provideRenderer(),
328
+ ...providers
329
+ })
330
+ └─ LynxDocument.createRootElement()
331
+ └─ __CreatePage() → <page> appended to document.body
332
+ └─ Angular renders component tree
333
+ └─ __CreateView / __CreateText / … → JSDOM children
334
+ └─ LynxRendererFactory2.end()
335
+ └─ __FlushElementTree() (no-op in tests)
336
+ └─ appRef.tick() → force synchronous CD
337
+ └─ return { container: elementTree.root, ...queries }
338
+ ```
339
+
340
+ ### Event dispatch
341
+
342
+ AngularLynx registers event handlers via `__AddEvent(element, eventType, eventName, { type: 'worklet', value: callback })`. The testing environment stores this as a DOM event listener under the key `"${eventType}:${eventName}"` (e.g. `"bindEvent:tap"`).
343
+
344
+ `fireEvent.tap(el)` dispatches a `"bindEvent:tap"` DOM event, which triggers the listener. The listener calls `runWorklet(callback, [event])` which invokes the Angular handler directly on the main thread.
345
+
346
+ Template binding → PAPI registration → DOM listener key:
347
+
348
+ | Template | Listener key |
349
+ | -------------- | ------------------ |
350
+ | `(bindtap)` | `bindEvent:tap` |
351
+ | `(catchtap)` | `catchEvent:tap` |
352
+ | `(bindinput)` | `bindEvent:input` |
353
+ | `(bindscroll)` | `bindEvent:scroll` |
354
+
355
+ ## Comparison with React/Vue Lynx testing libraries
356
+
357
+ | Feature | React Lynx | Vue Lynx | AngularLynx |
358
+ | ----------------------- | ----------------------- | ----------------- | ----------------------- |
359
+ | `render()` return | Sync | Sync | **Async** |
360
+ | Thread model | Dual (BG+MT) | Dual (BG+MT) | **Main only** |
361
+ | Auto-cleanup env var | `PTL_SKIP_AUTO_CLEANUP` | — | `ATL_SKIP_AUTO_CLEANUP` |
362
+ | `renderHook()` | ✓ | — | — |
363
+ | Snapshot serializer | ✓ (RefProxy) | — | — |
364
+ | Vitest plugin | ✓ | — | — |
365
+ | Named fireEvent helpers | 30+ | 16 | 16 |
366
+ | Thread-aware fireEvent | ✓ | ✓ | — (single thread) |
367
+ | Async state helper | `waitSchedule()` | `waitForUpdate()` | `waitForUpdate()` |
@@ -0,0 +1,128 @@
1
+ /**
2
+ * fireEvent — dispatches Lynx events through the dual-thread pipeline.
3
+ *
4
+ * Lynx events are registered via __AddEvent with a key of "bindEvent:<name>"
5
+ * (e.g. "bindEvent:tap"). The event handler stored on the element calls
6
+ * lynxCoreInject.tt.publishEvent(handlerSign, eventData) which routes the
7
+ * event to the background thread (not used by AngularLynx, which runs on
8
+ * the main thread).
9
+ *
10
+ * For AngularLynx, the (event) bindings in templates are wired by
11
+ * LynxRenderer.listen() which calls __AddEvent internally. The DOM event
12
+ * dispatched here triggers that listener directly on the main thread.
13
+ */
14
+ export declare const eventMap: {
15
+ tap: {
16
+ defaultInit: {};
17
+ };
18
+ longtap: {
19
+ defaultInit: {};
20
+ };
21
+ touchstart: {
22
+ defaultInit: {};
23
+ };
24
+ touchmove: {
25
+ defaultInit: {};
26
+ };
27
+ touchcancel: {
28
+ defaultInit: {};
29
+ };
30
+ touchend: {
31
+ defaultInit: {};
32
+ };
33
+ longpress: {
34
+ defaultInit: {};
35
+ };
36
+ keydown: {
37
+ defaultInit: {};
38
+ };
39
+ keyup: {
40
+ defaultInit: {};
41
+ };
42
+ mousedown: {
43
+ defaultInit: {};
44
+ };
45
+ mouseup: {
46
+ defaultInit: {};
47
+ };
48
+ mousemove: {
49
+ defaultInit: {};
50
+ };
51
+ mouseclick: {
52
+ defaultInit: {};
53
+ };
54
+ mousedblclick: {
55
+ defaultInit: {};
56
+ };
57
+ mouselongpress: {
58
+ defaultInit: {};
59
+ };
60
+ wheel: {
61
+ defaultInit: {};
62
+ };
63
+ scroll: {
64
+ defaultInit: {};
65
+ };
66
+ scrollend: {
67
+ defaultInit: {};
68
+ };
69
+ scrolltoupper: {
70
+ defaultInit: {};
71
+ };
72
+ scrolltolower: {
73
+ defaultInit: {};
74
+ };
75
+ contentsizechanged: {
76
+ defaultInit: {};
77
+ };
78
+ scrolltoupperedge: {
79
+ defaultInit: {};
80
+ };
81
+ scrolltoloweredge: {
82
+ defaultInit: {};
83
+ };
84
+ scrolltonormalstate: {
85
+ defaultInit: {};
86
+ };
87
+ focus: {
88
+ defaultInit: {};
89
+ };
90
+ blur: {
91
+ defaultInit: {};
92
+ };
93
+ input: {
94
+ defaultInit: {};
95
+ };
96
+ confirm: {
97
+ defaultInit: {};
98
+ };
99
+ layoutchange: {
100
+ defaultInit: {};
101
+ };
102
+ bgload: {
103
+ defaultInit: {};
104
+ };
105
+ bgerror: {
106
+ defaultInit: {};
107
+ };
108
+ transitionstart: {
109
+ defaultInit: {};
110
+ };
111
+ transitionend: {
112
+ defaultInit: {};
113
+ };
114
+ transitioncancel: {
115
+ defaultInit: {};
116
+ };
117
+ animationend: {
118
+ defaultInit: {};
119
+ };
120
+ };
121
+ type EventName = keyof typeof eventMap;
122
+ type EventHandler = (elem: Element, init?: Record<string, unknown>) => boolean;
123
+ type FireEvent = ((elem: Element, event: Event) => boolean) & {
124
+ [K in EventName]: EventHandler;
125
+ };
126
+ export declare const fireEvent: FireEvent;
127
+ export {};
128
+ //# sourceMappingURL=fire-event.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fire-event.d.ts","sourceRoot":"","sources":["../src/fire-event.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2CpB,CAAC;AAEF,KAAK,SAAS,GAAG,MAAM,OAAO,QAAQ,CAAC;AACvC,KAAK,YAAY,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC;AAC/E,KAAK,SAAS,GAAG,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC,GAAG;KAC3D,CAAC,IAAI,SAAS,GAAG,YAAY;CAC/B,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,SAET,CAAC"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * fireEvent — dispatches Lynx events through the dual-thread pipeline.
3
+ *
4
+ * Lynx events are registered via __AddEvent with a key of "bindEvent:<name>"
5
+ * (e.g. "bindEvent:tap"). The event handler stored on the element calls
6
+ * lynxCoreInject.tt.publishEvent(handlerSign, eventData) which routes the
7
+ * event to the background thread (not used by AngularLynx, which runs on
8
+ * the main thread).
9
+ *
10
+ * For AngularLynx, the (event) bindings in templates are wired by
11
+ * LynxRenderer.listen() which calls __AddEvent internally. The DOM event
12
+ * dispatched here triggers that listener directly on the main thread.
13
+ */
14
+ import { createEvent, fireEvent as domFireEvent } from '@testing-library/dom';
15
+ const EVENT_INIT_KEYS = new Set(['bubbles', 'cancelable', 'composed']);
16
+ export const eventMap = {
17
+ // Touch / gesture
18
+ tap: { defaultInit: {} },
19
+ longtap: { defaultInit: {} },
20
+ touchstart: { defaultInit: {} },
21
+ touchmove: { defaultInit: {} },
22
+ touchcancel: { defaultInit: {} },
23
+ touchend: { defaultInit: {} },
24
+ longpress: { defaultInit: {} },
25
+ // Keyboard
26
+ keydown: { defaultInit: {} },
27
+ keyup: { defaultInit: {} },
28
+ // Mouse
29
+ mousedown: { defaultInit: {} },
30
+ mouseup: { defaultInit: {} },
31
+ mousemove: { defaultInit: {} },
32
+ mouseclick: { defaultInit: {} },
33
+ mousedblclick: { defaultInit: {} },
34
+ mouselongpress: { defaultInit: {} },
35
+ wheel: { defaultInit: {} },
36
+ // Scroll / viewport
37
+ scroll: { defaultInit: {} },
38
+ scrollend: { defaultInit: {} },
39
+ scrolltoupper: { defaultInit: {} },
40
+ scrolltolower: { defaultInit: {} },
41
+ contentsizechanged: { defaultInit: {} },
42
+ scrolltoupperedge: { defaultInit: {} },
43
+ scrolltoloweredge: { defaultInit: {} },
44
+ scrolltonormalstate: { defaultInit: {} },
45
+ // Form / focus
46
+ focus: { defaultInit: {} },
47
+ blur: { defaultInit: {} },
48
+ input: { defaultInit: {} },
49
+ confirm: { defaultInit: {} },
50
+ // Layout / image
51
+ layoutchange: { defaultInit: {} },
52
+ bgload: { defaultInit: {} },
53
+ bgerror: { defaultInit: {} },
54
+ // Transition / animation
55
+ transitionstart: { defaultInit: {} },
56
+ transitionend: { defaultInit: {} },
57
+ transitioncancel: { defaultInit: {} },
58
+ animationend: { defaultInit: {} },
59
+ };
60
+ export const fireEvent = ((elem, event) => {
61
+ return domFireEvent(elem, event);
62
+ });
63
+ for (const key of Object.keys(eventMap)) {
64
+ fireEvent[key] = (elem, init) => {
65
+ const eventType = init?.['eventType'] ?? 'bindEvent';
66
+ const eventInit = {
67
+ eventType,
68
+ eventName: key,
69
+ ...eventMap[key].defaultInit,
70
+ ...init,
71
+ };
72
+ const event = createEvent(`${eventType}:${key}`, elem, eventInit);
73
+ // Assign Lynx-specific custom properties (eventType, eventName, etc.).
74
+ // Standard EventInit keys are read-only on constructed events so we skip them.
75
+ const customProps = Object.fromEntries(Object.entries(eventInit).filter(([k]) => !EVENT_INIT_KEYS.has(k)));
76
+ Object.assign(event, customProps);
77
+ return domFireEvent(elem, event);
78
+ };
79
+ }
@@ -0,0 +1,6 @@
1
+ export { render, cleanup, waitForUpdate } from './render.js';
2
+ export type { RenderOptions, RenderResult } from './render.js';
3
+ export type { ComponentRef } from '@angular/core';
4
+ export { fireEvent, eventMap } from './fire-event.js';
5
+ export { screen, within, getQueriesForElement } from '@testing-library/dom';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC7D,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC/D,YAAY,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,29 @@
1
+ export { render, cleanup, waitForUpdate } from './render.js';
2
+ export { fireEvent, eventMap } from './fire-event.js';
3
+ export { screen, within, getQueriesForElement } from '@testing-library/dom';
4
+ import { cleanup } from './render.js';
5
+ /**
6
+ * Auto-cleanup after each test (matches @testing-library/react convention).
7
+ * Set ATL_SKIP_AUTO_CLEANUP=true to opt out. Uses the global afterEach so this
8
+ * works with vitest, jest, and jasmine.
9
+ */
10
+ const registerAutoCleanup = () => {
11
+ const _afterEach = globalThis.afterEach;
12
+ if (typeof _afterEach === 'function') {
13
+ _afterEach(() => {
14
+ cleanup();
15
+ globalThis.lynxTestingEnv?.reset();
16
+ });
17
+ }
18
+ };
19
+ // `process` is absent in non-Node runners (e.g. a browser). That guard cannot
20
+ // execute under Node/Vitest — and faking a process-less global crashes Angular's
21
+ // import (it reads process.platform) — so it is excluded from coverage. The
22
+ // Node path (the opt-out env check) is fully tested.
23
+ /* v8 ignore next 2 -- @preserve non-Node guard: unreachable + untestable under Vitest */
24
+ if (typeof process === 'undefined') {
25
+ registerAutoCleanup();
26
+ }
27
+ else if (process.env['ATL_SKIP_AUTO_CLEANUP'] !== 'true') {
28
+ registerAutoCleanup();
29
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * render() — bootstraps an Angular standalone component through the full
3
+ * Lynx dual-thread pipeline and returns a container element (JSDOM) with
4
+ * @testing-library/dom queries.
5
+ *
6
+ * Pipeline:
7
+ * 1. switchToMainThread() → PAPI globals (__CreatePage, __CreateView, …) active
8
+ * 2. bootstrapApplication(component, { provideRenderer(), … })
9
+ * → LynxDocument.createRootElement() → __CreatePage → JSDOM <page> in body
10
+ * → Angular renders tree → __CreateView/Text/… → JSDOM children
11
+ * → LynxRendererFactory2.end() → __FlushElementTree() (no-op in tests)
12
+ * 3. appRef.tick() forces synchronous CD so all signal-driven updates land
13
+ * 4. Return JSDOM container + @testing-library/dom queries
14
+ */
15
+ import { type ComponentRef, type EnvironmentProviders, type Provider, type Type } from '@angular/core';
16
+ import { type BoundFunctions, type Queries, queries as domQueries } from '@testing-library/dom';
17
+ export type RenderOptions<Q extends Queries = typeof domQueries> = {
18
+ providers?: (Provider | EnvironmentProviders)[];
19
+ /**
20
+ * Custom query helpers to merge with the standard `@testing-library/dom` queries.
21
+ * Each helper receives the container as its first argument when bound.
22
+ * Workaround if omitted: call `getQueriesForElement(container, customQueries)` directly.
23
+ */
24
+ queries?: Q;
25
+ };
26
+ export type RenderResult<Q extends Queries = typeof domQueries> = {
27
+ /**
28
+ * The JSDOM root element (page element) containing all rendered children.
29
+ */
30
+ container: Element;
31
+ /**
32
+ * Unmount the current Angular app.
33
+ */
34
+ unmount: () => void;
35
+ /**
36
+ * Re-render a (possibly different) component into the same body position,
37
+ * replacing the running app without manually calling cleanup() first.
38
+ */
39
+ rerender: <R extends Queries = Q>(component: Type<unknown>, options?: RenderOptions<R>) => Promise<RenderResult<R>>;
40
+ /**
41
+ * Returns a DocumentFragment snapshot of the container — use for snapshot testing.
42
+ */
43
+ asFragment: () => DocumentFragment;
44
+ /**
45
+ * The Angular ComponentRef for the bootstrapped root component.
46
+ *
47
+ * Key uses:
48
+ * - `componentRef.injector.get(Token)` — retrieve a service provided to the app.
49
+ * - `componentRef.instance.someSignal.set(v)` — drive writable signal state directly.
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.
55
+ */
56
+ componentRef: ComponentRef<unknown>;
57
+ } & BoundFunctions<typeof domQueries> & BoundFunctions<Q>;
58
+ export declare const cleanup: () => void;
59
+ export declare const render: <Q extends Queries = typeof domQueries>(rootComponent: Type<unknown>, options?: RenderOptions<Q>) => Promise<RenderResult<Q>>;
60
+ /**
61
+ * Wait for all pending Angular reactive updates to flush through the pipeline.
62
+ * Call this after programmatic signal changes that happen outside event handlers.
63
+ */
64
+ export declare const waitForUpdate: () => Promise<void>;
65
+ //# sourceMappingURL=render.d.ts.map
@@ -0,0 +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"}
package/dist/render.js ADDED
@@ -0,0 +1,74 @@
1
+ /**
2
+ * render() — bootstraps an Angular standalone component through the full
3
+ * Lynx dual-thread pipeline and returns a container element (JSDOM) with
4
+ * @testing-library/dom queries.
5
+ *
6
+ * Pipeline:
7
+ * 1. switchToMainThread() → PAPI globals (__CreatePage, __CreateView, …) active
8
+ * 2. bootstrapApplication(component, { provideRenderer(), … })
9
+ * → LynxDocument.createRootElement() → __CreatePage → JSDOM <page> in body
10
+ * → Angular renders tree → __CreateView/Text/… → JSDOM children
11
+ * → LynxRendererFactory2.end() → __FlushElementTree() (no-op in tests)
12
+ * 3. appRef.tick() forces synchronous CD so all signal-driven updates land
13
+ * 4. Return JSDOM container + @testing-library/dom queries
14
+ */
15
+ import { provideZonelessChangeDetection, } from '@angular/core';
16
+ import { bootstrapApplication } from '@angular/platform-browser';
17
+ import { queries as domQueries, getQueriesForElement, } from '@testing-library/dom';
18
+ import { provideRenderer } from '@blotch/angular-lynx';
19
+ let currentAppRef = null;
20
+ export const cleanup = () => {
21
+ if (currentAppRef) {
22
+ const ref = currentAppRef;
23
+ currentAppRef = null;
24
+ ref.destroy();
25
+ }
26
+ };
27
+ export const render = async (rootComponent, options = {}) => {
28
+ cleanup();
29
+ // Switch to main thread so PAPI functions (__CreatePage, __CreateView, …)
30
+ // are available as globals. LynxDocument requires them to create elements.
31
+ const env = globalThis.lynxTestingEnv;
32
+ env.switchToMainThread();
33
+ // Clear JSDOM body so previous render's elements are gone.
34
+ const doc = globalThis.document;
35
+ doc.body.innerHTML = '';
36
+ // Bootstrap Angular. LynxDocument.createRootElement() runs during bootstrap,
37
+ // calling __CreatePage which appends the <page> element to document.body.
38
+ const appRef = await bootstrapApplication(rootComponent, {
39
+ providers: [
40
+ provideZonelessChangeDetection(),
41
+ provideRenderer(),
42
+ ...(options.providers ?? []),
43
+ ],
44
+ });
45
+ currentAppRef = appRef;
46
+ // Force a synchronous CD cycle so signal-initialized state is reflected in
47
+ // the DOM before we return. bootstrapApplication already triggers one CD,
48
+ // but zoneless scheduling may defer follow-up updates to a microtask.
49
+ appRef.tick();
50
+ // elementTree.root is the JSDOM <page> element created by __CreatePage.
51
+ const container = globalThis.elementTree.root;
52
+ const componentRef = appRef.components[0];
53
+ return {
54
+ container,
55
+ unmount: () => cleanup(),
56
+ rerender: (newComponent, newOptions) => render(newComponent, newOptions ?? {}),
57
+ asFragment: () => document.createRange().createContextualFragment(container.outerHTML),
58
+ componentRef,
59
+ ...getQueriesForElement(container, {
60
+ ...domQueries,
61
+ ...options.queries,
62
+ }),
63
+ };
64
+ };
65
+ /**
66
+ * Wait for all pending Angular reactive updates to flush through the pipeline.
67
+ * Call this after programmatic signal changes that happen outside event handlers.
68
+ */
69
+ export const waitForUpdate = async () => {
70
+ // Yield to the microtask/macrotask queue so zoneless CD scheduler can enqueue.
71
+ await new Promise((resolve) => setTimeout(resolve, 0));
72
+ // Then force synchronous CD to apply any queued updates to the DOM.
73
+ currentAppRef?.tick();
74
+ };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Vitest setup file for AngularLynx testing library.
3
+ *
4
+ * Runs in the jsdom environment and:
5
+ * 1. Creates LynxTestingEnv wrapping the existing jsdom window.
6
+ * 2. Polyfills PAPI functions that the testing-environment doesn't implement
7
+ * but the AngularLynx runtime requires.
8
+ * 3. Switches to main thread so PAPI globals are active before Angular bootstraps.
9
+ *
10
+ * AngularLynx runs on the main thread (__MAIN_THREAD__ = true):
11
+ * - LynxDocument uses PAPI functions (__CreatePage, __CreateView, …)
12
+ * - LynxRendererFactory2.end() calls __FlushElementTree (no-op in tests)
13
+ *
14
+ * Why we polyfill: the @lynx-js/testing-environment's ElementPAPI covers the
15
+ * core PAPI surface used by React/Vue Lynx, but AngularLynx calls additional
16
+ * PAPI functions (__GetParent, __NextElement, __QuerySelector*, etc.) that the
17
+ * testing environment does not implement. Since testing-environment elements ARE
18
+ * real JSDOM HTMLElements, we can implement these with native DOM methods.
19
+ */
20
+ import '@angular/compiler';
21
+ import '@testing-library/jest-dom/vitest';
22
+ //# sourceMappingURL=setup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,mBAAmB,CAAC;AAC3B,OAAO,kCAAkC,CAAC"}
package/dist/setup.js ADDED
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Vitest setup file for AngularLynx testing library.
3
+ *
4
+ * Runs in the jsdom environment and:
5
+ * 1. Creates LynxTestingEnv wrapping the existing jsdom window.
6
+ * 2. Polyfills PAPI functions that the testing-environment doesn't implement
7
+ * but the AngularLynx runtime requires.
8
+ * 3. Switches to main thread so PAPI globals are active before Angular bootstraps.
9
+ *
10
+ * AngularLynx runs on the main thread (__MAIN_THREAD__ = true):
11
+ * - LynxDocument uses PAPI functions (__CreatePage, __CreateView, …)
12
+ * - LynxRendererFactory2.end() calls __FlushElementTree (no-op in tests)
13
+ *
14
+ * Why we polyfill: the @lynx-js/testing-environment's ElementPAPI covers the
15
+ * core PAPI surface used by React/Vue Lynx, but AngularLynx calls additional
16
+ * PAPI functions (__GetParent, __NextElement, __QuerySelector*, etc.) that the
17
+ * testing environment does not implement. Since testing-environment elements ARE
18
+ * real JSDOM HTMLElements, we can implement these with native DOM methods.
19
+ */
20
+ // Must come before @angular/platform-browser to enable JIT compilation
21
+ // in tests (TypeScript source is not pre-compiled with the Angular compiler).
22
+ import '@angular/compiler';
23
+ import '@testing-library/jest-dom/vitest';
24
+ import { LynxTestingEnv } from '@lynx-js/testing-environment';
25
+ // ─── CSS fetch intercept ─────────────────────────────────────────────────────
26
+ /**
27
+ * Angular JIT calls fetch('./component.css') to load external styleUrls.
28
+ * jsdom has no base URL, so relative URLs fail. Return empty 200 for .css fetches.
29
+ */
30
+ const _originalFetch = globalThis.fetch;
31
+ globalThis.fetch = (url, ...args) => {
32
+ if (typeof url === 'string' && url.endsWith('.css')) {
33
+ return Promise.resolve(new Response('', { status: 200 }));
34
+ }
35
+ return _originalFetch(url, ...args);
36
+ };
37
+ // ─── Element.prototype.animate stub ──────────────────────────────────────────
38
+ // jsdom does not implement the Web Animations API. Define a no-op stub so
39
+ // vi.spyOn(Element.prototype, 'animate') can mock it in individual tests.
40
+ if (typeof Element.prototype.animate !== 'function') {
41
+ Element.prototype.animate = () => ({ cancel: () => { }, finished: Promise.resolve() });
42
+ }
43
+ // ─── Install LynxTestingEnv ──────────────────────────────────────────────────
44
+ const lynxTestingEnv = new LynxTestingEnv({ window: globalThis.window });
45
+ globalThis.lynxTestingEnv = lynxTestingEnv;
46
+ // Expose Node so @testing-library/dom detects element types correctly.
47
+ globalThis.Node = globalThis.window.Node;
48
+ // ─── Switch to main thread ───────────────────────────────────────────────────
49
+ // PAPI globals (__CreatePage, __CreateView, elementTree, …) are active on the
50
+ // main thread. AngularLynx must run here because LynxDocument uses these APIs.
51
+ lynxTestingEnv.switchToMainThread();
52
+ // ─── runWorklet polyfill ─────────────────────────────────────────────────────
53
+ // __AddEvent in the testing environment calls runWorklet(handler, [event]) when
54
+ // an event handler is registered as type:'worklet'. AngularLynx registers all
55
+ // event handlers this way (see LynxElement.addEventListener). runWorklet simply
56
+ // invokes the callback — same as the polyfill in runtime.ts for the real device.
57
+ globalThis.runWorklet = (value, params) => {
58
+ if (typeof value === 'function') {
59
+ value(...params);
60
+ }
61
+ };
62
+ // ─── Missing PAPI polyfills ──────────────────────────────────────────────────
63
+ // The testing-environment's ElementPAPI uses real JSDOM HTMLElements as the
64
+ // native element representation. So DOM traversal methods work natively on them.
65
+ // We polyfill the subset of PAPI that AngularLynx uses but testing-environment
66
+ // does not implement.
67
+ /**
68
+ * Returns the parent of a JSDOM element (equivalent to element.parentNode).
69
+ */
70
+ globalThis.__GetParent = (e) => e.parentElement ?? e.parentNode;
71
+ /**
72
+ * Returns the next sibling element.
73
+ */
74
+ globalThis.__NextElement = (e) => e.nextElementSibling ?? e.nextSibling;
75
+ /**
76
+ * querySelector on a JSDOM element.
77
+ */
78
+ globalThis.__QuerySelector = (e, selector) => e.querySelector(selector);
79
+ /**
80
+ * querySelectorAll on a JSDOM element — returns an array.
81
+ */
82
+ globalThis.__QuerySelectorAll = (e, selector) => Array.from(e.querySelectorAll(selector));
83
+ /**
84
+ * Returns the eventMap stored on the element by __AddEvent.
85
+ * LynxElement.addEventListener uses this to identify listeners to remove.
86
+ */
87
+ globalThis.__GetEvents = (e) => e.eventMap ?? {};
88
+ /**
89
+ * Replaces the eventMap on the element. Used by the removeEventListener path.
90
+ */
91
+ globalThis.__SetEvents = (e, events) => {
92
+ e.eventMap = Object.fromEntries(events.map((ev) => [ev.type + ':' + ev.name, ev.jsFunction]));
93
+ };
94
+ // Element creators that @lynx-js/testing-environment doesn't implement.
95
+ // They fall back to __CreateElement (also a JSDOM element with a custom tag).
96
+ for (const tag of ['block', 'for', 'frame', 'if']) {
97
+ globalThis[`__Create${tag.charAt(0).toUpperCase() + tag.slice(1)}`] =
98
+ (parentComponentUniqueId) => globalThis.__CreateElement(tag, parentComponentUniqueId);
99
+ }
100
+ /**
101
+ * __CreateNonElement creates an invisible placeholder (comment anchor).
102
+ */
103
+ globalThis.__CreateNonElement = () => globalThis.__CreateElement('non-element', 0);
104
+ /**
105
+ * Class manipulation helpers.
106
+ */
107
+ globalThis.__AddClass = (e, cls) => e.classList.add(cls);
108
+ // Must return an array — LynxElement.removeClass() calls .filter() on the result.
109
+ globalThis.__GetClasses = (e) => Array.from(e.classList);
110
+ globalThis.__SetClasses = (e, classes) => {
111
+ e.className = classes;
112
+ };
113
+ /**
114
+ * Returns the direct element children of a JSDOM element.
115
+ * __CreateRawText returns a DOM TextNode (not HTMLElement), so text content
116
+ * inside <text> elements is excluded here — matching Lynx PAPI semantics where
117
+ * __GetChildren returns Lynx elements only, not raw-text nodes. This means
118
+ * LynxElement.remove()'s child-parking loop becomes a no-op for <text>
119
+ * elements, letting __RemoveElement proceed and correctly detach the element.
120
+ */
121
+ globalThis.__GetChildren = (e) => Array.from(e.children);
122
+ /**
123
+ * __SetConfig is called during bootstrap to pass Lynx config; no-op in tests.
124
+ */
125
+ globalThis.__SetConfig = () => { };
126
+ /**
127
+ * Invokes a native UI method by name (e.g. the `setValue` UIMethod that Lynx
128
+ * inputs/textareas require for programmatic text updates — see
129
+ * LynxElement.invoke() in the runtime). LynxElement.invoke() always passes an
130
+ * empty result callback, so a no-op that still calls back (matching the real
131
+ * host's async {code, data} contract) is sufficient here.
132
+ */
133
+ globalThis.__InvokeUIMethod = (_element, _methodName, _params, callback) => {
134
+ callback?.({ code: 0 });
135
+ };
136
+ // ─── After reset ─────────────────────────────────────────────────────────────
137
+ // lynxTestingEnv.reset() re-injects globals from scratch. Re-establish main
138
+ // thread context and restore our polyfills afterwards.
139
+ globalThis.onResetLynxTestingEnv = () => {
140
+ lynxTestingEnv.switchToMainThread();
141
+ // runWorklet is a polyfill on globalThis; it survives reset since we set it
142
+ // on the outer globalThis (not the per-thread globalThis).
143
+ };
package/package.json CHANGED
@@ -1,6 +1,64 @@
1
1
  {
2
2
  "name": "@blotch/angular-lynx-testing-library",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "22.2.1",
4
+ "description": "Testing utilities for AngularLynx apps",
5
+ "homepage": "https://github.com/Blotch-Smart-Frames/AngularLynx#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/Blotch-Smart-Frames/AngularLynx/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/Blotch-Smart-Frames/AngularLynx.git",
12
+ "directory": "packages/testing-library"
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "type": "module",
18
+ "main": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js"
24
+ },
25
+ "./setup": {
26
+ "types": "./dist/setup.d.ts",
27
+ "import": "./dist/setup.js"
28
+ }
29
+ },
30
+ "scripts": {
31
+ "build": "tsc -p tsconfig.build.json",
32
+ "test": "vitest run",
33
+ "test:watch": "vitest",
34
+ "test:coverage": "vitest run --coverage",
35
+ "test:ci:coverage": "vitest run --coverage",
36
+ "test:ui": "vitest --ui --coverage.enabled=true",
37
+ "lint": "oxlint .",
38
+ "lint:fix": "oxlint --fix .",
39
+ "format": "oxfmt --write .",
40
+ "format:check": "oxfmt --check .",
41
+ "cspell": "cspell .",
42
+ "publish:local": "yalc publish --push"
43
+ },
44
+ "dependencies": {
45
+ "@lynx-js/testing-environment": "^0.3.4",
46
+ "@testing-library/dom": "^10.4.1"
47
+ },
48
+ "devDependencies": {
49
+ "@angular/compiler": "^22.2.1",
50
+ "@angular/core": "^22.2.1",
51
+ "@angular/platform-browser": "^22.2.1",
52
+ "@blotch/angular-lynx": "*",
53
+ "@testing-library/jest-dom": "^6.9.1",
54
+ "@types/node": "^22.0.0",
55
+ "jsdom": "^25.0.0",
56
+ "typescript": "~6.0.0"
57
+ },
58
+ "peerDependencies": {
59
+ "@angular/compiler": "^22.2.0",
60
+ "@angular/core": "^22.2.0",
61
+ "@angular/platform-browser": "^22.2.0",
62
+ "@blotch/angular-lynx": "^22.2.0"
63
+ }
64
+ }