@symbiote-native/screen-orientation 0.0.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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +171 -0
  3. package/build/angular/index.d.ts +2 -0
  4. package/build/angular/index.js +4 -0
  5. package/build/angular/services/screen-orientation.service/index.d.ts +6 -0
  6. package/build/angular/services/screen-orientation.service/index.js +75 -0
  7. package/build/core/index.d.ts +3 -0
  8. package/build/core/index.js +2 -0
  9. package/build/core/native-module.d.ts +13 -0
  10. package/build/core/native-module.js +3 -0
  11. package/build/core/screen-orientation.d.ts +24 -0
  12. package/build/core/screen-orientation.js +152 -0
  13. package/build/core/types.d.ts +85 -0
  14. package/build/core/types.js +64 -0
  15. package/build/react/hooks/use-screen-orientation/index.d.ts +2 -0
  16. package/build/react/hooks/use-screen-orientation/index.js +20 -0
  17. package/build/react/index.d.ts +2 -0
  18. package/build/react/index.js +3 -0
  19. package/build/vue/composables/use-screen-orientation/index.d.ts +3 -0
  20. package/build/vue/composables/use-screen-orientation/index.js +27 -0
  21. package/build/vue/index.d.ts +2 -0
  22. package/build/vue/index.js +3 -0
  23. package/build-ngc/angular/index.d.ts +2 -0
  24. package/build-ngc/angular/index.js +5 -0
  25. package/build-ngc/angular/index.js.map +1 -0
  26. package/build-ngc/angular/services/screen-orientation.service/index.d.ts +9 -0
  27. package/build-ngc/angular/services/screen-orientation.service/index.js +34 -0
  28. package/build-ngc/angular/services/screen-orientation.service/index.js.map +1 -0
  29. package/build-ngc/core/index.d.ts +3 -0
  30. package/build-ngc/core/index.js +3 -0
  31. package/build-ngc/core/index.js.map +1 -0
  32. package/build-ngc/core/native-module.d.ts +13 -0
  33. package/build-ngc/core/native-module.js +4 -0
  34. package/build-ngc/core/native-module.js.map +1 -0
  35. package/build-ngc/core/screen-orientation.d.ts +24 -0
  36. package/build-ngc/core/screen-orientation.js +153 -0
  37. package/build-ngc/core/screen-orientation.js.map +1 -0
  38. package/build-ngc/core/types.d.ts +85 -0
  39. package/build-ngc/core/types.js +65 -0
  40. package/build-ngc/core/types.js.map +1 -0
  41. package/native-link.json +12 -0
  42. package/package.json +107 -0
  43. package/src/angular/index.ts +26 -0
  44. package/src/angular/services/screen-orientation.service/index.ts +45 -0
  45. package/src/angular/services/screen-orientation.service/screen-orientation.service.test.ts +101 -0
  46. package/src/core/index.ts +25 -0
  47. package/src/core/native-module.ts +36 -0
  48. package/src/core/screen-orientation.test.ts +307 -0
  49. package/src/core/screen-orientation.ts +186 -0
  50. package/src/core/types.ts +98 -0
  51. package/src/react/hooks/use-screen-orientation/index.ts +34 -0
  52. package/src/react/hooks/use-screen-orientation/use-screen-orientation.test.tsx +93 -0
  53. package/src/react/index.ts +26 -0
  54. package/src/vue/composables/use-screen-orientation/index.ts +42 -0
  55. package/src/vue/composables/use-screen-orientation/use-screen-orientation.test.ts +98 -0
  56. package/src/vue/index.ts +26 -0
package/package.json ADDED
@@ -0,0 +1,107 @@
1
+ {
2
+ "name": "@symbiote-native/screen-orientation",
3
+ "version": "0.0.1",
4
+ "description": "expo-screen-orientation wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, and Angular adapters. Orientation locking (lockAsync/lockPlatformAsync/unlockAsync), the current orientation and lock, and an auto-updating orientation-change hook/composable/service.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/OneEyed1366/symbiote-native.git",
9
+ "directory": "packages/screen-orientation"
10
+ },
11
+ "homepage": "https://github.com/OneEyed1366/symbiote-native/tree/master/packages/screen-orientation#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/OneEyed1366/symbiote-native/issues"
14
+ },
15
+ "author": "Andrey Prokopenko <psevdoproger@gmail.com>",
16
+ "type": "module",
17
+ "main": "./build/core/index.js",
18
+ "module": "./build/core/index.js",
19
+ "types": "./build/core/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./build/core/index.d.ts",
23
+ "default": "./build/core/index.js"
24
+ },
25
+ "./vue": {
26
+ "types": "./build/vue/index.d.ts",
27
+ "default": "./build/vue/index.js"
28
+ },
29
+ "./react": {
30
+ "types": "./build/react/index.d.ts",
31
+ "default": "./build/react/index.js"
32
+ },
33
+ "./angular": {
34
+ "types": "./build-ngc/angular/index.d.ts",
35
+ "react-native": "./build-ngc/angular/index.js",
36
+ "default": "./src/angular/index.ts"
37
+ }
38
+ },
39
+ "files": [
40
+ "src",
41
+ "build",
42
+ "build-ngc",
43
+ "native-link.json"
44
+ ],
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "dependencies": {
49
+ "expo-screen-orientation": "57.0.1",
50
+ "expo-modules-core": "57.0.5"
51
+ },
52
+ "peerDependencies": {
53
+ "@symbiote-native/engine": ">=0.1.7",
54
+ "@angular/core": ">=20",
55
+ "@vue/runtime-core": "^3.5.13",
56
+ "react": ">=19.0.0",
57
+ "react-native": ">=0.86",
58
+ "vue": ">=3.5.0",
59
+ "@symbiote-native/angular": "0.6.1",
60
+ "@symbiote-native/react": "0.2.8",
61
+ "@symbiote-native/vue": "0.3.8"
62
+ },
63
+ "peerDependenciesMeta": {
64
+ "@symbiote-native/angular": {
65
+ "optional": true
66
+ },
67
+ "@symbiote-native/react": {
68
+ "optional": true
69
+ },
70
+ "@symbiote-native/vue": {
71
+ "optional": true
72
+ },
73
+ "@angular/core": {
74
+ "optional": true
75
+ },
76
+ "@vue/runtime-core": {
77
+ "optional": true
78
+ },
79
+ "react": {
80
+ "optional": true
81
+ },
82
+ "vue": {
83
+ "optional": true
84
+ }
85
+ },
86
+ "devDependencies": {
87
+ "@angular/compiler": "^22",
88
+ "@angular/compiler-cli": "^22",
89
+ "@angular/core": "^22",
90
+ "@types/node": "^26.0.0",
91
+ "@types/react": "^19.2.0",
92
+ "@vue/runtime-core": "^3.5.13",
93
+ "react": "19.2.3",
94
+ "typescript": "~6.0.0",
95
+ "@symbiote-native/test-utils": "0.1.6",
96
+ "@symbiote-native/engine": "0.1.7",
97
+ "@symbiote-native/angular": "0.6.1",
98
+ "@symbiote-native/vue": "0.3.8",
99
+ "@symbiote-native/react": "0.2.8"
100
+ },
101
+ "scripts": {
102
+ "typecheck": "tsc --build",
103
+ "clean": "rm -rf build-ngc",
104
+ "ng:build": "pnpm run clean && ngc -p tsconfig.angular.json",
105
+ "format": "prettier --write \"src/**/*.{ts,tsx}\""
106
+ }
107
+ }
@@ -0,0 +1,26 @@
1
+ // ScreenOrientationService is the Angular-only lifecycle half; the free functions and event
2
+ // subscription plumbing all live in core, shared with React/Vue.
3
+ export { ScreenOrientationService } from './services/screen-orientation.service';
4
+ export {
5
+ lockAsync,
6
+ lockPlatformAsync,
7
+ unlockAsync,
8
+ getOrientationAsync,
9
+ getOrientationLockAsync,
10
+ getPlatformOrientationLockAsync,
11
+ supportsOrientationLockAsync,
12
+ addOrientationChangeListener,
13
+ removeOrientationChangeListeners,
14
+ removeOrientationChangeListener,
15
+ Orientation,
16
+ OrientationLock,
17
+ SizeClassIOS,
18
+ WebOrientationLock,
19
+ WebOrientation,
20
+ type PlatformOrientationInfo,
21
+ type ScreenOrientationInfo,
22
+ type OrientationChangeEvent,
23
+ type OrientationChangeListener,
24
+ type ScreenOrientationState,
25
+ type EventSubscription,
26
+ } from '../core';
@@ -0,0 +1,45 @@
1
+ import { effect, inject, Injectable, Injector, signal, type Signal } from '@angular/core';
2
+ import {
3
+ addOrientationChangeListener,
4
+ getOrientationAsync,
5
+ getOrientationLockAsync,
6
+ Orientation,
7
+ OrientationLock,
8
+ type ScreenOrientationState,
9
+ } from '../../../core';
10
+
11
+ // Angular twin of React's `useScreenOrientation` hook and Vue's `useScreenOrientation`
12
+ // composable.
13
+ //
14
+ // readonly screenOrientation = inject(ScreenOrientationService).connect();
15
+ // // template: {{ screenOrientation().orientation }}
16
+ @Injectable({ providedIn: 'root' })
17
+ export class ScreenOrientationService {
18
+ private readonly injector = inject(Injector);
19
+
20
+ connect(): Signal<ScreenOrientationState> {
21
+ const screenOrientation = signal<ScreenOrientationState>({
22
+ orientation: Orientation.UNKNOWN,
23
+ orientationLock: OrientationLock.UNKNOWN,
24
+ });
25
+
26
+ effect(
27
+ onCleanup => {
28
+ Promise.all([getOrientationAsync(), getOrientationLockAsync()]).then(
29
+ ([orientation, orientationLock]) =>
30
+ screenOrientation.set({ orientation, orientationLock }),
31
+ );
32
+ const subscription = addOrientationChangeListener(event =>
33
+ screenOrientation.set({
34
+ orientation: event.orientationInfo.orientation,
35
+ orientationLock: event.orientationLock,
36
+ }),
37
+ );
38
+ onCleanup(() => subscription.remove());
39
+ },
40
+ { injector: this.injector },
41
+ );
42
+
43
+ return screenOrientation.asReadonly();
44
+ }
45
+ }
@@ -0,0 +1,101 @@
1
+ // Co-located Angular-driven test (ADR 0025) for ScreenOrientationService. See network's
2
+ // network-state.service.test.ts for the shared rationale.
3
+
4
+ import '@angular/compiler';
5
+ import { Component, inject, type Signal } from '@angular/core';
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7
+ import { mount, unmount } from '@symbiote-native/angular';
8
+ import { installFabric } from '@symbiote-native/test-utils';
9
+ import { ScreenOrientationService } from './index';
10
+
11
+ type IScreenOrientationState = { orientation: number; orientationLock: number };
12
+ type IOrientationChangeEvent = {
13
+ orientationLock: number;
14
+ orientationInfo: { orientation: number };
15
+ };
16
+
17
+ const addListenerMock = vi.fn();
18
+ const removeMock = vi.fn();
19
+ const getOrientationAsyncMock = vi.fn(async () => 1);
20
+ const getOrientationLockAsyncMock = vi.fn(async () => 0);
21
+
22
+ vi.mock('../../../core', () => ({
23
+ addOrientationChangeListener: (listener: (event: IOrientationChangeEvent) => void) =>
24
+ addListenerMock(listener),
25
+ getOrientationAsync: () => getOrientationAsyncMock(),
26
+ getOrientationLockAsync: () => getOrientationLockAsyncMock(),
27
+ Orientation: { UNKNOWN: 0, PORTRAIT_UP: 1 },
28
+ OrientationLock: { UNKNOWN: 9, DEFAULT: 0 },
29
+ }));
30
+
31
+ const ROOT_TAG = 974;
32
+ const fabric = installFabric();
33
+ const tick = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));
34
+
35
+ let capturedResult: Signal<IScreenOrientationState> | undefined;
36
+ let capturedListener: ((event: IOrientationChangeEvent) => void) | undefined;
37
+
38
+ @Component({
39
+ selector: 'symbiote-screen-orientation-host',
40
+ standalone: true,
41
+ template: '',
42
+ })
43
+ class ScreenOrientationHost {
44
+ readonly screenOrientation = inject(ScreenOrientationService).connect();
45
+
46
+ constructor() {
47
+ capturedResult = this.screenOrientation;
48
+ }
49
+ }
50
+
51
+ beforeEach(() => {
52
+ capturedResult = undefined;
53
+ capturedListener = undefined;
54
+ getOrientationAsyncMock.mockResolvedValue(1);
55
+ getOrientationLockAsyncMock.mockResolvedValue(0);
56
+ addListenerMock.mockImplementation(listener => {
57
+ capturedListener = listener;
58
+ return { remove: removeMock };
59
+ });
60
+ });
61
+
62
+ afterEach(() => {
63
+ unmount(ROOT_TAG);
64
+ fabric.reset();
65
+ vi.clearAllMocks();
66
+ });
67
+
68
+ describe('ScreenOrientationService.connect', () => {
69
+ it('reports Orientation/OrientationLock UNKNOWN before the initial fetch resolves', async () => {
70
+ mount(ROOT_TAG, ScreenOrientationHost);
71
+
72
+ expect(capturedResult?.()).toEqual({ orientation: 0, orientationLock: 9 });
73
+ });
74
+
75
+ it('reports the fetched state once getOrientationAsync()/getOrientationLockAsync() resolve', async () => {
76
+ mount(ROOT_TAG, ScreenOrientationHost);
77
+ await tick();
78
+
79
+ expect(capturedResult?.()).toEqual({ orientation: 1, orientationLock: 0 });
80
+ });
81
+
82
+ it('updates the signal when the registered listener fires', async () => {
83
+ mount(ROOT_TAG, ScreenOrientationHost);
84
+ await tick();
85
+
86
+ if (capturedListener === undefined) throw new Error('addListener callback was not captured');
87
+ capturedListener({ orientationLock: 5, orientationInfo: { orientation: 3 } });
88
+
89
+ expect(capturedResult?.()).toEqual({ orientation: 3, orientationLock: 5 });
90
+ });
91
+
92
+ it('removes the subscription when the host component is unmounted', async () => {
93
+ mount(ROOT_TAG, ScreenOrientationHost);
94
+ await tick();
95
+
96
+ unmount(ROOT_TAG);
97
+ await tick();
98
+
99
+ expect(removeMock).toHaveBeenCalledOnce();
100
+ });
101
+ });
@@ -0,0 +1,25 @@
1
+ export {
2
+ lockAsync,
3
+ lockPlatformAsync,
4
+ unlockAsync,
5
+ getOrientationAsync,
6
+ getOrientationLockAsync,
7
+ getPlatformOrientationLockAsync,
8
+ supportsOrientationLockAsync,
9
+ addOrientationChangeListener,
10
+ removeOrientationChangeListeners,
11
+ removeOrientationChangeListener,
12
+ } from './screen-orientation';
13
+ export {
14
+ Orientation,
15
+ OrientationLock,
16
+ SizeClassIOS,
17
+ WebOrientationLock,
18
+ WebOrientation,
19
+ type PlatformOrientationInfo,
20
+ type ScreenOrientationInfo,
21
+ type OrientationChangeEvent,
22
+ type OrientationChangeListener,
23
+ type ScreenOrientationState,
24
+ } from './types';
25
+ export type { EventSubscription } from 'expo-modules-core';
@@ -0,0 +1,36 @@
1
+ import { requireNativeModule } from 'expo-modules-core';
2
+ import type { EventSubscription } from 'expo-modules-core';
3
+ import type {
4
+ Orientation,
5
+ OrientationChangeEvent,
6
+ OrientationLock,
7
+ WebOrientationLock,
8
+ } from './types';
9
+
10
+ const EXPO_SCREEN_ORIENTATION_MODULE_NAME = 'ExpoScreenOrientation';
11
+
12
+ // The platform-specific lock param/return shape is a genuine per-platform union (Android sends/
13
+ // returns a numeric constant, iOS an Orientation[], web a WebOrientationLock string) — never `any`,
14
+ // so screen-orientation.ts can narrow it with typeof/Array.isArray instead of casting.
15
+ export type IPlatformOrientationParam = number | Orientation[] | WebOrientationLock;
16
+
17
+ // Every method is optional for the same reason every sibling package's native module type is
18
+ // (network, device, battery) — each call site in screen-orientation.ts checks for the method's
19
+ // presence itself and throws an UnavailabilityError, matching upstream's own per-platform
20
+ // capability checks rather than assuming the native module implements the whole surface.
21
+ export type INativeScreenOrientationModule = {
22
+ lockAsync?(orientationLock: OrientationLock): Promise<void>;
23
+ lockPlatformAsync?(platformOrientationParam: IPlatformOrientationParam): Promise<void>;
24
+ getOrientationAsync?(): Promise<Orientation>;
25
+ getOrientationLockAsync?(): Promise<OrientationLock>;
26
+ getPlatformOrientationLockAsync?(): Promise<IPlatformOrientationParam>;
27
+ supportsOrientationLockAsync?(orientationLock: OrientationLock): Promise<boolean>;
28
+ addListener(
29
+ eventName: 'expoDidUpdateDimensions',
30
+ listener: (event: OrientationChangeEvent) => void,
31
+ ): EventSubscription;
32
+ };
33
+
34
+ export const expoScreenOrientation = requireNativeModule<INativeScreenOrientationModule>(
35
+ EXPO_SCREEN_ORIENTATION_MODULE_NAME,
36
+ );
@@ -0,0 +1,307 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const FAKE_NATIVE_SCREEN_ORIENTATION = {
4
+ lockAsync: vi.fn(async () => undefined),
5
+ lockPlatformAsync: vi.fn(async () => undefined),
6
+ getOrientationAsync: vi.fn(async () => 1),
7
+ getOrientationLockAsync: vi.fn(async () => 0),
8
+ getPlatformOrientationLockAsync: vi.fn(async () => 0),
9
+ supportsOrientationLockAsync: vi.fn(async () => true),
10
+ addListener: vi.fn(() => ({ remove: vi.fn() })),
11
+ };
12
+
13
+ // The real ExpoScreenOrientation native module only exists on device — resolving it via
14
+ // requireNativeModule() at import time would throw in this headless test run, so the
15
+ // module-lookup file is faked in place of expo-modules-core's runtime resolution, same pattern
16
+ // packages/network/src/core/network.test.ts uses.
17
+ vi.mock('./native-module', () => ({
18
+ expoScreenOrientation: FAKE_NATIVE_SCREEN_ORIENTATION,
19
+ }));
20
+
21
+ // expo-modules-core's real entry transitively imports 'react-native' for Platform/
22
+ // TurboModuleRegistry, whose Flow-typed source Vitest's Oxc transform can't parse — same fake
23
+ // packages/cellular/src/core/cellular.test.ts uses. Platform.OS is mutable per-test.
24
+ const fakePlatform = { OS: 'ios' as 'ios' | 'android' | 'web' };
25
+
26
+ vi.mock('expo-modules-core', () => ({
27
+ Platform: fakePlatform,
28
+ UnavailabilityError: class UnavailabilityError extends Error {
29
+ constructor(moduleName: string, propertyName: string) {
30
+ super(`${propertyName} is not available on ${moduleName}`);
31
+ }
32
+ },
33
+ }));
34
+
35
+ // Android doesn't emit `expoDidUpdateDimensions` — the module piggybacks on RN's own Dimensions
36
+ // change event there instead (see the function-level comment in screen-orientation.ts).
37
+ vi.mock('react-native', () => ({
38
+ Dimensions: { addEventListener: vi.fn(() => ({ remove: vi.fn() })) },
39
+ }));
40
+
41
+ const {
42
+ lockAsync,
43
+ lockPlatformAsync,
44
+ unlockAsync,
45
+ getOrientationAsync,
46
+ getOrientationLockAsync,
47
+ getPlatformOrientationLockAsync,
48
+ supportsOrientationLockAsync,
49
+ addOrientationChangeListener,
50
+ removeOrientationChangeListeners,
51
+ removeOrientationChangeListener,
52
+ } = await import('./screen-orientation');
53
+ const { Orientation, OrientationLock, WebOrientationLock } = await import('./types');
54
+ const { Dimensions } = await import('react-native');
55
+
56
+ afterEach(() => {
57
+ fakePlatform.OS = 'ios';
58
+ vi.clearAllMocks();
59
+ removeOrientationChangeListeners();
60
+ });
61
+
62
+ describe('lockAsync', () => {
63
+ it('delegates to the native module', async () => {
64
+ await lockAsync(OrientationLock.PORTRAIT_UP);
65
+
66
+ expect(FAKE_NATIVE_SCREEN_ORIENTATION.lockAsync).toHaveBeenCalledWith(
67
+ OrientationLock.PORTRAIT_UP,
68
+ );
69
+ });
70
+
71
+ it('no-ops on OrientationLock.OTHER without calling the native module', async () => {
72
+ await lockAsync(OrientationLock.OTHER);
73
+
74
+ expect(FAKE_NATIVE_SCREEN_ORIENTATION.lockAsync).not.toHaveBeenCalled();
75
+ });
76
+
77
+ it('throws a TypeError on an invalid lock value', async () => {
78
+ // @ts-expect-error -- simulating a caller passing a bogus lock value
79
+ await expect(lockAsync(999)).rejects.toThrow('Invalid Orientation Lock');
80
+ });
81
+
82
+ it('throws UnavailabilityError when the native method is absent', async () => {
83
+ const { lockAsync: native } = FAKE_NATIVE_SCREEN_ORIENTATION;
84
+ // @ts-expect-error -- simulating a platform where the native module has no such method
85
+ FAKE_NATIVE_SCREEN_ORIENTATION.lockAsync = undefined;
86
+
87
+ await expect(lockAsync(OrientationLock.PORTRAIT_UP)).rejects.toThrow(
88
+ 'lockAsync is not available on ScreenOrientation',
89
+ );
90
+
91
+ FAKE_NATIVE_SCREEN_ORIENTATION.lockAsync = native;
92
+ });
93
+ });
94
+
95
+ describe('lockPlatformAsync', () => {
96
+ it('forwards the Android numeric constant on Android', async () => {
97
+ fakePlatform.OS = 'android';
98
+
99
+ await lockPlatformAsync({ screenOrientationConstantAndroid: 1 });
100
+
101
+ expect(FAKE_NATIVE_SCREEN_ORIENTATION.lockPlatformAsync).toHaveBeenCalledWith(1);
102
+ });
103
+
104
+ it('forwards the iOS orientation array on iOS', async () => {
105
+ fakePlatform.OS = 'ios';
106
+
107
+ await lockPlatformAsync({ screenOrientationArrayIOS: [Orientation.PORTRAIT_UP] });
108
+
109
+ expect(FAKE_NATIVE_SCREEN_ORIENTATION.lockPlatformAsync).toHaveBeenCalledWith([
110
+ Orientation.PORTRAIT_UP,
111
+ ]);
112
+ });
113
+
114
+ it('forwards the web orientation lock on web', async () => {
115
+ fakePlatform.OS = 'web';
116
+
117
+ await lockPlatformAsync({ screenOrientationLockWeb: WebOrientationLock.LANDSCAPE });
118
+
119
+ expect(FAKE_NATIVE_SCREEN_ORIENTATION.lockPlatformAsync).toHaveBeenCalledWith(
120
+ WebOrientationLock.LANDSCAPE,
121
+ );
122
+ });
123
+
124
+ it('throws a TypeError on an invalid iOS orientation entry', async () => {
125
+ fakePlatform.OS = 'ios';
126
+
127
+ // @ts-expect-error -- simulating a caller passing a bogus orientation value
128
+ await expect(lockPlatformAsync({ screenOrientationArrayIOS: [999] })).rejects.toThrow(
129
+ 'is not a valid Orientation',
130
+ );
131
+ });
132
+
133
+ it('throws a TypeError when no matching platform option is supplied', async () => {
134
+ fakePlatform.OS = 'ios';
135
+
136
+ await expect(lockPlatformAsync({})).rejects.toThrow(
137
+ 'cannot be called with undefined option properties',
138
+ );
139
+ });
140
+
141
+ it('throws UnavailabilityError when the native method is absent', async () => {
142
+ const { lockPlatformAsync: native } = FAKE_NATIVE_SCREEN_ORIENTATION;
143
+ // @ts-expect-error -- simulating a platform where the native module has no such method
144
+ FAKE_NATIVE_SCREEN_ORIENTATION.lockPlatformAsync = undefined;
145
+
146
+ await expect(lockPlatformAsync({ screenOrientationConstantAndroid: 1 })).rejects.toThrow(
147
+ 'lockPlatformAsync is not available on ScreenOrientation',
148
+ );
149
+
150
+ FAKE_NATIVE_SCREEN_ORIENTATION.lockPlatformAsync = native;
151
+ });
152
+ });
153
+
154
+ describe('unlockAsync', () => {
155
+ it('delegates to lockAsync with OrientationLock.DEFAULT', async () => {
156
+ await unlockAsync();
157
+
158
+ expect(FAKE_NATIVE_SCREEN_ORIENTATION.lockAsync).toHaveBeenCalledWith(OrientationLock.DEFAULT);
159
+ });
160
+ });
161
+
162
+ describe('getOrientationAsync', () => {
163
+ it('delegates to the native module', async () => {
164
+ await expect(getOrientationAsync()).resolves.toBe(1);
165
+ });
166
+
167
+ it('throws UnavailabilityError when the native method is absent', async () => {
168
+ const { getOrientationAsync: native } = FAKE_NATIVE_SCREEN_ORIENTATION;
169
+ // @ts-expect-error -- simulating a platform where the native module has no such method
170
+ FAKE_NATIVE_SCREEN_ORIENTATION.getOrientationAsync = undefined;
171
+
172
+ await expect(getOrientationAsync()).rejects.toThrow();
173
+
174
+ FAKE_NATIVE_SCREEN_ORIENTATION.getOrientationAsync = native;
175
+ });
176
+ });
177
+
178
+ describe('getOrientationLockAsync', () => {
179
+ it('delegates to the native module when present', async () => {
180
+ await expect(getOrientationLockAsync()).resolves.toBe(0);
181
+ });
182
+
183
+ it('falls back to the last lock set via lockAsync when the native method is absent', async () => {
184
+ await lockAsync(OrientationLock.LANDSCAPE);
185
+ const { getOrientationLockAsync: native } = FAKE_NATIVE_SCREEN_ORIENTATION;
186
+ // @ts-expect-error -- simulating a platform where the native module has no such method
187
+ FAKE_NATIVE_SCREEN_ORIENTATION.getOrientationLockAsync = undefined;
188
+
189
+ await expect(getOrientationLockAsync()).resolves.toBe(OrientationLock.LANDSCAPE);
190
+
191
+ FAKE_NATIVE_SCREEN_ORIENTATION.getOrientationLockAsync = native;
192
+ });
193
+ });
194
+
195
+ describe('getPlatformOrientationLockAsync', () => {
196
+ it('wraps a numeric native value as screenOrientationConstantAndroid on Android', async () => {
197
+ fakePlatform.OS = 'android';
198
+ FAKE_NATIVE_SCREEN_ORIENTATION.getPlatformOrientationLockAsync.mockResolvedValueOnce(1);
199
+
200
+ await expect(getPlatformOrientationLockAsync()).resolves.toEqual({
201
+ screenOrientationConstantAndroid: 1,
202
+ });
203
+ });
204
+
205
+ it('wraps an array native value as screenOrientationArrayIOS on iOS', async () => {
206
+ fakePlatform.OS = 'ios';
207
+ FAKE_NATIVE_SCREEN_ORIENTATION.getPlatformOrientationLockAsync.mockResolvedValueOnce([
208
+ Orientation.LANDSCAPE_LEFT,
209
+ ]);
210
+
211
+ await expect(getPlatformOrientationLockAsync()).resolves.toEqual({
212
+ screenOrientationArrayIOS: [Orientation.LANDSCAPE_LEFT],
213
+ });
214
+ });
215
+
216
+ it('wraps a string native value as screenOrientationLockWeb on web', async () => {
217
+ fakePlatform.OS = 'web';
218
+ FAKE_NATIVE_SCREEN_ORIENTATION.getPlatformOrientationLockAsync.mockResolvedValueOnce(
219
+ WebOrientationLock.ANY,
220
+ );
221
+
222
+ await expect(getPlatformOrientationLockAsync()).resolves.toEqual({
223
+ screenOrientationLockWeb: WebOrientationLock.ANY,
224
+ });
225
+ });
226
+ });
227
+
228
+ describe('supportsOrientationLockAsync', () => {
229
+ it('delegates to the native module', async () => {
230
+ await expect(supportsOrientationLockAsync(OrientationLock.PORTRAIT)).resolves.toBe(true);
231
+ });
232
+
233
+ it('throws a TypeError on an invalid lock value', async () => {
234
+ // @ts-expect-error -- simulating a caller passing a bogus lock value
235
+ await expect(supportsOrientationLockAsync(999)).rejects.toThrow('Invalid Orientation Lock');
236
+ });
237
+
238
+ it('throws UnavailabilityError when the native method is absent', async () => {
239
+ const { supportsOrientationLockAsync: native } = FAKE_NATIVE_SCREEN_ORIENTATION;
240
+ // @ts-expect-error -- simulating a platform where the native module has no such method
241
+ FAKE_NATIVE_SCREEN_ORIENTATION.supportsOrientationLockAsync = undefined;
242
+
243
+ await expect(supportsOrientationLockAsync(OrientationLock.PORTRAIT)).rejects.toThrow();
244
+
245
+ FAKE_NATIVE_SCREEN_ORIENTATION.supportsOrientationLockAsync = native;
246
+ });
247
+ });
248
+
249
+ describe('addOrientationChangeListener', () => {
250
+ it('subscribes through the native expoDidUpdateDimensions event on iOS', () => {
251
+ fakePlatform.OS = 'ios';
252
+ const listener = vi.fn();
253
+
254
+ addOrientationChangeListener(listener);
255
+
256
+ expect(FAKE_NATIVE_SCREEN_ORIENTATION.addListener).toHaveBeenCalledWith(
257
+ 'expoDidUpdateDimensions',
258
+ expect.any(Function),
259
+ );
260
+ expect(Dimensions.addEventListener).not.toHaveBeenCalled();
261
+ });
262
+
263
+ it('subscribes through RN Dimensions.addEventListener on Android', () => {
264
+ fakePlatform.OS = 'android';
265
+ const listener = vi.fn();
266
+
267
+ addOrientationChangeListener(listener);
268
+
269
+ expect(Dimensions.addEventListener).toHaveBeenCalledWith('change', expect.any(Function));
270
+ expect(FAKE_NATIVE_SCREEN_ORIENTATION.addListener).not.toHaveBeenCalled();
271
+ });
272
+
273
+ it('throws a TypeError when the listener is not a function', () => {
274
+ // @ts-expect-error -- simulating a caller passing a non-function listener
275
+ expect(() => addOrientationChangeListener('not-a-function')).toThrow();
276
+ });
277
+ });
278
+
279
+ describe('removeOrientationChangeListener / removeOrientationChangeListeners', () => {
280
+ it('removes a single subscription', () => {
281
+ const subscription = addOrientationChangeListener(vi.fn());
282
+ const removeSpy = vi.spyOn(subscription, 'remove');
283
+
284
+ removeOrientationChangeListener(subscription);
285
+
286
+ expect(removeSpy).toHaveBeenCalledTimes(1);
287
+ });
288
+
289
+ it('throws a TypeError when passed an invalid subscription', () => {
290
+ // @ts-expect-error -- simulating a caller passing a bogus subscription
291
+ expect(() => removeOrientationChangeListener(null)).toThrow(
292
+ 'Must pass in a valid subscription',
293
+ );
294
+ });
295
+
296
+ it('removes every registered subscription', () => {
297
+ const first = addOrientationChangeListener(vi.fn());
298
+ const second = addOrientationChangeListener(vi.fn());
299
+ const firstRemove = vi.spyOn(first, 'remove');
300
+ const secondRemove = vi.spyOn(second, 'remove');
301
+
302
+ removeOrientationChangeListeners();
303
+
304
+ expect(firstRemove).toHaveBeenCalledTimes(1);
305
+ expect(secondRemove).toHaveBeenCalledTimes(1);
306
+ });
307
+ });