@ng-native/testing 0.1.3 → 0.3.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/dist/index.d.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  */
9
9
  export { createFakeFabric, type FakeFabric, type FakeFabricNode } from './test-utils.ts';
10
10
  export { cleanup, render, screen, settle, waitForElementToBeRemoved, within, type RenderOptions, type RenderResult, } from './render.ts';
11
+ export { injectService, type InjectServiceOptions } from './inject-service.ts';
11
12
  export type { BoundQueries, ByRoleOptions, Matcher, TextMatchOptions } from './queries.ts';
12
13
  export { waitFor, type WaitForOptions } from './wait-for.ts';
13
14
  export { gestureOf } from './gesture-of.ts';
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@
8
8
  */
9
9
  export { createFakeFabric } from "./test-utils.js";
10
10
  export { cleanup, render, screen, settle, waitForElementToBeRemoved, within, } from "./render.js";
11
+ export { injectService } from "./inject-service.js";
11
12
  export { waitFor } from "./wait-for.js";
12
13
  export { gestureOf } from "./gesture-of.js";
13
14
  export { fireEvent, userEvent, } from "./events.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,gBAAgB,EAAwC,MAAM,iBAAiB,CAAC;AACzF,OAAO,EACL,OAAO,EACP,MAAM,EACN,MAAM,EACN,MAAM,EACN,yBAAyB,EACzB,MAAM,GAGP,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,OAAO,EAAuB,MAAM,eAAe,CAAC;AAC7D,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,OAAO,EACL,SAAS,EACT,SAAS,GAIV,MAAM,aAAa,CAAC","sourcesContent":["/**\n * The fake `nativeFabricUIManager`, and a Testing Library on top of it. Kept out of the engine so\n * nothing in a shipped bundle can reach it, and out of `test/` so an app's own tests can use it.\n *\n * The runners live beside `src/` rather than in it: `@ng-native/testing/vitest` and\n * `@ng-native/testing/register` are plain JavaScript, because they are loaded by Node before\n * anything can compile TypeScript for it.\n */\nexport { createFakeFabric, type FakeFabric, type FakeFabricNode } from './test-utils.ts';\nexport {\n cleanup,\n render,\n screen,\n settle,\n waitForElementToBeRemoved,\n within,\n type RenderOptions,\n type RenderResult,\n} from './render.ts';\nexport type { BoundQueries, ByRoleOptions, Matcher, TextMatchOptions } from './queries.ts';\nexport { waitFor, type WaitForOptions } from './wait-for.ts';\nexport { gestureOf } from './gesture-of.ts';\nexport type { TestGesture } from './gesture-handler.ts';\nexport {\n fireEvent,\n userEvent,\n type EventPayload,\n type TypeOptions,\n type UserEvent,\n} from './events.ts';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,gBAAgB,EAAwC,MAAM,iBAAiB,CAAC;AACzF,OAAO,EACL,OAAO,EACP,MAAM,EACN,MAAM,EACN,MAAM,EACN,yBAAyB,EACzB,MAAM,GAGP,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,aAAa,EAA6B,MAAM,qBAAqB,CAAC;AAE/E,OAAO,EAAE,OAAO,EAAuB,MAAM,eAAe,CAAC;AAC7D,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,OAAO,EACL,SAAS,EACT,SAAS,GAIV,MAAM,aAAa,CAAC","sourcesContent":["/**\n * The fake `nativeFabricUIManager`, and a Testing Library on top of it. Kept out of the engine so\n * nothing in a shipped bundle can reach it, and out of `test/` so an app's own tests can use it.\n *\n * The runners live beside `src/` rather than in it: `@ng-native/testing/vitest` and\n * `@ng-native/testing/register` are plain JavaScript, because they are loaded by Node before\n * anything can compile TypeScript for it.\n */\nexport { createFakeFabric, type FakeFabric, type FakeFabricNode } from './test-utils.ts';\nexport {\n cleanup,\n render,\n screen,\n settle,\n waitForElementToBeRemoved,\n within,\n type RenderOptions,\n type RenderResult,\n} from './render.ts';\nexport { injectService, type InjectServiceOptions } from './inject-service.ts';\nexport type { BoundQueries, ByRoleOptions, Matcher, TextMatchOptions } from './queries.ts';\nexport { waitFor, type WaitForOptions } from './wait-for.ts';\nexport { gestureOf } from './gesture-of.ts';\nexport type { TestGesture } from './gesture-handler.ts';\nexport {\n fireEvent,\n userEvent,\n type EventPayload,\n type TypeOptions,\n type UserEvent,\n} from './events.ts';\n"]}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A service on its own, with no component to render.
3
+ *
4
+ * There is no TestBed, and `Injector.create()` is no stand-in for it: an injector made that way
5
+ * has no root scope, so a `@Service()` or `providedIn: 'root'` class is not found in it, nor is
6
+ * anything such a class injects. The app injector `mount()` creates is the root an app has, so
7
+ * the service gets the same providers, `DestroyRef` and platform it gets on a device.
8
+ */
9
+ import { type EnvironmentProviders, type Provider, type ProviderToken } from '@angular/core';
10
+ export interface InjectServiceOptions {
11
+ /** Providers for the app the service is created in: a stand-in for what it injects. */
12
+ providers?: (Provider | EnvironmentProviders)[];
13
+ }
14
+ /**
15
+ * The instance of `token` from a fresh app's root injector, built with `providers`. The app is
16
+ * destroyed by `cleanup()`, which ends the service's `DestroyRef`.
17
+ */
18
+ export declare function injectService<T>(token: ProviderToken<T>, options?: InjectServiceOptions): T;
19
+ /**
20
+ * Destroy every app `injectService` created. Called by `cleanup()`. A teardown that throws does
21
+ * not stop the rest: each app is destroyed, then the first error is thrown.
22
+ */
23
+ export declare function destroyServiceApps(): void;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * A service on its own, with no component to render.
3
+ *
4
+ * There is no TestBed, and `Injector.create()` is no stand-in for it: an injector made that way
5
+ * has no root scope, so a `@Service()` or `providedIn: 'root'` class is not found in it, nor is
6
+ * anything such a class injects. The app injector `mount()` creates is the root an app has, so
7
+ * the service gets the same providers, `DestroyRef` and platform it gets on a device.
8
+ */
9
+ import { Component, } from '@angular/core';
10
+ import { mount } from '@ng-native/platform';
11
+ import { createFakeFabric } from "./test-utils.js";
12
+ import * as i0 from "@angular/core";
13
+ class ServiceHost {
14
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: ServiceHost, deps: [], target: i0.ɵɵFactoryTarget.Component });
15
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.2.0", type: ServiceHost, isStandalone: true, selector: "ng-native-service-host", ngImport: i0, template: '', isInline: true });
16
+ }
17
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: ServiceHost, decorators: [{
18
+ type: Component,
19
+ args: [{ selector: 'ng-native-service-host', template: '' }]
20
+ }] });
21
+ const apps = [];
22
+ /**
23
+ * The instance of `token` from a fresh app's root injector, built with `providers`. The app is
24
+ * destroyed by `cleanup()`, which ends the service's `DestroyRef`.
25
+ */
26
+ export function injectService(token, options = {}) {
27
+ const { applicationRef } = mount(1, ServiceHost, createFakeFabric(), {
28
+ providers: options.providers,
29
+ });
30
+ apps.push(applicationRef);
31
+ return applicationRef.injector.get(token);
32
+ }
33
+ /**
34
+ * Destroy every app `injectService` created. Called by `cleanup()`. A teardown that throws does
35
+ * not stop the rest: each app is destroyed, then the first error is thrown.
36
+ */
37
+ export function destroyServiceApps() {
38
+ const errors = [];
39
+ for (const app of apps.splice(0)) {
40
+ try {
41
+ app.destroy();
42
+ }
43
+ catch (error) {
44
+ errors.push(error);
45
+ }
46
+ }
47
+ if (errors.length > 0)
48
+ throw errors[0];
49
+ }
50
+ //# sourceMappingURL=inject-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inject-service.js","sourceRoot":"","sources":["../src/inject-service.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,SAAS,GAKV,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;;AAOnD,MACM,WAAW;uGAAX,WAAW;2FAAX,WAAW,kFAD0C,EAAE;;2FACvD,WAAW;kBADhB,SAAS;mBAAC,EAAE,QAAQ,EAAE,wBAAwB,EAAE,QAAQ,EAAE,EAAE,EAAE;;AAG/D,MAAM,IAAI,GAAqB,EAAE,CAAC;AAElC;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAI,KAAuB,EAAE,UAAgC,EAAE;IAC1F,MAAM,EAAE,cAAc,EAAE,GAAG,KAAK,CAAC,CAAC,EAAE,WAAW,EAAE,gBAAgB,EAAE,EAAE;QACnE,SAAS,EAAE,OAAO,CAAC,SAAS;KAC7B,CAAC,CAAC;IACH,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IAC1B,OAAO,cAAc,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AAC5C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB;IAChC,MAAM,MAAM,GAAc,EAAE,CAAC;IAC7B,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;QACjC,IAAI,CAAC;YACH,GAAG,CAAC,OAAO,EAAE,CAAC;QAChB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,MAAM,CAAC,CAAC,CAAC,CAAC;AACzC,CAAC","sourcesContent":["/**\n * A service on its own, with no component to render.\n *\n * There is no TestBed, and `Injector.create()` is no stand-in for it: an injector made that way\n * has no root scope, so a `@Service()` or `providedIn: 'root'` class is not found in it, nor is\n * anything such a class injects. The app injector `mount()` creates is the root an app has, so\n * the service gets the same providers, `DestroyRef` and platform it gets on a device.\n */\nimport {\n Component,\n type ApplicationRef,\n type EnvironmentProviders,\n type Provider,\n type ProviderToken,\n} from '@angular/core';\nimport { mount } from '@ng-native/platform';\nimport { createFakeFabric } from './test-utils.ts';\n\nexport interface InjectServiceOptions {\n /** Providers for the app the service is created in: a stand-in for what it injects. */\n providers?: (Provider | EnvironmentProviders)[];\n}\n\n@Component({ selector: 'ng-native-service-host', template: '' })\nclass ServiceHost {}\n\nconst apps: ApplicationRef[] = [];\n\n/**\n * The instance of `token` from a fresh app's root injector, built with `providers`. The app is\n * destroyed by `cleanup()`, which ends the service's `DestroyRef`.\n */\nexport function injectService<T>(token: ProviderToken<T>, options: InjectServiceOptions = {}): T {\n const { applicationRef } = mount(1, ServiceHost, createFakeFabric(), {\n providers: options.providers,\n });\n apps.push(applicationRef);\n return applicationRef.injector.get(token);\n}\n\n/**\n * Destroy every app `injectService` created. Called by `cleanup()`. A teardown that throws does\n * not stop the rest: each app is destroyed, then the first error is thrown.\n */\nexport function destroyServiceApps(): void {\n const errors: unknown[] = [];\n for (const app of apps.splice(0)) {\n try {\n app.destroy();\n } catch (error) {\n errors.push(error);\n }\n }\n if (errors.length > 0) throw errors[0];\n}\n"]}
package/dist/render.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * - onto a `createFakeFabric()`, so what a query reads is what native would have been handed.
7
7
  */
8
8
  import { type ApplicationRef, type ComponentRef, type EnvironmentProviders, type Provider, type Type } from '@angular/core';
9
- import type { EngineOptions } from '@ng-native/fabric';
9
+ import type { Engine, EngineOptions } from '@ng-native/fabric';
10
10
  import { type BoundQueries } from './queries.ts';
11
11
  import { type FakeFabric, type FakeFabricNode } from './test-utils.ts';
12
12
  import { type WaitForOptions } from './wait-for.ts';
@@ -56,16 +56,20 @@ export interface RenderResult<T> extends BoundQueries {
56
56
  }
57
57
  interface Mounted {
58
58
  fabric: FakeFabric;
59
+ engine: Engine;
59
60
  applicationRef: ApplicationRef;
60
61
  componentRef: ComponentRef<unknown>;
61
62
  }
62
63
  /**
63
64
  * Let zoneless change detection run and commit.
64
65
  *
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.
66
+ * One macrotask is enough for a change Angular sees: it schedules its pass on `setTimeout(0)`
67
+ * (racing an animation frame), so a timeout queued after the change runs after the pass. A change
68
+ * the engine sees outside change detection, such as a modal's reported dismissal with no
69
+ * `(dismiss)` listener, commits on the platform's next frame instead, 16ms away in Node, so this
70
+ * waits for that frame while a commit is owed.
67
71
  */
68
- export declare const settle: () => Promise<void>;
72
+ export declare function settle(): Promise<void>;
69
73
  export declare function ownerOf(node: FakeFabricNode): Mounted;
70
74
  /** The node as it is in the most recent commit, found by its tag, or null once it is gone. */
71
75
  export declare function currentOf(node: FakeFabricNode): FakeFabricNode | null;
@@ -78,7 +82,10 @@ export declare function currentOf(node: FakeFabricNode): FakeFabricNode | null;
78
82
  * first change detection so `input.required()` works.
79
83
  */
80
84
  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. */
85
+ /**
86
+ * Unmount everything rendered so far, and destroy every app `injectService` created. Registered
87
+ * as an `afterEach` where one is global.
88
+ */
82
89
  export declare function cleanup(): void;
83
90
  /** Queries over whatever was rendered most recently, as Testing Library's `screen` is. */
84
91
  export declare const screen: BoundQueries & {
package/dist/render.js CHANGED
@@ -15,19 +15,29 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
15
15
  */
16
16
  import { ChangeDetectorRef, Component, } from '@angular/core';
17
17
  import { mount } from '@ng-native/platform';
18
+ import { destroyServiceApps } from "./inject-service.js";
18
19
  import { bindQueries, describeTree, flatten } from "./queries.js";
19
20
  import { createFakeFabric } from "./test-utils.js";
20
21
  import { waitFor } from "./wait-for.js";
21
22
  const mounted = [];
22
23
  /** Which render a node a query returned belongs to, so an event on it reaches the right fake. */
23
24
  const owners = new WeakMap();
25
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
24
26
  /**
25
27
  * Let zoneless change detection run and commit.
26
28
  *
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
+ * One macrotask is enough for a change Angular sees: it schedules its pass on `setTimeout(0)`
30
+ * (racing an animation frame), so a timeout queued after the change runs after the pass. A change
31
+ * the engine sees outside change detection, such as a modal's reported dismissal with no
32
+ * `(dismiss)` listener, commits on the platform's next frame instead, 16ms away in Node, so this
33
+ * waits for that frame while a commit is owed.
29
34
  */
30
- export const settle = () => new Promise((resolve) => setTimeout(resolve, 0));
35
+ export async function settle() {
36
+ await sleep(0);
37
+ for (let frames = 0; frames < 3 && mounted.some((m) => m.engine.pending); frames++) {
38
+ await sleep(16);
39
+ }
40
+ }
31
41
  function latest() {
32
42
  const current = mounted.at(-1);
33
43
  if (!current)
@@ -89,6 +99,7 @@ export async function render(component, options = {}) {
89
99
  });
90
100
  const self = {
91
101
  fabric,
102
+ engine: app.engine,
92
103
  applicationRef: app.applicationRef,
93
104
  componentRef: app.componentRef,
94
105
  };
@@ -135,10 +146,14 @@ function unmount(target) {
135
146
  mounted.splice(at, 1);
136
147
  target.applicationRef.destroy();
137
148
  }
138
- /** Unmount everything rendered so far. Registered as an `afterEach` where one is global. */
149
+ /**
150
+ * Unmount everything rendered so far, and destroy every app `injectService` created. Registered
151
+ * as an `afterEach` where one is global.
152
+ */
139
153
  export function cleanup() {
140
154
  for (const target of [...mounted])
141
155
  unmount(target);
156
+ destroyServiceApps();
142
157
  }
143
158
  /** Queries over whatever was rendered most recently, as Testing Library's `screen` is. */
144
159
  export const screen = {
@@ -1 +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"]}
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":";;;;;;;;AAAA;;;;;;GAMG;AACH,OAAO,EACL,iBAAiB,EACjB,SAAS,GAMV,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAC5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,OAAO,EAAqB,MAAM,cAAc,CAAC;AACrF,OAAO,EAAE,gBAAgB,EAAwC,MAAM,iBAAiB,CAAC;AACzF,OAAO,EAAE,OAAO,EAAuB,MAAM,eAAe,CAAC;AAuD7D,MAAM,OAAO,GAAc,EAAE,CAAC;AAC9B,iGAAiG;AACjG,MAAM,MAAM,GAAG,IAAI,OAAO,EAA2B,CAAC;AAEtD,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;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,MAAM;IAC1B,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC;IACf,KAAK,IAAI,MAAM,GAAG,CAAC,EAAE,MAAM,GAAG,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC;QACnF,MAAM,KAAK,CAAC,EAAE,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,SAAS,MAAM;IACb,MAAM,OAAO,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/B,IAAI,CAAC,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;IAC3E,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,IAAoB;IAC1C,OAAO,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,MAAM,EAAE,CAAC;AACtC,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,SAAS,CAAC,IAAoB;IAC5C,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC9F,IAAI,GAAG;QAAE,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACxC,OAAO,GAAG,IAAI,IAAI,CAAC;AACrB,CAAC;AAED,MAAM,IAAI,GAAG,CAAC,KAAgC,EAAU,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,WAAW,CAAC;AAE9F,SAAS,UAAU,CAAC,KAAsC,EAAE,KAAoB;IAC9E,OAAO,WAAW,CAChB,KAAK,EACL,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,EACnB,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CACpC,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,KAAK,UAAU,OAAO,CAAI,QAAgB,EAAE,OAAyB;IACnE,MAAM,MAAM,kCAAC,mBAA6B,EAAC,CAAC;IAC5C,MAAM,UAAU,GAAG,OAAO,CAAC,mBAAmB,IAAI,EAAE,CAAC;IACrD,MAAM,gBAAgB;QACpB;YACE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;QAClC,CAAC;KACF;IACD,OAAO,SAAS,CAAC;QACf,QAAQ,EAAE,mBAAmB;QAC7B,QAAQ;QACR,OAAO,EAAE,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAoB;KACpD,CAAC,CAAC,gBAAgB,CAAC,CAAC;AACvB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,MAAM,CAC1B,SAA2B,EAC3B,UAA4B,EAAE;IAE9B,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE,mBAAmB,EAAE,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC;IACtF,MAAM,UAAU,GAAG,OAAO,SAAS,KAAK,QAAQ,CAAC;IACjD,MAAM,IAAI,GAAG,UAAU,CAAC,CAAC,CAAC,MAAM,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACxE,MAAM,MAAM,GAAG,gBAAgB,EAAE,CAAC;IAClC,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE;QACjC,GAAG,MAAM;QACT,SAAS;QACT,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM;KACjC,CAAC,CAAC;IACH,MAAM,IAAI,GAAY;QACpB,MAAM;QACN,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,cAAc,EAAE,GAAG,CAAC,cAAc;QAClC,YAAY,EAAE,GAAG,CAAC,YAAY;KAC/B,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEnB,MAAM,YAAY,GAAG,GAAG,CAAC,YAA+B,CAAC;IACzD,MAAM,QAAQ,GAAG,YAAY,CAAC,QAAuC,CAAC;IACtE,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QACtD,QAAQ,CAAC,IAAI,CAAqD,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IACzF,CAAC;IAED,MAAM,aAAa,GAAG,KAAK,IAAmB,EAAE;QAC9C,4FAA4F;QAC5F,wFAAwF;QACxF,YAAY,CAAC,QAAQ,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,YAAY,EAAE,CAAC;QAC5D,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;QAC1B,MAAM,MAAM,EAAE,CAAC;IACjB,CAAC,CAAC;IACF,MAAM,QAAQ,GAAgC,KAAK,EAAE,OAAO,GAAG,EAAE,EAAE,EAAE;QACnE,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,CAAC;YACjE,YAAY,CAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QACrC,CAAC;QACD,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,mBAAmB,CAAC,CAAC;QACrD,MAAM,aAAa,EAAE,CAAC;IACxB,CAAC,CAAC;IAEF,IAAI,CAAC,UAAU,IAAI,mBAAmB;QAAE,MAAM,QAAQ,CAAC,EAAE,mBAAmB,EAAE,CAAC,CAAC;;QAC3E,MAAM,MAAM,EAAE,CAAC;IAEpB,OAAO;QACL,GAAG,UAAU,CACX,GAAG,EAAE,CAAC,MAAM,CAAC,SAAS,EACtB,GAAG,EAAE,CAAC,IAAI,CACX;QACD,MAAM;QACN,YAAY;QACZ,QAAQ;QACR,OAAO,EAAE,EAAE,YAAY,EAAE,iBAAiB,EAAE,QAAQ,EAAE,aAAa,EAAE;QACrE,QAAQ;QACR,aAAa;QACb,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACvF,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;KAC7B,CAAC;AACJ,CAAC;AAED,SAAS,OAAO,CAAC,MAAe;IAC9B,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACnC,IAAI,EAAE,KAAK,CAAC,CAAC;QAAE,OAAO;IACtB,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IACtB,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,CAAC;AAClC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,OAAO;IACrB,KAAK,MAAM,MAAM,IAAI,CAAC,GAAG,OAAO,CAAC;QAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACnD,kBAAkB,EAAE,CAAC;AACvB,CAAC;AAED,0FAA0F;AAC1F,MAAM,CAAC,MAAM,MAAM,GAA0D;IAC3E,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC;IACtD,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;CACjG,CAAC;AAEF,uFAAuF;AACvF,MAAM,UAAU,MAAM,CAAC,IAAoB;IACzC,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,OAAO,UAAU,CACf,GAAG,EAAE;QACH,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;QAChC,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAClC,CAAC,EACD,GAAG,EAAE,CAAC,KAAK,CACZ,CAAC;AACJ,CAAC;AAID,kFAAkF;AAClF,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,MAAoE,EACpE,OAAwB;IAExB,MAAM,OAAO,GAAG,GAAY,EAAE;QAC5B,IAAI,KAAc,CAAC;QACnB,IAAI,CAAC;YACH,KAAK,GAAG,OAAO,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;QAC3D,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;QACD,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAE,EAAuB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAClE,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC;IACxD,CAAC,CAAC;IACF,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACb,yFAAyF,CAC1F,CAAC;IACJ,CAAC;IACD,MAAM,OAAO,CAAC,GAAG,EAAE;QACjB,IAAI,OAAO,EAAE;YACX,MAAM,IAAI,KAAK,CAAC,wCAAwC,IAAI,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IAC/F,CAAC,EAAE,OAAO,CAAC,CAAC;AACd,CAAC;AAED,kGAAkG;AAClG,wFAAwF;AACxF,MAAM,eAAe,GAAI,UAAuD,CAAC,SAAS,CAAC;AAC3F,IAAI,OAAO,eAAe,KAAK,UAAU;IAAE,eAAe,CAAC,OAAO,CAAC,CAAC","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 { Engine, EngineOptions } from '@ng-native/fabric';\nimport { mount } from '@ng-native/platform';\nimport { destroyServiceApps } from './inject-service.ts';\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 engine: Engine;\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\nconst sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));\n\n/**\n * Let zoneless change detection run and commit.\n *\n * One macrotask is enough for a change Angular sees: it schedules its pass on `setTimeout(0)`\n * (racing an animation frame), so a timeout queued after the change runs after the pass. A change\n * the engine sees outside change detection, such as a modal's reported dismissal with no\n * `(dismiss)` listener, commits on the platform's next frame instead, 16ms away in Node, so this\n * waits for that frame while a commit is owed.\n */\nexport async function settle(): Promise<void> {\n await sleep(0);\n for (let frames = 0; frames < 3 && mounted.some((m) => m.engine.pending); frames++) {\n await sleep(16);\n }\n}\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 engine: app.engine,\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/**\n * Unmount everything rendered so far, and destroy every app `injectService` created. Registered\n * as an `afterEach` where one is global.\n */\nexport function cleanup(): void {\n for (const target of [...mounted]) unmount(target);\n destroyServiceApps();\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"]}
@@ -41,8 +41,74 @@ export function createFakeFabric() {
41
41
  }
42
42
  parents.set(child.reactTag, parent);
43
43
  };
44
+ /*
45
+ * The views that have been unmounted, by tag. Fabric drops a view's event target once no
46
+ * committed tree holds it (`EventEmitter::setEnabled`) and never makes a new one, so a view
47
+ * committed again after leaving the tree dispatches every event with a null instance handle, as
48
+ * `emit` does here. A view that comes back has to be created again, as React does.
49
+ */
50
+ const unmounted = new Set();
51
+ const unmount = (n) => {
52
+ unmounted.add(n.reactTag);
53
+ for (const child of n.children)
54
+ unmount(child);
55
+ };
56
+ // Walks only what changed, as `updateMountedFlag` does: a subtree committed as it was is skipped.
57
+ const diff = (before, after) => {
58
+ if (before === after)
59
+ return;
60
+ const next = new Map(after.map((n) => [n.reactTag, n]));
61
+ for (const was of before) {
62
+ const now = next.get(was.reactTag);
63
+ if (!now)
64
+ unmount(was);
65
+ else if (now !== was)
66
+ diff(was.children, now.children);
67
+ }
68
+ };
69
+ /*
70
+ * iOS animates a presented modal out when it is committed with `visible: false`, then reports
71
+ * `topDismiss` once, and React Native's Modal.js keeps it in the tree until then. The fake
72
+ * reports it as soon as that commit is done, and lets one dismissal through per close, so a test
73
+ * that also sends it by hand does not see `(dismiss)` twice.
74
+ */
75
+ const dismissing = new Set();
76
+ const dismissed = new Set();
77
+ const dismissAfterCommit = () => {
78
+ if (dismissing.size === 0 || !handler)
79
+ return;
80
+ const tags = [...dismissing];
81
+ dismissing.clear();
82
+ queueMicrotask(() => {
83
+ for (const tag of tags) {
84
+ const host = findByTag(committed, tag);
85
+ if (host && host.props['visible'] === false)
86
+ fake.emit(host, 'topDismiss');
87
+ }
88
+ });
89
+ };
90
+ const findByTag = (nodes, tag) => {
91
+ for (const n of nodes) {
92
+ const hit = n.reactTag === tag ? n : findByTag(n.children, tag);
93
+ if (hit)
94
+ return hit;
95
+ }
96
+ return undefined;
97
+ };
98
+ /** A modal host committed as closing, or that left the tree closing. */
99
+ const closing = (target) => (findByTag(committed, target.reactTag) ?? target).props['visible'] === false;
100
+ /** A modal host's props are changing: a close owes one dismissal, and a show resets it. */
101
+ const noteModal = (source, props) => {
102
+ if (source.viewName !== 'ModalHostView' || !props || !('visible' in props))
103
+ return;
104
+ if (props['visible'] !== false)
105
+ dismissed.delete(source.reactTag);
106
+ else if (source.props['visible'] !== false)
107
+ dismissing.add(source.reactTag);
108
+ };
44
109
  const clone = (n, props, keepChildren = false) => {
45
110
  const source = n;
111
+ noteModal(source, props);
46
112
  return withHandle({
47
113
  ...source,
48
114
  handle: ++handle,
@@ -100,7 +166,9 @@ export function createFakeFabric() {
100
166
  },
101
167
  completeRoot(_rootTag, set) {
102
168
  calls.completeRoot++;
169
+ diff(committed, set);
103
170
  committed = set;
171
+ dismissAfterCommit();
104
172
  },
105
173
  registerEventHandler(fn) {
106
174
  handler = fn;
@@ -153,7 +221,13 @@ export function createFakeFabric() {
153
221
  emit(target, type, nativeEvent = {}) {
154
222
  if (!handler)
155
223
  throw new Error('no Fabric event handler registered');
156
- handler(target.instanceHandle, type, nativeEvent);
224
+ if (type === 'topDismiss' && target.viewName === 'ModalHostView') {
225
+ if (dismissed.has(target.reactTag))
226
+ return;
227
+ if (closing(target))
228
+ dismissed.add(target.reactTag);
229
+ }
230
+ handler(unmounted.has(target.reactTag) ? null : target.instanceHandle, type, nativeEvent);
157
231
  },
158
232
  reset() {
159
233
  calls.createNode = 0;
@@ -1 +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 // Keys sorted at every depth, so a golden does not depend on the order props were set in. An\n // array of keys as the replacer would sort too, but it is an allow-list for nested objects as\n // well, and printed `accessibilityState: { disabled: true }` as `{}`.\n const sorted = (_key: string, value: unknown): unknown =>\n value && typeof value === 'object' && !Array.isArray(value)\n ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))\n : value;\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, sorted)\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"]}
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 /*\n * The views that have been unmounted, by tag. Fabric drops a view's event target once no\n * committed tree holds it (`EventEmitter::setEnabled`) and never makes a new one, so a view\n * committed again after leaving the tree dispatches every event with a null instance handle, as\n * `emit` does here. A view that comes back has to be created again, as React does.\n */\n const unmounted = new Set<number>();\n const unmount = (n: FakeFabricNode): void => {\n unmounted.add(n.reactTag);\n for (const child of n.children) unmount(child);\n };\n // Walks only what changed, as `updateMountedFlag` does: a subtree committed as it was is skipped.\n const diff = (before: FakeFabricNode[], after: FakeFabricNode[]): void => {\n if (before === after) return;\n const next = new Map(after.map((n) => [n.reactTag, n]));\n for (const was of before) {\n const now = next.get(was.reactTag);\n if (!now) unmount(was);\n else if (now !== was) diff(was.children, now.children);\n }\n };\n\n /*\n * iOS animates a presented modal out when it is committed with `visible: false`, then reports\n * `topDismiss` once, and React Native's Modal.js keeps it in the tree until then. The fake\n * reports it as soon as that commit is done, and lets one dismissal through per close, so a test\n * that also sends it by hand does not see `(dismiss)` twice.\n */\n const dismissing = new Set<number>();\n const dismissed = new Set<number>();\n const dismissAfterCommit = (): void => {\n if (dismissing.size === 0 || !handler) return;\n const tags = [...dismissing];\n dismissing.clear();\n queueMicrotask(() => {\n for (const tag of tags) {\n const host = findByTag(committed, tag);\n if (host && host.props['visible'] === false) fake.emit(host, 'topDismiss');\n }\n });\n };\n const findByTag = (nodes: FakeFabricNode[], tag: number): FakeFabricNode | undefined => {\n for (const n of nodes) {\n const hit = n.reactTag === tag ? n : findByTag(n.children, tag);\n if (hit) return hit;\n }\n return undefined;\n };\n /** A modal host committed as closing, or that left the tree closing. */\n const closing = (target: FakeFabricNode): boolean =>\n (findByTag(committed, target.reactTag) ?? target).props['visible'] === false;\n /** A modal host's props are changing: a close owes one dismissal, and a show resets it. */\n const noteModal = (source: FakeFabricNode, props: Record<string, unknown> | undefined): void => {\n if (source.viewName !== 'ModalHostView' || !props || !('visible' in props)) return;\n if (props['visible'] !== false) dismissed.delete(source.reactTag);\n else if (source.props['visible'] !== false) dismissing.add(source.reactTag);\n };\n\n const clone = (n: FabricNode, props?: object, keepChildren = false): FabricNode => {\n const source = n as unknown as FakeFabricNode;\n noteModal(source, props as Record<string, unknown> | undefined);\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 // Keys sorted at every depth, so a golden does not depend on the order props were set in. An\n // array of keys as the replacer would sort too, but it is an allow-list for nested objects as\n // well, and printed `accessibilityState: { disabled: true }` as `{}`.\n const sorted = (_key: string, value: unknown): unknown =>\n value && typeof value === 'object' && !Array.isArray(value)\n ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))\n : value;\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, sorted)\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 diff(committed, set as FakeFabricNode[]);\n committed = set as FakeFabricNode[];\n dismissAfterCommit();\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 if (type === 'topDismiss' && target.viewName === 'ModalHostView') {\n if (dismissed.has(target.reactTag)) return;\n if (closing(target)) dismissed.add(target.reactTag);\n }\n handler(unmounted.has(target.reactTag) ? null : 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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ng-native/testing",
3
- "version": "0.1.3",
3
+ "version": "0.3.0",
4
4
  "description": "Test Angular Native components in Node with no simulator: a fake Fabric, Testing Library queries and events, and Vitest and node:test integration.",
5
5
  "keywords": [
6
6
  "angular",
@@ -34,9 +34,9 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@ng-native/metro": "0.1.3",
38
- "@ng-native/platform": "0.1.3",
39
- "@ng-native/fabric": "0.1.3"
37
+ "@ng-native/fabric": "0.3.0",
38
+ "@ng-native/metro": "0.3.0",
39
+ "@ng-native/platform": "0.3.0"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "@angular/compiler": "^22.0.0",
@@ -61,10 +61,10 @@
61
61
  "@angular/core": "22.2.0",
62
62
  "@angular/forms": "22.2.0",
63
63
  "@angular/router": "22.2.0",
64
+ "@ng-native/components": "0.3.0",
65
+ "@ng-native/router": "0.3.0",
64
66
  "vite": "^8.3.1",
65
- "vitest": "^5.0.2",
66
- "@ng-native/components": "0.1.3",
67
- "@ng-native/router": "0.1.3"
67
+ "vitest": "^5.0.2"
68
68
  },
69
69
  "nx": {
70
70
  "tags": [
package/runner/vitest.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `ngNative()`: the Vite plugin that lets Vitest run Angular Native code in Node, with no DOM.
3
3
  *
4
- * Four things, each of which Vitest would otherwise get wrong on its own:
4
+ * Five things, each of which Vitest would otherwise get wrong on its own:
5
5
  *
6
6
  * - It compiles. Every decorated `.ts` goes through `@ng-native/metro`'s AOT transform and every
7
7
  * partial-compiled package through the linker (see `compile.mjs`), before Vite strips types.
@@ -13,6 +13,9 @@
13
13
  * - It stubs assets. With no `require`, an app's `require('./logo.png')` would throw; it becomes
14
14
  * `{ testUri }` instead. And it stands in for the gesture and animation entry points, whose
15
15
  * React Native source Node cannot load (`STAND_INS`).
16
+ * - It resolves as Metro does. A workspace library's copy of a package at the app's version
17
+ * resolves to the app's copy (`appCopy`), so a test has one `@ng-native/components`, as the
18
+ * bundle does.
16
19
  *
17
20
  * The environment is Vitest's default, `node`. There is no DOM to emulate: the renderer talks to
18
21
  * a fake Fabric, and jsdom would only give Angular a `document` to misread.
@@ -88,23 +91,114 @@ function withConditions(conditions) {
88
91
  * a `require` regardless. A module that probes for one then requires React Native's Flow source.
89
92
  * A `var` of the same name shadows the one Vitest passes in, and is harmless where there is none.
90
93
  * On the first line, so every other line keeps its number. ES modules only: a CommonJS file needs
91
- * the real thing.
94
+ * the real thing. A `.js` file is one when its package says `"type": "module"`, as every published
95
+ * `@ng-native/*` package does.
92
96
  *
93
97
  * @param {string} code
94
98
  * @param {string} file
95
99
  */
96
100
  function hideRequire(code, file) {
97
- if (!/\.(ts|mts|mjs)$/.test(file) || !/\brequire\b/.test(code)) return code;
101
+ if (!/\brequire\b/.test(code) || !isModule(file)) return code;
98
102
  // A module that makes its own, with `createRequire`, already sees the one it means.
99
103
  if (/\b(?:const|let|var|function|class)\s+require\b/.test(code)) return code;
100
104
  return `var require = undefined; ${code}`;
101
105
  }
102
106
 
107
+ /** @param {string} dir */
108
+ function manifest(dir) {
109
+ try {
110
+ return JSON.parse(readFileSync(path.join(dir, 'package.json'), 'utf8'));
111
+ } catch {
112
+ return undefined;
113
+ }
114
+ }
115
+
116
+ /** @type {Map<string, boolean>} */
117
+ const moduleDirs = new Map();
118
+
119
+ /**
120
+ * Whether `.js` files in a directory are ES modules: the `type` of the nearest `package.json`, as
121
+ * Node decides it. A package's dist can hold a `package.json` of its own with only a `type` in it.
122
+ *
123
+ * @param {string} dir
124
+ * @returns {boolean}
125
+ */
126
+ function jsIsModule(dir) {
127
+ let found = moduleDirs.get(dir);
128
+ if (found === undefined) {
129
+ const own = manifest(dir);
130
+ const parent = path.dirname(dir);
131
+ found = own ? own.type === 'module' : parent !== dir && jsIsModule(parent);
132
+ moduleDirs.set(dir, found);
133
+ }
134
+ return found;
135
+ }
136
+
137
+ /** @param {string} file */
138
+ function isModule(file) {
139
+ if (/\.(ts|mts|mjs)$/.test(file)) return true;
140
+ return file.endsWith('.js') && jsIsModule(path.dirname(file));
141
+ }
142
+
143
+ /**
144
+ * The directory of the `name` package a resolved file is in, or nothing: installed, or a linked
145
+ * workspace package, whose real path has no `node_modules` in it.
146
+ *
147
+ * @param {string} id
148
+ * @param {string} name
149
+ */
150
+ function packageRoot(id, name) {
151
+ const file = id.split('?')[0];
152
+ const marker = `/node_modules/${name}/`;
153
+ const at = file.lastIndexOf(marker);
154
+ if (at !== -1) return file.slice(0, at + marker.length - 1);
155
+ if (!path.isAbsolute(file)) return undefined;
156
+ for (let dir = path.dirname(file); dir !== path.dirname(dir); dir = path.dirname(dir)) {
157
+ // A package's dist can hold a package.json of its own with only a `type` in it.
158
+ const found = manifest(dir)?.name;
159
+ if (found) return found === name ? dir : undefined;
160
+ }
161
+ return undefined;
162
+ }
163
+
164
+ /**
165
+ * The app's copy of a package, when a bare import resolved to another copy at the same version:
166
+ * the rule the Metro preset applies to a bundle, so a test loads what the device does.
167
+ *
168
+ * pnpm installs a package once per set of peers it resolves, so a workspace library that lists
169
+ * `@ng-native/components` gets a copy of its own at the app's version when it was installed after
170
+ * the app. Loaded from the library, that is a second component registry. A copy at another
171
+ * version is the library's own choice, and stays.
172
+ *
173
+ * @param {import('vite').Rollup.PluginContext} context
174
+ * @param {string} source
175
+ * @param {string} importer
176
+ * @param {object} options what Vite passed to `resolveId`
177
+ * @param {string} root the app's directory
178
+ */
179
+ async function appCopy(context, source, importer, options, root) {
180
+ const resolution = await context.resolve(source, importer, { ...options, skipSelf: true });
181
+ const pkg = /^(?![./\0])(@[^/]+\/[^/]+|[^/]+)/.exec(source)?.[1];
182
+ const own = pkg && resolution && !resolution.external && packageRoot(resolution.id, pkg);
183
+ if (!own) return resolution;
184
+ // The app's copy can fail where the library's resolved: another version that does not export
185
+ // the subpath. The library's copy is then its own choice.
186
+ const app = await context
187
+ .resolve(source, path.join(root, 'package.json'), { ...options, skipSelf: true })
188
+ .catch(() => null);
189
+ const appRoot = app && !app.external && packageRoot(app.id, pkg);
190
+ const version = (/** @type {string} */ dir) => manifest(dir)?.version;
191
+ return appRoot && appRoot !== own && version(appRoot) && version(appRoot) === version(own)
192
+ ? app
193
+ : resolution;
194
+ }
195
+
103
196
  /**
104
197
  * @param {{ inline?: (string | RegExp)[] }} [options]
105
198
  * @returns {import('vitest/config').Plugin}
106
199
  */
107
200
  export function ngNative(options = {}) {
201
+ let root = process.cwd();
108
202
  return {
109
203
  name: 'ng-native',
110
204
  enforce: 'pre',
@@ -123,8 +217,12 @@ export function ngNative(options = {}) {
123
217
  server: { deps: { inline: [...INLINE, ...(options.inline ?? [])] } },
124
218
  },
125
219
  }),
126
- resolveId(source) {
127
- return Object.hasOwn(STAND_INS, source) ? STAND_INS[source] : null;
220
+ configResolved(config) {
221
+ root = config.root;
222
+ },
223
+ resolveId(source, importer, resolveOptions) {
224
+ if (Object.hasOwn(STAND_INS, source)) return STAND_INS[source];
225
+ return importer ? appCopy(this, source, importer, resolveOptions, root) : null;
128
226
  },
129
227
  transform(source, id) {
130
228
  const file = id.split('?')[0];