@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
package/README.md CHANGED
@@ -78,7 +78,7 @@ export class AppComponent {
78
78
  ```
79
79
 
80
80
  The full canary is [`examples/angular`](../../examples/angular) — a stock RN 0.86 app whose
81
- [`App.ts`](../../examples/angular/App.ts) exercises the same surface as the React and Vue
81
+ [`App.ts`](../../examples/angular/src/App.ts) exercises the same surface as the React and Vue
82
82
  reference canaries, standalone components, zoneless change detection.
83
83
 
84
84
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/angular",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "SymbioteNative's Angular adapter — a Renderer2/RendererFactory2 with DOM-less bootstrap driving real native iOS/Android views through the same engine as the other adapters.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -42,7 +42,10 @@
42
42
  "metro-config.cjs",
43
43
  "babel-linker.cjs",
44
44
  "babel-register-composed.cjs",
45
- "tsconfig.angular.base.json"
45
+ "tsconfig.angular.base.json",
46
+ "!src/**/*.test.*",
47
+ "!src/**/*.spec.*",
48
+ "!src/**/*.detox.*"
46
49
  ],
47
50
  "bin": {
48
51
  "symbiote-angular-dev": "./bin/symbiote-angular-dev.cjs"
@@ -1,102 +0,0 @@
1
- // A flat-bag `onX` responder callback is invoked by the engine's event dispatch, entirely
2
- // outside Angular. In Angular 20 components compile as SignalView (not CheckAlways), so a plain
3
- // state mutation inside such a callback dirties nothing and the template stays stale — the "pan
4
- // does nothing" bug, while React/Vue repainted the same demo. SymbioteHostPropsDirective now
5
- // calls `cdr.markForCheck()` after each such callback (flags the component + ancestors with
6
- // RefreshView and notifies the scheduler), so the mutation repaints. This is the flat-root case;
7
- // responder-nested-cd.test.ts covers the App→child nesting the real app has.
8
-
9
- import '@angular/compiler';
10
- import { Component } from '@angular/core';
11
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
12
- import { installFabric, type IFakeNode } from '@symbiote-native/test-utils';
13
-
14
- import { mount, unmount } from '../render';
15
- import { ViewHost as View, TextHost as Text, SymbioteHostPropsDirective } from '../primitives';
16
-
17
- const ROOT_TAG = 970;
18
- const TOUCH_START = 'topTouchStart';
19
- const TOUCH_MOVE = 'topTouchMove';
20
- const TOUCH_END = 'topTouchEnd';
21
-
22
- const fabric = installFabric();
23
-
24
- // Fabric is clone-on-write: a prop update yields a NEW node object in the committed tree,
25
- // never in `created`. Walk the live committed child-set for post-mutation assertions.
26
- function findCommitted(predicate: (node: IFakeNode) => boolean): IFakeNode | undefined {
27
- const stack = [...fabric.committed];
28
- while (stack.length > 0) {
29
- const node = stack.pop();
30
- if (node === undefined) continue;
31
- if (predicate(node)) return node;
32
- stack.push(...node.children);
33
- }
34
- return undefined;
35
- }
36
-
37
- function statusText(testID: string): string | undefined {
38
- const node = findCommitted(n => n.props.testID === testID);
39
- const raw = node?.children[0];
40
- return typeof raw?.props.text === 'string' ? raw.props.text : undefined;
41
- }
42
-
43
- // The stable SymbioteNode (event target). Its identity and listener map survive clone-on-
44
- // write, so firing on the first-created handle reaches the live listeners.
45
- function handleFor(testID: string): unknown {
46
- const node = fabric.find(n => n.props.testID === testID);
47
- if (!node) throw new Error(`no node created with testID=${testID}`);
48
- return node.instanceHandle;
49
- }
50
-
51
- const flush = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));
52
-
53
- @Component({
54
- selector: 'symbiote-responder-cd-host',
55
- standalone: true,
56
- imports: [View, Text, SymbioteHostPropsDirective],
57
- template: `
58
- <View [symbioteHostProps]="handlers"></View>
59
- <Text [symbioteHostProps]="statusProps">{{ status }}</Text>
60
- `,
61
- })
62
- class ResponderCdHost {
63
- status = 'idle';
64
- statusProps = { testID: 'status' };
65
- handlers = {
66
- testID: 'chip',
67
- onStartShouldSetResponder: () => true,
68
- onResponderGrant: () => {
69
- this.status = 'granted';
70
- },
71
- onResponderMove: () => {
72
- this.status = 'moving';
73
- },
74
- onResponderRelease: () => {
75
- this.status = 'released';
76
- },
77
- };
78
- }
79
-
80
- beforeEach(() => fabric.reset());
81
- afterEach(() => unmount(ROOT_TAG));
82
-
83
- describe('Angular responder callback triggers change detection', () => {
84
- it('re-renders bound state mutated inside a flat-bag responder callback', async () => {
85
- mount(ROOT_TAG, ResponderCdHost);
86
- await flush();
87
- expect(statusText('status')).toBe('idle');
88
-
89
- const chip = handleFor('chip');
90
- fabric.fireEvent(chip, TOUCH_START);
91
- await flush();
92
- expect(statusText('status')).toBe('granted');
93
-
94
- fabric.fireEvent(chip, TOUCH_MOVE);
95
- await flush();
96
- expect(statusText('status')).toBe('moving');
97
-
98
- fabric.fireEvent(chip, TOUCH_END);
99
- await flush();
100
- expect(statusText('status')).toBe('released');
101
- });
102
- });
@@ -1,113 +0,0 @@
1
- // The device-faithful shape: a ROOT component whose template embeds a CHILD component that
2
- // owns the responder state + the {{status}} binding (App -> ResponderDemo). Angular 20 compiles
3
- // components as SignalView (not CheckAlways), so a plain (non-signal) state mutation inside a
4
- // flat-bag responder callback does NOT dirty the child's reactive consumer, and the scheduler's
5
- // root detectChanges() will NOT descend into it — the child's {{status}} used to stay stale
6
- // ("pan does nothing"). SymbioteHostPropsDirective now refreshes its own host component's view
7
- // right after the callback runs, so the nested child repaints.
8
-
9
- import '@angular/compiler';
10
- import { Component } from '@angular/core';
11
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
12
- import { installFabric, type IFakeNode } from '@symbiote-native/test-utils';
13
-
14
- import { mount, unmount } from '../render';
15
- import { ViewHost as View, TextHost as Text, SymbioteHostPropsDirective } from '../primitives';
16
- import { registerComposedComponent } from '../anchor-host-registry';
17
-
18
- const ROOT_TAG = 972;
19
- const TOUCH_START = 'topTouchStart';
20
- const TOUCH_MOVE = 'topTouchMove';
21
- const TOUCH_END = 'topTouchEnd';
22
-
23
- registerComposedComponent('nested-responder-inner');
24
-
25
- const fabric = installFabric();
26
-
27
- function findCommitted(predicate: (node: IFakeNode) => boolean): IFakeNode | undefined {
28
- const stack = [...fabric.committed];
29
- while (stack.length > 0) {
30
- const node = stack.pop();
31
- if (node === undefined) continue;
32
- if (predicate(node)) return node;
33
- stack.push(...node.children);
34
- }
35
- return undefined;
36
- }
37
-
38
- function statusText(testID: string): string | undefined {
39
- const node = findCommitted(n => n.props.testID === testID);
40
- const raw = node?.children[0];
41
- return typeof raw?.props.text === 'string' ? raw.props.text : undefined;
42
- }
43
-
44
- function handleFor(testID: string): unknown {
45
- const node = fabric.find(n => n.props.testID === testID);
46
- if (!node) throw new Error(`no node created with testID=${testID}`);
47
- return node.instanceHandle;
48
- }
49
-
50
- const flush = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));
51
-
52
- @Component({
53
- selector: 'nested-responder-inner',
54
- standalone: true,
55
- imports: [View, Text, SymbioteHostPropsDirective],
56
- template: `
57
- <View [symbioteHostProps]="handlers"></View>
58
- <Text [symbioteHostProps]="statusProps">{{ status }}</Text>
59
- `,
60
- })
61
- class NestedResponderInner {
62
- status = 'idle';
63
- statusProps = { testID: 'nested-status' };
64
- handlers = {
65
- testID: 'nested-chip',
66
- onStartShouldSetResponder: () => true,
67
- onResponderGrant: () => {
68
- this.status = 'granted';
69
- },
70
- onResponderMove: () => {
71
- this.status = 'moving';
72
- },
73
- onResponderRelease: () => {
74
- this.status = 'released';
75
- },
76
- };
77
- }
78
-
79
- @Component({
80
- selector: 'nested-responder-outer',
81
- standalone: true,
82
- imports: [View, NestedResponderInner],
83
- template: `
84
- <View>
85
- <nested-responder-inner></nested-responder-inner>
86
- </View>
87
- `,
88
- })
89
- class NestedResponderOuter {}
90
-
91
- beforeEach(() => fabric.reset());
92
- afterEach(() => unmount(ROOT_TAG));
93
-
94
- describe('Angular nested-child responder CD', () => {
95
- it('re-renders a nested child component`s state mutated in a flat-bag responder callback', async () => {
96
- mount(ROOT_TAG, NestedResponderOuter);
97
- await flush();
98
- expect(statusText('nested-status')).toBe('idle');
99
-
100
- const chip = handleFor('nested-chip');
101
- fabric.fireEvent(chip, TOUCH_START);
102
- await flush();
103
- expect(statusText('nested-status')).toBe('granted');
104
-
105
- fabric.fireEvent(chip, TOUCH_MOVE);
106
- await flush();
107
- expect(statusText('nested-status')).toBe('moving');
108
-
109
- fabric.fireEvent(chip, TOUCH_END);
110
- await flush();
111
- expect(statusText('nested-status')).toBe('released');
112
- });
113
- });
@@ -1,142 +0,0 @@
1
- /// <reference types="node" />
2
-
3
- import { readFileSync } from 'node:fs';
4
- import { describe, expect, it } from 'vitest';
5
-
6
- const readSource = (path: string): string => readFileSync(path, 'utf8');
7
-
8
- describe('Angular adapter gap regressions', () => {
9
- it('VirtualizedList exposes accessibility and aria inputs and forwards them to ScrollView', () => {
10
- const source = readSource('adapters/angular/src/components/virtualized-list/index.ts');
11
-
12
- expect(source).toContain('@Input() accessibilityLabel?: string');
13
- expect(source).toContain('@Input() ariaBusy?: boolean');
14
- expect(source).toContain('[accessibilityLabel]="foldedAccessibility.accessibilityLabel"');
15
- expect(source).toContain('[accessibilityState]="foldedAccessibility.accessibilityState"');
16
- expect(source).toContain('[accessibilityRole]="foldedAccessibility.accessibilityRole"');
17
- });
18
-
19
- it('Animated namespace exposes AOT-safe built-in entries and documents runtime HOC as a non-goal', () => {
20
- const animatedSource = readSource('adapters/angular/src/modules/animated/index.ts');
21
- const componentSource = readSource(
22
- 'adapters/angular/src/modules/animated/create-animated-component.ts',
23
- );
24
-
25
- expect(componentSource).toContain('export const AnimatedFlatList');
26
- expect(componentSource).toContain('export const AnimatedSectionList');
27
- expect(componentSource).toContain('if (base === View) return AnimatedView');
28
- expect(componentSource).toContain('if (base === Text) return AnimatedText');
29
- expect(componentSource).toContain('if (base === Image) return AnimatedImage');
30
- expect(componentSource).toContain('if (base === ScrollView) return AnimatedScrollView');
31
- expect(componentSource).toContain('Angular cannot synthesize a component at runtime');
32
- expect(componentSource).toContain('no JIT under AOT/Metro');
33
- expect(componentSource).toContain(
34
- 'Author an explicit standalone @Component extending AnimatedComponentBase',
35
- );
36
- expect(animatedSource).toContain('View: AnimatedView');
37
- expect(animatedSource).toContain('Text: AnimatedText');
38
- expect(animatedSource).toContain('Image: AnimatedImage');
39
- expect(animatedSource).toContain('ScrollView: AnimatedScrollView');
40
- expect(animatedSource).toContain('FlatList: AnimatedFlatList');
41
- expect(animatedSource).toContain('SectionList: AnimatedSectionList');
42
- });
43
-
44
- it('AnimatedImage uses composed Image props and normalized load events', () => {
45
- const source = readSource('adapters/angular/src/modules/animated/create-animated-component.ts');
46
-
47
- expect(source).toContain('export class AnimatedImage extends ImageBase');
48
- expect(source).toContain('inputs: ANIMATED_IMAGE_INPUTS');
49
- expect(source).toContain('outputs: IMAGE_OUTPUTS');
50
- expect(source).toContain('const resolved = resolveImageProps(reduced)');
51
- expect(source).toContain('(load)="handleLoad($event)"');
52
- expect(source).toContain('(error)="handleError($event)"');
53
- });
54
-
55
- it('Image prop resolution is shared with AnimatedImage', () => {
56
- const imageSource = readSource('adapters/angular/src/components/image/shared.ts');
57
-
58
- expect(imageSource).toContain('export function resolveImageProps');
59
- expect(imageSource).toContain('get imageProps(): Record<string, unknown>');
60
- expect(imageSource).toContain('return resolveImageProps(this.imageInputProps)');
61
- });
62
-
63
- it('Button forwards its full accessibility and TV-focus surface through TouchableOpacity', () => {
64
- const buttonSource = readSource('adapters/angular/src/components/button.ts');
65
- const touchableSource = readSource('adapters/angular/src/components/touchable/index.ts');
66
- const pressableSource = readSource('adapters/angular/src/components/pressable/index.ts');
67
-
68
- for (const binding of [
69
- '[accessible]="true"',
70
- '[accessibilityLabelledBy]="accessibilityLabelledBy"',
71
- '[importantForAccessibility]="importantForAccessibility"',
72
- '[accessibilityLiveRegion]="accessibilityLiveRegion"',
73
- '[screenReaderFocusable]="screenReaderFocusable"',
74
- '[accessibilityViewIsModal]="accessibilityViewIsModal"',
75
- '[accessibilityElementsHidden]="accessibilityElementsHidden"',
76
- '[accessibilityIgnoresInvertColors]="accessibilityIgnoresInvertColors"',
77
- '[accessibilityLanguage]="accessibilityLanguage"',
78
- '[accessibilityRespondsToUserInteraction]="accessibilityRespondsToUserInteraction"',
79
- '[accessibilityShowsLargeContentViewer]="accessibilityShowsLargeContentViewer"',
80
- '[accessibilityLargeContentTitle]="accessibilityLargeContentTitle"',
81
- '(accessibilityAction)="accessibilityAction.emit($event)"',
82
- '(accessibilityTap)="accessibilityTap.emit($event)"',
83
- '(magicTap)="magicTap.emit($event)"',
84
- '(accessibilityEscape)="accessibilityEscape.emit($event)"',
85
- '[ariaModal]="ariaModal"',
86
- '[ariaValueMax]="ariaValueMax"',
87
- '[ariaValueMin]="ariaValueMin"',
88
- '[ariaValueNow]="ariaValueNow"',
89
- '[ariaValueText]="ariaValueText"',
90
- '[hasTVPreferredFocus]="hasTVPreferredFocus"',
91
- '[nextFocusDown]="nextFocusDown"',
92
- '[nextFocusForward]="nextFocusForward"',
93
- '[nextFocusLeft]="nextFocusLeft"',
94
- '[nextFocusRight]="nextFocusRight"',
95
- '[nextFocusUp]="nextFocusUp"',
96
- ]) {
97
- expect(buttonSource).toContain(binding);
98
- }
99
-
100
- for (const binding of [
101
- '[accessible]="accessible"',
102
- '[accessibilityLabelledBy]="accessibilityLabelledBy"',
103
- '[importantForAccessibility]="importantForAccessibility"',
104
- '[accessibilityLiveRegion]="accessibilityLiveRegion"',
105
- '[screenReaderFocusable]="screenReaderFocusable"',
106
- '[accessibilityViewIsModal]="accessibilityViewIsModal"',
107
- '[accessibilityElementsHidden]="accessibilityElementsHidden"',
108
- '[accessibilityIgnoresInvertColors]="accessibilityIgnoresInvertColors"',
109
- '[accessibilityLanguage]="accessibilityLanguage"',
110
- '[accessibilityRespondsToUserInteraction]="accessibilityRespondsToUserInteraction"',
111
- '[accessibilityShowsLargeContentViewer]="accessibilityShowsLargeContentViewer"',
112
- '[accessibilityLargeContentTitle]="accessibilityLargeContentTitle"',
113
- '(accessibilityAction)="accessibilityAction.emit($event)"',
114
- '(accessibilityTap)="accessibilityTap.emit($event)"',
115
- '(magicTap)="magicTap.emit($event)"',
116
- '(accessibilityEscape)="accessibilityEscape.emit($event)"',
117
- '[ariaModal]="ariaModal"',
118
- '[ariaValueMax]="ariaValueMax"',
119
- '[ariaValueMin]="ariaValueMin"',
120
- '[ariaValueNow]="ariaValueNow"',
121
- '[ariaValueText]="ariaValueText"',
122
- '[hasTVPreferredFocus]="hasTVPreferredFocus"',
123
- '[nextFocusDown]="nextFocusDown"',
124
- '[nextFocusForward]="nextFocusForward"',
125
- '[nextFocusLeft]="nextFocusLeft"',
126
- '[nextFocusRight]="nextFocusRight"',
127
- '[nextFocusUp]="nextFocusUp"',
128
- ]) {
129
- expect(touchableSource).toContain(binding);
130
- }
131
-
132
- // Pressable forwards its resolved props through the shared SymbioteHostPropsDirective
133
- // (adapters/angular/src/primitives/shared.ts) rather than one `[prop]="x"` binding per
134
- // key, so the contract to check is: the binding exists, and the `hostProps` getter it
135
- // reads from actually assembles `accessible` / the folded accessibility bag / TV-focus.
136
- expect(pressableSource).toContain('[symbioteHostProps]="hostProps"');
137
- expect(pressableSource).toContain('accessible: this.accessible');
138
- expect(pressableSource).toContain('...this.foldedAccessibility');
139
- expect(pressableSource).toContain('hasTVPreferredFocus: this.hasTVPreferredFocus');
140
- expect(pressableSource).toContain('(accessibilityAction)="emit(accessibilityAction, $event)"');
141
- });
142
- });
@@ -1,100 +0,0 @@
1
- // Angular source-import harness regression. Vitest imports adapter source (`src/*.ts`) directly,
2
- // not the partial-Ivy `ngc` output Metro consumes. The adapter tsconfig must therefore enable
3
- // TypeScript's legacy decorator lowering so Node never sees raw `@Component` / `@Directive`
4
- // syntax. Importing `@angular/compiler` enables JIT metadata creation for these source-only tests;
5
- // production still proves AOT/partial-Ivy through `pnpm --filter @symbiote-native/angular ng:build`.
6
-
7
- import '@angular/compiler';
8
- import { ElementRef, Injector, runInInjectionContext } from '@angular/core';
9
- import { describe, expect, it } from 'vitest';
10
- import { Image, ScrollView, Text, View } from './components';
11
- import { VirtualizedList, VListItemDirective } from './components/virtualized-list';
12
- import { SymbioteHostPropsDirective } from './primitives';
13
- import {
14
- Animated,
15
- AnimatedImage,
16
- AnimatedScrollView,
17
- AnimatedText,
18
- AnimatedView,
19
- createAnimatedComponent,
20
- } from './modules/animated';
21
-
22
- interface IAngularCompiledComponent {
23
- ɵcmp?: { selectors?: unknown };
24
- }
25
-
26
- interface IAngularCompiledDirective {
27
- ɵdir?: { selectors?: unknown };
28
- }
29
-
30
- describe('Angular source imports under Vitest', () => {
31
- it('imports decorated adapter components and directives directly from src', () => {
32
- expect(VirtualizedList).toBeTypeOf('function');
33
- expect(VListItemDirective).toBeTypeOf('function');
34
- expect(AnimatedView).toBeTypeOf('function');
35
- expect(SymbioteHostPropsDirective).toBeTypeOf('function');
36
- });
37
-
38
- it('keeps Angular decorator metadata available to the JIT test runtime', () => {
39
- expect((VirtualizedList as IAngularCompiledComponent).ɵcmp?.selectors).toEqual([
40
- ['VirtualizedList'],
41
- ]);
42
- expect((AnimatedView as IAngularCompiledComponent).ɵcmp?.selectors).toEqual([
43
- ['AnimatedView'],
44
- ['symbiote-animated-view'],
45
- ]);
46
- expect((VListItemDirective as IAngularCompiledDirective).ɵdir?.selectors).toEqual([
47
- ['', 'vListItem', ''],
48
- ]);
49
- expect((SymbioteHostPropsDirective as IAngularCompiledDirective).ɵdir?.selectors).toEqual([
50
- ['', 'symbioteHostProps', ''],
51
- ]);
52
- });
53
-
54
- it('imports the Animated namespace without losing component identity', () => {
55
- expect(Animated.View).toBe(AnimatedView);
56
- expect(Animated.Text).toBe(AnimatedText);
57
- expect(Animated.Image).toBe(AnimatedImage);
58
- expect(Animated.ScrollView).toBe(AnimatedScrollView);
59
- expect(Animated.FlatList).toBeTypeOf('function');
60
- expect(Animated.SectionList).toBeTypeOf('function');
61
- });
62
-
63
- it('keeps createAnimatedComponent limited to pre-authored AOT-safe Angular wrappers', () => {
64
- class CustomComponent {}
65
-
66
- expect(createAnimatedComponent(View)).toBe(AnimatedView);
67
- expect(createAnimatedComponent(Text)).toBe(AnimatedText);
68
- expect(createAnimatedComponent(Image)).toBe(AnimatedImage);
69
- expect(createAnimatedComponent(ScrollView)).toBe(AnimatedScrollView);
70
- expect(() => createAnimatedComponent(CustomComponent)).toThrow(
71
- /Angular cannot synthesize a component at runtime \(no JIT compiler under AOT\/Metro\)/,
72
- );
73
- expect(() => createAnimatedComponent(CustomComponent)).toThrow(
74
- /Author an explicit standalone @Component extending AnimatedComponentBase instead/,
75
- );
76
- });
77
-
78
- it('resolves AnimatedImage props through the composed Image path', () => {
79
- // AnimatedImage is an ANCHOR_HOST_COMPONENT: its field initializer injects its own
80
- // ElementRef (anchorHostStyle merge, see create-animated-component.ts), so constructing it
81
- // outside Angular's component machinery needs an explicit injection context.
82
- const injector = Injector.create({
83
- providers: [{ provide: ElementRef, useValue: new ElementRef({}) }],
84
- });
85
- const image = runInInjectionContext(injector, () => new AnimatedImage());
86
- image.src = 'https://example.invalid/image.png';
87
- image.width = 32;
88
- image.height = 24;
89
- image.alt = 'Preview';
90
- image.animatedProps = { style: { opacity: 0.5 }, testID: 'animated-image' };
91
-
92
- expect(image.animatedImageProps).toMatchObject({
93
- testID: 'animated-image',
94
- accessible: true,
95
- accessibilityLabel: 'Preview',
96
- source: [{ uri: 'https://example.invalid/image.png' }],
97
- style: [undefined, [{ width: 32, height: 24 }, { opacity: 0.5 }]],
98
- });
99
- });
100
- });
@@ -1,44 +0,0 @@
1
- // Regression test for the anchor/class bug: ActivityIndicator is its own
2
- // ANCHOR_HOST_COMPONENTS entry AND renders through DescriptorOutlet (`symbiote-descriptor-outlet`,
3
- // itself also an anchor entry) — a class= on <ActivityIndicator> resolves onto ActivityIndicator's
4
- // OWN anchor, two levels up from the real committed wrapper View the descriptor builds, and needs
5
- // its OWN anchorHostStyle merge (see shared.ts's `descriptor` getter). Mirrors pressable.test.ts's
6
- // "resolves a class=" case.
7
- import '@angular/compiler';
8
- import { Component } from '@angular/core';
9
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
10
- import { clearGlobalStyles, registerStyles } from '@symbiote-native/engine';
11
- import { installFabric } from '@symbiote-native/test-utils';
12
-
13
- import { mount, unmount } from '../../render';
14
- import { ActivityIndicator } from './index';
15
-
16
- const ROOT_TAG = 909;
17
- const fabric = installFabric();
18
-
19
- @Component({
20
- selector: 'symbiote-activity-indicator-class-host',
21
- standalone: true,
22
- imports: [ActivityIndicator],
23
- template: `<ActivityIndicator class="card" />`,
24
- })
25
- class ActivityIndicatorClassHost {}
26
-
27
- beforeEach(() => {
28
- fabric.reset();
29
- registerStyles({ card: { backgroundColor: 'red' } });
30
- });
31
- afterEach(() => {
32
- unmount(ROOT_TAG);
33
- clearGlobalStyles();
34
- });
35
-
36
- describe('ActivityIndicator anchor class= resolution', () => {
37
- it('resolves a class= on the ActivityIndicator use site onto the real committed wrapper View', async () => {
38
- mount(ROOT_TAG, ActivityIndicatorClassHost);
39
- await new Promise<void>(resolve => setTimeout(resolve, 0));
40
-
41
- const node = fabric.find(n => n.props.backgroundColor === 'red');
42
- expect(node, 'a real Fabric node carries the class-derived style').toBeDefined();
43
- });
44
- });
@@ -1,131 +0,0 @@
1
- import '@angular/compiler';
2
- import { Component, signal } from '@angular/core';
3
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
- import { installFabric, type IFakeNode } from '@symbiote-native/test-utils';
5
-
6
- import { mount, unmount } from '../../render';
7
- import { ActivityIndicator } from './index';
8
-
9
- const ROOT_TAG = 905;
10
- const fabric = installFabric();
11
-
12
- let capturedHost: ActivityIndicatorHost | undefined;
13
-
14
- function findCommitted(predicate: (node: IFakeNode) => boolean): IFakeNode | undefined {
15
- const visit = (node: IFakeNode): IFakeNode | undefined => {
16
- if (predicate(node)) return node;
17
- for (const child of node.children) {
18
- const found = visit(child);
19
- if (found) return found;
20
- }
21
- return undefined;
22
- };
23
- for (const node of fabric.committed) {
24
- const found = visit(node);
25
- if (found) return found;
26
- }
27
- return undefined;
28
- }
29
-
30
- function findSpinner(): IFakeNode {
31
- const node = findCommitted(n => n.viewName === 'ActivityIndicatorView');
32
- if (!node) throw new Error('no ActivityIndicatorView was created');
33
- return node;
34
- }
35
-
36
- function findWrapper(): IFakeNode {
37
- const node = findCommitted(n => n.viewName === 'RCTView' && n.props.pointerEvents !== 'box-none');
38
- if (!node) throw new Error('no ActivityIndicator wrapper was created');
39
- return node;
40
- }
41
-
42
- @Component({
43
- selector: 'symbiote-activity-indicator-host',
44
- standalone: true,
45
- imports: [ActivityIndicator],
46
- template: `
47
- <ActivityIndicator
48
- [size]="size()"
49
- [color]="color()"
50
- [animating]="animating()"
51
- [testID]="testID()"
52
- [accessible]="true"
53
- [accessibilityLabel]="'loading'"
54
- (layout)="onLayout($event)"
55
- />
56
- `,
57
- })
58
- class ActivityIndicatorHost {
59
- readonly size = signal<'small' | 'large' | number>('large');
60
- readonly color = signal('#0000ff');
61
- readonly animating = signal(false);
62
- readonly testID = signal('spinner-wrapper');
63
- onLayout = vi.fn();
64
-
65
- constructor() {
66
- // Captures the live component instance so the test can drive its signals after mount.
67
- // eslint-disable-next-line @typescript-eslint/no-this-alias
68
- capturedHost = this;
69
- }
70
- }
71
-
72
- beforeEach(() => {
73
- capturedHost = undefined;
74
- fabric.reset();
75
- });
76
- afterEach(() => unmount(ROOT_TAG));
77
-
78
- describe('ActivityIndicator', () => {
79
- it('renders through the shared descriptor shape and forwards wrapper props', async () => {
80
- mount(ROOT_TAG, ActivityIndicatorHost);
81
- await new Promise<void>(resolve => setTimeout(resolve, 0));
82
-
83
- expect(fabric.serialize(fabric.appRoot().children)).toBe('RCTView(ActivityIndicatorView)');
84
-
85
- const spinner = findSpinner();
86
- expect(spinner.props.animating).toBe(false);
87
- expect(spinner.props.color).toBe('#0000ff');
88
- expect(spinner.props.hidesWhenStopped).toBe(true);
89
- expect(spinner.props.size).toBe('large');
90
- expect(spinner.props.width).toBe(36);
91
- expect(spinner.props.height).toBe(36);
92
-
93
- const wrapper = findWrapper();
94
- expect(wrapper.props.testID).toBe('spinner-wrapper');
95
- expect(wrapper.props.accessible).toBe(true);
96
- expect(wrapper.props.accessibilityLabel).toBe('loading');
97
- expect(wrapper.props.alignItems).toBe('center');
98
- expect(wrapper.props.justifyContent).toBe('center');
99
-
100
- fabric.fireEvent(wrapper.instanceHandle, 'topLayout', {});
101
- expect(capturedHost?.onLayout).toHaveBeenCalledOnce();
102
- });
103
-
104
- it('patches the descriptor without remounting the native wrapper', async () => {
105
- mount(ROOT_TAG, ActivityIndicatorHost);
106
- await new Promise<void>(resolve => setTimeout(resolve, 0));
107
-
108
- const before = findWrapper();
109
- const createdBefore = fabric.counts.createNode;
110
-
111
- if (!capturedHost) throw new Error('host was not captured');
112
- capturedHost.size.set(48);
113
- capturedHost.color.set('#ff0000');
114
- capturedHost.animating.set(true);
115
- capturedHost.testID.set('spinner-wrapper-updated');
116
- await Promise.resolve();
117
- await new Promise<void>(resolve => setTimeout(resolve, 0));
118
-
119
- const after = findWrapper();
120
- expect(after.instanceHandle).toBe(before.instanceHandle);
121
- expect(fabric.counts.createNode).toBe(createdBefore);
122
- expect(after.props.testID).toBe('spinner-wrapper-updated');
123
- const spinner = findSpinner();
124
- expect(spinner.props.color).toBe('#ff0000');
125
- expect(spinner.props.animating).toBe(true);
126
- expect(spinner.props.width).toBe(48);
127
- expect(spinner.props.height).toBe(48);
128
- // Updating from string size to numeric size sends null to reset the previous native enum.
129
- expect(spinner.props.size).toBeNull();
130
- });
131
- });