@ng-native/testing 0.0.1 → 0.1.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/LICENSE +21 -0
- package/README.md +67 -1
- package/dist/events.d.ts +45 -0
- package/dist/events.js +144 -0
- package/dist/events.js.map +1 -0
- package/dist/gesture-handler.d.ts +39 -0
- package/dist/gesture-handler.js +42 -0
- package/dist/gesture-handler.js.map +1 -0
- package/dist/gesture-of.d.ts +8 -0
- package/dist/gesture-of.js +29 -0
- package/dist/gesture-of.js.map +1 -0
- package/dist/gestures.d.ts +16 -0
- package/dist/gestures.js +46 -0
- package/dist/gestures.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/queries.d.ts +73 -0
- package/dist/queries.js +148 -0
- package/dist/queries.js.map +1 -0
- package/dist/reanimated-library.d.ts +62 -0
- package/dist/reanimated-library.js +95 -0
- package/dist/reanimated-library.js.map +1 -0
- package/dist/reanimated.d.ts +30 -0
- package/dist/reanimated.js +80 -0
- package/dist/reanimated.js.map +1 -0
- package/dist/render.d.ts +92 -0
- package/dist/render.js +182 -0
- package/dist/render.js.map +1 -0
- package/dist/test-utils.d.ts +66 -0
- package/dist/test-utils.js +162 -0
- package/dist/test-utils.js.map +1 -0
- package/dist/wait-for.d.ts +25 -0
- package/dist/wait-for.js +21 -0
- package/dist/wait-for.js.map +1 -0
- package/dist/worklets-library.d.ts +10 -0
- package/dist/worklets-library.js +23 -0
- package/dist/worklets-library.js.map +1 -0
- package/package.json +86 -3
- package/runner/compile.mjs +82 -0
- package/runner/loader.mjs +73 -0
- package/runner/register.mjs +17 -0
- package/runner/setup.mjs +10 -0
- package/runner/vitest.d.mts +12 -0
- package/runner/vitest.mjs +142 -0
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `render()`, `screen` and `within()`, shaped like Angular Testing Library's and running on the
|
|
3
|
+
* fake Fabric instead of TestBed and a DOM.
|
|
4
|
+
*
|
|
5
|
+
* A render is a real `mount()` - the same bootstrap an app runs, with the same renderer and engine
|
|
6
|
+
* - onto a `createFakeFabric()`, so what a query reads is what native would have been handed.
|
|
7
|
+
*/
|
|
8
|
+
import { type ApplicationRef, type ComponentRef, type EnvironmentProviders, type Provider, type Type } from '@angular/core';
|
|
9
|
+
import type { EngineOptions } from '@ng-native/fabric';
|
|
10
|
+
import { type BoundQueries } from './queries.ts';
|
|
11
|
+
import { type FakeFabric, type FakeFabricNode } from './test-utils.ts';
|
|
12
|
+
import { type WaitForOptions } from './wait-for.ts';
|
|
13
|
+
/**
|
|
14
|
+
* What `render()` takes: Angular Testing Library's options, plus the engine's own.
|
|
15
|
+
*
|
|
16
|
+
* The engine options (`globalStyles`, `conditions`, `tokens` and the rest) are what `mount()`
|
|
17
|
+
* takes on a device, so a test styles a component with the same compiled sheet an app would.
|
|
18
|
+
*/
|
|
19
|
+
export interface RenderOptions<T> extends EngineOptions {
|
|
20
|
+
/** The component's inputs, set before its first change detection, so `required` ones work. */
|
|
21
|
+
inputs?: Record<string, unknown>;
|
|
22
|
+
/** Output handlers, by output name. */
|
|
23
|
+
on?: Record<string, (value: never) => void>;
|
|
24
|
+
/** Environment providers for the app the component is mounted in. */
|
|
25
|
+
providers?: (Provider | EnvironmentProviders)[];
|
|
26
|
+
/** The template form only: what the template may use. */
|
|
27
|
+
imports?: unknown[];
|
|
28
|
+
/** Fields set on the instance: the template form's host, or the component itself. */
|
|
29
|
+
componentProperties?: Partial<T> & Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* What `render()` resolves to: every query, bound to this render, and the handles to drive it.
|
|
33
|
+
*
|
|
34
|
+
* The queries here stay on this render even after a later one; `screen` moves to the latest.
|
|
35
|
+
*/
|
|
36
|
+
export interface RenderResult<T> extends BoundQueries {
|
|
37
|
+
fabric: FakeFabric;
|
|
38
|
+
componentRef: ComponentRef<T>;
|
|
39
|
+
instance: T;
|
|
40
|
+
/** `ComponentFixture`'s shape, for tests written against it. */
|
|
41
|
+
fixture: {
|
|
42
|
+
componentRef: ComponentRef<T>;
|
|
43
|
+
componentInstance: T;
|
|
44
|
+
detectChanges(): Promise<void>;
|
|
45
|
+
};
|
|
46
|
+
/** Set inputs and properties again, then let the result commit. */
|
|
47
|
+
rerender(changes?: {
|
|
48
|
+
inputs?: Record<string, unknown>;
|
|
49
|
+
componentProperties?: Record<string, unknown>;
|
|
50
|
+
}): Promise<void>;
|
|
51
|
+
/** Run change detection and wait for the commit. */
|
|
52
|
+
detectChanges(): Promise<void>;
|
|
53
|
+
/** Print the committed tree, or one node's subtree. */
|
|
54
|
+
debug(node?: FakeFabricNode): void;
|
|
55
|
+
unmount(): void;
|
|
56
|
+
}
|
|
57
|
+
interface Mounted {
|
|
58
|
+
fabric: FakeFabric;
|
|
59
|
+
applicationRef: ApplicationRef;
|
|
60
|
+
componentRef: ComponentRef<unknown>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Let zoneless change detection run and commit.
|
|
64
|
+
*
|
|
65
|
+
* One macrotask is enough: Angular schedules its pass on `setTimeout(0)` (racing an animation
|
|
66
|
+
* frame) when something changes, so a timeout queued after the change runs after the pass.
|
|
67
|
+
*/
|
|
68
|
+
export declare const settle: () => Promise<void>;
|
|
69
|
+
export declare function ownerOf(node: FakeFabricNode): Mounted;
|
|
70
|
+
/** The node as it is in the most recent commit, found by its tag, or null once it is gone. */
|
|
71
|
+
export declare function currentOf(node: FakeFabricNode): FakeFabricNode | null;
|
|
72
|
+
/**
|
|
73
|
+
* Mount a component, or a template string, onto a fresh fake Fabric and wait for the first commit.
|
|
74
|
+
*
|
|
75
|
+
* The same `mount()` an app boots with, so what the queries read is exactly what native would
|
|
76
|
+
* have been sent. A string is compiled as a host component's template, with `imports` and
|
|
77
|
+
* `componentProperties` for what it uses; a class is rendered as-is, with `inputs` set before its
|
|
78
|
+
* first change detection so `input.required()` works.
|
|
79
|
+
*/
|
|
80
|
+
export declare function render<T>(component: Type<T> | string, options?: RenderOptions<T>): Promise<RenderResult<T>>;
|
|
81
|
+
/** Unmount everything rendered so far. Registered as an `afterEach` where one is global. */
|
|
82
|
+
export declare function cleanup(): void;
|
|
83
|
+
/** Queries over whatever was rendered most recently, as Testing Library's `screen` is. */
|
|
84
|
+
export declare const screen: BoundQueries & {
|
|
85
|
+
debug(node?: FakeFabricNode): void;
|
|
86
|
+
};
|
|
87
|
+
/** Queries scoped to one node and what is under it, in whichever commit is current. */
|
|
88
|
+
export declare function within(node: FakeFabricNode): BoundQueries;
|
|
89
|
+
type Present = FakeFabricNode | readonly FakeFabricNode[] | null | undefined;
|
|
90
|
+
/** Wait for a node, or whatever a callback finds, to leave the committed tree. */
|
|
91
|
+
export declare function waitForElementToBeRemoved(target: FakeFabricNode | readonly FakeFabricNode[] | (() => Present), options?: WaitForOptions): Promise<void>;
|
|
92
|
+
export {};
|
package/dist/render.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* `render()`, `screen` and `within()`, shaped like Angular Testing Library's and running on the
|
|
11
|
+
* fake Fabric instead of TestBed and a DOM.
|
|
12
|
+
*
|
|
13
|
+
* A render is a real `mount()` - the same bootstrap an app runs, with the same renderer and engine
|
|
14
|
+
* - onto a `createFakeFabric()`, so what a query reads is what native would have been handed.
|
|
15
|
+
*/
|
|
16
|
+
import { ChangeDetectorRef, Component, } from '@angular/core';
|
|
17
|
+
import { mount } from '@ng-native/platform';
|
|
18
|
+
import { bindQueries, describeTree, flatten } from "./queries.js";
|
|
19
|
+
import { createFakeFabric } from "./test-utils.js";
|
|
20
|
+
import { waitFor } from "./wait-for.js";
|
|
21
|
+
const mounted = [];
|
|
22
|
+
/** Which render a node a query returned belongs to, so an event on it reaches the right fake. */
|
|
23
|
+
const owners = new WeakMap();
|
|
24
|
+
/**
|
|
25
|
+
* Let zoneless change detection run and commit.
|
|
26
|
+
*
|
|
27
|
+
* One macrotask is enough: Angular schedules its pass on `setTimeout(0)` (racing an animation
|
|
28
|
+
* frame) when something changes, so a timeout queued after the change runs after the pass.
|
|
29
|
+
*/
|
|
30
|
+
export const settle = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
31
|
+
function latest() {
|
|
32
|
+
const current = mounted.at(-1);
|
|
33
|
+
if (!current)
|
|
34
|
+
throw new Error('Nothing is rendered: call render() first.');
|
|
35
|
+
return current;
|
|
36
|
+
}
|
|
37
|
+
export function ownerOf(node) {
|
|
38
|
+
return owners.get(node) ?? latest();
|
|
39
|
+
}
|
|
40
|
+
/** The node as it is in the most recent commit, found by its tag, or null once it is gone. */
|
|
41
|
+
export function currentOf(node) {
|
|
42
|
+
const hit = flatten(ownerOf(node).fabric.committed).find((n) => n.reactTag === node.reactTag);
|
|
43
|
+
if (hit)
|
|
44
|
+
owners.set(hit, ownerOf(node));
|
|
45
|
+
return hit ?? null;
|
|
46
|
+
}
|
|
47
|
+
const tree = (nodes) => describeTree(nodes) || '(nothing)';
|
|
48
|
+
function queriesFor(roots, owner) {
|
|
49
|
+
return bindQueries(roots, () => tree(roots()), (node) => owners.set(node, owner()));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The template form, compiled just in time.
|
|
53
|
+
*
|
|
54
|
+
* ponytail: this needs `@angular/compiler` at runtime, the one place anything here does - the
|
|
55
|
+
* class form is AOT like the app. An AOT compile of the string would mean evaluating generated
|
|
56
|
+
* code that imports the caller's own classes by name; do that if JIT ever disagrees with AOT.
|
|
57
|
+
*/
|
|
58
|
+
async function hostFor(template, options) {
|
|
59
|
+
await import(__rewriteRelativeImportExtension('@angular/compiler'));
|
|
60
|
+
const properties = options.componentProperties ?? {};
|
|
61
|
+
class WrapperComponent {
|
|
62
|
+
constructor() {
|
|
63
|
+
Object.assign(this, properties);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return Component({
|
|
67
|
+
selector: 'ng-native-wrapper',
|
|
68
|
+
template,
|
|
69
|
+
imports: (options.imports ?? []),
|
|
70
|
+
})(WrapperComponent);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Mount a component, or a template string, onto a fresh fake Fabric and wait for the first commit.
|
|
74
|
+
*
|
|
75
|
+
* The same `mount()` an app boots with, so what the queries read is exactly what native would
|
|
76
|
+
* have been sent. A string is compiled as a host component's template, with `imports` and
|
|
77
|
+
* `componentProperties` for what it uses; a class is rendered as-is, with `inputs` set before its
|
|
78
|
+
* first change detection so `input.required()` works.
|
|
79
|
+
*/
|
|
80
|
+
export async function render(component, options = {}) {
|
|
81
|
+
const { inputs, on, providers, imports: _, componentProperties, ...engine } = options;
|
|
82
|
+
const isTemplate = typeof component === 'string';
|
|
83
|
+
const type = isTemplate ? await hostFor(component, options) : component;
|
|
84
|
+
const fabric = createFakeFabric();
|
|
85
|
+
const app = mount(1, type, fabric, {
|
|
86
|
+
...engine,
|
|
87
|
+
providers,
|
|
88
|
+
inputs: isTemplate ? {} : inputs,
|
|
89
|
+
});
|
|
90
|
+
const self = {
|
|
91
|
+
fabric,
|
|
92
|
+
applicationRef: app.applicationRef,
|
|
93
|
+
componentRef: app.componentRef,
|
|
94
|
+
};
|
|
95
|
+
mounted.push(self);
|
|
96
|
+
const componentRef = app.componentRef;
|
|
97
|
+
const instance = componentRef.instance;
|
|
98
|
+
for (const [name, handler] of Object.entries(on ?? {})) {
|
|
99
|
+
instance[name].subscribe(handler);
|
|
100
|
+
}
|
|
101
|
+
const detectChanges = async () => {
|
|
102
|
+
// The component's own view, not the host's: `componentRef.changeDetectorRef` marks the host
|
|
103
|
+
// and upwards, which leaves an OnPush component with a changed field exactly as it was.
|
|
104
|
+
componentRef.injector.get(ChangeDetectorRef).markForCheck();
|
|
105
|
+
app.applicationRef.tick();
|
|
106
|
+
await settle();
|
|
107
|
+
};
|
|
108
|
+
const rerender = async (changes = {}) => {
|
|
109
|
+
for (const [name, value] of Object.entries(changes.inputs ?? {})) {
|
|
110
|
+
componentRef.setInput(name, value);
|
|
111
|
+
}
|
|
112
|
+
Object.assign(instance, changes.componentProperties);
|
|
113
|
+
await detectChanges();
|
|
114
|
+
};
|
|
115
|
+
if (!isTemplate && componentProperties)
|
|
116
|
+
await rerender({ componentProperties });
|
|
117
|
+
else
|
|
118
|
+
await settle();
|
|
119
|
+
return {
|
|
120
|
+
...queriesFor(() => fabric.committed, () => self),
|
|
121
|
+
fabric,
|
|
122
|
+
componentRef,
|
|
123
|
+
instance,
|
|
124
|
+
fixture: { componentRef, componentInstance: instance, detectChanges },
|
|
125
|
+
rerender,
|
|
126
|
+
detectChanges,
|
|
127
|
+
debug: (node) => console.log(tree(node ? [currentOf(node) ?? node] : fabric.committed)),
|
|
128
|
+
unmount: () => unmount(self),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function unmount(target) {
|
|
132
|
+
const at = mounted.indexOf(target);
|
|
133
|
+
if (at === -1)
|
|
134
|
+
return;
|
|
135
|
+
mounted.splice(at, 1);
|
|
136
|
+
target.applicationRef.destroy();
|
|
137
|
+
}
|
|
138
|
+
/** Unmount everything rendered so far. Registered as an `afterEach` where one is global. */
|
|
139
|
+
export function cleanup() {
|
|
140
|
+
for (const target of [...mounted])
|
|
141
|
+
unmount(target);
|
|
142
|
+
}
|
|
143
|
+
/** Queries over whatever was rendered most recently, as Testing Library's `screen` is. */
|
|
144
|
+
export const screen = {
|
|
145
|
+
...queriesFor(() => latest().fabric.committed, latest),
|
|
146
|
+
debug: (node) => console.log(tree(node ? [currentOf(node) ?? node] : latest().fabric.committed)),
|
|
147
|
+
};
|
|
148
|
+
/** Queries scoped to one node and what is under it, in whichever commit is current. */
|
|
149
|
+
export function within(node) {
|
|
150
|
+
const owner = ownerOf(node);
|
|
151
|
+
return queriesFor(() => {
|
|
152
|
+
const current = currentOf(node);
|
|
153
|
+
return current ? [current] : [];
|
|
154
|
+
}, () => owner);
|
|
155
|
+
}
|
|
156
|
+
/** Wait for a node, or whatever a callback finds, to leave the committed tree. */
|
|
157
|
+
export async function waitForElementToBeRemoved(target, options) {
|
|
158
|
+
const present = () => {
|
|
159
|
+
let found;
|
|
160
|
+
try {
|
|
161
|
+
found = typeof target === 'function' ? target() : target;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
const nodes = found ? [].concat(found) : [];
|
|
167
|
+
return nodes.some((node) => currentOf(node) !== null);
|
|
168
|
+
};
|
|
169
|
+
if (!present()) {
|
|
170
|
+
throw new Error('waitForElementToBeRemoved needs the node to be there when it starts; it already is not.');
|
|
171
|
+
}
|
|
172
|
+
await waitFor(() => {
|
|
173
|
+
if (present())
|
|
174
|
+
throw new Error(`Still rendered after the timeout.\n\n${tree(latest().fabric.committed)}`);
|
|
175
|
+
}, options);
|
|
176
|
+
}
|
|
177
|
+
// Testing Library's own convention: clean up after every test when the runner exposes `afterEach`
|
|
178
|
+
// globally (Vitest with `globals: true`, Jest). Otherwise each `render()` stands alone.
|
|
179
|
+
const globalAfterEach = globalThis.afterEach;
|
|
180
|
+
if (typeof globalAfterEach === 'function')
|
|
181
|
+
globalAfterEach(cleanup);
|
|
182
|
+
//# sourceMappingURL=render.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `render()`, `screen` and `within()`, shaped like Angular Testing Library's and running on the\n * fake Fabric instead of TestBed and a DOM.\n *\n * A render is a real `mount()` - the same bootstrap an app runs, with the same renderer and engine\n * - onto a `createFakeFabric()`, so what a query reads is what native would have been handed.\n */\nimport {\n ChangeDetectorRef,\n Component,\n type ApplicationRef,\n type ComponentRef,\n type EnvironmentProviders,\n type Provider,\n type Type,\n} from '@angular/core';\nimport type { EngineOptions } from '@ng-native/fabric';\nimport { mount } from '@ng-native/platform';\nimport { bindQueries, describeTree, flatten, type BoundQueries } from './queries.ts';\nimport { createFakeFabric, type FakeFabric, type FakeFabricNode } from './test-utils.ts';\nimport { waitFor, type WaitForOptions } from './wait-for.ts';\n\n/**\n * What `render()` takes: Angular Testing Library's options, plus the engine's own.\n *\n * The engine options (`globalStyles`, `conditions`, `tokens` and the rest) are what `mount()`\n * takes on a device, so a test styles a component with the same compiled sheet an app would.\n */\nexport interface RenderOptions<T> extends EngineOptions {\n /** The component's inputs, set before its first change detection, so `required` ones work. */\n inputs?: Record<string, unknown>;\n /** Output handlers, by output name. */\n on?: Record<string, (value: never) => void>;\n /** Environment providers for the app the component is mounted in. */\n providers?: (Provider | EnvironmentProviders)[];\n /** The template form only: what the template may use. */\n imports?: unknown[];\n /** Fields set on the instance: the template form's host, or the component itself. */\n componentProperties?: Partial<T> & Record<string, unknown>;\n}\n\n/**\n * What `render()` resolves to: every query, bound to this render, and the handles to drive it.\n *\n * The queries here stay on this render even after a later one; `screen` moves to the latest.\n */\nexport interface RenderResult<T> extends BoundQueries {\n fabric: FakeFabric;\n componentRef: ComponentRef<T>;\n instance: T;\n /** `ComponentFixture`'s shape, for tests written against it. */\n fixture: {\n componentRef: ComponentRef<T>;\n componentInstance: T;\n detectChanges(): Promise<void>;\n };\n /** Set inputs and properties again, then let the result commit. */\n rerender(changes?: {\n inputs?: Record<string, unknown>;\n componentProperties?: Record<string, unknown>;\n }): Promise<void>;\n /** Run change detection and wait for the commit. */\n detectChanges(): Promise<void>;\n /** Print the committed tree, or one node's subtree. */\n debug(node?: FakeFabricNode): void;\n unmount(): void;\n}\n\ninterface Mounted {\n fabric: FakeFabric;\n applicationRef: ApplicationRef;\n componentRef: ComponentRef<unknown>;\n}\n\nconst mounted: Mounted[] = [];\n/** Which render a node a query returned belongs to, so an event on it reaches the right fake. */\nconst owners = new WeakMap<FakeFabricNode, Mounted>();\n\n/**\n * Let zoneless change detection run and commit.\n *\n * One macrotask is enough: Angular schedules its pass on `setTimeout(0)` (racing an animation\n * frame) when something changes, so a timeout queued after the change runs after the pass.\n */\nexport const settle = (): Promise<void> => new Promise((resolve) => setTimeout(resolve, 0));\n\nfunction latest(): Mounted {\n const current = mounted.at(-1);\n if (!current) throw new Error('Nothing is rendered: call render() first.');\n return current;\n}\n\nexport function ownerOf(node: FakeFabricNode): Mounted {\n return owners.get(node) ?? latest();\n}\n\n/** The node as it is in the most recent commit, found by its tag, or null once it is gone. */\nexport function currentOf(node: FakeFabricNode): FakeFabricNode | null {\n const hit = flatten(ownerOf(node).fabric.committed).find((n) => n.reactTag === node.reactTag);\n if (hit) owners.set(hit, ownerOf(node));\n return hit ?? null;\n}\n\nconst tree = (nodes: readonly FakeFabricNode[]): string => describeTree(nodes) || '(nothing)';\n\nfunction queriesFor(roots: () => readonly FakeFabricNode[], owner: () => Mounted): BoundQueries {\n return bindQueries(\n roots,\n () => tree(roots()),\n (node) => owners.set(node, owner()),\n );\n}\n\n/**\n * The template form, compiled just in time.\n *\n * ponytail: this needs `@angular/compiler` at runtime, the one place anything here does - the\n * class form is AOT like the app. An AOT compile of the string would mean evaluating generated\n * code that imports the caller's own classes by name; do that if JIT ever disagrees with AOT.\n */\nasync function hostFor<T>(template: string, options: RenderOptions<T>): Promise<Type<unknown>> {\n await import('@angular/compiler' as string);\n const properties = options.componentProperties ?? {};\n class WrapperComponent {\n constructor() {\n Object.assign(this, properties);\n }\n }\n return Component({\n selector: 'ng-native-wrapper',\n template,\n imports: (options.imports ?? []) as Type<unknown>[],\n })(WrapperComponent);\n}\n\n/**\n * Mount a component, or a template string, onto a fresh fake Fabric and wait for the first commit.\n *\n * The same `mount()` an app boots with, so what the queries read is exactly what native would\n * have been sent. A string is compiled as a host component's template, with `imports` and\n * `componentProperties` for what it uses; a class is rendered as-is, with `inputs` set before its\n * first change detection so `input.required()` works.\n */\nexport async function render<T>(\n component: Type<T> | string,\n options: RenderOptions<T> = {},\n): Promise<RenderResult<T>> {\n const { inputs, on, providers, imports: _, componentProperties, ...engine } = options;\n const isTemplate = typeof component === 'string';\n const type = isTemplate ? await hostFor(component, options) : component;\n const fabric = createFakeFabric();\n const app = mount(1, type, fabric, {\n ...engine,\n providers,\n inputs: isTemplate ? {} : inputs,\n });\n const self: Mounted = {\n fabric,\n applicationRef: app.applicationRef,\n componentRef: app.componentRef,\n };\n mounted.push(self);\n\n const componentRef = app.componentRef as ComponentRef<T>;\n const instance = componentRef.instance as T & Record<string, unknown>;\n for (const [name, handler] of Object.entries(on ?? {})) {\n (instance[name] as { subscribe(fn: (value: never) => void): void }).subscribe(handler);\n }\n\n const detectChanges = async (): Promise<void> => {\n // The component's own view, not the host's: `componentRef.changeDetectorRef` marks the host\n // and upwards, which leaves an OnPush component with a changed field exactly as it was.\n componentRef.injector.get(ChangeDetectorRef).markForCheck();\n app.applicationRef.tick();\n await settle();\n };\n const rerender: RenderResult<T>['rerender'] = async (changes = {}) => {\n for (const [name, value] of Object.entries(changes.inputs ?? {})) {\n componentRef.setInput(name, value);\n }\n Object.assign(instance, changes.componentProperties);\n await detectChanges();\n };\n\n if (!isTemplate && componentProperties) await rerender({ componentProperties });\n else await settle();\n\n return {\n ...queriesFor(\n () => fabric.committed,\n () => self,\n ),\n fabric,\n componentRef,\n instance,\n fixture: { componentRef, componentInstance: instance, detectChanges },\n rerender,\n detectChanges,\n debug: (node) => console.log(tree(node ? [currentOf(node) ?? node] : fabric.committed)),\n unmount: () => unmount(self),\n };\n}\n\nfunction unmount(target: Mounted): void {\n const at = mounted.indexOf(target);\n if (at === -1) return;\n mounted.splice(at, 1);\n target.applicationRef.destroy();\n}\n\n/** Unmount everything rendered so far. Registered as an `afterEach` where one is global. */\nexport function cleanup(): void {\n for (const target of [...mounted]) unmount(target);\n}\n\n/** Queries over whatever was rendered most recently, as Testing Library's `screen` is. */\nexport const screen: BoundQueries & { debug(node?: FakeFabricNode): void } = {\n ...queriesFor(() => latest().fabric.committed, latest),\n debug: (node) => console.log(tree(node ? [currentOf(node) ?? node] : latest().fabric.committed)),\n};\n\n/** Queries scoped to one node and what is under it, in whichever commit is current. */\nexport function within(node: FakeFabricNode): BoundQueries {\n const owner = ownerOf(node);\n return queriesFor(\n () => {\n const current = currentOf(node);\n return current ? [current] : [];\n },\n () => owner,\n );\n}\n\ntype Present = FakeFabricNode | readonly FakeFabricNode[] | null | undefined;\n\n/** Wait for a node, or whatever a callback finds, to leave the committed tree. */\nexport async function waitForElementToBeRemoved(\n target: FakeFabricNode | readonly FakeFabricNode[] | (() => Present),\n options?: WaitForOptions,\n): Promise<void> {\n const present = (): boolean => {\n let found: Present;\n try {\n found = typeof target === 'function' ? target() : target;\n } catch {\n return false;\n }\n const nodes = found ? ([] as FakeFabricNode[]).concat(found) : [];\n return nodes.some((node) => currentOf(node) !== null);\n };\n if (!present()) {\n throw new Error(\n 'waitForElementToBeRemoved needs the node to be there when it starts; it already is not.',\n );\n }\n await waitFor(() => {\n if (present())\n throw new Error(`Still rendered after the timeout.\\n\\n${tree(latest().fabric.committed)}`);\n }, options);\n}\n\n// Testing Library's own convention: clean up after every test when the runner exposes `afterEach`\n// globally (Vitest with `globals: true`, Jest). Otherwise each `render()` stands alone.\nconst globalAfterEach = (globalThis as { afterEach?: (fn: () => void) => void }).afterEach;\nif (typeof globalAfterEach === 'function') globalAfterEach(cleanup);\n"]}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fake `global.nativeFabricUIManager`. Records every host call so tests can assert on
|
|
3
|
+
* the committed tree without a simulator. This is what lets the whole seam be tested in Node.
|
|
4
|
+
*/
|
|
5
|
+
import type { FabricUIManager } from '@ng-native/fabric';
|
|
6
|
+
export interface FakeFabricNode {
|
|
7
|
+
handle: number;
|
|
8
|
+
reactTag: number;
|
|
9
|
+
viewName: string;
|
|
10
|
+
props: Record<string, unknown>;
|
|
11
|
+
children: FakeFabricNode[];
|
|
12
|
+
instanceHandle: unknown;
|
|
13
|
+
}
|
|
14
|
+
export interface FakeFabric extends FabricUIManager {
|
|
15
|
+
/** Root child set from the most recent `completeRoot`. */
|
|
16
|
+
readonly committed: FakeFabricNode[];
|
|
17
|
+
readonly calls: {
|
|
18
|
+
createNode: number;
|
|
19
|
+
cloneWithChildren: number;
|
|
20
|
+
cloneWithProps: number;
|
|
21
|
+
cloneWithChildrenAndProps: number;
|
|
22
|
+
completeRoot: number;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Indented view-name tree, for golden assertions. `props: true` includes the flattened
|
|
26
|
+
* prop payload, so a parity test catches prop regressions as well as shape ones.
|
|
27
|
+
*/
|
|
28
|
+
render(options?: {
|
|
29
|
+
props?: boolean;
|
|
30
|
+
}): string;
|
|
31
|
+
find(viewName: string): FakeFabricNode | undefined;
|
|
32
|
+
/** Drive the registered Fabric event handler, as the C++ side would. */
|
|
33
|
+
emit(target: FakeFabricNode, topLevelType: string, nativeEvent?: unknown): void;
|
|
34
|
+
/** Every `setIsJSResponder` the renderer made, in order. */
|
|
35
|
+
readonly responderCalls: {
|
|
36
|
+
viewName: string;
|
|
37
|
+
isResponder: boolean;
|
|
38
|
+
block: boolean;
|
|
39
|
+
}[];
|
|
40
|
+
/**
|
|
41
|
+
* Every `dispatchCommand`, in order. `node` is the node it was sent to, as a non-enumerable
|
|
42
|
+
* property: there to read, and left out of a `deepEqual`.
|
|
43
|
+
*/
|
|
44
|
+
readonly commands: {
|
|
45
|
+
viewName: string;
|
|
46
|
+
name: string;
|
|
47
|
+
args: readonly unknown[];
|
|
48
|
+
readonly node?: FakeFabricNode;
|
|
49
|
+
}[];
|
|
50
|
+
/**
|
|
51
|
+
* What `measureInWindow` answers, keyed by `nativeID` and falling back to view name.
|
|
52
|
+
*
|
|
53
|
+
* A test sets the frames it cares about; a node with no entry is not measured at all, which is
|
|
54
|
+
* what the real thing does for a view the platform has not laid out. The two keys are for the
|
|
55
|
+
* two shapes a test comes in: one candidate on screen, where the view name says all there is to
|
|
56
|
+
* say, and several, where which one got measured is the whole question.
|
|
57
|
+
*/
|
|
58
|
+
readonly frames: Map<string, {
|
|
59
|
+
x: number;
|
|
60
|
+
y: number;
|
|
61
|
+
width: number;
|
|
62
|
+
height: number;
|
|
63
|
+
}>;
|
|
64
|
+
reset(): void;
|
|
65
|
+
}
|
|
66
|
+
export declare function createFakeFabric(): FakeFabric;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keep the instance handle off the node's enumerable props. It is the engine's own node, which
|
|
3
|
+
* reaches the whole application, and a node a query returns ends up printed in any failed
|
|
4
|
+
* assertion about it: Node's inspector and Vitest's formatter both walk enumerable keys only.
|
|
5
|
+
*/
|
|
6
|
+
function withHandle(node, instanceHandle) {
|
|
7
|
+
Object.defineProperty(node, 'instanceHandle', {
|
|
8
|
+
value: instanceHandle,
|
|
9
|
+
enumerable: false,
|
|
10
|
+
writable: true,
|
|
11
|
+
});
|
|
12
|
+
return node;
|
|
13
|
+
}
|
|
14
|
+
export function createFakeFabric() {
|
|
15
|
+
let handle = 0;
|
|
16
|
+
let committed = [];
|
|
17
|
+
let handler = null;
|
|
18
|
+
const responderCalls = [];
|
|
19
|
+
const commands = [];
|
|
20
|
+
const frames = new Map();
|
|
21
|
+
const calls = {
|
|
22
|
+
createNode: 0,
|
|
23
|
+
cloneWithChildren: 0,
|
|
24
|
+
cloneWithProps: 0,
|
|
25
|
+
cloneWithChildrenAndProps: 0,
|
|
26
|
+
completeRoot: 0,
|
|
27
|
+
};
|
|
28
|
+
/*
|
|
29
|
+
* The parent each view has been appended to, by tag. Native keeps one parent per view for as
|
|
30
|
+
* long as it lives - `ShadowNodeFamily::setParent` asserts it, in a debug build, and a release
|
|
31
|
+
* build goes on with the old parent and measures and lays out against the wrong ancestry - so a
|
|
32
|
+
* view that moves has to be created again, as React does. This fails the test that tries.
|
|
33
|
+
*/
|
|
34
|
+
const parents = new Map();
|
|
35
|
+
const ROOT = -1;
|
|
36
|
+
const adopt = (parent, child) => {
|
|
37
|
+
const was = parents.get(child.reactTag);
|
|
38
|
+
if (was !== undefined && was !== parent) {
|
|
39
|
+
throw new Error(`${child.viewName} ${child.reactTag} was appended to ${parent} after ${was}: native ` +
|
|
40
|
+
'never re-parents a view, and asserts on it. Create it again instead.');
|
|
41
|
+
}
|
|
42
|
+
parents.set(child.reactTag, parent);
|
|
43
|
+
};
|
|
44
|
+
const clone = (n, props, keepChildren = false) => {
|
|
45
|
+
const source = n;
|
|
46
|
+
return withHandle({
|
|
47
|
+
...source,
|
|
48
|
+
handle: ++handle,
|
|
49
|
+
// Fabric merges raw props onto the source props rather than replacing them.
|
|
50
|
+
props: props ? { ...source.props, ...props } : source.props,
|
|
51
|
+
children: keepChildren ? source.children : [],
|
|
52
|
+
}, source.instanceHandle);
|
|
53
|
+
};
|
|
54
|
+
const node = (reactTag, viewName, props, instanceHandle) => withHandle({ handle: ++handle, reactTag, viewName, props: { ...props }, children: [] }, instanceHandle);
|
|
55
|
+
const walk = (nodes, depth, withProps) => nodes.flatMap((n) => {
|
|
56
|
+
const text = typeof n.props['text'] === 'string' ? ` "${n.props['text']}"` : '';
|
|
57
|
+
const props = withProps && Object.keys(n.props).length && !text
|
|
58
|
+
? ' ' + JSON.stringify(n.props, Object.keys(n.props).sort())
|
|
59
|
+
: '';
|
|
60
|
+
return [
|
|
61
|
+
' '.repeat(depth) + n.viewName + text + props,
|
|
62
|
+
...walk(n.children, depth + 1, withProps),
|
|
63
|
+
];
|
|
64
|
+
});
|
|
65
|
+
const fake = {
|
|
66
|
+
createNode(reactTag, viewName, _rootTag, props, instanceHandle) {
|
|
67
|
+
calls.createNode++;
|
|
68
|
+
return node(reactTag, viewName, props, instanceHandle);
|
|
69
|
+
},
|
|
70
|
+
cloneNodeWithNewChildren(n) {
|
|
71
|
+
calls.cloneWithChildren++;
|
|
72
|
+
return clone(n);
|
|
73
|
+
},
|
|
74
|
+
cloneNodeWithNewProps(n, newProps) {
|
|
75
|
+
calls.cloneWithProps++;
|
|
76
|
+
return clone(n, newProps, true);
|
|
77
|
+
},
|
|
78
|
+
cloneNodeWithNewChildrenAndProps(n, newProps) {
|
|
79
|
+
calls.cloneWithChildrenAndProps++;
|
|
80
|
+
return clone(n, newProps);
|
|
81
|
+
},
|
|
82
|
+
appendChild(parent, child) {
|
|
83
|
+
const into = parent;
|
|
84
|
+
adopt(into.reactTag, child);
|
|
85
|
+
into.children.push(child);
|
|
86
|
+
return parent;
|
|
87
|
+
},
|
|
88
|
+
createChildSet() {
|
|
89
|
+
return [];
|
|
90
|
+
},
|
|
91
|
+
appendChildToSet(set, child) {
|
|
92
|
+
adopt(ROOT, child);
|
|
93
|
+
set.push(child);
|
|
94
|
+
},
|
|
95
|
+
completeRoot(_rootTag, set) {
|
|
96
|
+
calls.completeRoot++;
|
|
97
|
+
committed = set;
|
|
98
|
+
},
|
|
99
|
+
registerEventHandler(fn) {
|
|
100
|
+
handler = fn;
|
|
101
|
+
},
|
|
102
|
+
setIsJSResponder(node, isResponder, block) {
|
|
103
|
+
responderCalls.push({
|
|
104
|
+
viewName: node.viewName,
|
|
105
|
+
isResponder,
|
|
106
|
+
block,
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
measureInWindow(node, onSuccess) {
|
|
110
|
+
// By `nativeID` first, so a test that cares which of two views was measured can say so; by
|
|
111
|
+
// view name otherwise, which is enough when there is only one candidate on screen.
|
|
112
|
+
const fake = node;
|
|
113
|
+
const frame = frames.get(String(fake.props['nativeID'])) ?? frames.get(fake.viewName);
|
|
114
|
+
if (frame)
|
|
115
|
+
onSuccess(frame.x, frame.y, frame.width, frame.height);
|
|
116
|
+
},
|
|
117
|
+
dispatchCommand(node, name, args) {
|
|
118
|
+
const command = { viewName: node.viewName, name, args };
|
|
119
|
+
// Which node, for a test that has several of one view; off the enumerable keys, so a
|
|
120
|
+
// `deepEqual` against `{ viewName, name, args }` still holds.
|
|
121
|
+
Object.defineProperty(command, 'node', { value: node, enumerable: false });
|
|
122
|
+
commands.push(command);
|
|
123
|
+
},
|
|
124
|
+
responderCalls,
|
|
125
|
+
commands,
|
|
126
|
+
frames,
|
|
127
|
+
get committed() {
|
|
128
|
+
return committed;
|
|
129
|
+
},
|
|
130
|
+
calls,
|
|
131
|
+
render(options) {
|
|
132
|
+
return walk(committed, 0, options?.props === true).join('\n');
|
|
133
|
+
},
|
|
134
|
+
find(viewName) {
|
|
135
|
+
const search = (nodes) => {
|
|
136
|
+
for (const n of nodes) {
|
|
137
|
+
if (n.viewName === viewName)
|
|
138
|
+
return n;
|
|
139
|
+
const hit = search(n.children);
|
|
140
|
+
if (hit)
|
|
141
|
+
return hit;
|
|
142
|
+
}
|
|
143
|
+
return undefined;
|
|
144
|
+
};
|
|
145
|
+
return search(committed);
|
|
146
|
+
},
|
|
147
|
+
emit(target, type, nativeEvent = {}) {
|
|
148
|
+
if (!handler)
|
|
149
|
+
throw new Error('no Fabric event handler registered');
|
|
150
|
+
handler(target.instanceHandle, type, nativeEvent);
|
|
151
|
+
},
|
|
152
|
+
reset() {
|
|
153
|
+
calls.createNode = 0;
|
|
154
|
+
calls.cloneWithChildren = 0;
|
|
155
|
+
calls.cloneWithProps = 0;
|
|
156
|
+
calls.cloneWithChildrenAndProps = 0;
|
|
157
|
+
calls.completeRoot = 0;
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
return fake;
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=test-utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-utils.js","sourceRoot":"","sources":["../src/test-utils.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Fake `global.nativeFabricUIManager`. Records every host call so tests can assert on\n * the committed tree without a simulator. This is what lets the whole seam be tested in Node.\n */\nimport type { FabricNode, FabricNodeSet, FabricUIManager } from '@ng-native/fabric';\n\nexport interface FakeFabricNode {\n handle: number;\n reactTag: number;\n viewName: string;\n props: Record<string, unknown>;\n children: FakeFabricNode[];\n instanceHandle: unknown;\n}\n\nexport interface FakeFabric extends FabricUIManager {\n /** Root child set from the most recent `completeRoot`. */\n readonly committed: FakeFabricNode[];\n readonly calls: {\n createNode: number;\n cloneWithChildren: number;\n cloneWithProps: number;\n cloneWithChildrenAndProps: number;\n completeRoot: number;\n };\n /**\n * Indented view-name tree, for golden assertions. `props: true` includes the flattened\n * prop payload, so a parity test catches prop regressions as well as shape ones.\n */\n render(options?: { props?: boolean }): string;\n find(viewName: string): FakeFabricNode | undefined;\n /** Drive the registered Fabric event handler, as the C++ side would. */\n emit(target: FakeFabricNode, topLevelType: string, nativeEvent?: unknown): void;\n /** Every `setIsJSResponder` the renderer made, in order. */\n readonly responderCalls: { viewName: string; isResponder: boolean; block: boolean }[];\n /**\n * Every `dispatchCommand`, in order. `node` is the node it was sent to, as a non-enumerable\n * property: there to read, and left out of a `deepEqual`.\n */\n readonly commands: {\n viewName: string;\n name: string;\n args: readonly unknown[];\n readonly node?: FakeFabricNode;\n }[];\n /**\n * What `measureInWindow` answers, keyed by `nativeID` and falling back to view name.\n *\n * A test sets the frames it cares about; a node with no entry is not measured at all, which is\n * what the real thing does for a view the platform has not laid out. The two keys are for the\n * two shapes a test comes in: one candidate on screen, where the view name says all there is to\n * say, and several, where which one got measured is the whole question.\n */\n readonly frames: Map<string, { x: number; y: number; width: number; height: number }>;\n reset(): void;\n}\n\n/**\n * Keep the instance handle off the node's enumerable props. It is the engine's own node, which\n * reaches the whole application, and a node a query returns ends up printed in any failed\n * assertion about it: Node's inspector and Vitest's formatter both walk enumerable keys only.\n */\nfunction withHandle(node: Omit<FakeFabricNode, 'instanceHandle'>, instanceHandle: unknown) {\n Object.defineProperty(node, 'instanceHandle', {\n value: instanceHandle,\n enumerable: false,\n writable: true,\n });\n return node as FakeFabricNode;\n}\n\nexport function createFakeFabric(): FakeFabric {\n let handle = 0;\n let committed: FakeFabricNode[] = [];\n let handler: ((h: unknown, t: string, e: unknown) => void) | null = null;\n const responderCalls: { viewName: string; isResponder: boolean; block: boolean }[] = [];\n const commands: { viewName: string; name: string; args: readonly unknown[] }[] = [];\n const frames = new Map<string, { x: number; y: number; width: number; height: number }>();\n const calls = {\n createNode: 0,\n cloneWithChildren: 0,\n cloneWithProps: 0,\n cloneWithChildrenAndProps: 0,\n completeRoot: 0,\n };\n\n /*\n * The parent each view has been appended to, by tag. Native keeps one parent per view for as\n * long as it lives - `ShadowNodeFamily::setParent` asserts it, in a debug build, and a release\n * build goes on with the old parent and measures and lays out against the wrong ancestry - so a\n * view that moves has to be created again, as React does. This fails the test that tries.\n */\n const parents = new Map<number, number>();\n const ROOT = -1;\n const adopt = (parent: number, child: FakeFabricNode): void => {\n const was = parents.get(child.reactTag);\n if (was !== undefined && was !== parent) {\n throw new Error(\n `${child.viewName} ${child.reactTag} was appended to ${parent} after ${was}: native ` +\n 'never re-parents a view, and asserts on it. Create it again instead.',\n );\n }\n parents.set(child.reactTag, parent);\n };\n\n const clone = (n: FabricNode, props?: object, keepChildren = false): FabricNode => {\n const source = n as unknown as FakeFabricNode;\n return withHandle(\n {\n ...source,\n handle: ++handle,\n // Fabric merges raw props onto the source props rather than replacing them.\n props: props ? { ...source.props, ...props } : source.props,\n children: keepChildren ? source.children : [],\n },\n source.instanceHandle,\n ) as unknown as FabricNode;\n };\n\n const node = (\n reactTag: number,\n viewName: string,\n props: object,\n instanceHandle: unknown,\n ): FakeFabricNode =>\n withHandle(\n { handle: ++handle, reactTag, viewName, props: { ...props }, children: [] },\n instanceHandle,\n );\n\n const walk = (nodes: FakeFabricNode[], depth: number, withProps: boolean): string[] =>\n nodes.flatMap((n) => {\n const text = typeof n.props['text'] === 'string' ? ` \"${n.props['text']}\"` : '';\n const props =\n withProps && Object.keys(n.props).length && !text\n ? ' ' + JSON.stringify(n.props, Object.keys(n.props).sort())\n : '';\n return [\n ' '.repeat(depth) + n.viewName + text + props,\n ...walk(n.children, depth + 1, withProps),\n ];\n });\n\n const fake: FakeFabric = {\n createNode(reactTag, viewName, _rootTag, props, instanceHandle) {\n calls.createNode++;\n return node(reactTag, viewName, props, instanceHandle) as unknown as FabricNode;\n },\n cloneNodeWithNewChildren(n) {\n calls.cloneWithChildren++;\n return clone(n);\n },\n cloneNodeWithNewProps(n, newProps) {\n calls.cloneWithProps++;\n return clone(n, newProps, true);\n },\n cloneNodeWithNewChildrenAndProps(n, newProps) {\n calls.cloneWithChildrenAndProps++;\n return clone(n, newProps);\n },\n appendChild(parent, child) {\n const into = parent as unknown as FakeFabricNode;\n adopt(into.reactTag, child as unknown as FakeFabricNode);\n into.children.push(child as unknown as FakeFabricNode);\n return parent;\n },\n createChildSet() {\n return [] as FabricNodeSet;\n },\n appendChildToSet(set, child) {\n adopt(ROOT, child as unknown as FakeFabricNode);\n (set as FakeFabricNode[]).push(child as unknown as FakeFabricNode);\n },\n completeRoot(_rootTag, set) {\n calls.completeRoot++;\n committed = set as FakeFabricNode[];\n },\n registerEventHandler(fn) {\n handler = fn;\n },\n setIsJSResponder(node, isResponder, block) {\n responderCalls.push({\n viewName: (node as unknown as FakeFabricNode).viewName,\n isResponder,\n block,\n });\n },\n measureInWindow(node, onSuccess) {\n // By `nativeID` first, so a test that cares which of two views was measured can say so; by\n // view name otherwise, which is enough when there is only one candidate on screen.\n const fake = node as unknown as FakeFabricNode;\n const frame = frames.get(String(fake.props['nativeID'])) ?? frames.get(fake.viewName);\n if (frame) onSuccess(frame.x, frame.y, frame.width, frame.height);\n },\n dispatchCommand(node, name, args) {\n const command = { viewName: (node as unknown as FakeFabricNode).viewName, name, args };\n // Which node, for a test that has several of one view; off the enumerable keys, so a\n // `deepEqual` against `{ viewName, name, args }` still holds.\n Object.defineProperty(command, 'node', { value: node, enumerable: false });\n commands.push(command as typeof command & { node: FakeFabricNode });\n },\n responderCalls,\n commands,\n frames,\n get committed() {\n return committed;\n },\n calls,\n render(options) {\n return walk(committed, 0, options?.props === true).join('\\n');\n },\n find(viewName) {\n const search = (nodes: FakeFabricNode[]): FakeFabricNode | undefined => {\n for (const n of nodes) {\n if (n.viewName === viewName) return n;\n const hit = search(n.children);\n if (hit) return hit;\n }\n return undefined;\n };\n return search(committed);\n },\n emit(target, type, nativeEvent = {}) {\n if (!handler) throw new Error('no Fabric event handler registered');\n handler(target.instanceHandle, type, nativeEvent);\n },\n reset() {\n calls.createNode = 0;\n calls.cloneWithChildren = 0;\n calls.cloneWithProps = 0;\n calls.cloneWithChildrenAndProps = 0;\n calls.completeRoot = 0;\n },\n };\n\n return fake;\n}\n"]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `waitFor`, as Testing Library means it: run the callback until it stops throwing, or give up and
|
|
3
|
+
* throw what it last threw.
|
|
4
|
+
*
|
|
5
|
+
* Polled on real timers. Each wait is a macrotask, which is also what lets zoneless change
|
|
6
|
+
* detection run and commit between attempts, so a retry sees the next frame rather than the same
|
|
7
|
+
* one again.
|
|
8
|
+
*
|
|
9
|
+
* ponytail: real timers only. Under a fake clock the interval never elapses; advance the clock
|
|
10
|
+
* yourself, or use `settle()`, until someone needs the Jest/Vitest timer integration.
|
|
11
|
+
*/
|
|
12
|
+
/** How long `waitFor`, and every `findBy` query, keeps trying. */
|
|
13
|
+
export interface WaitForOptions {
|
|
14
|
+
/** How long to keep trying, in milliseconds. Default 1000. */
|
|
15
|
+
timeout?: number;
|
|
16
|
+
/** How long between attempts, in milliseconds. Default 50. */
|
|
17
|
+
interval?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Retry `callback` until it returns without throwing, and resolve to what it returned.
|
|
21
|
+
*
|
|
22
|
+
* Rejects with the callback's last error once `timeout` has passed, so a failing `waitFor` reads
|
|
23
|
+
* like the assertion inside it rather than like a timeout.
|
|
24
|
+
*/
|
|
25
|
+
export declare function waitFor<T>(callback: () => T | Promise<T>, { timeout, interval }?: WaitForOptions): Promise<T>;
|
package/dist/wait-for.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2
|
+
/**
|
|
3
|
+
* Retry `callback` until it returns without throwing, and resolve to what it returned.
|
|
4
|
+
*
|
|
5
|
+
* Rejects with the callback's last error once `timeout` has passed, so a failing `waitFor` reads
|
|
6
|
+
* like the assertion inside it rather than like a timeout.
|
|
7
|
+
*/
|
|
8
|
+
export async function waitFor(callback, { timeout = 1000, interval = 50 } = {}) {
|
|
9
|
+
const deadline = Date.now() + timeout;
|
|
10
|
+
for (;;) {
|
|
11
|
+
try {
|
|
12
|
+
return await callback();
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (Date.now() >= deadline)
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
await sleep(interval);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=wait-for.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wait-for.js","sourceRoot":"","sources":["../src/wait-for.ts"],"names":[],"mappings":"AAmBA,MAAM,KAAK,GAAG,CAAC,EAAU,EAAiB,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAE/F;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,QAA8B,EAC9B,EAAE,OAAO,GAAG,IAAI,EAAE,QAAQ,GAAG,EAAE,KAAqB,EAAE;IAEtD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC;IACtC,SAAS,CAAC;QACR,IAAI,CAAC;YACH,OAAO,MAAM,QAAQ,EAAE,CAAC;QAC1B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,QAAQ;gBAAE,MAAM,KAAK,CAAC;QAC1C,CAAC;QACD,MAAM,KAAK,CAAC,QAAQ,CAAC,CAAC;IACxB,CAAC;AACH,CAAC","sourcesContent":["/**\n * `waitFor`, as Testing Library means it: run the callback until it stops throwing, or give up and\n * throw what it last threw.\n *\n * Polled on real timers. Each wait is a macrotask, which is also what lets zoneless change\n * detection run and commit between attempts, so a retry sees the next frame rather than the same\n * one again.\n *\n * ponytail: real timers only. Under a fake clock the interval never elapses; advance the clock\n * yourself, or use `settle()`, until someone needs the Jest/Vitest timer integration.\n */\n/** How long `waitFor`, and every `findBy` query, keeps trying. */\nexport interface WaitForOptions {\n /** How long to keep trying, in milliseconds. Default 1000. */\n timeout?: number;\n /** How long between attempts, in milliseconds. Default 50. */\n interval?: number;\n}\n\nconst sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));\n\n/**\n * Retry `callback` until it returns without throwing, and resolve to what it returned.\n *\n * Rejects with the callback's last error once `timeout` has passed, so a failing `waitFor` reads\n * like the assertion inside it rather than like a timeout.\n */\nexport async function waitFor<T>(\n callback: () => T | Promise<T>,\n { timeout = 1000, interval = 50 }: WaitForOptions = {},\n): Promise<T> {\n const deadline = Date.now() + timeout;\n for (;;) {\n try {\n return await callback();\n } catch (error) {\n if (Date.now() >= deadline) throw error;\n }\n await sleep(interval);\n }\n}\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a test gets for `react-native-worklets`, whose source Node cannot load. There is one
|
|
3
|
+
* runtime in a test, so scheduling a function on either runtime calls it at once.
|
|
4
|
+
*/
|
|
5
|
+
export declare function scheduleOnRN<A extends unknown[]>(fn: (...args: A) => unknown, ...args: A): void;
|
|
6
|
+
export declare function scheduleOnUI<A extends unknown[]>(fn: (...args: A) => unknown, ...args: A): void;
|
|
7
|
+
export declare function runOnUISync<A extends unknown[], R>(fn: (...args: A) => R, ...args: A): R;
|
|
8
|
+
export declare function runOnJS<A extends unknown[], R>(fn: (...args: A) => R): (...args: A) => R;
|
|
9
|
+
export declare function runOnUI<A extends unknown[], R>(fn: (...args: A) => R): (...args: A) => R;
|
|
10
|
+
export declare function isWorkletFunction(_value: unknown): boolean;
|