@symbiote-native/angular 0.6.0 → 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 (193) hide show
  1. package/README.md +1 -1
  2. package/babel-register-composed.cjs +11 -3
  3. package/build/angular/anchor-host-registry.d.ts +2 -0
  4. package/build/angular/anchor-host-registry.js +84 -0
  5. package/build/angular/anchor-host-registry.js.map +1 -0
  6. package/build/angular/components/input-accessory-view/index.d.ts +2 -1
  7. package/build/angular/components/input-accessory-view/index.js +28 -39
  8. package/build/angular/components/input-accessory-view/index.js.map +1 -1
  9. package/build/angular/components/scroll-view/projection.js +70 -48
  10. package/build/angular/components/scroll-view/projection.js.map +1 -1
  11. package/build/angular/components/scroll-view/shared.js +28 -25
  12. package/build/angular/components/scroll-view/shared.js.map +1 -1
  13. package/build/angular/components/scroll-view/sticky-header.d.ts +6 -7
  14. package/build/angular/components/scroll-view/sticky-header.js +82 -65
  15. package/build/angular/components/scroll-view/sticky-header.js.map +1 -1
  16. package/build/angular/components/touchable/index.d.ts +2 -5
  17. package/build/angular/components/touchable/index.js +36 -56
  18. package/build/angular/components/touchable/index.js.map +1 -1
  19. package/build/angular/components/virtualized-list/index.d.ts +5 -20
  20. package/build/angular/components/virtualized-list/index.js +145 -275
  21. package/build/angular/components/virtualized-list/index.js.map +1 -1
  22. package/build/angular/components/virtualized-section-list/index.js +2 -4
  23. package/build/angular/components/virtualized-section-list/index.js.map +1 -1
  24. package/build/angular/index.d.ts +2 -1
  25. package/build/angular/index.js +6 -1
  26. package/build/angular/index.js.map +1 -1
  27. package/build/angular/renderer/index.d.ts +0 -1
  28. package/build/angular/renderer/index.js +5 -60
  29. package/build/angular/renderer/index.js.map +1 -1
  30. package/package.json +8 -4
  31. package/src/anchor-host-registry.ts +88 -0
  32. package/src/components/input-accessory-view/index.ts +30 -39
  33. package/src/components/scroll-view/projection.ts +76 -46
  34. package/src/components/scroll-view/shared.ts +27 -19
  35. package/src/components/scroll-view/sticky-header.ts +89 -64
  36. package/src/components/touchable/index.ts +52 -66
  37. package/src/components/virtualized-list/index.ts +157 -368
  38. package/src/components/virtualized-section-list/index.ts +6 -3
  39. package/src/index.ts +6 -1
  40. package/src/renderer/index.ts +5 -63
  41. package/build/bootstrap.d.ts +0 -7
  42. package/build/bootstrap.js +0 -14
  43. package/build/components/activity-indicator/index.android.d.ts +0 -9
  44. package/build/components/activity-indicator/index.android.js +0 -64
  45. package/build/components/activity-indicator/index.d.ts +0 -1
  46. package/build/components/activity-indicator/index.ios.d.ts +0 -5
  47. package/build/components/activity-indicator/index.ios.js +0 -64
  48. package/build/components/activity-indicator/index.js +0 -3
  49. package/build/components/activity-indicator/shared.d.ts +0 -64
  50. package/build/components/activity-indicator/shared.js +0 -424
  51. package/build/components/button.d.ts +0 -60
  52. package/build/components/button.js +0 -456
  53. package/build/components/flat-list/index.d.ts +0 -119
  54. package/build/components/flat-list/index.js +0 -691
  55. package/build/components/image/index.android.d.ts +0 -7
  56. package/build/components/image/index.android.js +0 -88
  57. package/build/components/image/index.d.ts +0 -1
  58. package/build/components/image/index.ios.d.ts +0 -7
  59. package/build/components/image/index.ios.js +0 -88
  60. package/build/components/image/index.js +0 -2
  61. package/build/components/image/shared.d.ts +0 -101
  62. package/build/components/image/shared.js +0 -405
  63. package/build/components/image-background.d.ts +0 -78
  64. package/build/components/image-background.js +0 -583
  65. package/build/components/input-accessory-view/index.d.ts +0 -56
  66. package/build/components/input-accessory-view/index.js +0 -403
  67. package/build/components/keyboard-avoiding-view/index.d.ts +0 -79
  68. package/build/components/keyboard-avoiding-view/index.js +0 -504
  69. package/build/components/modal/index.d.ts +0 -89
  70. package/build/components/modal/index.js +0 -529
  71. package/build/components/pressable/index.d.ts +0 -120
  72. package/build/components/pressable/index.js +0 -639
  73. package/build/components/refresh-control/index.d.ts +0 -83
  74. package/build/components/refresh-control/index.js +0 -509
  75. package/build/components/safe-area-view/index.d.ts +0 -54
  76. package/build/components/safe-area-view/index.js +0 -388
  77. package/build/components/scroll-view/index.android.d.ts +0 -16
  78. package/build/components/scroll-view/index.android.js +0 -228
  79. package/build/components/scroll-view/index.d.ts +0 -3
  80. package/build/components/scroll-view/index.ios.d.ts +0 -6
  81. package/build/components/scroll-view/index.ios.js +0 -113
  82. package/build/components/scroll-view/index.js +0 -5
  83. package/build/components/scroll-view/projection.d.ts +0 -32
  84. package/build/components/scroll-view/projection.js +0 -311
  85. package/build/components/scroll-view/shared.d.ts +0 -225
  86. package/build/components/scroll-view/shared.js +0 -757
  87. package/build/components/scroll-view/sticky-header.d.ts +0 -38
  88. package/build/components/scroll-view/sticky-header.js +0 -257
  89. package/build/components/section-list/index.d.ts +0 -113
  90. package/build/components/section-list/index.js +0 -768
  91. package/build/components/switch/index.android.d.ts +0 -17
  92. package/build/components/switch/index.android.js +0 -107
  93. package/build/components/switch/index.d.ts +0 -1
  94. package/build/components/switch/index.ios.d.ts +0 -16
  95. package/build/components/switch/index.ios.js +0 -106
  96. package/build/components/switch/index.js +0 -3
  97. package/build/components/switch/shared.d.ts +0 -37
  98. package/build/components/switch/shared.js +0 -176
  99. package/build/components/text-input.d.ts +0 -150
  100. package/build/components/text-input.js +0 -821
  101. package/build/components/touchable/index.d.ts +0 -241
  102. package/build/components/touchable/index.js +0 -1672
  103. package/build/components/touchable-native-feedback/index.d.ts +0 -80
  104. package/build/components/touchable-native-feedback/index.js +0 -545
  105. package/build/components/virtualized-list/directives.d.ts +0 -41
  106. package/build/components/virtualized-list/directives.js +0 -249
  107. package/build/components/virtualized-list/index.d.ts +0 -265
  108. package/build/components/virtualized-list/index.js +0 -1343
  109. package/build/components/virtualized-section-list/directives.d.ts +0 -28
  110. package/build/components/virtualized-section-list/directives.js +0 -148
  111. package/build/components/virtualized-section-list/index.d.ts +0 -175
  112. package/build/components/virtualized-section-list/index.js +0 -854
  113. package/build/components.d.ts +0 -39
  114. package/build/components.js +0 -37
  115. package/build/create-portal/index.d.ts +0 -17
  116. package/build/create-portal/index.js +0 -128
  117. package/build/create-tunnel/index.d.ts +0 -29
  118. package/build/create-tunnel/index.js +0 -181
  119. package/build/descriptor-to-angular/index.d.ts +0 -19
  120. package/build/descriptor-to-angular/index.js +0 -186
  121. package/build/host-instance/index.d.ts +0 -2
  122. package/build/host-instance/index.js +0 -36
  123. package/build/index.d.ts +0 -21
  124. package/build/index.js +0 -40
  125. package/build/modules/animated/animated-leaf-binder/index.d.ts +0 -14
  126. package/build/modules/animated/animated-leaf-binder/index.js +0 -90
  127. package/build/modules/animated/animated-leaf-binder.d.ts +0 -14
  128. package/build/modules/animated/animated-leaf-binder.js +0 -90
  129. package/build/modules/animated/create-animated-component.d.ts +0 -48
  130. package/build/modules/animated/create-animated-component.js +0 -425
  131. package/build/modules/animated/index.d.ts +0 -51
  132. package/build/modules/animated/index.js +0 -58
  133. package/build/modules/app-registry/index.d.ts +0 -7
  134. package/build/modules/app-registry/index.js +0 -22
  135. package/build/modules/status-bar/index.d.ts +0 -17
  136. package/build/modules/status-bar/index.js +0 -121
  137. package/build/primitives/index.d.ts +0 -32
  138. package/build/primitives/index.js +0 -386
  139. package/build/primitives/shared.d.ts +0 -83
  140. package/build/primitives/shared.js +0 -230
  141. package/build/render/index.d.ts +0 -11
  142. package/build/render/index.js +0 -170
  143. package/build/renderer/index.d.ts +0 -38
  144. package/build/renderer/index.js +0 -355
  145. package/build/services/color-scheme.service.d.ts +0 -13
  146. package/build/services/color-scheme.service.js +0 -72
  147. package/build/services/index.d.ts +0 -2
  148. package/build/services/index.js +0 -2
  149. package/build/services/window-dimensions.service.d.ts +0 -13
  150. package/build/services/window-dimensions.service.js +0 -83
  151. package/src/__tests__/responder-change-detection.test.ts +0 -102
  152. package/src/__tests__/responder-nested-cd.test.ts +0 -113
  153. package/src/angular-gaps.test.ts +0 -142
  154. package/src/angular-imports.test.ts +0 -100
  155. package/src/components/activity-indicator/activity-indicator-class-style.test.ts +0 -44
  156. package/src/components/activity-indicator/activity-indicator.test.ts +0 -131
  157. package/src/components/flat-list/column-wrapper-style-class.test.ts +0 -116
  158. package/src/components/flat-list/flat-list-array-style.test.ts +0 -131
  159. package/src/components/flat-list/flat-list-class-style.test.ts +0 -74
  160. package/src/components/flat-list/flat-list-scroll-containment.test.ts +0 -107
  161. package/src/components/flat-list/flat-list.test.ts +0 -99
  162. package/src/components/image/image-class-style.test.ts +0 -46
  163. package/src/components/image-background-class-style.test.ts +0 -48
  164. package/src/components/image-background-style-class.test.ts +0 -79
  165. package/src/components/input-accessory-view/input-accessory-view.test.ts +0 -43
  166. package/src/components/keyboard-avoiding-view/keyboard-avoiding-view.test.ts +0 -54
  167. package/src/components/modal/modal.test.ts +0 -43
  168. package/src/components/pressable/pressable.test.ts +0 -85
  169. package/src/components/refresh-control/refresh-control.test.ts +0 -41
  170. package/src/components/safe-area-view/safe-area-view.test.ts +0 -43
  171. package/src/components/scroll-view/android-scroll-view-axis-projection.test.ts +0 -123
  172. package/src/components/scroll-view/content-container-style-class.test.ts +0 -75
  173. package/src/components/scroll-view/scroll-view-class-style.test.ts +0 -134
  174. package/src/components/scroll-view/scroll-view-projection.test.ts +0 -194
  175. package/src/components/switch/android-switch-class.test.ts +0 -42
  176. package/src/components/switch/switch.test.ts +0 -92
  177. package/src/components/text-input-class-style.test.ts +0 -54
  178. package/src/components/touchable/touchable.test.ts +0 -102
  179. package/src/components/touchable-native-feedback/touchable-native-feedback.test.ts +0 -49
  180. package/src/components/virtualized-list/virtualized-list-class-style.test.ts +0 -76
  181. package/src/components/virtualized-section-list/virtualized-section-list-class-style.test.ts +0 -60
  182. package/src/create-portal/create-portal.test.ts +0 -90
  183. package/src/create-tunnel/create-tunnel.test.ts +0 -120
  184. package/src/descriptor-to-angular/descriptor-outlet.test.ts +0 -94
  185. package/src/host-instance/host-instance.test.ts +0 -49
  186. package/src/modules/animated/animated-leaf-binder/animated-leaf-binder.test.ts +0 -116
  187. package/src/modules/animated/animated-leaf-binder.test.ts +0 -116
  188. package/src/modules/animated/animated-scroll-view.test.ts +0 -102
  189. package/src/modules/app-registry/app-registry.test.ts +0 -106
  190. package/src/modules/status-bar/status-bar.test.ts +0 -50
  191. package/src/render/render.test.ts +0 -147
  192. package/src/renderer/renderer.test.ts +0 -202
  193. package/src/services/services.test.ts +0 -163
@@ -1,106 +0,0 @@
1
- // Co-located Angular-driven test, Angular twin of adapters/react/src/modules/
2
- // app-registry/app-registry.test.tsx. Proves the AppRegistry entry point:
3
- // `registerComponent(appKey, () => App)` stores a runnable that calls `mount` (driving
4
- // @symbiote-native/engine) AND bridges it to the host registrar (RN's own AppRegistry, injected via
5
- // `setHostRegistrar`) so native can find it by key. Also proves the Angular-specific
6
- // `setWrapperComponentProvider` seam: a pre-authored standalone wrapper receives the root's
7
- // host node as projected `<ng-content>`, the AOT-safe twin of React's
8
- // createElement(Wrapper, null, rootElement).
9
-
10
- import '@angular/compiler';
11
- import { Component, Input, type Type } from '@angular/core';
12
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
13
- import { installFabric } from '@symbiote-native/test-utils';
14
- import { AppRegistry, setHostRegistrar, type IAppParameters, type IRunnable } from '../..';
15
- import { unmount } from '../../render';
16
-
17
- const APP_KEY = 'canary';
18
- const ROOT_TAG = 212;
19
- const WRAPPED_APP_KEY = 'canary-wrapped';
20
- const WRAPPED_ROOT_TAG = 213;
21
-
22
- // Angular's mount() batches change detection on a microtask, so a committed native tag isn't
23
- // available synchronously — assertions on the committed tree need one tick, same as Vue.
24
- const tick = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));
25
-
26
- class TestText {}
27
- Component({
28
- selector: 'symbiote-text',
29
- standalone: true,
30
- template: '<ng-content></ng-content>',
31
- })(TestText);
32
-
33
- class SmokeComponent {}
34
- Component({
35
- selector: 'symbiote-app-registry-smoke',
36
- standalone: true,
37
- imports: [TestText],
38
- template: '<symbiote-text>hi</symbiote-text>',
39
- })(SmokeComponent);
40
-
41
- class WrapperComponent {
42
- @Input() label = '';
43
- }
44
- Component({
45
- selector: 'symbiote-app-registry-wrapper',
46
- standalone: true,
47
- imports: [TestText],
48
- template: '<symbiote-text>{{ label }}</symbiote-text><ng-content></ng-content>',
49
- })(WrapperComponent);
50
-
51
- const fabric = installFabric();
52
-
53
- // The host registrar the native side drives (RN's AppRegistry stand-in).
54
- const hostRunnables = new Map<string, IRunnable>();
55
-
56
- beforeEach(() => {
57
- fabric.reset();
58
- hostRunnables.clear();
59
- setHostRegistrar({
60
- registerRunnable: (appKey: string, run: IRunnable): string => {
61
- hostRunnables.set(appKey, run);
62
- return appKey;
63
- },
64
- });
65
- AppRegistry.registerComponent(APP_KEY, () => SmokeComponent);
66
- });
67
- afterEach(() => {
68
- unmount(ROOT_TAG);
69
- unmount(WRAPPED_ROOT_TAG);
70
- });
71
-
72
- describe('AppRegistry', () => {
73
- it('exposes the app key and bridges the runnable to the host registrar', () => {
74
- expect(AppRegistry.getAppKeys()).toContain(APP_KEY);
75
- expect(hostRunnables.get(APP_KEY)).toBeDefined();
76
- });
77
-
78
- it('mounts the tree when the host invokes the runnable with a rootTag', async () => {
79
- const hostRun = hostRunnables.get(APP_KEY);
80
- expect(hostRun).toBeDefined();
81
-
82
- const nativeParams: IAppParameters = { rootTag: ROOT_TAG };
83
- hostRun!(nativeParams);
84
- await tick();
85
-
86
- expect(fabric.find(n => n.viewName === 'RCTText')).toBeDefined();
87
- });
88
-
89
- it('runApplication drives the same runnable locally', async () => {
90
- AppRegistry.runApplication(APP_KEY, { rootTag: ROOT_TAG });
91
- await tick();
92
-
93
- expect(fabric.find(n => n.viewName === 'RCTText')).toBeDefined();
94
- });
95
-
96
- it('projects the root component into a registered wrapper via <ng-content>', async () => {
97
- AppRegistry.setWrapperComponentProvider(() => WrapperComponent as Type<unknown>);
98
- AppRegistry.registerComponent(WRAPPED_APP_KEY, () => SmokeComponent);
99
-
100
- AppRegistry.runApplication(WRAPPED_APP_KEY, { rootTag: WRAPPED_ROOT_TAG });
101
- await tick();
102
-
103
- const texts = fabric.appRoot().children.flatMap(n => fabric.serialize([n]));
104
- expect(texts.join('')).toContain('hi');
105
- });
106
- });
@@ -1,50 +0,0 @@
1
- import '@angular/compiler';
2
- import { Component } from '@angular/core';
3
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
- import * as engine from '@symbiote-native/engine';
5
- import { installFabric } from '@symbiote-native/test-utils';
6
-
7
- import { mount, unmount } from '../../render';
8
- import { StatusBar } from './index';
9
-
10
- const ROOT_TAG = 901;
11
- const fabric = installFabric();
12
-
13
- @Component({
14
- selector: 'symbiote-status-bar-host',
15
- standalone: true,
16
- imports: [StatusBar],
17
- template: ` <StatusBar [barStyle]="'dark-content'" [hidden]="true" [animated]="false" /> `,
18
- })
19
- class StatusBarHost {}
20
-
21
- beforeEach(() => fabric.reset());
22
- afterEach(() => unmount(ROOT_TAG));
23
-
24
- describe('StatusBar', () => {
25
- it('applies status bar props on mount and renders no Fabric node', async () => {
26
- const spy = vi.spyOn(engine, 'applyStatusBarProps').mockReturnValue(undefined);
27
-
28
- mount(ROOT_TAG, StatusBarHost);
29
- await new Promise<void>(resolve => setTimeout(resolve, 0));
30
-
31
- expect(spy).toHaveBeenCalledWith(
32
- expect.objectContaining({
33
- barStyle: 'dark-content',
34
- hidden: true,
35
- animated: false,
36
- }),
37
- );
38
-
39
- const root = fabric.appRoot();
40
- expect(root.children).toHaveLength(0);
41
- });
42
-
43
- it('exposes imperative statics on the component value', () => {
44
- expect(typeof StatusBar.setHidden).toBe('function');
45
- expect(typeof StatusBar.setBarStyle).toBe('function');
46
- expect(typeof StatusBar.setNetworkActivityIndicatorVisible).toBe('function');
47
- expect(typeof StatusBar.setBackgroundColor).toBe('function');
48
- expect(typeof StatusBar.setTranslucent).toBe('function');
49
- });
50
- });
@@ -1,147 +0,0 @@
1
- // Integration coverage: run the real Angular runtime (createComponent + RendererFactory2)
2
- // over the headless fake Fabric slot. Unlike renderer.test.ts, this proves mount() wires
3
- // Angular's bootstrap to the Symbiote renderer, so a standalone template paints and updates
4
- // through the engine.
5
-
6
- import '@angular/compiler';
7
- import { Component } from '@angular/core';
8
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
9
- import { installFabric } from '@symbiote-native/test-utils';
10
- import { mount, unmount } from './index';
11
-
12
- const ROOT_TAG = 808;
13
-
14
- const fabric = installFabric();
15
- const tick = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));
16
- const drainAngularAndCommit = async (): Promise<void> => {
17
- await tick();
18
- await tick();
19
- };
20
-
21
- class TestView {}
22
- Component({
23
- selector: 'symbiote-view',
24
- standalone: true,
25
- template: '<ng-content></ng-content>',
26
- })(TestView);
27
-
28
- class TestText {}
29
- Component({
30
- selector: 'symbiote-text',
31
- standalone: true,
32
- template: '<ng-content></ng-content>',
33
- })(TestText);
34
-
35
- class SmokeComponent {
36
- name = 'Angular';
37
- count = 0;
38
- boxStyle = { padding: 12 };
39
-
40
- increment(): void {
41
- this.count += 1;
42
- }
43
- }
44
-
45
- Component({
46
- selector: 'symbiote-angular-smoke',
47
- standalone: true,
48
- imports: [TestView, TestText],
49
- template: `<symbiote-view [style]="boxStyle"><symbiote-text>Hello {{ name }}</symbiote-text><symbiote-view testID="counter" (press)="increment()"><symbiote-text>tapped {{ count }}×</symbiote-text></symbiote-view></symbiote-view>`,
50
- })(SmokeComponent);
51
-
52
- beforeEach(() => fabric.reset());
53
- afterEach(() => unmount(ROOT_TAG));
54
-
55
- // A plain (non-OnPush) child component compiles as SignalView in Angular 20+, so an UNTOUCHED
56
- // sibling child is skipped by the per-view CheckAlways/Dirty/RefreshView gate inside
57
- // detectChangesInView — true with or without ApplicationRef (the gate lives inside
58
- // detectChangesInView regardless of who calls it). This is NOT a claim that the root's own
59
- // template is spared: markViewDirty (used by both native (event) bindings and
60
- // ChangeDetectorRef.markForCheck(), see SymbioteHostPropsDirective) unconditionally sets
61
- // RefreshView on every ancestor up to the root — a press anywhere always re-runs the root's
62
- // own template, in every Angular app, regardless of scheduler. What this guards is narrower
63
- // but still real: a SIBLING component with no dirty descendant of its own must not be dragged
64
- // along for the ride.
65
- let unrelatedRenderCount = 0;
66
- class CounterChild {
67
- count = 0;
68
-
69
- increment(): void {
70
- this.count += 1;
71
- }
72
- }
73
- Component({
74
- selector: 'counter-child',
75
- standalone: true,
76
- imports: [TestView, TestText],
77
- template:
78
- '<symbiote-view testID="counter" (press)="increment()"><symbiote-text>tapped {{ count }}×</symbiote-text></symbiote-view>',
79
- })(CounterChild);
80
-
81
- class UnrelatedSiblingChild {
82
- trackRender(): number {
83
- unrelatedRenderCount++;
84
- return unrelatedRenderCount;
85
- }
86
- }
87
- Component({
88
- selector: 'unrelated-sibling-child',
89
- standalone: true,
90
- template: '<symbiote-text>unrelated {{ trackRender() }}</symbiote-text>',
91
- })(UnrelatedSiblingChild);
92
-
93
- class TargetedComponent {}
94
- Component({
95
- selector: 'symbiote-angular-targeted',
96
- standalone: true,
97
- imports: [TestView, CounterChild, UnrelatedSiblingChild],
98
- template: `<symbiote-view><counter-child /><unrelated-sibling-child /></symbiote-view>`,
99
- })(TargetedComponent);
100
-
101
- describe('Angular mount', () => {
102
- it('bootstraps a standalone component into a committed Fabric tree', async () => {
103
- mount(ROOT_TAG, SmokeComponent);
104
- await tick();
105
-
106
- const root = fabric.appRoot();
107
- expect(fabric.serialize(root.children)).toBe(
108
- 'RCTView(RCTText(RCTRawText "Hello Angular")RCTView(RCTText(RCTRawText "tapped 0×")))',
109
- );
110
- expect(root.children[0]?.props).toMatchObject({ padding: 12 });
111
- });
112
-
113
- it('runs Angular change detection after a native press and recommits text', async () => {
114
- mount(ROOT_TAG, SmokeComponent);
115
- await tick();
116
-
117
- const counter = fabric.find(node => node.props.testID === 'counter');
118
- expect(counter).toBeDefined();
119
-
120
- fabric.fireEvent(counter?.instanceHandle, 'topTouchStart');
121
- fabric.fireEvent(counter?.instanceHandle, 'topTouchEnd');
122
- await drainAngularAndCommit();
123
-
124
- expect(fabric.serialize(fabric.appRoot().children)).toContain('RCTRawText "tapped 1×"');
125
- });
126
-
127
- it('does not re-check a sibling child component on a press inside a different child', async () => {
128
- unrelatedRenderCount = 0;
129
- mount(ROOT_TAG, TargetedComponent);
130
- await tick();
131
-
132
- const afterFirstPaint = unrelatedRenderCount;
133
- expect(afterFirstPaint).toBeGreaterThan(0);
134
-
135
- const counter = fabric.find(node => node.props.testID === 'counter');
136
- fabric.fireEvent(counter?.instanceHandle, 'topTouchStart');
137
- fabric.fireEvent(counter?.instanceHandle, 'topTouchEnd');
138
- await drainAngularAndCommit();
139
-
140
- expect(fabric.serialize(fabric.appRoot().children)).toContain('RCTRawText "tapped 1×"');
141
- // UnrelatedSiblingChild has no dirty descendant of its own, so it must not be re-checked
142
- // just because CounterChild (a completely separate branch) got pressed. This does NOT prove
143
- // the root's own template stays untouched — the root's template still re-runs on every
144
- // press (see render.ts).
145
- expect(unrelatedRenderCount).toBe(afterFirstPaint);
146
- });
147
- });
@@ -1,202 +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 { registerComposedComponent, SymbioteRenderer, SymbioteRendererFactory } from './index';
18
-
19
- const ROOT_TAG = 707;
20
- const PROBE_ID = 'probe';
21
-
22
- const fabric = installFabric();
23
-
24
- // A macrotask boundary drains the engine's coalesced (requestCommit) commit before asserting.
25
- const tick = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));
26
-
27
- function setup(): { surface: ReturnType<typeof createSurface>; renderer: SymbioteRenderer } {
28
- const surface = createSurface(ROOT_TAG);
29
- const renderer = new SymbioteRendererFactory(surface).createRenderer(null, null);
30
- if (!(renderer instanceof SymbioteRenderer))
31
- throw new Error('unreachable: factory built the renderer');
32
- return { surface, renderer };
33
- }
34
-
35
- beforeEach(() => fabric.reset());
36
- afterEach(() => {
37
- disposeRoot(ROOT_TAG);
38
- clearGlobalStyles();
39
- });
40
-
41
- describe('Angular SymbioteRenderer drives the engine', () => {
42
- it('maps createElement / createText / appendChild into a committed Fabric tree', async () => {
43
- const { surface, renderer } = setup();
44
- const view = renderer.createElement('symbiote-view');
45
- const text = renderer.createElement('symbiote-text');
46
- const raw = renderer.createText('Hello');
47
- renderer.appendChild(text, raw);
48
- renderer.appendChild(view, text);
49
- renderer.appendChild(surface, view);
50
- await tick();
51
-
52
- // The engine wraps surface.children in the synthetic box-none AppContainer root.
53
- const root = fabric.appRoot();
54
- expect(fabric.serialize(root.children)).toBe('RCTView(RCTText(RCTRawText "Hello"))');
55
- });
56
-
57
- it('maps additional symbiote intrinsics through the shared descriptor table', async () => {
58
- const { surface, renderer } = setup();
59
- const spinner = renderer.createElement('symbiote-activity-indicator');
60
- renderer.setProperty(spinner, 'testID', 'spinner');
61
- renderer.setProperty(spinner, 'animating', true);
62
- const image = renderer.createElement('symbiote-image');
63
- renderer.setProperty(image, 'testID', 'image');
64
- renderer.setProperty(image, 'source', { uri: 'https://example.invalid/image.png' });
65
- renderer.appendChild(surface, spinner);
66
- renderer.appendChild(surface, image);
67
- await tick();
68
-
69
- const committed = fabric.find(node => node.props.testID === 'spinner');
70
- expect(committed?.viewName).toBe('ActivityIndicatorView');
71
- expect(committed?.props.animating).toBe(true);
72
- const committedImage = fabric.find(node => node.props.testID === 'image');
73
- expect(committedImage?.viewName).toBe('RCTImageView');
74
- expect(committedImage?.props.source).toEqual({ uri: 'https://example.invalid/image.png' });
75
- });
76
-
77
- it('commits setProperty and merges per-key setStyle into one style prop', async () => {
78
- const { surface, renderer } = setup();
79
- const view = renderer.createElement('symbiote-view');
80
- renderer.setProperty(view, 'nativeID', PROBE_ID);
81
- renderer.setStyle(view, 'padding', 24);
82
- renderer.setStyle(view, 'opacity', 0.5);
83
- renderer.appendChild(surface, view);
84
- await tick();
85
-
86
- const committed = fabric.find(node => node.props.nativeID === PROBE_ID);
87
- expect(committed, 'the probed RCTView committed').toBeDefined();
88
- // Angular emits a [style] binding as per-key setStyle; the seam folds them into one style
89
- // object, then the engine HOISTS style keys to top-level Fabric props (RN's flat C++ props
90
- // contract — style is never a nested key on a committed node).
91
- expect(committed?.props).toMatchObject({ nativeID: PROBE_ID, padding: 24, opacity: 0.5 });
92
- });
93
-
94
- it('resolves addClass tokens through the shared style registry and lets explicit style win', async () => {
95
- registerStyles({ card: { padding: 10, backgroundColor: 'red' } });
96
- const { surface, renderer } = setup();
97
- const view = renderer.createElement('symbiote-view');
98
- renderer.setProperty(view, 'nativeID', PROBE_ID);
99
- // Ivy compiles class="card highlight" to one addClass call per token, never a single string.
100
- renderer.addClass(view, 'card');
101
- renderer.addClass(view, 'highlight');
102
- renderer.setStyle(view, 'backgroundColor', 'blue');
103
- renderer.appendChild(surface, view);
104
- await tick();
105
-
106
- const committed = fabric.find(node => node.props.nativeID === PROBE_ID);
107
- // padding comes from the class; backgroundColor is explicit style, so it wins over the
108
- // class-derived red — same precedence Vue's class="..."/:style="..." merge guarantees.
109
- expect(committed?.props).toMatchObject({ padding: 10, backgroundColor: 'blue' });
110
- });
111
-
112
- it('removeClass drops a token and recomputes the resolved style', async () => {
113
- registerStyles({ card: { padding: 10 }, highlight: { opacity: 0.5 } });
114
- const { surface, renderer } = setup();
115
- const view = renderer.createElement('symbiote-view');
116
- renderer.setProperty(view, 'nativeID', PROBE_ID);
117
- renderer.addClass(view, 'card');
118
- renderer.addClass(view, 'highlight');
119
- renderer.removeClass(view, 'highlight');
120
- renderer.appendChild(surface, view);
121
- await tick();
122
-
123
- const committed = fabric.find(node => node.props.nativeID === PROBE_ID);
124
- expect(committed?.props.padding).toBe(10);
125
- expect(committed?.props.opacity).toBeUndefined();
126
- });
127
-
128
- it('removeChild detaches a node from the next commit', async () => {
129
- const { surface, renderer } = setup();
130
- const keep = renderer.createElement('symbiote-view');
131
- const drop = renderer.createElement('symbiote-view');
132
- renderer.setProperty(drop, 'nativeID', PROBE_ID);
133
- renderer.appendChild(surface, keep);
134
- renderer.appendChild(surface, drop);
135
- await tick();
136
- expect(
137
- fabric.find(node => node.props.nativeID === PROBE_ID),
138
- 'present before remove',
139
- ).toBeDefined();
140
-
141
- fabric.reset();
142
- renderer.removeChild(surface, drop);
143
- await tick();
144
- expect(
145
- fabric.find(node => node.props.nativeID === PROBE_ID),
146
- 'gone after remove',
147
- ).toBeUndefined();
148
- });
149
-
150
- it('createComment yields an anchor the commit walk skips but flattens its children', async () => {
151
- const { surface, renderer } = setup();
152
- const anchor = renderer.createComment();
153
- renderer.appendChild(anchor, renderer.createElement('symbiote-view'));
154
- renderer.appendChild(surface, anchor);
155
- renderer.appendChild(surface, renderer.createElement('symbiote-text'));
156
- await tick();
157
-
158
- // The anchor never reaches Fabric; its child keeps the same sibling position. Angular
159
- // composed components use this to make their framework host element disappear.
160
- const root = fabric.appRoot();
161
- expect(fabric.serialize(root.children)).toBe('RCTViewRCTText');
162
- });
163
-
164
- it('listen attaches an explicit event listener and the unlisten fn removes it', () => {
165
- const { renderer } = setup();
166
- const view = renderer.createElement('symbiote-view');
167
- expect(isSymbioteNode(view)).toBe(true);
168
- if (!isSymbioteNode(view)) throw new Error('unreachable: createElement returns a node');
169
-
170
- const unlisten = renderer.listen(view, 'press', () => {});
171
- expect(view.listeners?.has('press'), 'listener attached under the explicit name').toBe(true);
172
- unlisten();
173
- expect(view.listeners?.has('press'), 'unlisten removed it').toBe(false);
174
- });
175
-
176
- it('listen on a global target (window/document) is an inert no-op', () => {
177
- const { renderer } = setup();
178
- expect(() => renderer.listen('window', 'resize', () => {})()).not.toThrow();
179
- });
180
-
181
- // App/third-party selectors must not be hardcoded into ANCHOR_HOST_COMPONENTS — the owning
182
- // package registers itself via registerComposedComponent, same as an adapter-owned composed
183
- // component. 'RefApiDemo' names examples/angular's demo component; this test never imports
184
- // it, it only proves the renderer treats an unregistered selector as a raw Fabric view name,
185
- // not a hardcoded anchor host. MUST run before the registration test below, since
186
- // registerComposedComponent mutates the module-level Set for the rest of the file.
187
- it('does not anchor-host an app-owned selector unless it self-registers', () => {
188
- const { renderer } = setup();
189
- const node = renderer.createElement('RefApiDemo');
190
- if (!isSymbioteNode(node)) throw new Error('unreachable: createElement returns a node');
191
- expect(isAnchor(node), 'falls through to a raw Fabric view, not an anchor').toBe(false);
192
- expect(node.component).toBe('RefApiDemo');
193
- });
194
-
195
- it('anchor-hosts an app-owned selector once it self-registers via registerComposedComponent', () => {
196
- registerComposedComponent('RefApiDemo');
197
- const { renderer } = setup();
198
- const node = renderer.createElement('RefApiDemo');
199
- if (!isSymbioteNode(node)) throw new Error('unreachable: createElement returns a node');
200
- expect(isAnchor(node)).toBe(true);
201
- });
202
- });
@@ -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
- });