@symbiote-native/angular 0.6.1 → 0.6.2

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.
Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/package.json +5 -2
  3. package/src/__tests__/responder-change-detection.test.ts +0 -102
  4. package/src/__tests__/responder-nested-cd.test.ts +0 -113
  5. package/src/angular-gaps.test.ts +0 -142
  6. package/src/angular-imports.test.ts +0 -100
  7. package/src/components/activity-indicator/activity-indicator-class-style.test.ts +0 -44
  8. package/src/components/activity-indicator/activity-indicator.test.ts +0 -131
  9. package/src/components/flat-list/column-wrapper-style-class.test.ts +0 -116
  10. package/src/components/flat-list/flat-list-array-style.test.ts +0 -131
  11. package/src/components/flat-list/flat-list-class-style.test.ts +0 -74
  12. package/src/components/flat-list/flat-list-scroll-containment.test.ts +0 -107
  13. package/src/components/flat-list/flat-list.test.ts +0 -99
  14. package/src/components/image/image-class-style.test.ts +0 -46
  15. package/src/components/image-background-class-style.test.ts +0 -48
  16. package/src/components/image-background-style-class.test.ts +0 -79
  17. package/src/components/input-accessory-view/input-accessory-view.test.ts +0 -43
  18. package/src/components/keyboard-avoiding-view/keyboard-avoiding-view.test.ts +0 -54
  19. package/src/components/modal/modal.test.ts +0 -43
  20. package/src/components/pressable/pressable.test.ts +0 -85
  21. package/src/components/refresh-control/refresh-control.test.ts +0 -41
  22. package/src/components/safe-area-view/safe-area-view.test.ts +0 -43
  23. package/src/components/scroll-view/android-scroll-view-axis-projection.test.ts +0 -123
  24. package/src/components/scroll-view/content-container-style-class.test.ts +0 -75
  25. package/src/components/scroll-view/scroll-view-class-style.test.ts +0 -134
  26. package/src/components/scroll-view/scroll-view-projection.test.ts +0 -194
  27. package/src/components/switch/android-switch-class.test.ts +0 -42
  28. package/src/components/switch/switch.test.ts +0 -92
  29. package/src/components/text-input-class-style.test.ts +0 -54
  30. package/src/components/touchable/touchable.test.ts +0 -102
  31. package/src/components/touchable-native-feedback/touchable-native-feedback.test.ts +0 -49
  32. package/src/components/virtualized-list/virtualized-list-class-style.test.ts +0 -76
  33. package/src/components/virtualized-section-list/virtualized-section-list-class-style.test.ts +0 -60
  34. package/src/create-portal/create-portal.test.ts +0 -90
  35. package/src/create-tunnel/create-tunnel.test.ts +0 -120
  36. package/src/descriptor-to-angular/descriptor-outlet.test.ts +0 -94
  37. package/src/host-instance/host-instance.test.ts +0 -49
  38. package/src/modules/animated/animated-leaf-binder/animated-leaf-binder.test.ts +0 -116
  39. package/src/modules/animated/animated-leaf-binder.test.ts +0 -116
  40. package/src/modules/animated/animated-scroll-view.test.ts +0 -102
  41. package/src/modules/app-registry/app-registry.test.ts +0 -106
  42. package/src/modules/status-bar/status-bar.test.ts +0 -50
  43. package/src/render/render.test.ts +0 -147
  44. package/src/renderer/renderer.test.ts +0 -203
  45. package/src/services/services.test.ts +0 -163
@@ -1,203 +0,0 @@
1
- // Co-located headless test for the Angular renderer seam. Drives SymbioteRenderer
2
- // directly — no Angular runtime, no compiler — against the shared fake Fabric slot, proving
3
- // each Renderer2 method maps onto the engine mutation API and commits a correct Fabric tree.
4
- // The bootstrap (mount → createComponent) is validated separately on a real host / the AOT
5
- // example; this isolates the seam, which is the deterministic, framework-free half.
6
-
7
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
8
- import {
9
- clearGlobalStyles,
10
- createSurface,
11
- disposeRoot,
12
- isAnchor,
13
- isSymbioteNode,
14
- registerStyles,
15
- } from '@symbiote-native/engine';
16
- import { installFabric } from '@symbiote-native/test-utils';
17
- import { SymbioteRenderer, SymbioteRendererFactory } from './index';
18
- import { registerComposedComponent } from '../anchor-host-registry';
19
-
20
- const ROOT_TAG = 707;
21
- const PROBE_ID = 'probe';
22
-
23
- const fabric = installFabric();
24
-
25
- // A macrotask boundary drains the engine's coalesced (requestCommit) commit before asserting.
26
- const tick = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));
27
-
28
- function setup(): { surface: ReturnType<typeof createSurface>; renderer: SymbioteRenderer } {
29
- const surface = createSurface(ROOT_TAG);
30
- const renderer = new SymbioteRendererFactory(surface).createRenderer(null, null);
31
- if (!(renderer instanceof SymbioteRenderer))
32
- throw new Error('unreachable: factory built the renderer');
33
- return { surface, renderer };
34
- }
35
-
36
- beforeEach(() => fabric.reset());
37
- afterEach(() => {
38
- disposeRoot(ROOT_TAG);
39
- clearGlobalStyles();
40
- });
41
-
42
- describe('Angular SymbioteRenderer drives the engine', () => {
43
- it('maps createElement / createText / appendChild into a committed Fabric tree', async () => {
44
- const { surface, renderer } = setup();
45
- const view = renderer.createElement('symbiote-view');
46
- const text = renderer.createElement('symbiote-text');
47
- const raw = renderer.createText('Hello');
48
- renderer.appendChild(text, raw);
49
- renderer.appendChild(view, text);
50
- renderer.appendChild(surface, view);
51
- await tick();
52
-
53
- // The engine wraps surface.children in the synthetic box-none AppContainer root.
54
- const root = fabric.appRoot();
55
- expect(fabric.serialize(root.children)).toBe('RCTView(RCTText(RCTRawText "Hello"))');
56
- });
57
-
58
- it('maps additional symbiote intrinsics through the shared descriptor table', async () => {
59
- const { surface, renderer } = setup();
60
- const spinner = renderer.createElement('symbiote-activity-indicator');
61
- renderer.setProperty(spinner, 'testID', 'spinner');
62
- renderer.setProperty(spinner, 'animating', true);
63
- const image = renderer.createElement('symbiote-image');
64
- renderer.setProperty(image, 'testID', 'image');
65
- renderer.setProperty(image, 'source', { uri: 'https://example.invalid/image.png' });
66
- renderer.appendChild(surface, spinner);
67
- renderer.appendChild(surface, image);
68
- await tick();
69
-
70
- const committed = fabric.find(node => node.props.testID === 'spinner');
71
- expect(committed?.viewName).toBe('ActivityIndicatorView');
72
- expect(committed?.props.animating).toBe(true);
73
- const committedImage = fabric.find(node => node.props.testID === 'image');
74
- expect(committedImage?.viewName).toBe('RCTImageView');
75
- expect(committedImage?.props.source).toEqual({ uri: 'https://example.invalid/image.png' });
76
- });
77
-
78
- it('commits setProperty and merges per-key setStyle into one style prop', async () => {
79
- const { surface, renderer } = setup();
80
- const view = renderer.createElement('symbiote-view');
81
- renderer.setProperty(view, 'nativeID', PROBE_ID);
82
- renderer.setStyle(view, 'padding', 24);
83
- renderer.setStyle(view, 'opacity', 0.5);
84
- renderer.appendChild(surface, view);
85
- await tick();
86
-
87
- const committed = fabric.find(node => node.props.nativeID === PROBE_ID);
88
- expect(committed, 'the probed RCTView committed').toBeDefined();
89
- // Angular emits a [style] binding as per-key setStyle; the seam folds them into one style
90
- // object, then the engine HOISTS style keys to top-level Fabric props (RN's flat C++ props
91
- // contract — style is never a nested key on a committed node).
92
- expect(committed?.props).toMatchObject({ nativeID: PROBE_ID, padding: 24, opacity: 0.5 });
93
- });
94
-
95
- it('resolves addClass tokens through the shared style registry and lets explicit style win', async () => {
96
- registerStyles({ card: { padding: 10, backgroundColor: 'red' } });
97
- const { surface, renderer } = setup();
98
- const view = renderer.createElement('symbiote-view');
99
- renderer.setProperty(view, 'nativeID', PROBE_ID);
100
- // Ivy compiles class="card highlight" to one addClass call per token, never a single string.
101
- renderer.addClass(view, 'card');
102
- renderer.addClass(view, 'highlight');
103
- renderer.setStyle(view, 'backgroundColor', 'blue');
104
- renderer.appendChild(surface, view);
105
- await tick();
106
-
107
- const committed = fabric.find(node => node.props.nativeID === PROBE_ID);
108
- // padding comes from the class; backgroundColor is explicit style, so it wins over the
109
- // class-derived red — same precedence Vue's class="..."/:style="..." merge guarantees.
110
- expect(committed?.props).toMatchObject({ padding: 10, backgroundColor: 'blue' });
111
- });
112
-
113
- it('removeClass drops a token and recomputes the resolved style', async () => {
114
- registerStyles({ card: { padding: 10 }, highlight: { opacity: 0.5 } });
115
- const { surface, renderer } = setup();
116
- const view = renderer.createElement('symbiote-view');
117
- renderer.setProperty(view, 'nativeID', PROBE_ID);
118
- renderer.addClass(view, 'card');
119
- renderer.addClass(view, 'highlight');
120
- renderer.removeClass(view, 'highlight');
121
- renderer.appendChild(surface, view);
122
- await tick();
123
-
124
- const committed = fabric.find(node => node.props.nativeID === PROBE_ID);
125
- expect(committed?.props.padding).toBe(10);
126
- expect(committed?.props.opacity).toBeUndefined();
127
- });
128
-
129
- it('removeChild detaches a node from the next commit', async () => {
130
- const { surface, renderer } = setup();
131
- const keep = renderer.createElement('symbiote-view');
132
- const drop = renderer.createElement('symbiote-view');
133
- renderer.setProperty(drop, 'nativeID', PROBE_ID);
134
- renderer.appendChild(surface, keep);
135
- renderer.appendChild(surface, drop);
136
- await tick();
137
- expect(
138
- fabric.find(node => node.props.nativeID === PROBE_ID),
139
- 'present before remove',
140
- ).toBeDefined();
141
-
142
- fabric.reset();
143
- renderer.removeChild(surface, drop);
144
- await tick();
145
- expect(
146
- fabric.find(node => node.props.nativeID === PROBE_ID),
147
- 'gone after remove',
148
- ).toBeUndefined();
149
- });
150
-
151
- it('createComment yields an anchor the commit walk skips but flattens its children', async () => {
152
- const { surface, renderer } = setup();
153
- const anchor = renderer.createComment();
154
- renderer.appendChild(anchor, renderer.createElement('symbiote-view'));
155
- renderer.appendChild(surface, anchor);
156
- renderer.appendChild(surface, renderer.createElement('symbiote-text'));
157
- await tick();
158
-
159
- // The anchor never reaches Fabric; its child keeps the same sibling position. Angular
160
- // composed components use this to make their framework host element disappear.
161
- const root = fabric.appRoot();
162
- expect(fabric.serialize(root.children)).toBe('RCTViewRCTText');
163
- });
164
-
165
- it('listen attaches an explicit event listener and the unlisten fn removes it', () => {
166
- const { renderer } = setup();
167
- const view = renderer.createElement('symbiote-view');
168
- expect(isSymbioteNode(view)).toBe(true);
169
- if (!isSymbioteNode(view)) throw new Error('unreachable: createElement returns a node');
170
-
171
- const unlisten = renderer.listen(view, 'press', () => {});
172
- expect(view.listeners?.has('press'), 'listener attached under the explicit name').toBe(true);
173
- unlisten();
174
- expect(view.listeners?.has('press'), 'unlisten removed it').toBe(false);
175
- });
176
-
177
- it('listen on a global target (window/document) is an inert no-op', () => {
178
- const { renderer } = setup();
179
- expect(() => renderer.listen('window', 'resize', () => {})()).not.toThrow();
180
- });
181
-
182
- // App/third-party selectors must not be hardcoded into ANCHOR_HOST_COMPONENTS — the owning
183
- // package registers itself via registerComposedComponent, same as an adapter-owned composed
184
- // component. 'RefApiDemo' names examples/angular's demo component; this test never imports
185
- // it, it only proves the renderer treats an unregistered selector as a raw Fabric view name,
186
- // not a hardcoded anchor host. MUST run before the registration test below, since
187
- // registerComposedComponent mutates the module-level Set for the rest of the file.
188
- it('does not anchor-host an app-owned selector unless it self-registers', () => {
189
- const { renderer } = setup();
190
- const node = renderer.createElement('RefApiDemo');
191
- if (!isSymbioteNode(node)) throw new Error('unreachable: createElement returns a node');
192
- expect(isAnchor(node), 'falls through to a raw Fabric view, not an anchor').toBe(false);
193
- expect(node.component).toBe('RefApiDemo');
194
- });
195
-
196
- it('anchor-hosts an app-owned selector once it self-registers via registerComposedComponent', () => {
197
- registerComposedComponent('RefApiDemo');
198
- const { renderer } = setup();
199
- const node = renderer.createElement('RefApiDemo');
200
- if (!isSymbioteNode(node)) throw new Error('unreachable: createElement returns a node');
201
- expect(isAnchor(node)).toBe(true);
202
- });
203
- });
@@ -1,163 +0,0 @@
1
- import '@angular/compiler';
2
- import { Component, inject } from '@angular/core';
3
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
- import { Appearance, Dimensions } from '@symbiote-native/engine';
5
- import { installFabric } from '@symbiote-native/test-utils';
6
-
7
- import { mount, unmount } from '../render';
8
- import { ColorSchemeService } from './color-scheme.service';
9
- import { WindowDimensionsService } from './window-dimensions.service';
10
-
11
- const ROOT_TAG = 900;
12
- const fabric = installFabric();
13
-
14
- let capturedColorSchemeService: ColorSchemeService | undefined;
15
- let capturedWindowDimensionsService: WindowDimensionsService | undefined;
16
-
17
- @Component({
18
- selector: 'symbiote-color-scheme-consumer',
19
- standalone: true,
20
- providers: [ColorSchemeService],
21
- template: '',
22
- })
23
- class ColorSchemeConsumer {
24
- readonly service = inject(ColorSchemeService);
25
- constructor() {
26
- capturedColorSchemeService = this.service;
27
- }
28
- }
29
-
30
- @Component({
31
- selector: 'symbiote-window-dimensions-consumer',
32
- standalone: true,
33
- providers: [WindowDimensionsService],
34
- template: '',
35
- })
36
- class WindowDimensionsConsumer {
37
- readonly service = inject(WindowDimensionsService);
38
- constructor() {
39
- capturedWindowDimensionsService = this.service;
40
- }
41
- }
42
-
43
- @Component({
44
- selector: 'symbiote-root-window-dimensions-consumer',
45
- standalone: true,
46
- template: '',
47
- })
48
- class RootWindowDimensionsConsumer {
49
- readonly service = inject(WindowDimensionsService);
50
- constructor() {
51
- capturedWindowDimensionsService = this.service;
52
- }
53
- }
54
-
55
- beforeEach(() => {
56
- capturedColorSchemeService = undefined;
57
- capturedWindowDimensionsService = undefined;
58
- });
59
-
60
- afterEach(() => {
61
- unmount(ROOT_TAG);
62
- fabric.reset();
63
- vi.restoreAllMocks();
64
- });
65
-
66
- describe('Angular DI services over engine modules', () => {
67
- it('ColorSchemeService subscribes to Appearance and cleans up on unmount', async () => {
68
- type IGenericListener = (...args: unknown[]) => void;
69
- const activeListeners = new Set<IGenericListener>();
70
- let emitToListeners: (preferences: { colorScheme: 'light' | 'dark' | null }) => void = () => {};
71
-
72
- vi.spyOn(Appearance, 'getColorScheme').mockReturnValue('light');
73
- vi.spyOn(Appearance, 'addChangeListener').mockImplementation(listener => {
74
- activeListeners.add(listener as IGenericListener);
75
- emitToListeners = preferences => {
76
- for (const l of activeListeners) l(preferences);
77
- };
78
- return {
79
- remove: () => {
80
- activeListeners.delete(listener as IGenericListener);
81
- },
82
- };
83
- });
84
-
85
- mount(ROOT_TAG, ColorSchemeConsumer);
86
- await new Promise<void>(resolve => setTimeout(resolve, 0));
87
-
88
- const service = capturedColorSchemeService;
89
- if (!service) throw new Error('ColorSchemeService was not captured');
90
-
91
- expect(service.colorScheme()).toBe('light');
92
- expect(Appearance.addChangeListener).toHaveBeenCalledOnce();
93
-
94
- emitToListeners({ colorScheme: 'dark' });
95
- expect(service.colorScheme()).toBe('dark');
96
-
97
- emitToListeners({ colorScheme: 'light' });
98
- expect(service.colorScheme()).toBe('light');
99
-
100
- unmount(ROOT_TAG);
101
- expect(activeListeners.size).toBe(0);
102
-
103
- // After unmount, the signal must stop reacting to further events.
104
- emitToListeners({ colorScheme: 'dark' });
105
- expect(service.colorScheme()).toBe('light');
106
- });
107
-
108
- it('provides WindowDimensionsService from the Symbiote root injector', async () => {
109
- const initialMetrics = { width: 100, height: 200, scale: 1, fontScale: 1 };
110
- vi.spyOn(Dimensions, 'get').mockReturnValue(initialMetrics);
111
- vi.spyOn(Dimensions, 'addEventListener').mockReturnValue({ remove: vi.fn() });
112
-
113
- mount(ROOT_TAG, RootWindowDimensionsConsumer);
114
- await new Promise<void>(resolve => setTimeout(resolve, 0));
115
-
116
- expect(capturedWindowDimensionsService?.dimensions()).toEqual(initialMetrics);
117
- });
118
-
119
- it('WindowDimensionsService subscribes to Dimensions and ignores no-op updates', async () => {
120
- const remove = vi.fn();
121
- let capturedListener:
122
- | ((set: {
123
- window: { width: number; height: number; scale: number; fontScale: number };
124
- screen: { width: number; height: number; scale: number; fontScale: number };
125
- }) => void)
126
- | undefined;
127
-
128
- const initialMetrics = { width: 100, height: 200, scale: 1, fontScale: 1 };
129
- vi.spyOn(Dimensions, 'get').mockReturnValue(initialMetrics);
130
- vi.spyOn(Dimensions, 'addEventListener').mockImplementation((_type, listener) => {
131
- capturedListener = set =>
132
- listener(set as { window: typeof initialMetrics; screen: typeof initialMetrics });
133
- return { remove };
134
- });
135
-
136
- mount(ROOT_TAG, WindowDimensionsConsumer);
137
- await new Promise<void>(resolve => setTimeout(resolve, 0));
138
-
139
- const service = capturedWindowDimensionsService;
140
- if (!service) throw new Error('WindowDimensionsService was not captured');
141
-
142
- const initial = service.dimensions();
143
- expect(initial).toEqual(initialMetrics);
144
- expect(Dimensions.addEventListener).toHaveBeenCalledOnce();
145
-
146
- // Same metrics: signal identity must be preserved (equality guard).
147
- capturedListener?.({
148
- window: { width: 100, height: 200, scale: 1, fontScale: 1 },
149
- screen: { width: 100, height: 200, scale: 1, fontScale: 1 },
150
- });
151
- expect(service.dimensions()).toBe(initial);
152
-
153
- // Different metrics: signal updates.
154
- capturedListener?.({
155
- window: { width: 300, height: 400, scale: 2, fontScale: 2 },
156
- screen: { width: 300, height: 400, scale: 2, fontScale: 2 },
157
- });
158
- expect(service.dimensions()).toEqual({ width: 300, height: 400, scale: 2, fontScale: 2 });
159
-
160
- unmount(ROOT_TAG);
161
- expect(remove).toHaveBeenCalledOnce();
162
- });
163
- });