@symbiote-native/sensors 0.1.0 → 0.2.1
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/README.md +5 -5
- package/native-link.json +52 -0
- package/package.json +11 -7
- package/src/angular/services/accelerometer.service/accelerometer.service.test.ts +0 -118
- package/src/angular/services/barometer.service/barometer.service.test.ts +0 -122
- package/src/angular/services/device-motion.service/device-motion.service.test.ts +0 -125
- package/src/angular/services/gyroscope.service/gyroscope.service.test.ts +0 -118
- package/src/angular/services/light-sensor.service/light-sensor.service.test.ts +0 -118
- package/src/angular/services/magnetometer-uncalibrated.service/magnetometer-uncalibrated.service.test.ts +0 -123
- package/src/angular/services/magnetometer.service/magnetometer.service.test.ts +0 -118
- package/src/angular/services/pedometer.service/pedometer.service.test.ts +0 -86
- package/src/core/accelerometer.test.ts +0 -49
- package/src/core/barometer.test.ts +0 -46
- package/src/core/device-motion.test.ts +0 -60
- package/src/core/device-sensor.test.ts +0 -76
- package/src/core/gyroscope.test.ts +0 -46
- package/src/core/light-sensor.test.ts +0 -49
- package/src/core/magnetometer-uncalibrated.test.ts +0 -49
- package/src/core/magnetometer.test.ts +0 -49
- package/src/core/pedometer.test.ts +0 -118
- package/src/react/hooks/use-accelerometer/use-accelerometer.test.tsx +0 -81
- package/src/react/hooks/use-barometer/use-barometer.test.tsx +0 -83
- package/src/react/hooks/use-device-motion/use-device-motion.test.tsx +0 -88
- package/src/react/hooks/use-gyroscope/use-gyroscope.test.tsx +0 -79
- package/src/react/hooks/use-light-sensor/use-light-sensor.test.tsx +0 -79
- package/src/react/hooks/use-magnetometer/use-magnetometer.test.tsx +0 -81
- package/src/react/hooks/use-magnetometer-uncalibrated/use-magnetometer-uncalibrated.test.tsx +0 -88
- package/src/react/hooks/use-pedometer/use-pedometer.test.tsx +0 -69
- package/src/vue/composables/use-accelerometer/use-accelerometer.test.ts +0 -96
- package/src/vue/composables/use-barometer/use-barometer.test.ts +0 -100
- package/src/vue/composables/use-device-motion/use-device-motion.test.ts +0 -104
- package/src/vue/composables/use-gyroscope/use-gyroscope.test.ts +0 -96
- package/src/vue/composables/use-light-sensor/use-light-sensor.test.ts +0 -96
- package/src/vue/composables/use-magnetometer/use-magnetometer.test.ts +0 -96
- package/src/vue/composables/use-magnetometer-uncalibrated/use-magnetometer-uncalibrated.test.ts +0 -103
- package/src/vue/composables/use-pedometer/use-pedometer.test.ts +0 -77
package/README.md
CHANGED
|
@@ -38,8 +38,8 @@ package with zero further changes:
|
|
|
38
38
|
|
|
39
39
|
Full mechanics — the Podfile pieces that normally ship inside the `expo` package, the `expo`
|
|
40
40
|
peer-dependency exclusion list, per-sensor permission strings — live in the
|
|
41
|
-
`symbiote-expo-native-module` skill. Reference implementation:
|
|
42
|
-
|
|
41
|
+
`symbiote-expo-native-module` skill. Reference implementation: `examples/expo-react/ios/Podfile`
|
|
42
|
+
and `examples/expo-react/android/app/src/main/java/com/canaryexpo/MainApplication.kt`.
|
|
43
43
|
|
|
44
44
|
Permissions ship with the native module itself — nothing to reimplement, just the platform
|
|
45
45
|
permission string each sensor needs (e.g. DeviceMotion/Pedometer need `NSMotionUsageDescription`
|
|
@@ -99,9 +99,9 @@ export class SensorsScreen {
|
|
|
99
99
|
}
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
All three examples above mirror the real canary demo screens —
|
|
103
|
+
`examples/expo-react/screens/SensorsScreen.tsx`, `examples/expo-vue-sfc/screens/SensorsScreen.vue`,
|
|
104
|
+
`examples/expo-vue-tsx/screens/SensorsScreen.tsx`, `examples/expo-angular/src/screens/SensorsScreen.ts`.
|
|
105
105
|
|
|
106
106
|
Every hook/composable/`connect()` takes an optional `updateIntervalMs` and returns `null` until the
|
|
107
107
|
first native reading arrives — check `isAvailableAsync()` separately if you need to distinguish
|
package/native-link.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"android": {
|
|
3
|
+
"gradleProjectName": "expo-sensors",
|
|
4
|
+
"modules": [
|
|
5
|
+
{
|
|
6
|
+
"importPath": "expo.modules.sensors.modules.AccelerometerModule",
|
|
7
|
+
"className": "AccelerometerModule",
|
|
8
|
+
"nativeName": "ExponentAccelerometer"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"importPath": "expo.modules.sensors.modules.BarometerModule",
|
|
12
|
+
"className": "BarometerModule",
|
|
13
|
+
"nativeName": "ExpoBarometer"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"importPath": "expo.modules.sensors.modules.DeviceMotionModule",
|
|
17
|
+
"className": "DeviceMotionModule",
|
|
18
|
+
"nativeName": "ExponentDeviceMotion"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"importPath": "expo.modules.sensors.modules.GyroscopeModule",
|
|
22
|
+
"className": "GyroscopeModule",
|
|
23
|
+
"nativeName": "ExponentGyroscope"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"importPath": "expo.modules.sensors.modules.LightSensorModule",
|
|
27
|
+
"className": "LightSensorModule",
|
|
28
|
+
"nativeName": "ExpoLightSensor"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"importPath": "expo.modules.sensors.modules.MagnetometerModule",
|
|
32
|
+
"className": "MagnetometerModule",
|
|
33
|
+
"nativeName": "ExponentMagnetometer"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"importPath": "expo.modules.sensors.modules.MagnetometerUncalibratedModule",
|
|
37
|
+
"className": "MagnetometerUncalibratedModule",
|
|
38
|
+
"nativeName": "ExponentMagnetometerUncalibrated"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"importPath": "expo.modules.sensors.modules.PedometerModule",
|
|
42
|
+
"className": "PedometerModule",
|
|
43
|
+
"nativeName": "ExponentPedometer"
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
"ios": {
|
|
48
|
+
"infoPlistKeys": {
|
|
49
|
+
"NSMotionUsageDescription": "This app uses motion sensor data."
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbiote-native/sensors",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "expo-sensors wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, and Angular adapters. Accelerometer, Barometer, DeviceMotion, Gyroscope, LightSensor, Magnetometer, MagnetometerUncalibrated, Pedometer.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -39,7 +39,11 @@
|
|
|
39
39
|
"files": [
|
|
40
40
|
"src",
|
|
41
41
|
"build",
|
|
42
|
-
"build-ngc"
|
|
42
|
+
"build-ngc",
|
|
43
|
+
"native-link.json",
|
|
44
|
+
"!src/**/*.test.*",
|
|
45
|
+
"!src/**/*.spec.*",
|
|
46
|
+
"!src/**/*.detox.*"
|
|
43
47
|
],
|
|
44
48
|
"publishConfig": {
|
|
45
49
|
"access": "public"
|
|
@@ -55,7 +59,7 @@
|
|
|
55
59
|
"react": ">=19.0.0",
|
|
56
60
|
"react-native": ">=0.86",
|
|
57
61
|
"vue": ">=3.5.0",
|
|
58
|
-
"@symbiote-native/angular": "0.6.
|
|
62
|
+
"@symbiote-native/angular": "0.6.2",
|
|
59
63
|
"@symbiote-native/react": "0.2.8",
|
|
60
64
|
"@symbiote-native/vue": "0.3.8"
|
|
61
65
|
},
|
|
@@ -91,11 +95,11 @@
|
|
|
91
95
|
"@vue/runtime-core": "^3.5.13",
|
|
92
96
|
"react": "19.2.3",
|
|
93
97
|
"typescript": "~6.0.0",
|
|
94
|
-
"@symbiote-native/angular": "0.6.
|
|
95
|
-
"@symbiote-native/vue": "0.3.8",
|
|
96
|
-
"@symbiote-native/test-utils": "0.1.6",
|
|
98
|
+
"@symbiote-native/angular": "0.6.2",
|
|
97
99
|
"@symbiote-native/engine": "0.1.7",
|
|
98
|
-
"@symbiote-native/react": "0.2.8"
|
|
100
|
+
"@symbiote-native/react": "0.2.8",
|
|
101
|
+
"@symbiote-native/test-utils": "0.1.6",
|
|
102
|
+
"@symbiote-native/vue": "0.3.8"
|
|
99
103
|
},
|
|
100
104
|
"scripts": {
|
|
101
105
|
"typecheck": "tsc --build",
|
|
@@ -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
|
-
});
|