@blotch/angular-lynx-testing-library 0.0.0-stage → 22.2.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 +366 -2
- package/dist/fire-event.d.ts +128 -0
- package/dist/fire-event.d.ts.map +1 -0
- package/dist/fire-event.js +79 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/render.d.ts +65 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +74 -0
- package/dist/setup.d.ts +22 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +143 -0
- package/package.json +62 -4
package/README.md
CHANGED
|
@@ -1,3 +1,367 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @blotch/angular-lynx-testing-library
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/render.d.ts
ADDED
|
@@ -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
|
+
};
|
package/dist/setup.d.ts
ADDED
|
@@ -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": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "22.2.0",
|
|
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
|
+
}
|