@symbiote-native/sensors 0.2.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.
Files changed (36) hide show
  1. package/README.md +5 -5
  2. package/native-link.json +52 -0
  3. package/package.json +8 -4
  4. package/src/angular/services/accelerometer.service/accelerometer.service.test.ts +0 -118
  5. package/src/angular/services/barometer.service/barometer.service.test.ts +0 -122
  6. package/src/angular/services/device-motion.service/device-motion.service.test.ts +0 -125
  7. package/src/angular/services/gyroscope.service/gyroscope.service.test.ts +0 -118
  8. package/src/angular/services/light-sensor.service/light-sensor.service.test.ts +0 -118
  9. package/src/angular/services/magnetometer-uncalibrated.service/magnetometer-uncalibrated.service.test.ts +0 -123
  10. package/src/angular/services/magnetometer.service/magnetometer.service.test.ts +0 -118
  11. package/src/angular/services/pedometer.service/pedometer.service.test.ts +0 -86
  12. package/src/core/accelerometer.test.ts +0 -49
  13. package/src/core/barometer.test.ts +0 -46
  14. package/src/core/device-motion.test.ts +0 -60
  15. package/src/core/device-sensor.test.ts +0 -76
  16. package/src/core/gyroscope.test.ts +0 -46
  17. package/src/core/light-sensor.test.ts +0 -49
  18. package/src/core/magnetometer-uncalibrated.test.ts +0 -49
  19. package/src/core/magnetometer.test.ts +0 -49
  20. package/src/core/pedometer.test.ts +0 -118
  21. package/src/react/hooks/use-accelerometer/use-accelerometer.test.tsx +0 -81
  22. package/src/react/hooks/use-barometer/use-barometer.test.tsx +0 -83
  23. package/src/react/hooks/use-device-motion/use-device-motion.test.tsx +0 -88
  24. package/src/react/hooks/use-gyroscope/use-gyroscope.test.tsx +0 -79
  25. package/src/react/hooks/use-light-sensor/use-light-sensor.test.tsx +0 -79
  26. package/src/react/hooks/use-magnetometer/use-magnetometer.test.tsx +0 -81
  27. package/src/react/hooks/use-magnetometer-uncalibrated/use-magnetometer-uncalibrated.test.tsx +0 -88
  28. package/src/react/hooks/use-pedometer/use-pedometer.test.tsx +0 -69
  29. package/src/vue/composables/use-accelerometer/use-accelerometer.test.ts +0 -96
  30. package/src/vue/composables/use-barometer/use-barometer.test.ts +0 -100
  31. package/src/vue/composables/use-device-motion/use-device-motion.test.ts +0 -104
  32. package/src/vue/composables/use-gyroscope/use-gyroscope.test.ts +0 -96
  33. package/src/vue/composables/use-light-sensor/use-light-sensor.test.ts +0 -96
  34. package/src/vue/composables/use-magnetometer/use-magnetometer.test.ts +0 -96
  35. package/src/vue/composables/use-magnetometer-uncalibrated/use-magnetometer-uncalibrated.test.ts +0 -103
  36. package/src/vue/composables/use-pedometer/use-pedometer.test.ts +0 -77
@@ -1,46 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest';
2
-
3
- const FAKE_NATIVE_GYROSCOPE = {
4
- addListener: vi.fn(() => ({ remove: vi.fn() })),
5
- listenerCount: vi.fn(() => 0),
6
- removeAllListeners: vi.fn(),
7
- setUpdateInterval: vi.fn(),
8
- };
9
-
10
- // The real ExponentGyroscope native module only exists on device — resolving it via
11
- // requireNativeModule() at import time would throw in this headless test run, so the
12
- // module-lookup file is faked in place of `expo-modules-core`'s runtime resolution.
13
- vi.mock('./native/exponent-gyroscope', () => ({
14
- exponentGyroscope: FAKE_NATIVE_GYROSCOPE,
15
- }));
16
-
17
- // device-sensor.ts (imported transitively through ./gyroscope) pulls Platform/PermissionStatus
18
- // from expo-modules-core, whose real entry drags in the Flow-typed 'react-native' source that
19
- // Vitest's Oxc transform can't parse — same fake as device-sensor.test.ts.
20
- vi.mock('expo-modules-core', () => ({
21
- Platform: { OS: 'ios' },
22
- PermissionStatus: { GRANTED: 'granted', UNDETERMINED: 'undetermined', DENIED: 'denied' },
23
- }));
24
-
25
- const { Gyroscope } = await import('./gyroscope');
26
-
27
- afterEach(() => {
28
- Gyroscope.removeAllListeners();
29
- vi.clearAllMocks();
30
- });
31
-
32
- describe('Gyroscope', () => {
33
- it('sets the update interval', () => {
34
- Gyroscope.setUpdateInterval(1234);
35
-
36
- expect(FAKE_NATIVE_GYROSCOPE.setUpdateInterval).toHaveBeenCalledTimes(1);
37
- expect(FAKE_NATIVE_GYROSCOPE.setUpdateInterval).toHaveBeenCalledWith(1234);
38
- });
39
-
40
- it('subscribes through the shared gyroscopeDidUpdate event name', () => {
41
- const listener = vi.fn();
42
- Gyroscope.addListener(listener);
43
-
44
- expect(FAKE_NATIVE_GYROSCOPE.addListener).toHaveBeenCalledWith('gyroscopeDidUpdate', listener);
45
- });
46
- });
@@ -1,49 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest';
2
-
3
- const FAKE_NATIVE_LIGHT_SENSOR = {
4
- addListener: vi.fn(() => ({ remove: vi.fn() })),
5
- listenerCount: vi.fn(() => 0),
6
- removeAllListeners: vi.fn(),
7
- setUpdateInterval: vi.fn(),
8
- };
9
-
10
- // The real ExpoLightSensor native module only exists on device — resolving it via
11
- // requireNativeModule() at import time would throw in this headless test run, so the
12
- // module-lookup file is faked in place of `expo-modules-core`'s runtime resolution.
13
- vi.mock('./native/exponent-light-sensor', () => ({
14
- exponentLightSensor: FAKE_NATIVE_LIGHT_SENSOR,
15
- }));
16
-
17
- // device-sensor.ts (imported transitively through ./light-sensor) pulls Platform/PermissionStatus
18
- // from expo-modules-core, whose real entry drags in the Flow-typed 'react-native' source that
19
- // Vitest's Oxc transform can't parse — same fake as device-sensor.test.ts.
20
- vi.mock('expo-modules-core', () => ({
21
- Platform: { OS: 'ios' },
22
- PermissionStatus: { GRANTED: 'granted', UNDETERMINED: 'undetermined', DENIED: 'denied' },
23
- }));
24
-
25
- const { LightSensor } = await import('./light-sensor');
26
-
27
- afterEach(() => {
28
- LightSensor.removeAllListeners();
29
- vi.clearAllMocks();
30
- });
31
-
32
- describe('LightSensor', () => {
33
- it('sets the update interval', () => {
34
- LightSensor.setUpdateInterval(1234);
35
-
36
- expect(FAKE_NATIVE_LIGHT_SENSOR.setUpdateInterval).toHaveBeenCalledTimes(1);
37
- expect(FAKE_NATIVE_LIGHT_SENSOR.setUpdateInterval).toHaveBeenCalledWith(1234);
38
- });
39
-
40
- it('subscribes through the shared lightSensorDidUpdate event name', () => {
41
- const listener = vi.fn();
42
- LightSensor.addListener(listener);
43
-
44
- expect(FAKE_NATIVE_LIGHT_SENSOR.addListener).toHaveBeenCalledWith(
45
- 'lightSensorDidUpdate',
46
- listener,
47
- );
48
- });
49
- });
@@ -1,49 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest';
2
-
3
- const FAKE_NATIVE_MAGNETOMETER_UNCALIBRATED = {
4
- addListener: vi.fn(() => ({ remove: vi.fn() })),
5
- listenerCount: vi.fn(() => 0),
6
- removeAllListeners: vi.fn(),
7
- setUpdateInterval: vi.fn(),
8
- };
9
-
10
- // The real ExponentMagnetometerUncalibrated native module only exists on device — resolving it
11
- // via requireNativeModule() at import time would throw in this headless test run, so the
12
- // module-lookup file is faked in place of `expo-modules-core`'s runtime resolution.
13
- vi.mock('./native/exponent-magnetometer-uncalibrated', () => ({
14
- exponentMagnetometerUncalibrated: FAKE_NATIVE_MAGNETOMETER_UNCALIBRATED,
15
- }));
16
-
17
- // device-sensor.ts (imported transitively through ./magnetometer-uncalibrated) pulls
18
- // Platform/PermissionStatus from expo-modules-core, whose real entry drags in the Flow-typed
19
- // 'react-native' source that Vitest's Oxc transform can't parse — same fake as device-sensor.test.ts.
20
- vi.mock('expo-modules-core', () => ({
21
- Platform: { OS: 'ios' },
22
- PermissionStatus: { GRANTED: 'granted', UNDETERMINED: 'undetermined', DENIED: 'denied' },
23
- }));
24
-
25
- const { MagnetometerUncalibrated } = await import('./magnetometer-uncalibrated');
26
-
27
- afterEach(() => {
28
- MagnetometerUncalibrated.removeAllListeners();
29
- vi.clearAllMocks();
30
- });
31
-
32
- describe('MagnetometerUncalibrated', () => {
33
- it('sets the update interval', () => {
34
- MagnetometerUncalibrated.setUpdateInterval(1234);
35
-
36
- expect(FAKE_NATIVE_MAGNETOMETER_UNCALIBRATED.setUpdateInterval).toHaveBeenCalledTimes(1);
37
- expect(FAKE_NATIVE_MAGNETOMETER_UNCALIBRATED.setUpdateInterval).toHaveBeenCalledWith(1234);
38
- });
39
-
40
- it('subscribes through the shared magnetometerUncalibratedDidUpdate event name', () => {
41
- const listener = vi.fn();
42
- MagnetometerUncalibrated.addListener(listener);
43
-
44
- expect(FAKE_NATIVE_MAGNETOMETER_UNCALIBRATED.addListener).toHaveBeenCalledWith(
45
- 'magnetometerUncalibratedDidUpdate',
46
- listener,
47
- );
48
- });
49
- });
@@ -1,49 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest';
2
-
3
- const FAKE_NATIVE_MAGNETOMETER = {
4
- addListener: vi.fn(() => ({ remove: vi.fn() })),
5
- listenerCount: vi.fn(() => 0),
6
- removeAllListeners: vi.fn(),
7
- setUpdateInterval: vi.fn(),
8
- };
9
-
10
- // The real ExponentMagnetometer native module only exists on device — resolving it via
11
- // requireNativeModule() at import time would throw in this headless test run, so the
12
- // module-lookup file is faked in place of `expo-modules-core`'s runtime resolution.
13
- vi.mock('./native/exponent-magnetometer', () => ({
14
- exponentMagnetometer: FAKE_NATIVE_MAGNETOMETER,
15
- }));
16
-
17
- // device-sensor.ts (imported transitively through ./magnetometer) pulls Platform/PermissionStatus
18
- // from expo-modules-core, whose real entry drags in the Flow-typed 'react-native' source that
19
- // Vitest's Oxc transform can't parse — same fake as device-sensor.test.ts.
20
- vi.mock('expo-modules-core', () => ({
21
- Platform: { OS: 'ios' },
22
- PermissionStatus: { GRANTED: 'granted', UNDETERMINED: 'undetermined', DENIED: 'denied' },
23
- }));
24
-
25
- const { Magnetometer } = await import('./magnetometer');
26
-
27
- afterEach(() => {
28
- Magnetometer.removeAllListeners();
29
- vi.clearAllMocks();
30
- });
31
-
32
- describe('Magnetometer', () => {
33
- it('sets the update interval', () => {
34
- Magnetometer.setUpdateInterval(1234);
35
-
36
- expect(FAKE_NATIVE_MAGNETOMETER.setUpdateInterval).toHaveBeenCalledTimes(1);
37
- expect(FAKE_NATIVE_MAGNETOMETER.setUpdateInterval).toHaveBeenCalledWith(1234);
38
- });
39
-
40
- it('subscribes through the shared magnetometerDidUpdate event name', () => {
41
- const listener = vi.fn();
42
- Magnetometer.addListener(listener);
43
-
44
- expect(FAKE_NATIVE_MAGNETOMETER.addListener).toHaveBeenCalledWith(
45
- 'magnetometerDidUpdate',
46
- listener,
47
- );
48
- });
49
- });
@@ -1,118 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest';
2
-
3
- const FAKE_NATIVE_PEDOMETER = {
4
- addListener: vi.fn(() => ({ remove: vi.fn() })),
5
- getStepCountAsync: vi.fn(async () => ({ steps: 42 })),
6
- isAvailableAsync: vi.fn(async () => true),
7
- getPermissionsAsync: vi.fn(),
8
- requestPermissionsAsync: vi.fn(),
9
- };
10
-
11
- // The real ExponentPedometer native module only exists on device — resolving it via
12
- // requireNativeModule() at import time would throw in this headless test run, so the
13
- // module-lookup file is faked in place of `expo-modules-core`'s runtime resolution.
14
- vi.mock('./native/exponent-pedometer', () => ({
15
- exponentPedometer: FAKE_NATIVE_PEDOMETER,
16
- }));
17
-
18
- // pedometer.ts pulls PermissionStatus/UnavailabilityError from expo-modules-core, whose real
19
- // entry transitively drags in the Flow-typed 'react-native' source that Vitest's Oxc transform
20
- // can't parse — same fake as device-sensor.test.ts / accelerometer.test.ts.
21
- vi.mock('expo-modules-core', () => ({
22
- PermissionStatus: { GRANTED: 'granted', UNDETERMINED: 'undetermined', DENIED: 'denied' },
23
- UnavailabilityError: class UnavailabilityError extends Error {
24
- constructor(moduleName: string, propertyName: string) {
25
- super(`${propertyName} is not available on ${moduleName}`);
26
- }
27
- },
28
- }));
29
-
30
- const {
31
- watchStepCount,
32
- getStepCountAsync,
33
- isAvailableAsync,
34
- getPermissionsAsync,
35
- requestPermissionsAsync,
36
- } = await import('./pedometer');
37
-
38
- afterEach(() => {
39
- vi.clearAllMocks();
40
- });
41
-
42
- describe('Pedometer', () => {
43
- it('watchStepCount subscribes through the dotted Exponent.pedometerUpdate event name', () => {
44
- const listener = vi.fn();
45
- watchStepCount(listener);
46
-
47
- expect(FAKE_NATIVE_PEDOMETER.addListener).toHaveBeenCalledWith(
48
- 'Exponent.pedometerUpdate',
49
- listener,
50
- );
51
- });
52
-
53
- it('getStepCountAsync throws when the start date is after the end date', async () => {
54
- const start = new Date('2026-07-15T00:00:00Z');
55
- const end = new Date('2026-07-01T00:00:00Z');
56
-
57
- await expect(getStepCountAsync(start, end)).rejects.toThrow(
58
- 'Pedometer: the start date must precede the end date.',
59
- );
60
- expect(FAKE_NATIVE_PEDOMETER.getStepCountAsync).not.toHaveBeenCalled();
61
- });
62
-
63
- it('getStepCountAsync throws an UnavailabilityError-shaped error when the native method is absent', async () => {
64
- const { getStepCountAsync: nativeGetStepCountAsync } = FAKE_NATIVE_PEDOMETER;
65
- // @ts-expect-error -- simulating a platform where the native module has no such method
66
- FAKE_NATIVE_PEDOMETER.getStepCountAsync = undefined;
67
-
68
- const start = new Date('2026-07-01T00:00:00Z');
69
- const end = new Date('2026-07-15T00:00:00Z');
70
-
71
- await expect(getStepCountAsync(start, end)).rejects.toThrow(
72
- 'getStepCountAsync is not available on ExponentPedometer',
73
- );
74
-
75
- FAKE_NATIVE_PEDOMETER.getStepCountAsync = nativeGetStepCountAsync;
76
- });
77
-
78
- it('getStepCountAsync calls through with millisecond values from each Date', async () => {
79
- const start = new Date('2026-07-01T00:00:00Z');
80
- const end = new Date('2026-07-15T00:00:00Z');
81
-
82
- const result = await getStepCountAsync(start, end);
83
-
84
- expect(FAKE_NATIVE_PEDOMETER.getStepCountAsync).toHaveBeenCalledWith(
85
- start.getTime(),
86
- end.getTime(),
87
- );
88
- expect(result).toEqual({ steps: 42 });
89
- });
90
-
91
- it('isAvailableAsync delegates to the native module', async () => {
92
- await expect(isAvailableAsync()).resolves.toBe(true);
93
- expect(FAKE_NATIVE_PEDOMETER.isAvailableAsync).toHaveBeenCalledTimes(1);
94
- });
95
-
96
- it('falls back to a granted default permission response when the native module has none', async () => {
97
- const {
98
- getPermissionsAsync: nativeGetPermissionsAsync,
99
- requestPermissionsAsync: nativeRequestPermissionsAsync,
100
- } = FAKE_NATIVE_PEDOMETER;
101
- // @ts-expect-error -- simulating a platform where the native module exposes no permission methods
102
- FAKE_NATIVE_PEDOMETER.getPermissionsAsync = undefined;
103
- // @ts-expect-error -- simulating a platform where the native module exposes no permission methods
104
- FAKE_NATIVE_PEDOMETER.requestPermissionsAsync = undefined;
105
-
106
- const expectedDefault = {
107
- granted: true,
108
- expires: 'never',
109
- canAskAgain: true,
110
- status: 'granted',
111
- };
112
- await expect(getPermissionsAsync()).resolves.toEqual(expectedDefault);
113
- await expect(requestPermissionsAsync()).resolves.toEqual(expectedDefault);
114
-
115
- FAKE_NATIVE_PEDOMETER.getPermissionsAsync = nativeGetPermissionsAsync;
116
- FAKE_NATIVE_PEDOMETER.requestPermissionsAsync = nativeRequestPermissionsAsync;
117
- });
118
- });
@@ -1,81 +0,0 @@
1
- // Co-located React-driven test (ADR 0025) for useAccelerometer. Mocks the whole `core`
2
- // module rather than expo-modules-core internals — this hook's own lifecycle wiring
3
- // (subscribe/unsubscribe/interval) is what's under test, not the core port itself, which
4
- // already has its own coverage in packages/sensors/src/core/*.test.ts.
5
-
6
- import { createElement, type ReactElement } from 'react';
7
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
8
- import { mount, unmount, View } from '@symbiote-native/react';
9
- import { installFabric } from '@symbiote-native/test-utils';
10
- import { useAccelerometer } from './index';
11
- import type { IAccelerometerMeasurement } from '../../../core';
12
-
13
- const { addListener, removeAllListeners, setUpdateInterval, remove } = vi.hoisted(() => {
14
- const remove = vi.fn();
15
- return {
16
- addListener: vi.fn((_listener: (measurement: IAccelerometerMeasurement) => void) => ({
17
- remove,
18
- })),
19
- removeAllListeners: vi.fn(),
20
- setUpdateInterval: vi.fn(),
21
- remove,
22
- };
23
- });
24
-
25
- vi.mock('../../../core', () => ({
26
- Accelerometer: { addListener, removeAllListeners, setUpdateInterval },
27
- }));
28
-
29
- const ROOT_TAG = 901;
30
-
31
- const results: Array<IAccelerometerMeasurement | null> = [];
32
-
33
- function Probe(props: { updateIntervalMs?: number }): ReactElement {
34
- results.push(useAccelerometer(props.updateIntervalMs));
35
- return createElement(View);
36
- }
37
-
38
- const fabric = installFabric();
39
-
40
- beforeEach(() => {
41
- fabric.reset();
42
- results.length = 0;
43
- vi.clearAllMocks();
44
- });
45
-
46
- afterEach(() => unmount(ROOT_TAG));
47
-
48
- describe('useAccelerometer', () => {
49
- it('reports null before any native measurement arrives', () => {
50
- mount(ROOT_TAG, createElement(Probe, {}));
51
-
52
- expect(results[results.length - 1]).toBeNull();
53
- });
54
-
55
- it('updates to the latest measurement once the native listener fires', async () => {
56
- mount(ROOT_TAG, createElement(Probe, {}));
57
-
58
- const measurement: IAccelerometerMeasurement = { x: 0.1, y: 0.2, z: 0.3, timestamp: 123 };
59
- const listener = addListener.mock.calls[0][0];
60
- listener(measurement);
61
-
62
- // The mock invokes the listener directly, outside the engine's event dispatcher
63
- // (setEventDispatcher in render.ts), which is what normally flushes a native-driven
64
- // setState synchronously — so the resulting re-render lands on a later microtask here.
65
- await vi.waitFor(() => expect(results[results.length - 1]).toEqual(measurement));
66
- });
67
-
68
- it('unsubscribes from the native listener on unmount', () => {
69
- mount(ROOT_TAG, createElement(Probe, {}));
70
-
71
- unmount(ROOT_TAG);
72
-
73
- expect(remove).toHaveBeenCalledTimes(1);
74
- });
75
-
76
- it('sets the native update interval when updateIntervalMs is provided', () => {
77
- mount(ROOT_TAG, createElement(Probe, { updateIntervalMs: 100 }));
78
-
79
- expect(setUpdateInterval).toHaveBeenCalledWith(100);
80
- });
81
- });
@@ -1,83 +0,0 @@
1
- // Co-located React-driven test (ADR 0025) for useBarometer. Mocks the whole `core`
2
- // module rather than expo-modules-core internals — this hook's own lifecycle wiring
3
- // (subscribe/unsubscribe/interval) is what's under test, not the core port itself, which
4
- // already has its own coverage in packages/sensors/src/core/*.test.ts.
5
-
6
- import { createElement, type ReactElement } from 'react';
7
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
8
- import { mount, unmount, View } from '@symbiote-native/react';
9
- import { installFabric } from '@symbiote-native/test-utils';
10
- import { useBarometer } from './index';
11
- import type { IBarometerMeasurement } from '../../../core';
12
-
13
- const { addListener, removeAllListeners, setUpdateInterval, remove } = vi.hoisted(() => {
14
- const remove = vi.fn();
15
- return {
16
- addListener: vi.fn((_listener: (measurement: IBarometerMeasurement) => void) => ({ remove })),
17
- removeAllListeners: vi.fn(),
18
- setUpdateInterval: vi.fn(),
19
- remove,
20
- };
21
- });
22
-
23
- vi.mock('../../../core', () => ({
24
- Barometer: { addListener, removeAllListeners, setUpdateInterval },
25
- }));
26
-
27
- const ROOT_TAG = 902;
28
-
29
- const results: Array<IBarometerMeasurement | null> = [];
30
-
31
- function Probe(props: { updateIntervalMs?: number }): ReactElement {
32
- results.push(useBarometer(props.updateIntervalMs));
33
- return createElement(View);
34
- }
35
-
36
- const fabric = installFabric();
37
-
38
- beforeEach(() => {
39
- fabric.reset();
40
- results.length = 0;
41
- vi.clearAllMocks();
42
- });
43
-
44
- afterEach(() => unmount(ROOT_TAG));
45
-
46
- describe('useBarometer', () => {
47
- it('reports null before any native measurement arrives', () => {
48
- mount(ROOT_TAG, createElement(Probe, {}));
49
-
50
- expect(results[results.length - 1]).toBeNull();
51
- });
52
-
53
- it('updates to the latest measurement once the native listener fires', async () => {
54
- mount(ROOT_TAG, createElement(Probe, {}));
55
-
56
- const measurement: IBarometerMeasurement = {
57
- pressure: 1013.25,
58
- relativeAltitude: 12.3,
59
- timestamp: 123,
60
- };
61
- const listener = addListener.mock.calls[0][0];
62
- listener(measurement);
63
-
64
- // The mock invokes the listener directly, outside the engine's event dispatcher
65
- // (setEventDispatcher in render.ts), which is what normally flushes a native-driven
66
- // setState synchronously — so the resulting re-render lands on a later microtask here.
67
- await vi.waitFor(() => expect(results[results.length - 1]).toEqual(measurement));
68
- });
69
-
70
- it('unsubscribes from the native listener on unmount', () => {
71
- mount(ROOT_TAG, createElement(Probe, {}));
72
-
73
- unmount(ROOT_TAG);
74
-
75
- expect(remove).toHaveBeenCalledTimes(1);
76
- });
77
-
78
- it('sets the native update interval when updateIntervalMs is provided', () => {
79
- mount(ROOT_TAG, createElement(Probe, { updateIntervalMs: 100 }));
80
-
81
- expect(setUpdateInterval).toHaveBeenCalledWith(100);
82
- });
83
- });
@@ -1,88 +0,0 @@
1
- // Co-located React-driven test (ADR 0025) for useDeviceMotion. Mocks the whole `core`
2
- // module rather than expo-modules-core internals — this hook's own lifecycle wiring
3
- // (subscribe/unsubscribe/interval) is what's under test, not the core port itself, which
4
- // already has its own coverage in packages/sensors/src/core/*.test.ts.
5
-
6
- import { createElement, type ReactElement } from 'react';
7
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
8
- import { mount, unmount, View } from '@symbiote-native/react';
9
- import { installFabric } from '@symbiote-native/test-utils';
10
- import { useDeviceMotion } from './index';
11
- import type { IDeviceMotionMeasurement } from '../../../core';
12
-
13
- const { addListener, removeAllListeners, setUpdateInterval, remove } = vi.hoisted(() => {
14
- const remove = vi.fn();
15
- return {
16
- addListener: vi.fn((_listener: (measurement: IDeviceMotionMeasurement) => void) => ({
17
- remove,
18
- })),
19
- removeAllListeners: vi.fn(),
20
- setUpdateInterval: vi.fn(),
21
- remove,
22
- };
23
- });
24
-
25
- vi.mock('../../../core', () => ({
26
- DeviceMotion: { addListener, removeAllListeners, setUpdateInterval },
27
- }));
28
-
29
- const ROOT_TAG = 902;
30
-
31
- const results: Array<IDeviceMotionMeasurement | null> = [];
32
-
33
- function Probe(props: { updateIntervalMs?: number }): ReactElement {
34
- results.push(useDeviceMotion(props.updateIntervalMs));
35
- return createElement(View);
36
- }
37
-
38
- const fabric = installFabric();
39
-
40
- beforeEach(() => {
41
- fabric.reset();
42
- results.length = 0;
43
- vi.clearAllMocks();
44
- });
45
-
46
- afterEach(() => unmount(ROOT_TAG));
47
-
48
- describe('useDeviceMotion', () => {
49
- it('reports null before any native measurement arrives', () => {
50
- mount(ROOT_TAG, createElement(Probe, {}));
51
-
52
- expect(results[results.length - 1]).toBeNull();
53
- });
54
-
55
- it('updates to the latest measurement once the native listener fires', async () => {
56
- mount(ROOT_TAG, createElement(Probe, {}));
57
-
58
- const measurement: IDeviceMotionMeasurement = {
59
- acceleration: { x: 0.1, y: 0.2, z: 0.3, timestamp: 123 },
60
- accelerationIncludingGravity: { x: 0.1, y: 0.2, z: 9.8, timestamp: 123 },
61
- rotation: { alpha: 1, beta: 2, gamma: 3, timestamp: 123 },
62
- rotationRate: { alpha: 0.1, beta: 0.2, gamma: 0.3, timestamp: 123 },
63
- interval: 16,
64
- orientation: 0, // DeviceMotionOrientation.Portrait
65
- };
66
- const listener = addListener.mock.calls[0][0];
67
- listener(measurement);
68
-
69
- // The mock invokes the listener directly, outside the engine's event dispatcher
70
- // (setEventDispatcher in render.ts), which is what normally flushes a native-driven
71
- // setState synchronously — so the resulting re-render lands on a later microtask here.
72
- await vi.waitFor(() => expect(results[results.length - 1]).toEqual(measurement));
73
- });
74
-
75
- it('unsubscribes from the native listener on unmount', () => {
76
- mount(ROOT_TAG, createElement(Probe, {}));
77
-
78
- unmount(ROOT_TAG);
79
-
80
- expect(remove).toHaveBeenCalledTimes(1);
81
- });
82
-
83
- it('sets the native update interval when updateIntervalMs is provided', () => {
84
- mount(ROOT_TAG, createElement(Probe, { updateIntervalMs: 100 }));
85
-
86
- expect(setUpdateInterval).toHaveBeenCalledWith(100);
87
- });
88
- });
@@ -1,79 +0,0 @@
1
- // Co-located React-driven test (ADR 0025) for useGyroscope. Mocks the whole `core`
2
- // module rather than expo-modules-core internals — this hook's own lifecycle wiring
3
- // (subscribe/unsubscribe/interval) is what's under test, not the core port itself, which
4
- // already has its own coverage in packages/sensors/src/core/*.test.ts.
5
-
6
- import { createElement, type ReactElement } from 'react';
7
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
8
- import { mount, unmount, View } from '@symbiote-native/react';
9
- import { installFabric } from '@symbiote-native/test-utils';
10
- import { useGyroscope } from './index';
11
- import type { IGyroscopeMeasurement } from '../../../core';
12
-
13
- const { addListener, removeAllListeners, setUpdateInterval, remove } = vi.hoisted(() => {
14
- const remove = vi.fn();
15
- return {
16
- addListener: vi.fn((_listener: (measurement: IGyroscopeMeasurement) => void) => ({ remove })),
17
- removeAllListeners: vi.fn(),
18
- setUpdateInterval: vi.fn(),
19
- remove,
20
- };
21
- });
22
-
23
- vi.mock('../../../core', () => ({
24
- Gyroscope: { addListener, removeAllListeners, setUpdateInterval },
25
- }));
26
-
27
- const ROOT_TAG = 902;
28
-
29
- const results: Array<IGyroscopeMeasurement | null> = [];
30
-
31
- function Probe(props: { updateIntervalMs?: number }): ReactElement {
32
- results.push(useGyroscope(props.updateIntervalMs));
33
- return createElement(View);
34
- }
35
-
36
- const fabric = installFabric();
37
-
38
- beforeEach(() => {
39
- fabric.reset();
40
- results.length = 0;
41
- vi.clearAllMocks();
42
- });
43
-
44
- afterEach(() => unmount(ROOT_TAG));
45
-
46
- describe('useGyroscope', () => {
47
- it('reports null before any native measurement arrives', () => {
48
- mount(ROOT_TAG, createElement(Probe, {}));
49
-
50
- expect(results[results.length - 1]).toBeNull();
51
- });
52
-
53
- it('updates to the latest measurement once the native listener fires', async () => {
54
- mount(ROOT_TAG, createElement(Probe, {}));
55
-
56
- const measurement: IGyroscopeMeasurement = { x: 0.1, y: 0.2, z: 0.3, timestamp: 123 };
57
- const listener = addListener.mock.calls[0][0];
58
- listener(measurement);
59
-
60
- // The mock invokes the listener directly, outside the engine's event dispatcher
61
- // (setEventDispatcher in render.ts), which is what normally flushes a native-driven
62
- // setState synchronously — so the resulting re-render lands on a later microtask here.
63
- await vi.waitFor(() => expect(results[results.length - 1]).toEqual(measurement));
64
- });
65
-
66
- it('unsubscribes from the native listener on unmount', () => {
67
- mount(ROOT_TAG, createElement(Probe, {}));
68
-
69
- unmount(ROOT_TAG);
70
-
71
- expect(remove).toHaveBeenCalledTimes(1);
72
- });
73
-
74
- it('sets the native update interval when updateIntervalMs is provided', () => {
75
- mount(ROOT_TAG, createElement(Probe, { updateIntervalMs: 100 }));
76
-
77
- expect(setUpdateInterval).toHaveBeenCalledWith(100);
78
- });
79
- });