@symbiote-native/sensors 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +5 -5
  2. package/build/angular/services/accelerometer.service/index.js +7 -12
  3. package/build/angular/services/barometer.service/index.js +7 -12
  4. package/build/angular/services/device-motion.service/index.js +7 -12
  5. package/build/angular/services/gyroscope.service/index.js +7 -12
  6. package/build/angular/services/light-sensor.service/index.js +7 -12
  7. package/build/angular/services/magnetometer-uncalibrated.service/index.js +8 -12
  8. package/build/angular/services/magnetometer.service/index.js +7 -12
  9. package/build/angular/services/pedometer.service/index.js +6 -10
  10. package/build/core/device-motion.js +3 -4
  11. package/build/svelte/index.d.ts +16 -0
  12. package/build/svelte/index.js +14 -0
  13. package/build/svelte/runes/use-accelerometer.svelte.d.ts +4 -0
  14. package/build/svelte/runes/use-accelerometer.svelte.js +31 -0
  15. package/build/svelte/runes/use-barometer.svelte.d.ts +4 -0
  16. package/build/svelte/runes/use-barometer.svelte.js +22 -0
  17. package/build/svelte/runes/use-device-motion.svelte.d.ts +4 -0
  18. package/build/svelte/runes/use-device-motion.svelte.js +22 -0
  19. package/build/svelte/runes/use-gyroscope.svelte.d.ts +4 -0
  20. package/build/svelte/runes/use-gyroscope.svelte.js +22 -0
  21. package/build/svelte/runes/use-light-sensor.svelte.d.ts +4 -0
  22. package/build/svelte/runes/use-light-sensor.svelte.js +22 -0
  23. package/build/svelte/runes/use-magnetometer-uncalibrated.svelte.d.ts +4 -0
  24. package/build/svelte/runes/use-magnetometer-uncalibrated.svelte.js +22 -0
  25. package/build/svelte/runes/use-magnetometer.svelte.d.ts +4 -0
  26. package/build/svelte/runes/use-magnetometer.svelte.js +22 -0
  27. package/build/svelte/runes/use-pedometer.svelte.d.ts +4 -0
  28. package/build/svelte/runes/use-pedometer.svelte.js +20 -0
  29. package/build-ngc/angular/services/accelerometer.service/index.js +10 -15
  30. package/build-ngc/angular/services/accelerometer.service/index.js.map +1 -1
  31. package/build-ngc/angular/services/barometer.service/index.js +10 -15
  32. package/build-ngc/angular/services/barometer.service/index.js.map +1 -1
  33. package/build-ngc/angular/services/device-motion.service/index.js +10 -15
  34. package/build-ngc/angular/services/device-motion.service/index.js.map +1 -1
  35. package/build-ngc/angular/services/gyroscope.service/index.js +10 -15
  36. package/build-ngc/angular/services/gyroscope.service/index.js.map +1 -1
  37. package/build-ngc/angular/services/light-sensor.service/index.js +10 -15
  38. package/build-ngc/angular/services/light-sensor.service/index.js.map +1 -1
  39. package/build-ngc/angular/services/magnetometer-uncalibrated.service/index.js +11 -15
  40. package/build-ngc/angular/services/magnetometer-uncalibrated.service/index.js.map +1 -1
  41. package/build-ngc/angular/services/magnetometer.service/index.js +10 -15
  42. package/build-ngc/angular/services/magnetometer.service/index.js.map +1 -1
  43. package/build-ngc/angular/services/pedometer.service/index.js +9 -13
  44. package/build-ngc/angular/services/pedometer.service/index.js.map +1 -1
  45. package/build-ngc/core/device-motion.js +3 -4
  46. package/build-ngc/core/device-motion.js.map +1 -1
  47. package/native-link.json +52 -0
  48. package/package.json +31 -13
  49. package/src/angular/services/accelerometer.service/index.ts +7 -12
  50. package/src/angular/services/barometer.service/index.ts +7 -12
  51. package/src/angular/services/device-motion.service/index.ts +7 -12
  52. package/src/angular/services/gyroscope.service/index.ts +7 -12
  53. package/src/angular/services/light-sensor.service/index.ts +7 -12
  54. package/src/angular/services/magnetometer-uncalibrated.service/index.ts +8 -12
  55. package/src/angular/services/magnetometer.service/index.ts +7 -12
  56. package/src/angular/services/pedometer.service/index.ts +6 -10
  57. package/src/core/device-motion.ts +3 -4
  58. package/src/svelte/index.ts +29 -0
  59. package/src/svelte/runes/use-accelerometer.svelte.ts +36 -0
  60. package/src/svelte/runes/use-barometer.svelte.ts +27 -0
  61. package/src/svelte/runes/use-device-motion.svelte.ts +27 -0
  62. package/src/svelte/runes/use-gyroscope.svelte.ts +27 -0
  63. package/src/svelte/runes/use-light-sensor.svelte.ts +27 -0
  64. package/src/svelte/runes/use-magnetometer-uncalibrated.svelte.ts +31 -0
  65. package/src/svelte/runes/use-magnetometer.svelte.ts +27 -0
  66. package/src/svelte/runes/use-pedometer.svelte.ts +23 -0
  67. package/src/svelte/svelte-ambient.d.ts +6 -0
  68. package/src/angular/services/accelerometer.service/accelerometer.service.test.ts +0 -118
  69. package/src/angular/services/barometer.service/barometer.service.test.ts +0 -122
  70. package/src/angular/services/device-motion.service/device-motion.service.test.ts +0 -125
  71. package/src/angular/services/gyroscope.service/gyroscope.service.test.ts +0 -118
  72. package/src/angular/services/light-sensor.service/light-sensor.service.test.ts +0 -118
  73. package/src/angular/services/magnetometer-uncalibrated.service/magnetometer-uncalibrated.service.test.ts +0 -123
  74. package/src/angular/services/magnetometer.service/magnetometer.service.test.ts +0 -118
  75. package/src/angular/services/pedometer.service/pedometer.service.test.ts +0 -86
  76. package/src/core/accelerometer.test.ts +0 -49
  77. package/src/core/barometer.test.ts +0 -46
  78. package/src/core/device-motion.test.ts +0 -60
  79. package/src/core/device-sensor.test.ts +0 -76
  80. package/src/core/gyroscope.test.ts +0 -46
  81. package/src/core/light-sensor.test.ts +0 -49
  82. package/src/core/magnetometer-uncalibrated.test.ts +0 -49
  83. package/src/core/magnetometer.test.ts +0 -49
  84. package/src/core/pedometer.test.ts +0 -118
  85. package/src/react/hooks/use-accelerometer/use-accelerometer.test.tsx +0 -81
  86. package/src/react/hooks/use-barometer/use-barometer.test.tsx +0 -83
  87. package/src/react/hooks/use-device-motion/use-device-motion.test.tsx +0 -88
  88. package/src/react/hooks/use-gyroscope/use-gyroscope.test.tsx +0 -79
  89. package/src/react/hooks/use-light-sensor/use-light-sensor.test.tsx +0 -79
  90. package/src/react/hooks/use-magnetometer/use-magnetometer.test.tsx +0 -81
  91. package/src/react/hooks/use-magnetometer-uncalibrated/use-magnetometer-uncalibrated.test.tsx +0 -88
  92. package/src/react/hooks/use-pedometer/use-pedometer.test.tsx +0 -69
  93. package/src/vue/composables/use-accelerometer/use-accelerometer.test.ts +0 -96
  94. package/src/vue/composables/use-barometer/use-barometer.test.ts +0 -100
  95. package/src/vue/composables/use-device-motion/use-device-motion.test.ts +0 -104
  96. package/src/vue/composables/use-gyroscope/use-gyroscope.test.ts +0 -96
  97. package/src/vue/composables/use-light-sensor/use-light-sensor.test.ts +0 -96
  98. package/src/vue/composables/use-magnetometer/use-magnetometer.test.ts +0 -96
  99. package/src/vue/composables/use-magnetometer-uncalibrated/use-magnetometer-uncalibrated.test.ts +0 -103
  100. package/src/vue/composables/use-pedometer/use-pedometer.test.ts +0 -77
@@ -0,0 +1,27 @@
1
+ // Svelte lifecycle wiring over the framework-agnostic Magnetometer singleton (core/) — mirrors
2
+ // use-accelerometer.svelte.ts's shape exactly. See its header for why this lives in `runes/` with
3
+ // a `.svelte.ts` extension, returns a boxed getter object, and takes a plain `updateIntervalMs`
4
+ // rather than a getter.
5
+ import { Magnetometer, type EventSubscription, type IMagnetometerMeasurement } from '../../core';
6
+
7
+ export function useMagnetometer(updateIntervalMs?: number): {
8
+ readonly current: IMagnetometerMeasurement | null;
9
+ } {
10
+ let measurement = $state<IMagnetometerMeasurement | null>(null);
11
+
12
+ $effect(() => {
13
+ if (updateIntervalMs !== undefined) {
14
+ Magnetometer.setUpdateInterval(updateIntervalMs);
15
+ }
16
+ const subscription: EventSubscription = Magnetometer.addListener(next => {
17
+ measurement = next;
18
+ });
19
+ return () => subscription.remove();
20
+ });
21
+
22
+ return {
23
+ get current(): IMagnetometerMeasurement | null {
24
+ return measurement;
25
+ },
26
+ };
27
+ }
@@ -0,0 +1,23 @@
1
+ // Svelte lifecycle wiring over the framework-agnostic watchStepCount free function (core/) —
2
+ // mirrors use-accelerometer.svelte.ts's shape. Pedometer has no setUpdateInterval, so unlike
3
+ // useAccelerometer there is no interval param to apply at subscribe time. See
4
+ // use-accelerometer.svelte.ts's header for why this lives in `runes/` with a `.svelte.ts`
5
+ // extension and returns a boxed getter object.
6
+ import { watchStepCount, type EventSubscription, type IPedometerResult } from '../../core';
7
+
8
+ export function usePedometer(): { readonly current: IPedometerResult | null } {
9
+ let result = $state<IPedometerResult | null>(null);
10
+
11
+ $effect(() => {
12
+ const subscription: EventSubscription = watchStepCount(next => {
13
+ result = next;
14
+ });
15
+ return () => subscription.remove();
16
+ });
17
+
18
+ return {
19
+ get current(): IPedometerResult | null {
20
+ return result;
21
+ },
22
+ };
23
+ }
@@ -0,0 +1,6 @@
1
+ // Pulls in svelte's ambient global declarations ($state/$effect/...) for this package's tsc
2
+ // program. adapters/svelte gets these for free because render.ts does a real value import from
3
+ // 'svelte' (any file in a program importing a module loads its ambient globals for the whole
4
+ // program); this package has no such import, so a triple-slash reference does the same job
5
+ // explicitly rather than relying on an incidental side-effect.
6
+ /// <reference types="svelte" />
@@ -1,118 +0,0 @@
1
- // Co-located Angular-driven test (ADR 0025) for AccelerometerService. Mounts a real host
2
- // component through @symbiote-native/angular so `connect()` runs the same way an app would call
3
- // it — inside the component's own injection context — and drives the returned signal through a
4
- // full mount/unmount lifecycle, because `effect()`'s injector-scoped cleanup only fires
5
- // correctly when torn down through a real Angular injection context.
6
-
7
- import '@angular/compiler';
8
- import { Component, inject, type Signal } from '@angular/core';
9
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
10
- import { mount, unmount } from '@symbiote-native/angular';
11
- import { installFabric } from '@symbiote-native/test-utils';
12
- import type { IAccelerometerMeasurement } from '../../../core';
13
- import { AccelerometerService } from './index';
14
-
15
- const addListenerMock = vi.fn();
16
- const removeMock = vi.fn();
17
- const setUpdateIntervalMock = vi.fn();
18
-
19
- vi.mock('../../../core', () => ({
20
- Accelerometer: {
21
- addListener: (listener: (measurement: IAccelerometerMeasurement) => void) =>
22
- addListenerMock(listener),
23
- setUpdateInterval: (intervalMs: number) => setUpdateIntervalMock(intervalMs),
24
- },
25
- }));
26
-
27
- const ROOT_TAG = 941;
28
- const fabric = installFabric();
29
- const tick = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));
30
-
31
- const MEASUREMENT: IAccelerometerMeasurement = { x: 0.1, y: 0.2, z: 0.3, timestamp: 123 };
32
-
33
- let capturedResult: Signal<IAccelerometerMeasurement | null> | undefined;
34
- let capturedListener: ((measurement: IAccelerometerMeasurement) => void) | undefined;
35
-
36
- @Component({
37
- selector: 'symbiote-accelerometer-host',
38
- standalone: true,
39
- template: '',
40
- })
41
- class AccelerometerHost {
42
- readonly measurement = inject(AccelerometerService).connect();
43
-
44
- constructor() {
45
- capturedResult = this.measurement;
46
- }
47
- }
48
-
49
- @Component({
50
- selector: 'symbiote-accelerometer-interval-host',
51
- standalone: true,
52
- template: '',
53
- })
54
- class AccelerometerIntervalHost {
55
- readonly measurement = inject(AccelerometerService).connect(500);
56
-
57
- constructor() {
58
- capturedResult = this.measurement;
59
- }
60
- }
61
-
62
- beforeEach(() => {
63
- capturedResult = undefined;
64
- capturedListener = undefined;
65
- addListenerMock.mockImplementation(listener => {
66
- capturedListener = listener;
67
- return { remove: removeMock };
68
- });
69
- });
70
-
71
- afterEach(() => {
72
- unmount(ROOT_TAG);
73
- fabric.reset();
74
- vi.clearAllMocks();
75
- });
76
-
77
- describe('AccelerometerService.connect', () => {
78
- it('reports null before any measurement event fires', async () => {
79
- mount(ROOT_TAG, AccelerometerHost);
80
- await tick();
81
-
82
- expect(capturedResult?.()).toBeNull();
83
- });
84
-
85
- it('updates the signal when the registered listener fires with a measurement', async () => {
86
- mount(ROOT_TAG, AccelerometerHost);
87
- await tick();
88
-
89
- if (capturedListener === undefined) throw new Error('addListener callback was not captured');
90
- capturedListener(MEASUREMENT);
91
-
92
- expect(capturedResult?.()).toEqual(MEASUREMENT);
93
- });
94
-
95
- it('removes the subscription when the host component is unmounted', async () => {
96
- mount(ROOT_TAG, AccelerometerHost);
97
- await tick();
98
-
99
- unmount(ROOT_TAG);
100
- await tick();
101
-
102
- expect(removeMock).toHaveBeenCalledOnce();
103
- });
104
-
105
- it('calls setUpdateInterval with the given interval when one is passed', async () => {
106
- mount(ROOT_TAG, AccelerometerIntervalHost);
107
- await tick();
108
-
109
- expect(setUpdateIntervalMock).toHaveBeenCalledWith(500);
110
- });
111
-
112
- it('does not call setUpdateInterval when no interval is passed', async () => {
113
- mount(ROOT_TAG, AccelerometerHost);
114
- await tick();
115
-
116
- expect(setUpdateIntervalMock).not.toHaveBeenCalled();
117
- });
118
- });
@@ -1,122 +0,0 @@
1
- // Co-located Angular-driven test (ADR 0025) for BarometerService. Mounts a real host
2
- // component through @symbiote-native/angular so `connect()` runs the same way an app would call
3
- // it — inside the component's own injection context — and drives the returned signal through a
4
- // full mount/unmount lifecycle, because `effect()`'s injector-scoped cleanup only fires
5
- // correctly when torn down through a real Angular injection context.
6
-
7
- import '@angular/compiler';
8
- import { Component, inject, type Signal } from '@angular/core';
9
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
10
- import { mount, unmount } from '@symbiote-native/angular';
11
- import { installFabric } from '@symbiote-native/test-utils';
12
- import type { IBarometerMeasurement } from '../../../core';
13
- import { BarometerService } from './index';
14
-
15
- const addListenerMock = vi.fn();
16
- const removeMock = vi.fn();
17
- const setUpdateIntervalMock = vi.fn();
18
-
19
- vi.mock('../../../core', () => ({
20
- Barometer: {
21
- addListener: (listener: (measurement: IBarometerMeasurement) => void) =>
22
- addListenerMock(listener),
23
- setUpdateInterval: (intervalMs: number) => setUpdateIntervalMock(intervalMs),
24
- },
25
- }));
26
-
27
- const ROOT_TAG = 942;
28
- const fabric = installFabric();
29
- const tick = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));
30
-
31
- const MEASUREMENT: IBarometerMeasurement = {
32
- pressure: 1013.25,
33
- relativeAltitude: 12.3,
34
- timestamp: 123,
35
- };
36
-
37
- let capturedResult: Signal<IBarometerMeasurement | null> | undefined;
38
- let capturedListener: ((measurement: IBarometerMeasurement) => void) | undefined;
39
-
40
- @Component({
41
- selector: 'symbiote-barometer-host',
42
- standalone: true,
43
- template: '',
44
- })
45
- class BarometerHost {
46
- readonly measurement = inject(BarometerService).connect();
47
-
48
- constructor() {
49
- capturedResult = this.measurement;
50
- }
51
- }
52
-
53
- @Component({
54
- selector: 'symbiote-barometer-interval-host',
55
- standalone: true,
56
- template: '',
57
- })
58
- class BarometerIntervalHost {
59
- readonly measurement = inject(BarometerService).connect(500);
60
-
61
- constructor() {
62
- capturedResult = this.measurement;
63
- }
64
- }
65
-
66
- beforeEach(() => {
67
- capturedResult = undefined;
68
- capturedListener = undefined;
69
- addListenerMock.mockImplementation(listener => {
70
- capturedListener = listener;
71
- return { remove: removeMock };
72
- });
73
- });
74
-
75
- afterEach(() => {
76
- unmount(ROOT_TAG);
77
- fabric.reset();
78
- vi.clearAllMocks();
79
- });
80
-
81
- describe('BarometerService.connect', () => {
82
- it('reports null before any measurement event fires', async () => {
83
- mount(ROOT_TAG, BarometerHost);
84
- await tick();
85
-
86
- expect(capturedResult?.()).toBeNull();
87
- });
88
-
89
- it('updates the signal when the registered listener fires with a measurement', async () => {
90
- mount(ROOT_TAG, BarometerHost);
91
- await tick();
92
-
93
- if (capturedListener === undefined) throw new Error('addListener callback was not captured');
94
- capturedListener(MEASUREMENT);
95
-
96
- expect(capturedResult?.()).toEqual(MEASUREMENT);
97
- });
98
-
99
- it('removes the subscription when the host component is unmounted', async () => {
100
- mount(ROOT_TAG, BarometerHost);
101
- await tick();
102
-
103
- unmount(ROOT_TAG);
104
- await tick();
105
-
106
- expect(removeMock).toHaveBeenCalledOnce();
107
- });
108
-
109
- it('calls setUpdateInterval with the given interval when one is passed', async () => {
110
- mount(ROOT_TAG, BarometerIntervalHost);
111
- await tick();
112
-
113
- expect(setUpdateIntervalMock).toHaveBeenCalledWith(500);
114
- });
115
-
116
- it('does not call setUpdateInterval when no interval is passed', async () => {
117
- mount(ROOT_TAG, BarometerHost);
118
- await tick();
119
-
120
- expect(setUpdateIntervalMock).not.toHaveBeenCalled();
121
- });
122
- });
@@ -1,125 +0,0 @@
1
- // Co-located Angular-driven test (ADR 0025) for DeviceMotionService. Mounts a real host
2
- // component through @symbiote-native/angular so `connect()` runs the same way an app would call
3
- // it — inside the component's own injection context — and drives the returned signal through a
4
- // full mount/unmount lifecycle, because `effect()`'s injector-scoped cleanup only fires
5
- // correctly when torn down through a real Angular injection context.
6
-
7
- import '@angular/compiler';
8
- import { Component, inject, type Signal } from '@angular/core';
9
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
10
- import { mount, unmount } from '@symbiote-native/angular';
11
- import { installFabric } from '@symbiote-native/test-utils';
12
- import type { IDeviceMotionMeasurement } from '../../../core';
13
- import { DeviceMotionService } from './index';
14
-
15
- const addListenerMock = vi.fn();
16
- const removeMock = vi.fn();
17
- const setUpdateIntervalMock = vi.fn();
18
-
19
- vi.mock('../../../core', () => ({
20
- DeviceMotion: {
21
- addListener: (listener: (measurement: IDeviceMotionMeasurement) => void) =>
22
- addListenerMock(listener),
23
- setUpdateInterval: (intervalMs: number) => setUpdateIntervalMock(intervalMs),
24
- },
25
- }));
26
-
27
- const ROOT_TAG = 942;
28
- const fabric = installFabric();
29
- const tick = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));
30
-
31
- const MEASUREMENT: IDeviceMotionMeasurement = {
32
- acceleration: { x: 0.1, y: 0.2, z: 0.3, timestamp: 123 },
33
- accelerationIncludingGravity: { x: 0.1, y: 0.2, z: 9.8, timestamp: 123 },
34
- rotation: { alpha: 1, beta: 2, gamma: 3, timestamp: 123 },
35
- rotationRate: { alpha: 0.1, beta: 0.2, gamma: 0.3, timestamp: 123 },
36
- interval: 16,
37
- orientation: 0, // DeviceMotionOrientation.Portrait
38
- };
39
-
40
- let capturedResult: Signal<IDeviceMotionMeasurement | null> | undefined;
41
- let capturedListener: ((measurement: IDeviceMotionMeasurement) => void) | undefined;
42
-
43
- @Component({
44
- selector: 'symbiote-device-motion-host',
45
- standalone: true,
46
- template: '',
47
- })
48
- class DeviceMotionHost {
49
- readonly measurement = inject(DeviceMotionService).connect();
50
-
51
- constructor() {
52
- capturedResult = this.measurement;
53
- }
54
- }
55
-
56
- @Component({
57
- selector: 'symbiote-device-motion-interval-host',
58
- standalone: true,
59
- template: '',
60
- })
61
- class DeviceMotionIntervalHost {
62
- readonly measurement = inject(DeviceMotionService).connect(500);
63
-
64
- constructor() {
65
- capturedResult = this.measurement;
66
- }
67
- }
68
-
69
- beforeEach(() => {
70
- capturedResult = undefined;
71
- capturedListener = undefined;
72
- addListenerMock.mockImplementation(listener => {
73
- capturedListener = listener;
74
- return { remove: removeMock };
75
- });
76
- });
77
-
78
- afterEach(() => {
79
- unmount(ROOT_TAG);
80
- fabric.reset();
81
- vi.clearAllMocks();
82
- });
83
-
84
- describe('DeviceMotionService.connect', () => {
85
- it('reports null before any measurement event fires', async () => {
86
- mount(ROOT_TAG, DeviceMotionHost);
87
- await tick();
88
-
89
- expect(capturedResult?.()).toBeNull();
90
- });
91
-
92
- it('updates the signal when the registered listener fires with a measurement', async () => {
93
- mount(ROOT_TAG, DeviceMotionHost);
94
- await tick();
95
-
96
- if (capturedListener === undefined) throw new Error('addListener callback was not captured');
97
- capturedListener(MEASUREMENT);
98
-
99
- expect(capturedResult?.()).toEqual(MEASUREMENT);
100
- });
101
-
102
- it('removes the subscription when the host component is unmounted', async () => {
103
- mount(ROOT_TAG, DeviceMotionHost);
104
- await tick();
105
-
106
- unmount(ROOT_TAG);
107
- await tick();
108
-
109
- expect(removeMock).toHaveBeenCalledOnce();
110
- });
111
-
112
- it('calls setUpdateInterval with the given interval when one is passed', async () => {
113
- mount(ROOT_TAG, DeviceMotionIntervalHost);
114
- await tick();
115
-
116
- expect(setUpdateIntervalMock).toHaveBeenCalledWith(500);
117
- });
118
-
119
- it('does not call setUpdateInterval when no interval is passed', async () => {
120
- mount(ROOT_TAG, DeviceMotionHost);
121
- await tick();
122
-
123
- expect(setUpdateIntervalMock).not.toHaveBeenCalled();
124
- });
125
- });
@@ -1,118 +0,0 @@
1
- // Co-located Angular-driven test (ADR 0025) for GyroscopeService. Mounts a real host
2
- // component through @symbiote-native/angular so `connect()` runs the same way an app would call
3
- // it — inside the component's own injection context — and drives the returned signal through a
4
- // full mount/unmount lifecycle, because `effect()`'s injector-scoped cleanup only fires
5
- // correctly when torn down through a real Angular injection context.
6
-
7
- import '@angular/compiler';
8
- import { Component, inject, type Signal } from '@angular/core';
9
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
10
- import { mount, unmount } from '@symbiote-native/angular';
11
- import { installFabric } from '@symbiote-native/test-utils';
12
- import type { IGyroscopeMeasurement } from '../../../core';
13
- import { GyroscopeService } from './index';
14
-
15
- const addListenerMock = vi.fn();
16
- const removeMock = vi.fn();
17
- const setUpdateIntervalMock = vi.fn();
18
-
19
- vi.mock('../../../core', () => ({
20
- Gyroscope: {
21
- addListener: (listener: (measurement: IGyroscopeMeasurement) => void) =>
22
- addListenerMock(listener),
23
- setUpdateInterval: (intervalMs: number) => setUpdateIntervalMock(intervalMs),
24
- },
25
- }));
26
-
27
- const ROOT_TAG = 942;
28
- const fabric = installFabric();
29
- const tick = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));
30
-
31
- const MEASUREMENT: IGyroscopeMeasurement = { x: 0.1, y: 0.2, z: 0.3, timestamp: 123 };
32
-
33
- let capturedResult: Signal<IGyroscopeMeasurement | null> | undefined;
34
- let capturedListener: ((measurement: IGyroscopeMeasurement) => void) | undefined;
35
-
36
- @Component({
37
- selector: 'symbiote-gyroscope-host',
38
- standalone: true,
39
- template: '',
40
- })
41
- class GyroscopeHost {
42
- readonly measurement = inject(GyroscopeService).connect();
43
-
44
- constructor() {
45
- capturedResult = this.measurement;
46
- }
47
- }
48
-
49
- @Component({
50
- selector: 'symbiote-gyroscope-interval-host',
51
- standalone: true,
52
- template: '',
53
- })
54
- class GyroscopeIntervalHost {
55
- readonly measurement = inject(GyroscopeService).connect(500);
56
-
57
- constructor() {
58
- capturedResult = this.measurement;
59
- }
60
- }
61
-
62
- beforeEach(() => {
63
- capturedResult = undefined;
64
- capturedListener = undefined;
65
- addListenerMock.mockImplementation(listener => {
66
- capturedListener = listener;
67
- return { remove: removeMock };
68
- });
69
- });
70
-
71
- afterEach(() => {
72
- unmount(ROOT_TAG);
73
- fabric.reset();
74
- vi.clearAllMocks();
75
- });
76
-
77
- describe('GyroscopeService.connect', () => {
78
- it('reports null before any measurement event fires', async () => {
79
- mount(ROOT_TAG, GyroscopeHost);
80
- await tick();
81
-
82
- expect(capturedResult?.()).toBeNull();
83
- });
84
-
85
- it('updates the signal when the registered listener fires with a measurement', async () => {
86
- mount(ROOT_TAG, GyroscopeHost);
87
- await tick();
88
-
89
- if (capturedListener === undefined) throw new Error('addListener callback was not captured');
90
- capturedListener(MEASUREMENT);
91
-
92
- expect(capturedResult?.()).toEqual(MEASUREMENT);
93
- });
94
-
95
- it('removes the subscription when the host component is unmounted', async () => {
96
- mount(ROOT_TAG, GyroscopeHost);
97
- await tick();
98
-
99
- unmount(ROOT_TAG);
100
- await tick();
101
-
102
- expect(removeMock).toHaveBeenCalledOnce();
103
- });
104
-
105
- it('calls setUpdateInterval with the given interval when one is passed', async () => {
106
- mount(ROOT_TAG, GyroscopeIntervalHost);
107
- await tick();
108
-
109
- expect(setUpdateIntervalMock).toHaveBeenCalledWith(500);
110
- });
111
-
112
- it('does not call setUpdateInterval when no interval is passed', async () => {
113
- mount(ROOT_TAG, GyroscopeHost);
114
- await tick();
115
-
116
- expect(setUpdateIntervalMock).not.toHaveBeenCalled();
117
- });
118
- });
@@ -1,118 +0,0 @@
1
- // Co-located Angular-driven test (ADR 0025) for LightSensorService. Mounts a real host
2
- // component through @symbiote-native/angular so `connect()` runs the same way an app would call
3
- // it — inside the component's own injection context — and drives the returned signal through a
4
- // full mount/unmount lifecycle, because `effect()`'s injector-scoped cleanup only fires
5
- // correctly when torn down through a real Angular injection context.
6
-
7
- import '@angular/compiler';
8
- import { Component, inject, type Signal } from '@angular/core';
9
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
10
- import { mount, unmount } from '@symbiote-native/angular';
11
- import { installFabric } from '@symbiote-native/test-utils';
12
- import type { ILightSensorMeasurement } from '../../../core';
13
- import { LightSensorService } from './index';
14
-
15
- const addListenerMock = vi.fn();
16
- const removeMock = vi.fn();
17
- const setUpdateIntervalMock = vi.fn();
18
-
19
- vi.mock('../../../core', () => ({
20
- LightSensor: {
21
- addListener: (listener: (measurement: ILightSensorMeasurement) => void) =>
22
- addListenerMock(listener),
23
- setUpdateInterval: (intervalMs: number) => setUpdateIntervalMock(intervalMs),
24
- },
25
- }));
26
-
27
- const ROOT_TAG = 942;
28
- const fabric = installFabric();
29
- const tick = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));
30
-
31
- const MEASUREMENT: ILightSensorMeasurement = { illuminance: 42, timestamp: 123 };
32
-
33
- let capturedResult: Signal<ILightSensorMeasurement | null> | undefined;
34
- let capturedListener: ((measurement: ILightSensorMeasurement) => void) | undefined;
35
-
36
- @Component({
37
- selector: 'symbiote-light-sensor-host',
38
- standalone: true,
39
- template: '',
40
- })
41
- class LightSensorHost {
42
- readonly measurement = inject(LightSensorService).connect();
43
-
44
- constructor() {
45
- capturedResult = this.measurement;
46
- }
47
- }
48
-
49
- @Component({
50
- selector: 'symbiote-light-sensor-interval-host',
51
- standalone: true,
52
- template: '',
53
- })
54
- class LightSensorIntervalHost {
55
- readonly measurement = inject(LightSensorService).connect(500);
56
-
57
- constructor() {
58
- capturedResult = this.measurement;
59
- }
60
- }
61
-
62
- beforeEach(() => {
63
- capturedResult = undefined;
64
- capturedListener = undefined;
65
- addListenerMock.mockImplementation(listener => {
66
- capturedListener = listener;
67
- return { remove: removeMock };
68
- });
69
- });
70
-
71
- afterEach(() => {
72
- unmount(ROOT_TAG);
73
- fabric.reset();
74
- vi.clearAllMocks();
75
- });
76
-
77
- describe('LightSensorService.connect', () => {
78
- it('reports null before any measurement event fires', async () => {
79
- mount(ROOT_TAG, LightSensorHost);
80
- await tick();
81
-
82
- expect(capturedResult?.()).toBeNull();
83
- });
84
-
85
- it('updates the signal when the registered listener fires with a measurement', async () => {
86
- mount(ROOT_TAG, LightSensorHost);
87
- await tick();
88
-
89
- if (capturedListener === undefined) throw new Error('addListener callback was not captured');
90
- capturedListener(MEASUREMENT);
91
-
92
- expect(capturedResult?.()).toEqual(MEASUREMENT);
93
- });
94
-
95
- it('removes the subscription when the host component is unmounted', async () => {
96
- mount(ROOT_TAG, LightSensorHost);
97
- await tick();
98
-
99
- unmount(ROOT_TAG);
100
- await tick();
101
-
102
- expect(removeMock).toHaveBeenCalledOnce();
103
- });
104
-
105
- it('calls setUpdateInterval with the given interval when one is passed', async () => {
106
- mount(ROOT_TAG, LightSensorIntervalHost);
107
- await tick();
108
-
109
- expect(setUpdateIntervalMock).toHaveBeenCalledWith(500);
110
- });
111
-
112
- it('does not call setUpdateInterval when no interval is passed', async () => {
113
- mount(ROOT_TAG, LightSensorHost);
114
- await tick();
115
-
116
- expect(setUpdateIntervalMock).not.toHaveBeenCalled();
117
- });
118
- });