@symbiote-native/angular 0.0.0-canary-20260724081123
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +157 -0
- package/babel-linker.cjs +7 -0
- package/babel-register-composed.cjs +127 -0
- package/bin/symbiote-angular-dev.cjs +93 -0
- package/build/angular/anchor-host-registry.d.ts +2 -0
- package/build/angular/anchor-host-registry.js +84 -0
- package/build/angular/anchor-host-registry.js.map +1 -0
- package/build/angular/bootstrap.d.ts +7 -0
- package/build/angular/bootstrap.js +15 -0
- package/build/angular/bootstrap.js.map +1 -0
- package/build/angular/components/activity-indicator/index.android.d.ts +12 -0
- package/build/angular/components/activity-indicator/index.android.js +21 -0
- package/build/angular/components/activity-indicator/index.android.js.map +1 -0
- package/build/angular/components/activity-indicator/index.d.ts +1 -0
- package/build/angular/components/activity-indicator/index.ios.d.ts +8 -0
- package/build/angular/components/activity-indicator/index.ios.js +21 -0
- package/build/angular/components/activity-indicator/index.ios.js.map +1 -0
- package/build/angular/components/activity-indicator/index.js +4 -0
- package/build/angular/components/activity-indicator/index.js.map +1 -0
- package/build/angular/components/activity-indicator/shared.d.ts +67 -0
- package/build/angular/components/activity-indicator/shared.js +260 -0
- package/build/angular/components/activity-indicator/shared.js.map +1 -0
- package/build/angular/components/button.d.ts +63 -0
- package/build/angular/components/button.js +332 -0
- package/build/angular/components/button.js.map +1 -0
- package/build/angular/components/flat-list/index.d.ts +122 -0
- package/build/angular/components/flat-list/index.js +688 -0
- package/build/angular/components/flat-list/index.js.map +1 -0
- package/build/angular/components/image/index.android.d.ts +10 -0
- package/build/angular/components/image/index.android.js +59 -0
- package/build/angular/components/image/index.android.js.map +1 -0
- package/build/angular/components/image/index.d.ts +1 -0
- package/build/angular/components/image/index.ios.d.ts +10 -0
- package/build/angular/components/image/index.ios.js +59 -0
- package/build/angular/components/image/index.ios.js.map +1 -0
- package/build/angular/components/image/index.js +3 -0
- package/build/angular/components/image/index.js.map +1 -0
- package/build/angular/components/image/shared.d.ts +101 -0
- package/build/angular/components/image/shared.js +406 -0
- package/build/angular/components/image/shared.js.map +1 -0
- package/build/angular/components/image-background.d.ts +81 -0
- package/build/angular/components/image-background.js +418 -0
- package/build/angular/components/image-background.js.map +1 -0
- package/build/angular/components/input-accessory-view/index.d.ts +60 -0
- package/build/angular/components/input-accessory-view/index.js +251 -0
- package/build/angular/components/input-accessory-view/index.js.map +1 -0
- package/build/angular/components/keyboard-avoiding-view/index.d.ts +82 -0
- package/build/angular/components/keyboard-avoiding-view/index.js +358 -0
- package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -0
- package/build/angular/components/modal/index.d.ts +92 -0
- package/build/angular/components/modal/index.js +360 -0
- package/build/angular/components/modal/index.js.map +1 -0
- package/build/angular/components/pressable/index.d.ts +123 -0
- package/build/angular/components/pressable/index.js +420 -0
- package/build/angular/components/pressable/index.js.map +1 -0
- package/build/angular/components/refresh-control/index.d.ts +86 -0
- package/build/angular/components/refresh-control/index.js +342 -0
- package/build/angular/components/refresh-control/index.js.map +1 -0
- package/build/angular/components/safe-area-view/index.d.ts +57 -0
- package/build/angular/components/safe-area-view/index.js +248 -0
- package/build/angular/components/safe-area-view/index.js.map +1 -0
- package/build/angular/components/scroll-view/index.android.d.ts +21 -0
- package/build/angular/components/scroll-view/index.android.js +235 -0
- package/build/angular/components/scroll-view/index.android.js.map +1 -0
- package/build/angular/components/scroll-view/index.d.ts +3 -0
- package/build/angular/components/scroll-view/index.ios.d.ts +9 -0
- package/build/angular/components/scroll-view/index.ios.js +86 -0
- package/build/angular/components/scroll-view/index.ios.js.map +1 -0
- package/build/angular/components/scroll-view/index.js +6 -0
- package/build/angular/components/scroll-view/index.js.map +1 -0
- package/build/angular/components/scroll-view/projection.d.ts +32 -0
- package/build/angular/components/scroll-view/projection.js +334 -0
- package/build/angular/components/scroll-view/projection.js.map +1 -0
- package/build/angular/components/scroll-view/shared.d.ts +230 -0
- package/build/angular/components/scroll-view/shared.js +682 -0
- package/build/angular/components/scroll-view/shared.js.map +1 -0
- package/build/angular/components/scroll-view/sticky-header.d.ts +40 -0
- package/build/angular/components/scroll-view/sticky-header.js +225 -0
- package/build/angular/components/scroll-view/sticky-header.js.map +1 -0
- package/build/angular/components/section-list/index.d.ts +116 -0
- package/build/angular/components/section-list/index.js +641 -0
- package/build/angular/components/section-list/index.js.map +1 -0
- package/build/angular/components/switch/index.android.d.ts +20 -0
- package/build/angular/components/switch/index.android.js +70 -0
- package/build/angular/components/switch/index.android.js.map +1 -0
- package/build/angular/components/switch/index.d.ts +1 -0
- package/build/angular/components/switch/index.ios.d.ts +19 -0
- package/build/angular/components/switch/index.ios.js +69 -0
- package/build/angular/components/switch/index.ios.js.map +1 -0
- package/build/angular/components/switch/index.js +4 -0
- package/build/angular/components/switch/index.js.map +1 -0
- package/build/angular/components/switch/shared.d.ts +40 -0
- package/build/angular/components/switch/shared.js +127 -0
- package/build/angular/components/switch/shared.js.map +1 -0
- package/build/angular/components/text-input.d.ts +153 -0
- package/build/angular/components/text-input.js +599 -0
- package/build/angular/components/text-input.js.map +1 -0
- package/build/angular/components/touchable/index.d.ts +245 -0
- package/build/angular/components/touchable/index.js +1186 -0
- package/build/angular/components/touchable/index.js.map +1 -0
- package/build/angular/components/touchable-native-feedback/index.d.ts +83 -0
- package/build/angular/components/touchable-native-feedback/index.js +371 -0
- package/build/angular/components/touchable-native-feedback/index.js.map +1 -0
- package/build/angular/components/virtualized-list/directives.d.ts +54 -0
- package/build/angular/components/virtualized-list/directives.js +153 -0
- package/build/angular/components/virtualized-list/directives.js.map +1 -0
- package/build/angular/components/virtualized-list/index.d.ts +253 -0
- package/build/angular/components/virtualized-list/index.js +1031 -0
- package/build/angular/components/virtualized-list/index.js.map +1 -0
- package/build/angular/components/virtualized-section-list/directives.d.ts +37 -0
- package/build/angular/components/virtualized-section-list/directives.js +76 -0
- package/build/angular/components/virtualized-section-list/directives.js.map +1 -0
- package/build/angular/components/virtualized-section-list/index.d.ts +178 -0
- package/build/angular/components/virtualized-section-list/index.js +720 -0
- package/build/angular/components/virtualized-section-list/index.js.map +1 -0
- package/build/angular/components.d.ts +39 -0
- package/build/angular/components.js +38 -0
- package/build/angular/components.js.map +1 -0
- package/build/angular/create-portal/index.d.ts +22 -0
- package/build/angular/create-portal/index.js +74 -0
- package/build/angular/create-portal/index.js.map +1 -0
- package/build/angular/create-tunnel/index.d.ts +34 -0
- package/build/angular/create-tunnel/index.js +126 -0
- package/build/angular/create-tunnel/index.js.map +1 -0
- package/build/angular/descriptor-to-angular/index.d.ts +22 -0
- package/build/angular/descriptor-to-angular/index.js +142 -0
- package/build/angular/descriptor-to-angular/index.js.map +1 -0
- package/build/angular/host-instance/index.d.ts +2 -0
- package/build/angular/host-instance/index.js +37 -0
- package/build/angular/host-instance/index.js.map +1 -0
- package/build/angular/index.d.ts +22 -0
- package/build/angular/index.js +46 -0
- package/build/angular/index.js.map +1 -0
- package/build/angular/modules/animated/animated-leaf-binder/index.d.ts +14 -0
- package/build/angular/modules/animated/animated-leaf-binder/index.js +91 -0
- package/build/angular/modules/animated/animated-leaf-binder/index.js.map +1 -0
- package/build/angular/modules/animated/animated-leaf-binder.d.ts +14 -0
- package/build/angular/modules/animated/animated-leaf-binder.js +91 -0
- package/build/angular/modules/animated/animated-leaf-binder.js.map +1 -0
- package/build/angular/modules/animated/create-animated-component.d.ts +59 -0
- package/build/angular/modules/animated/create-animated-component.js +364 -0
- package/build/angular/modules/animated/create-animated-component.js.map +1 -0
- package/build/angular/modules/animated/index.d.ts +51 -0
- package/build/angular/modules/animated/index.js +59 -0
- package/build/angular/modules/animated/index.js.map +1 -0
- package/build/angular/modules/app-registry/index.d.ts +7 -0
- package/build/angular/modules/app-registry/index.js +23 -0
- package/build/angular/modules/app-registry/index.js.map +1 -0
- package/build/angular/modules/status-bar/index.d.ts +20 -0
- package/build/angular/modules/status-bar/index.js +59 -0
- package/build/angular/modules/status-bar/index.js.map +1 -0
- package/build/angular/primitives/index.d.ts +63 -0
- package/build/angular/primitives/index.js +189 -0
- package/build/angular/primitives/index.js.map +1 -0
- package/build/angular/primitives/shared.d.ts +88 -0
- package/build/angular/primitives/shared.js +171 -0
- package/build/angular/primitives/shared.js.map +1 -0
- package/build/angular/render/index.d.ts +11 -0
- package/build/angular/render/index.js +171 -0
- package/build/angular/render/index.js.map +1 -0
- package/build/angular/renderer/index.d.ts +37 -0
- package/build/angular/renderer/index.js +301 -0
- package/build/angular/renderer/index.js.map +1 -0
- package/build/angular/services/color-scheme.service.d.ts +16 -0
- package/build/angular/services/color-scheme.service.js +31 -0
- package/build/angular/services/color-scheme.service.js.map +1 -0
- package/build/angular/services/index.d.ts +2 -0
- package/build/angular/services/index.js +3 -0
- package/build/angular/services/index.js.map +1 -0
- package/build/angular/services/window-dimensions.service.d.ts +16 -0
- package/build/angular/services/window-dimensions.service.js +42 -0
- package/build/angular/services/window-dimensions.service.js.map +1 -0
- package/metro-config.cjs +48 -0
- package/metro-css-parser.cjs +13 -0
- package/package.json +80 -0
- package/src/__tests__/responder-change-detection.test.ts +102 -0
- package/src/__tests__/responder-nested-cd.test.ts +113 -0
- package/src/anchor-host-registry.ts +88 -0
- package/src/angular-gaps.test.ts +142 -0
- package/src/angular-imports.test.ts +100 -0
- package/src/bootstrap.ts +26 -0
- package/src/components/activity-indicator/activity-indicator-class-style.test.ts +44 -0
- package/src/components/activity-indicator/activity-indicator.test.ts +131 -0
- package/src/components/activity-indicator/index.android.ts +16 -0
- package/src/components/activity-indicator/index.ios.ts +17 -0
- package/src/components/activity-indicator/index.ts +4 -0
- package/src/components/activity-indicator/shared.ts +206 -0
- package/src/components/button.ts +193 -0
- package/src/components/flat-list/column-wrapper-style-class.test.ts +116 -0
- package/src/components/flat-list/flat-list-array-style.test.ts +131 -0
- package/src/components/flat-list/flat-list-class-style.test.ts +74 -0
- package/src/components/flat-list/flat-list-scroll-containment.test.ts +107 -0
- package/src/components/flat-list/flat-list.test.ts +99 -0
- package/src/components/flat-list/index.ts +574 -0
- package/src/components/image/image-class-style.test.ts +46 -0
- package/src/components/image/index.android.ts +48 -0
- package/src/components/image/index.ios.ts +48 -0
- package/src/components/image/index.ts +3 -0
- package/src/components/image/shared.ts +479 -0
- package/src/components/image-background-class-style.test.ts +48 -0
- package/src/components/image-background-style-class.test.ts +79 -0
- package/src/components/image-background.ts +270 -0
- package/src/components/input-accessory-view/index.ts +185 -0
- package/src/components/input-accessory-view/input-accessory-view.test.ts +43 -0
- package/src/components/keyboard-avoiding-view/index.ts +303 -0
- package/src/components/keyboard-avoiding-view/keyboard-avoiding-view.test.ts +54 -0
- package/src/components/modal/index.ts +299 -0
- package/src/components/modal/modal.test.ts +43 -0
- package/src/components/pressable/index.ts +373 -0
- package/src/components/pressable/pressable.test.ts +85 -0
- package/src/components/refresh-control/index.ts +284 -0
- package/src/components/refresh-control/refresh-control.test.ts +41 -0
- package/src/components/safe-area-view/index.ts +177 -0
- package/src/components/safe-area-view/safe-area-view.test.ts +43 -0
- package/src/components/scroll-view/android-scroll-view-axis-projection.test.ts +123 -0
- package/src/components/scroll-view/content-container-style-class.test.ts +75 -0
- package/src/components/scroll-view/index.android.ts +190 -0
- package/src/components/scroll-view/index.ios.ts +79 -0
- package/src/components/scroll-view/index.ts +7 -0
- package/src/components/scroll-view/projection.ts +414 -0
- package/src/components/scroll-view/scroll-view-class-style.test.ts +134 -0
- package/src/components/scroll-view/scroll-view-projection.test.ts +194 -0
- package/src/components/scroll-view/shared.ts +844 -0
- package/src/components/scroll-view/sticky-header.ts +255 -0
- package/src/components/section-list/index.ts +445 -0
- package/src/components/switch/android-switch-class.test.ts +42 -0
- package/src/components/switch/index.android.ts +61 -0
- package/src/components/switch/index.ios.ts +60 -0
- package/src/components/switch/index.ts +4 -0
- package/src/components/switch/shared.ts +197 -0
- package/src/components/switch/switch.test.ts +92 -0
- package/src/components/text-input-class-style.test.ts +54 -0
- package/src/components/text-input.ts +531 -0
- package/src/components/touchable/index.ts +629 -0
- package/src/components/touchable/touchable.test.ts +102 -0
- package/src/components/touchable-native-feedback/index.ts +254 -0
- package/src/components/touchable-native-feedback/touchable-native-feedback.test.ts +49 -0
- package/src/components/virtualized-list/directives.ts +168 -0
- package/src/components/virtualized-list/index.ts +992 -0
- package/src/components/virtualized-list/virtualized-list-class-style.test.ts +76 -0
- package/src/components/virtualized-section-list/directives.ts +90 -0
- package/src/components/virtualized-section-list/index.ts +601 -0
- package/src/components/virtualized-section-list/virtualized-section-list-class-style.test.ts +60 -0
- package/src/components.ts +149 -0
- package/src/create-portal/create-portal.test.ts +90 -0
- package/src/create-portal/index.ts +74 -0
- package/src/create-tunnel/create-tunnel.test.ts +120 -0
- package/src/create-tunnel/index.ts +136 -0
- package/src/descriptor-to-angular/descriptor-outlet.test.ts +94 -0
- package/src/descriptor-to-angular/index.ts +190 -0
- package/src/host-instance/host-instance.test.ts +49 -0
- package/src/host-instance/index.ts +36 -0
- package/src/index.ts +261 -0
- package/src/modules/animated/animated-leaf-binder/animated-leaf-binder.test.ts +116 -0
- package/src/modules/animated/animated-leaf-binder/index.ts +101 -0
- package/src/modules/animated/animated-leaf-binder.test.ts +116 -0
- package/src/modules/animated/animated-leaf-binder.ts +101 -0
- package/src/modules/animated/animated-scroll-view.test.ts +102 -0
- package/src/modules/animated/create-animated-component.ts +365 -0
- package/src/modules/animated/index.ts +105 -0
- package/src/modules/app-registry/app-registry.test.ts +106 -0
- package/src/modules/app-registry/index.ts +60 -0
- package/src/modules/status-bar/index.ts +57 -0
- package/src/modules/status-bar/status-bar.test.ts +50 -0
- package/src/primitives/index.ts +119 -0
- package/src/primitives/shared.ts +190 -0
- package/src/render/index.ts +235 -0
- package/src/render/render.test.ts +147 -0
- package/src/renderer/index.ts +347 -0
- package/src/renderer/renderer.test.ts +203 -0
- package/src/services/color-scheme.service.ts +27 -0
- package/src/services/index.ts +2 -0
- package/src/services/services.test.ts +163 -0
- package/src/services/window-dimensions.service.ts +41 -0
- package/tsconfig.angular.base.json +30 -0
|
@@ -0,0 +1,203 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { DestroyRef, inject, Injectable, signal } from '@angular/core';
|
|
2
|
+
import { Appearance, type IColorSchemeName } from '@symbiote-native/engine';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Angular twin of React's `useColorScheme` and Vue's `useColorScheme` composable. The service
|
|
6
|
+
* holds a Signal that tracks the device color scheme; consumers read it via `inject`:
|
|
7
|
+
*
|
|
8
|
+
* readonly colorScheme = inject(ColorSchemeService).colorScheme;
|
|
9
|
+
*
|
|
10
|
+
* State and lifecycle live in DI, not in a function closure.
|
|
11
|
+
*/
|
|
12
|
+
@Injectable({ providedIn: 'root' })
|
|
13
|
+
export class ColorSchemeService {
|
|
14
|
+
readonly colorScheme = signal<IColorSchemeName | null>(Appearance.getColorScheme());
|
|
15
|
+
|
|
16
|
+
constructor() {
|
|
17
|
+
const destroyRef = inject(DestroyRef);
|
|
18
|
+
|
|
19
|
+
const subscription = Appearance.addChangeListener(preferences => {
|
|
20
|
+
this.colorScheme.set(preferences.colorScheme);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
destroyRef.onDestroy(() => {
|
|
24
|
+
subscription.remove();
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { DestroyRef, inject, Injectable, signal } from '@angular/core';
|
|
2
|
+
import { Dimensions, type IDimensionsSet, type IDisplayMetrics } from '@symbiote-native/engine';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Angular twin of React's `useWindowDimensions` and Vue's `useWindowDimensions` composable. The
|
|
6
|
+
* service holds a Signal that tracks window metrics; consumers read it via `inject`:
|
|
7
|
+
*
|
|
8
|
+
* readonly dimensions = inject(WindowDimensionsService).dimensions;
|
|
9
|
+
*
|
|
10
|
+
* State and lifecycle live in DI, not in a function closure.
|
|
11
|
+
*/
|
|
12
|
+
@Injectable({ providedIn: 'root' })
|
|
13
|
+
export class WindowDimensionsService {
|
|
14
|
+
readonly dimensions = signal<IDisplayMetrics>(Dimensions.get('window'));
|
|
15
|
+
|
|
16
|
+
constructor() {
|
|
17
|
+
const destroyRef = inject(DestroyRef);
|
|
18
|
+
|
|
19
|
+
const handleChange = (window: IDisplayMetrics): void => {
|
|
20
|
+
const current = this.dimensions();
|
|
21
|
+
if (
|
|
22
|
+
current.width !== window.width ||
|
|
23
|
+
current.height !== window.height ||
|
|
24
|
+
current.scale !== window.scale ||
|
|
25
|
+
current.fontScale !== window.fontScale
|
|
26
|
+
) {
|
|
27
|
+
this.dimensions.set(window);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const subscription = Dimensions.addEventListener('change', (set: IDimensionsSet) => {
|
|
32
|
+
handleChange(set.window);
|
|
33
|
+
});
|
|
34
|
+
// Re-check once after subscribing to close the gap between construction and the listener.
|
|
35
|
+
handleChange(Dimensions.get('window'));
|
|
36
|
+
|
|
37
|
+
destroyRef.onDestroy(() => {
|
|
38
|
+
subscription.remove();
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
// The original (pre-packaging) example config chain was tsconfig.angular.json → ./tsconfig.json
|
|
3
|
+
// → @react-native/typescript-config, with tsconfig.angular.json's own compilerOptions (target,
|
|
4
|
+
// module, noEmit, moduleResolution, allowImportingTsExtensions, types below) overriding the RN
|
|
5
|
+
// config's conflicting esnext/noEmit:true/jsx settings. Extending it here reproduces that same
|
|
6
|
+
// precedence for every consumer, not just this repo's examples — a plain Angular+RN app expects
|
|
7
|
+
// its Angular tsconfig to inherit the RN TS baseline the same way.
|
|
8
|
+
"extends": "@react-native/typescript-config",
|
|
9
|
+
"compilerOptions": {
|
|
10
|
+
"target": "ES2022",
|
|
11
|
+
"module": "ES2022",
|
|
12
|
+
"moduleResolution": "Bundler",
|
|
13
|
+
"strict": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"noEmit": false,
|
|
16
|
+
"allowImportingTsExtensions": false,
|
|
17
|
+
"declaration": false,
|
|
18
|
+
"sourceMap": true,
|
|
19
|
+
"lib": ["ES2022", "DOM"],
|
|
20
|
+
"types": ["node"],
|
|
21
|
+
"ignoreDeprecations": "6.0",
|
|
22
|
+
"esModuleInterop": true,
|
|
23
|
+
"allowSyntheticDefaultImports": true,
|
|
24
|
+
"resolveJsonModule": true
|
|
25
|
+
},
|
|
26
|
+
"angularCompilerOptions": {
|
|
27
|
+
"compilationMode": "partial",
|
|
28
|
+
"strictTemplates": true
|
|
29
|
+
}
|
|
30
|
+
}
|