@symbiote-native/sensors 0.1.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.
- package/LICENSE +21 -0
- package/README.md +138 -0
- package/build/angular/index.d.ts +16 -0
- package/build/angular/index.js +12 -0
- package/build/angular/services/accelerometer.service/index.d.ts +6 -0
- package/build/angular/services/accelerometer.service/index.js +81 -0
- package/build/angular/services/barometer.service/index.d.ts +6 -0
- package/build/angular/services/barometer.service/index.js +81 -0
- package/build/angular/services/device-motion.service/index.d.ts +6 -0
- package/build/angular/services/device-motion.service/index.js +81 -0
- package/build/angular/services/gyroscope.service/index.d.ts +6 -0
- package/build/angular/services/gyroscope.service/index.js +81 -0
- package/build/angular/services/light-sensor.service/index.d.ts +6 -0
- package/build/angular/services/light-sensor.service/index.js +81 -0
- package/build/angular/services/magnetometer-uncalibrated.service/index.d.ts +6 -0
- package/build/angular/services/magnetometer-uncalibrated.service/index.js +81 -0
- package/build/angular/services/magnetometer.service/index.d.ts +6 -0
- package/build/angular/services/magnetometer.service/index.js +81 -0
- package/build/angular/services/pedometer.service/index.d.ts +6 -0
- package/build/angular/services/pedometer.service/index.js +76 -0
- package/build/core/accelerometer.d.ts +10 -0
- package/build/core/accelerometer.js +6 -0
- package/build/core/barometer.d.ts +9 -0
- package/build/core/barometer.js +6 -0
- package/build/core/device-motion.d.ts +40 -0
- package/build/core/device-motion.js +21 -0
- package/build/core/device-sensor.d.ts +27 -0
- package/build/core/device-sensor.js +61 -0
- package/build/core/gyroscope.d.ts +10 -0
- package/build/core/gyroscope.js +6 -0
- package/build/core/index.d.ts +11 -0
- package/build/core/index.js +10 -0
- package/build/core/light-sensor.d.ts +8 -0
- package/build/core/light-sensor.js +6 -0
- package/build/core/magnetometer-uncalibrated.d.ts +10 -0
- package/build/core/magnetometer-uncalibrated.js +6 -0
- package/build/core/magnetometer.d.ts +10 -0
- package/build/core/magnetometer.js +6 -0
- package/build/core/native/exponent-accelerometer.d.ts +3 -0
- package/build/core/native/exponent-accelerometer.js +3 -0
- package/build/core/native/exponent-barometer.d.ts +3 -0
- package/build/core/native/exponent-barometer.js +3 -0
- package/build/core/native/exponent-device-motion.d.ts +7 -0
- package/build/core/native/exponent-device-motion.js +3 -0
- package/build/core/native/exponent-gyroscope.d.ts +3 -0
- package/build/core/native/exponent-gyroscope.js +3 -0
- package/build/core/native/exponent-light-sensor/index.android.d.ts +3 -0
- package/build/core/native/exponent-light-sensor/index.android.js +3 -0
- package/build/core/native/exponent-light-sensor/index.d.ts +1 -0
- package/build/core/native/exponent-light-sensor/index.ios.d.ts +3 -0
- package/build/core/native/exponent-light-sensor/index.ios.js +6 -0
- package/build/core/native/exponent-light-sensor/index.js +5 -0
- package/build/core/native/exponent-magnetometer-uncalibrated.d.ts +3 -0
- package/build/core/native/exponent-magnetometer-uncalibrated.js +3 -0
- package/build/core/native/exponent-magnetometer.d.ts +3 -0
- package/build/core/native/exponent-magnetometer.js +3 -0
- package/build/core/native/exponent-pedometer.d.ts +10 -0
- package/build/core/native/exponent-pedometer.js +3 -0
- package/build/core/pedometer.d.ts +10 -0
- package/build/core/pedometer.js +44 -0
- package/build/react/hooks/use-accelerometer/index.d.ts +2 -0
- package/build/react/hooks/use-accelerometer/index.js +17 -0
- package/build/react/hooks/use-barometer/index.d.ts +2 -0
- package/build/react/hooks/use-barometer/index.js +17 -0
- package/build/react/hooks/use-device-motion/index.d.ts +2 -0
- package/build/react/hooks/use-device-motion/index.js +17 -0
- package/build/react/hooks/use-gyroscope/index.d.ts +2 -0
- package/build/react/hooks/use-gyroscope/index.js +17 -0
- package/build/react/hooks/use-light-sensor/index.d.ts +2 -0
- package/build/react/hooks/use-light-sensor/index.js +17 -0
- package/build/react/hooks/use-magnetometer/index.d.ts +2 -0
- package/build/react/hooks/use-magnetometer/index.js +17 -0
- package/build/react/hooks/use-magnetometer-uncalibrated/index.d.ts +2 -0
- package/build/react/hooks/use-magnetometer-uncalibrated/index.js +17 -0
- package/build/react/hooks/use-pedometer/index.d.ts +2 -0
- package/build/react/hooks/use-pedometer/index.js +14 -0
- package/build/react/index.d.ts +16 -0
- package/build/react/index.js +14 -0
- package/build/vue/composables/use-accelerometer/index.d.ts +3 -0
- package/build/vue/composables/use-accelerometer/index.js +25 -0
- package/build/vue/composables/use-barometer/index.d.ts +3 -0
- package/build/vue/composables/use-barometer/index.js +25 -0
- package/build/vue/composables/use-device-motion/index.d.ts +3 -0
- package/build/vue/composables/use-device-motion/index.js +25 -0
- package/build/vue/composables/use-gyroscope/index.d.ts +3 -0
- package/build/vue/composables/use-gyroscope/index.js +25 -0
- package/build/vue/composables/use-light-sensor/index.d.ts +3 -0
- package/build/vue/composables/use-light-sensor/index.js +25 -0
- package/build/vue/composables/use-magnetometer/index.d.ts +3 -0
- package/build/vue/composables/use-magnetometer/index.js +25 -0
- package/build/vue/composables/use-magnetometer-uncalibrated/index.d.ts +3 -0
- package/build/vue/composables/use-magnetometer-uncalibrated/index.js +25 -0
- package/build/vue/composables/use-pedometer/index.d.ts +3 -0
- package/build/vue/composables/use-pedometer/index.js +20 -0
- package/build/vue/index.d.ts +16 -0
- package/build/vue/index.js +14 -0
- package/build-ngc/angular/index.d.ts +16 -0
- package/build-ngc/angular/index.js +13 -0
- package/build-ngc/angular/index.js.map +1 -0
- package/build-ngc/angular/services/accelerometer.service/index.d.ts +9 -0
- package/build-ngc/angular/services/accelerometer.service/index.js +40 -0
- package/build-ngc/angular/services/accelerometer.service/index.js.map +1 -0
- package/build-ngc/angular/services/barometer.service/index.d.ts +9 -0
- package/build-ngc/angular/services/barometer.service/index.js +40 -0
- package/build-ngc/angular/services/barometer.service/index.js.map +1 -0
- package/build-ngc/angular/services/device-motion.service/index.d.ts +9 -0
- package/build-ngc/angular/services/device-motion.service/index.js +40 -0
- package/build-ngc/angular/services/device-motion.service/index.js.map +1 -0
- package/build-ngc/angular/services/gyroscope.service/index.d.ts +9 -0
- package/build-ngc/angular/services/gyroscope.service/index.js +40 -0
- package/build-ngc/angular/services/gyroscope.service/index.js.map +1 -0
- package/build-ngc/angular/services/light-sensor.service/index.d.ts +9 -0
- package/build-ngc/angular/services/light-sensor.service/index.js +40 -0
- package/build-ngc/angular/services/light-sensor.service/index.js.map +1 -0
- package/build-ngc/angular/services/magnetometer-uncalibrated.service/index.d.ts +9 -0
- package/build-ngc/angular/services/magnetometer-uncalibrated.service/index.js +40 -0
- package/build-ngc/angular/services/magnetometer-uncalibrated.service/index.js.map +1 -0
- package/build-ngc/angular/services/magnetometer.service/index.d.ts +9 -0
- package/build-ngc/angular/services/magnetometer.service/index.js +40 -0
- package/build-ngc/angular/services/magnetometer.service/index.js.map +1 -0
- package/build-ngc/angular/services/pedometer.service/index.d.ts +9 -0
- package/build-ngc/angular/services/pedometer.service/index.js +35 -0
- package/build-ngc/angular/services/pedometer.service/index.js.map +1 -0
- package/build-ngc/core/accelerometer.d.ts +10 -0
- package/build-ngc/core/accelerometer.js +7 -0
- package/build-ngc/core/accelerometer.js.map +1 -0
- package/build-ngc/core/barometer.d.ts +9 -0
- package/build-ngc/core/barometer.js +7 -0
- package/build-ngc/core/barometer.js.map +1 -0
- package/build-ngc/core/device-motion.d.ts +40 -0
- package/build-ngc/core/device-motion.js +22 -0
- package/build-ngc/core/device-motion.js.map +1 -0
- package/build-ngc/core/device-sensor.d.ts +27 -0
- package/build-ngc/core/device-sensor.js +62 -0
- package/build-ngc/core/device-sensor.js.map +1 -0
- package/build-ngc/core/gyroscope.d.ts +10 -0
- package/build-ngc/core/gyroscope.js +7 -0
- package/build-ngc/core/gyroscope.js.map +1 -0
- package/build-ngc/core/index.d.ts +11 -0
- package/build-ngc/core/index.js +11 -0
- package/build-ngc/core/index.js.map +1 -0
- package/build-ngc/core/light-sensor.d.ts +8 -0
- package/build-ngc/core/light-sensor.js +7 -0
- package/build-ngc/core/light-sensor.js.map +1 -0
- package/build-ngc/core/magnetometer-uncalibrated.d.ts +10 -0
- package/build-ngc/core/magnetometer-uncalibrated.js +7 -0
- package/build-ngc/core/magnetometer-uncalibrated.js.map +1 -0
- package/build-ngc/core/magnetometer.d.ts +10 -0
- package/build-ngc/core/magnetometer.js +7 -0
- package/build-ngc/core/magnetometer.js.map +1 -0
- package/build-ngc/core/native/exponent-accelerometer.d.ts +3 -0
- package/build-ngc/core/native/exponent-accelerometer.js +4 -0
- package/build-ngc/core/native/exponent-accelerometer.js.map +1 -0
- package/build-ngc/core/native/exponent-barometer.d.ts +3 -0
- package/build-ngc/core/native/exponent-barometer.js +4 -0
- package/build-ngc/core/native/exponent-barometer.js.map +1 -0
- package/build-ngc/core/native/exponent-device-motion.d.ts +7 -0
- package/build-ngc/core/native/exponent-device-motion.js +4 -0
- package/build-ngc/core/native/exponent-device-motion.js.map +1 -0
- package/build-ngc/core/native/exponent-gyroscope.d.ts +3 -0
- package/build-ngc/core/native/exponent-gyroscope.js +4 -0
- package/build-ngc/core/native/exponent-gyroscope.js.map +1 -0
- package/build-ngc/core/native/exponent-light-sensor/index.android.d.ts +3 -0
- package/build-ngc/core/native/exponent-light-sensor/index.android.js +4 -0
- package/build-ngc/core/native/exponent-light-sensor/index.android.js.map +1 -0
- package/build-ngc/core/native/exponent-light-sensor/index.d.ts +1 -0
- package/build-ngc/core/native/exponent-light-sensor/index.ios.d.ts +3 -0
- package/build-ngc/core/native/exponent-light-sensor/index.ios.js +7 -0
- package/build-ngc/core/native/exponent-light-sensor/index.ios.js.map +1 -0
- package/build-ngc/core/native/exponent-light-sensor/index.js +6 -0
- package/build-ngc/core/native/exponent-light-sensor/index.js.map +1 -0
- package/build-ngc/core/native/exponent-magnetometer-uncalibrated.d.ts +3 -0
- package/build-ngc/core/native/exponent-magnetometer-uncalibrated.js +4 -0
- package/build-ngc/core/native/exponent-magnetometer-uncalibrated.js.map +1 -0
- package/build-ngc/core/native/exponent-magnetometer.d.ts +3 -0
- package/build-ngc/core/native/exponent-magnetometer.js +4 -0
- package/build-ngc/core/native/exponent-magnetometer.js.map +1 -0
- package/build-ngc/core/native/exponent-pedometer.d.ts +10 -0
- package/build-ngc/core/native/exponent-pedometer.js +4 -0
- package/build-ngc/core/native/exponent-pedometer.js.map +1 -0
- package/build-ngc/core/pedometer.d.ts +10 -0
- package/build-ngc/core/pedometer.js +45 -0
- package/build-ngc/core/pedometer.js.map +1 -0
- package/package.json +106 -0
- package/src/angular/index.ts +26 -0
- package/src/angular/services/accelerometer.service/accelerometer.service.test.ts +118 -0
- package/src/angular/services/accelerometer.service/index.ts +39 -0
- package/src/angular/services/barometer.service/barometer.service.test.ts +122 -0
- package/src/angular/services/barometer.service/index.ts +39 -0
- package/src/angular/services/device-motion.service/device-motion.service.test.ts +125 -0
- package/src/angular/services/device-motion.service/index.ts +39 -0
- package/src/angular/services/gyroscope.service/gyroscope.service.test.ts +118 -0
- package/src/angular/services/gyroscope.service/index.ts +39 -0
- package/src/angular/services/light-sensor.service/index.ts +39 -0
- package/src/angular/services/light-sensor.service/light-sensor.service.test.ts +118 -0
- package/src/angular/services/magnetometer-uncalibrated.service/index.ts +39 -0
- package/src/angular/services/magnetometer-uncalibrated.service/magnetometer-uncalibrated.service.test.ts +123 -0
- package/src/angular/services/magnetometer.service/index.ts +39 -0
- package/src/angular/services/magnetometer.service/magnetometer.service.test.ts +118 -0
- package/src/angular/services/pedometer.service/index.ts +34 -0
- package/src/angular/services/pedometer.service/pedometer.service.test.ts +86 -0
- package/src/core/accelerometer.test.ts +49 -0
- package/src/core/accelerometer.ts +19 -0
- package/src/core/barometer.test.ts +46 -0
- package/src/core/barometer.ts +15 -0
- package/src/core/device-motion.test.ts +60 -0
- package/src/core/device-motion.ts +41 -0
- package/src/core/device-sensor.test.ts +76 -0
- package/src/core/device-sensor.ts +95 -0
- package/src/core/gyroscope.test.ts +46 -0
- package/src/core/gyroscope.ts +16 -0
- package/src/core/index.ts +37 -0
- package/src/core/light-sensor.test.ts +49 -0
- package/src/core/light-sensor.ts +18 -0
- package/src/core/magnetometer-uncalibrated.test.ts +49 -0
- package/src/core/magnetometer-uncalibrated.ts +20 -0
- package/src/core/magnetometer.test.ts +49 -0
- package/src/core/magnetometer.ts +19 -0
- package/src/core/native/exponent-accelerometer.ts +9 -0
- package/src/core/native/exponent-barometer.ts +9 -0
- package/src/core/native/exponent-device-motion.ts +15 -0
- package/src/core/native/exponent-gyroscope.ts +9 -0
- package/src/core/native/exponent-light-sensor/index.android.ts +9 -0
- package/src/core/native/exponent-light-sensor/index.ios.ts +12 -0
- package/src/core/native/exponent-light-sensor/index.ts +5 -0
- package/src/core/native/exponent-magnetometer-uncalibrated.ts +9 -0
- package/src/core/native/exponent-magnetometer.ts +9 -0
- package/src/core/native/exponent-pedometer.ts +17 -0
- package/src/core/pedometer.test.ts +118 -0
- package/src/core/pedometer.ts +62 -0
- package/src/react/hooks/use-accelerometer/index.ts +21 -0
- package/src/react/hooks/use-accelerometer/use-accelerometer.test.tsx +81 -0
- package/src/react/hooks/use-barometer/index.ts +21 -0
- package/src/react/hooks/use-barometer/use-barometer.test.tsx +83 -0
- package/src/react/hooks/use-device-motion/index.ts +21 -0
- package/src/react/hooks/use-device-motion/use-device-motion.test.tsx +88 -0
- package/src/react/hooks/use-gyroscope/index.ts +21 -0
- package/src/react/hooks/use-gyroscope/use-gyroscope.test.tsx +79 -0
- package/src/react/hooks/use-light-sensor/index.ts +21 -0
- package/src/react/hooks/use-light-sensor/use-light-sensor.test.tsx +79 -0
- package/src/react/hooks/use-magnetometer/index.ts +21 -0
- package/src/react/hooks/use-magnetometer/use-magnetometer.test.tsx +81 -0
- package/src/react/hooks/use-magnetometer-uncalibrated/index.ts +23 -0
- package/src/react/hooks/use-magnetometer-uncalibrated/use-magnetometer-uncalibrated.test.tsx +88 -0
- package/src/react/hooks/use-pedometer/index.ts +17 -0
- package/src/react/hooks/use-pedometer/use-pedometer.test.tsx +69 -0
- package/src/react/index.ts +29 -0
- package/src/vue/composables/use-accelerometer/index.ts +34 -0
- package/src/vue/composables/use-accelerometer/use-accelerometer.test.ts +96 -0
- package/src/vue/composables/use-barometer/index.ts +30 -0
- package/src/vue/composables/use-barometer/use-barometer.test.ts +100 -0
- package/src/vue/composables/use-device-motion/index.ts +30 -0
- package/src/vue/composables/use-device-motion/use-device-motion.test.ts +104 -0
- package/src/vue/composables/use-gyroscope/index.ts +30 -0
- package/src/vue/composables/use-gyroscope/use-gyroscope.test.ts +96 -0
- package/src/vue/composables/use-light-sensor/index.ts +30 -0
- package/src/vue/composables/use-light-sensor/use-light-sensor.test.ts +96 -0
- package/src/vue/composables/use-magnetometer/index.ts +30 -0
- package/src/vue/composables/use-magnetometer/use-magnetometer.test.ts +96 -0
- package/src/vue/composables/use-magnetometer-uncalibrated/index.ts +36 -0
- package/src/vue/composables/use-magnetometer-uncalibrated/use-magnetometer-uncalibrated.test.ts +103 -0
- package/src/vue/composables/use-pedometer/index.ts +25 -0
- package/src/vue/composables/use-pedometer/use-pedometer.test.ts +77 -0
- package/src/vue/index.ts +29 -0
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { effect, inject, Injectable, Injector, signal, type Signal } from '@angular/core';
|
|
2
|
+
import { Accelerometer, type IAccelerometerMeasurement } from '../../../core';
|
|
3
|
+
|
|
4
|
+
// Angular twin of React's `useAccelerometer` hook and Vue's `useAccelerometer` composable.
|
|
5
|
+
// Angular has no per-instance hook — state and lifecycle live in DI instead, so `connect()`
|
|
6
|
+
// stands in for the hook's role: call it ONCE (typically from a component's field initializer,
|
|
7
|
+
// inside an injection context).
|
|
8
|
+
//
|
|
9
|
+
// readonly measurement = inject(AccelerometerService).connect();
|
|
10
|
+
// // template: {{ measurement()?.x }}
|
|
11
|
+
//
|
|
12
|
+
// Unlike HideAnimationService.connect(), there is no per-render config to re-sync — the
|
|
13
|
+
// subscription doesn't depend on anything the caller's own signals could change between
|
|
14
|
+
// renders — so a single effect() that subscribes once and cleans up once is enough; no
|
|
15
|
+
// `updateConfig`-style effect that re-runs on every read is needed here.
|
|
16
|
+
@Injectable({ providedIn: 'root' })
|
|
17
|
+
export class AccelerometerService {
|
|
18
|
+
// Captured in the constructor (itself always run inside an injection context by Angular's
|
|
19
|
+
// own DI) so `connect()` can create an `effect()` even when called from plain field-initializer
|
|
20
|
+
// code that is not, on its own, an active injection context — mirrors HideAnimationService.
|
|
21
|
+
private readonly injector = inject(Injector);
|
|
22
|
+
|
|
23
|
+
connect(updateIntervalMs?: number): Signal<IAccelerometerMeasurement | null> {
|
|
24
|
+
const measurement = signal<IAccelerometerMeasurement | null>(null);
|
|
25
|
+
|
|
26
|
+
effect(
|
|
27
|
+
onCleanup => {
|
|
28
|
+
if (updateIntervalMs !== undefined) {
|
|
29
|
+
Accelerometer.setUpdateInterval(updateIntervalMs);
|
|
30
|
+
}
|
|
31
|
+
const subscription = Accelerometer.addListener(next => measurement.set(next));
|
|
32
|
+
onCleanup(() => subscription.remove());
|
|
33
|
+
},
|
|
34
|
+
{ injector: this.injector },
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
return measurement.asReadonly();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { effect, inject, Injectable, Injector, signal, type Signal } from '@angular/core';
|
|
2
|
+
import { Barometer, type IBarometerMeasurement } from '../../../core';
|
|
3
|
+
|
|
4
|
+
// Angular twin of React's `useBarometer` hook and Vue's `useBarometer` composable.
|
|
5
|
+
// Angular has no per-instance hook — state and lifecycle live in DI instead, so `connect()`
|
|
6
|
+
// stands in for the hook's role: call it ONCE (typically from a component's field initializer,
|
|
7
|
+
// inside an injection context).
|
|
8
|
+
//
|
|
9
|
+
// readonly measurement = inject(BarometerService).connect();
|
|
10
|
+
// // template: {{ measurement()?.pressure }}
|
|
11
|
+
//
|
|
12
|
+
// Unlike HideAnimationService.connect(), there is no per-render config to re-sync — the
|
|
13
|
+
// subscription doesn't depend on anything the caller's own signals could change between
|
|
14
|
+
// renders — so a single effect() that subscribes once and cleans up once is enough; no
|
|
15
|
+
// `updateConfig`-style effect that re-runs on every read is needed here.
|
|
16
|
+
@Injectable({ providedIn: 'root' })
|
|
17
|
+
export class BarometerService {
|
|
18
|
+
// Captured in the constructor (itself always run inside an injection context by Angular's
|
|
19
|
+
// own DI) so `connect()` can create an `effect()` even when called from plain field-initializer
|
|
20
|
+
// code that is not, on its own, an active injection context — mirrors HideAnimationService.
|
|
21
|
+
private readonly injector = inject(Injector);
|
|
22
|
+
|
|
23
|
+
connect(updateIntervalMs?: number): Signal<IBarometerMeasurement | null> {
|
|
24
|
+
const measurement = signal<IBarometerMeasurement | null>(null);
|
|
25
|
+
|
|
26
|
+
effect(
|
|
27
|
+
onCleanup => {
|
|
28
|
+
if (updateIntervalMs !== undefined) {
|
|
29
|
+
Barometer.setUpdateInterval(updateIntervalMs);
|
|
30
|
+
}
|
|
31
|
+
const subscription = Barometer.addListener(next => measurement.set(next));
|
|
32
|
+
onCleanup(() => subscription.remove());
|
|
33
|
+
},
|
|
34
|
+
{ injector: this.injector },
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
return measurement.asReadonly();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { effect, inject, Injectable, Injector, signal, type Signal } from '@angular/core';
|
|
2
|
+
import { DeviceMotion, type IDeviceMotionMeasurement } from '../../../core';
|
|
3
|
+
|
|
4
|
+
// Angular twin of React's `useDeviceMotion` hook and Vue's `useDeviceMotion` composable.
|
|
5
|
+
// Angular has no per-instance hook — state and lifecycle live in DI instead, so `connect()`
|
|
6
|
+
// stands in for the hook's role: call it ONCE (typically from a component's field initializer,
|
|
7
|
+
// inside an injection context).
|
|
8
|
+
//
|
|
9
|
+
// readonly measurement = inject(DeviceMotionService).connect();
|
|
10
|
+
// // template: {{ measurement()?.rotation?.alpha }}
|
|
11
|
+
//
|
|
12
|
+
// Unlike HideAnimationService.connect(), there is no per-render config to re-sync — the
|
|
13
|
+
// subscription doesn't depend on anything the caller's own signals could change between
|
|
14
|
+
// renders — so a single effect() that subscribes once and cleans up once is enough; no
|
|
15
|
+
// `updateConfig`-style effect that re-runs on every read is needed here.
|
|
16
|
+
@Injectable({ providedIn: 'root' })
|
|
17
|
+
export class DeviceMotionService {
|
|
18
|
+
// Captured in the constructor (itself always run inside an injection context by Angular's
|
|
19
|
+
// own DI) so `connect()` can create an `effect()` even when called from plain field-initializer
|
|
20
|
+
// code that is not, on its own, an active injection context — mirrors HideAnimationService.
|
|
21
|
+
private readonly injector = inject(Injector);
|
|
22
|
+
|
|
23
|
+
connect(updateIntervalMs?: number): Signal<IDeviceMotionMeasurement | null> {
|
|
24
|
+
const measurement = signal<IDeviceMotionMeasurement | null>(null);
|
|
25
|
+
|
|
26
|
+
effect(
|
|
27
|
+
onCleanup => {
|
|
28
|
+
if (updateIntervalMs !== undefined) {
|
|
29
|
+
DeviceMotion.setUpdateInterval(updateIntervalMs);
|
|
30
|
+
}
|
|
31
|
+
const subscription = DeviceMotion.addListener(next => measurement.set(next));
|
|
32
|
+
onCleanup(() => subscription.remove());
|
|
33
|
+
},
|
|
34
|
+
{ injector: this.injector },
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
return measurement.asReadonly();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { effect, inject, Injectable, Injector, signal, type Signal } from '@angular/core';
|
|
2
|
+
import { Gyroscope, type IGyroscopeMeasurement } from '../../../core';
|
|
3
|
+
|
|
4
|
+
// Angular twin of React's `useGyroscope` hook and Vue's `useGyroscope` composable.
|
|
5
|
+
// Angular has no per-instance hook — state and lifecycle live in DI instead, so `connect()`
|
|
6
|
+
// stands in for the hook's role: call it ONCE (typically from a component's field initializer,
|
|
7
|
+
// inside an injection context).
|
|
8
|
+
//
|
|
9
|
+
// readonly measurement = inject(GyroscopeService).connect();
|
|
10
|
+
// // template: {{ measurement()?.x }}
|
|
11
|
+
//
|
|
12
|
+
// Unlike HideAnimationService.connect(), there is no per-render config to re-sync — the
|
|
13
|
+
// subscription doesn't depend on anything the caller's own signals could change between
|
|
14
|
+
// renders — so a single effect() that subscribes once and cleans up once is enough; no
|
|
15
|
+
// `updateConfig`-style effect that re-runs on every read is needed here.
|
|
16
|
+
@Injectable({ providedIn: 'root' })
|
|
17
|
+
export class GyroscopeService {
|
|
18
|
+
// Captured in the constructor (itself always run inside an injection context by Angular's
|
|
19
|
+
// own DI) so `connect()` can create an `effect()` even when called from plain field-initializer
|
|
20
|
+
// code that is not, on its own, an active injection context — mirrors HideAnimationService.
|
|
21
|
+
private readonly injector = inject(Injector);
|
|
22
|
+
|
|
23
|
+
connect(updateIntervalMs?: number): Signal<IGyroscopeMeasurement | null> {
|
|
24
|
+
const measurement = signal<IGyroscopeMeasurement | null>(null);
|
|
25
|
+
|
|
26
|
+
effect(
|
|
27
|
+
onCleanup => {
|
|
28
|
+
if (updateIntervalMs !== undefined) {
|
|
29
|
+
Gyroscope.setUpdateInterval(updateIntervalMs);
|
|
30
|
+
}
|
|
31
|
+
const subscription = Gyroscope.addListener(next => measurement.set(next));
|
|
32
|
+
onCleanup(() => subscription.remove());
|
|
33
|
+
},
|
|
34
|
+
{ injector: this.injector },
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
return measurement.asReadonly();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { effect, inject, Injectable, Injector, signal, type Signal } from '@angular/core';
|
|
2
|
+
import { LightSensor, type ILightSensorMeasurement } from '../../../core';
|
|
3
|
+
|
|
4
|
+
// Angular twin of React's `useLightSensor` hook and Vue's `useLightSensor` composable.
|
|
5
|
+
// Angular has no per-instance hook — state and lifecycle live in DI instead, so `connect()`
|
|
6
|
+
// stands in for the hook's role: call it ONCE (typically from a component's field initializer,
|
|
7
|
+
// inside an injection context).
|
|
8
|
+
//
|
|
9
|
+
// readonly measurement = inject(LightSensorService).connect();
|
|
10
|
+
// // template: {{ measurement()?.illuminance }}
|
|
11
|
+
//
|
|
12
|
+
// Unlike HideAnimationService.connect(), there is no per-render config to re-sync — the
|
|
13
|
+
// subscription doesn't depend on anything the caller's own signals could change between
|
|
14
|
+
// renders — so a single effect() that subscribes once and cleans up once is enough; no
|
|
15
|
+
// `updateConfig`-style effect that re-runs on every read is needed here.
|
|
16
|
+
@Injectable({ providedIn: 'root' })
|
|
17
|
+
export class LightSensorService {
|
|
18
|
+
// Captured in the constructor (itself always run inside an injection context by Angular's
|
|
19
|
+
// own DI) so `connect()` can create an `effect()` even when called from plain field-initializer
|
|
20
|
+
// code that is not, on its own, an active injection context — mirrors HideAnimationService.
|
|
21
|
+
private readonly injector = inject(Injector);
|
|
22
|
+
|
|
23
|
+
connect(updateIntervalMs?: number): Signal<ILightSensorMeasurement | null> {
|
|
24
|
+
const measurement = signal<ILightSensorMeasurement | null>(null);
|
|
25
|
+
|
|
26
|
+
effect(
|
|
27
|
+
onCleanup => {
|
|
28
|
+
if (updateIntervalMs !== undefined) {
|
|
29
|
+
LightSensor.setUpdateInterval(updateIntervalMs);
|
|
30
|
+
}
|
|
31
|
+
const subscription = LightSensor.addListener(next => measurement.set(next));
|
|
32
|
+
onCleanup(() => subscription.remove());
|
|
33
|
+
},
|
|
34
|
+
{ injector: this.injector },
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
return measurement.asReadonly();
|
|
38
|
+
}
|
|
39
|
+
}
|