1'use strict';
2
3import * as ScreenOrientation from 'expo-screen-orientation';
4import { Platform } from 'react-native';
5
6export const name = 'ScreenOrientation';
7
8// Wait until we are in desiredOrientation
9// Fail if we are not in a validOrientation
10const applyAsync = ({ desiredOrientationLock, desiredOrientations, validOrientations }) => {
11  return new Promise(async function (resolve, reject) {
12    let subscriptionCancelled = false;
13    const subscription = ScreenOrientation.addOrientationChangeListener(
14      ({ orientationInfo, orientationLock }) => {
15        const { orientation } = orientationInfo;
16        if (validOrientations && !validOrientations.includes(orientation)) {
17          reject(new Error(`Should not have received an orientation of ${orientation}`));
18        }
19        if (desiredOrientations && !desiredOrientations.includes(orientation)) {
20          return;
21        } else if (desiredOrientationLock && orientationLock !== desiredOrientationLock) {
22          return;
23        }
24
25        // We have met all the desired orientation conditions
26        // remove itself
27        if (!subscriptionCancelled) {
28          ScreenOrientation.removeOrientationChangeListener(subscription);
29          subscriptionCancelled = true;
30        }
31
32        // resolve promise
33        resolve();
34      }
35    );
36
37    if (desiredOrientationLock) {
38      // set the screen orientation to desired orientation lock
39      await ScreenOrientation.lockAsync(desiredOrientationLock);
40    }
41
42    const orientation = await ScreenOrientation.getOrientationAsync();
43    const orientationLock = await ScreenOrientation.getOrientationLockAsync();
44    if (desiredOrientations && !desiredOrientations.includes(orientation)) {
45      return;
46    } else if (desiredOrientationLock && orientationLock !== desiredOrientationLock) {
47      return;
48    }
49
50    // We have met all the desired orientation conditions
51    // remove previous subscription
52    if (!subscriptionCancelled) {
53      ScreenOrientation.removeOrientationChangeListener(subscription);
54      subscriptionCancelled = true;
55    }
56    resolve();
57  });
58};
59
60export function test(t) {
61  t.describe('Screen Orientation', () => {
62    t.describe('Screen Orientation locking, getters, setters, listeners, etc', () => {
63      t.beforeEach(async () => {
64        // Put the screen back to PORTRAIT_UP
65        const desiredOrientation = ScreenOrientation.Orientation.PORTRAIT_UP;
66
67        await applyAsync({
68          desiredOrientationLock: ScreenOrientation.OrientationLock.PORTRAIT_UP,
69          desiredOrientations: [desiredOrientation],
70        });
71      });
72
73      t.afterEach(() => {
74        ScreenOrientation.removeOrientationChangeListeners();
75      });
76
77      t.afterAll(async () => {
78        await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.DEFAULT);
79      });
80
81      t.it(
82        'Sets screen to landscape orientation and gets the correct orientationLock',
83        async () => {
84          // set the screen orientation to LANDSCAPE LEFT lock
85          await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE_LEFT);
86
87          // detect the correct orientationLock policy immediately
88          const orientationLock = await ScreenOrientation.getOrientationLockAsync();
89          t.expect(orientationLock).toBe(ScreenOrientation.OrientationLock.LANDSCAPE_LEFT);
90        }
91      );
92
93      t.it('Sets screen to landscape orientation and gets the correct orientation', async () => {
94        const desiredOrientationLock = ScreenOrientation.OrientationLock.LANDSCAPE_LEFT;
95        const desiredOrientation = ScreenOrientation.Orientation.LANDSCAPE_LEFT;
96        const validOrientations = [
97          ScreenOrientation.Orientation.LANDSCAPE_LEFT,
98          ScreenOrientation.Orientation.PORTRAIT_UP,
99        ];
100        await applyAsync({
101          desiredOrientationLock,
102          desiredOrientations: [desiredOrientation],
103          validOrientations,
104        });
105
106        const orientation = await ScreenOrientation.getOrientationAsync();
107        t.expect(orientation).toBe(ScreenOrientation.Orientation.LANDSCAPE_LEFT);
108      });
109
110      // We rely on RN to emit `didUpdateDimensions`
111      // If this method no longer works, it's possible that the underlying RN implementation has changed
112      // see https://github.com/facebook/react-native/blob/c31f79fe478b882540d7fd31ee37b53ddbd60a17/ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/DeviceInfoModule.java#L90
113      t.it(
114        'Register for the callback, set to landscape orientation and get the correct orientation',
115        async () => {
116          const callListenerAsync = new Promise(async function (resolve, reject) {
117            // Register for screen orientation changes
118            ScreenOrientation.addOrientationChangeListener(({ orientationInfo }) => {
119              const { orientation } = orientationInfo;
120              if (orientation === ScreenOrientation.Orientation.PORTRAIT_UP) {
121                // orientation update has not happened yet
122              } else if (orientation === ScreenOrientation.Orientation.LANDSCAPE_LEFT) {
123                resolve();
124              } else {
125                reject(new Error(`Should not be in orientation: ${orientation}`));
126              }
127            });
128
129            // Put the screen to LANDSCAPE_LEFT
130            await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE_LEFT);
131          });
132
133          // Wait for listener to get called
134          await callListenerAsync;
135        }
136      );
137
138      t.it('Unlock the screen orientation back to default', async () => {
139        // Put the screen to LANDSCAPE_LEFT
140        await applyAsync({
141          desiredOrientationLock: ScreenOrientation.OrientationLock.LANDSCAPE_LEFT,
142          desiredOrientations: [ScreenOrientation.Orientation.LANDSCAPE_LEFT],
143        });
144
145        // Unlock the screen orientation
146        await ScreenOrientation.unlockAsync();
147
148        // detect the correct orientationLock policy immediately
149        const orientationLock = await ScreenOrientation.getOrientationLockAsync();
150        t.expect(orientationLock).toBe(ScreenOrientation.OrientationLock.DEFAULT);
151      });
152
153      // This test only applies to android devices
154      if (Platform.OS === 'android') {
155        t.it('Apply a native android lock', async () => {
156          // Apply the native USER_LANDSCAPE android lock (11)
157          // https://developer.android.com/reference/android/R.attr#screenOrientation
158          await ScreenOrientation.lockPlatformAsync({ screenOrientationConstantAndroid: 11 });
159
160          // detect the correct orientationLock policy immediately
161          const orientationLock = await ScreenOrientation.getOrientationLockAsync();
162          t.expect(orientationLock).toBe(ScreenOrientation.OrientationLock.OTHER);
163
164          // expect the native platform getter to return correctly
165          const platformInfo = await ScreenOrientation.getPlatformOrientationLockAsync();
166          const { screenOrientationConstantAndroid } = platformInfo;
167          t.expect(screenOrientationConstantAndroid).toBe(11);
168
169          const desiredOrientations = [
170            ScreenOrientation.Orientation.LANDSCAPE_RIGHT,
171            ScreenOrientation.Orientation.LANDSCAPE_LEFT,
172          ];
173          const validOrientations = [
174            ScreenOrientation.Orientation.LANDSCAPE_RIGHT,
175            ScreenOrientation.Orientation.LANDSCAPE_LEFT,
176            ScreenOrientation.Orientation.PORTRAIT_UP,
177          ];
178          await applyAsync({ desiredOrientations, validOrientations });
179        });
180      }
181
182      // This test only applies to ios devices
183      if (Platform.OS === 'ios') {
184        t.it('Apply a native iOS lock', async () => {
185          // Allow only PORTRAIT_UP and LANDSCAPE_RIGHT
186          await ScreenOrientation.lockPlatformAsync({
187            screenOrientationArrayIOS: [
188              ScreenOrientation.Orientation.PORTRAIT_UP,
189              ScreenOrientation.Orientation.LANDSCAPE_RIGHT,
190            ],
191          });
192
193          // detect the correct orientationLock policy immediately
194          const orientationLock = await ScreenOrientation.getOrientationLockAsync();
195          t.expect(orientationLock).toBe(ScreenOrientation.OrientationLock.OTHER);
196
197          // expect the native platform getter to return correctly
198          const platformInfo = await ScreenOrientation.getPlatformOrientationLockAsync();
199          const { screenOrientationArrayIOS } = platformInfo;
200          t.expect(screenOrientationArrayIOS).toContain(
201            ScreenOrientation.Orientation.LANDSCAPE_RIGHT
202          );
203          t.expect(screenOrientationArrayIOS).toContain(ScreenOrientation.Orientation.PORTRAIT_UP);
204
205          const desiredOrientations = [
206            ScreenOrientation.Orientation.PORTRAIT_UP,
207            ScreenOrientation.Orientation.LANDSCAPE_RIGHT,
208          ];
209          const validOrientations = [
210            ScreenOrientation.Orientation.PORTRAIT_UP,
211            ScreenOrientation.Orientation.LANDSCAPE_RIGHT,
212          ];
213          await applyAsync({ desiredOrientations, validOrientations });
214        });
215      }
216
217      t.it('Remove all listeners and expect them never to be called', async () => {
218        // Register for screen orientation changes
219        let listenerWasCalled = false;
220        ScreenOrientation.addOrientationChangeListener(() => {
221          listenerWasCalled = true;
222        });
223
224        ScreenOrientation.addOrientationChangeListener(() => {
225          listenerWasCalled = true;
226        });
227
228        ScreenOrientation.removeOrientationChangeListeners();
229
230        // set the screen orientation to LANDSCAPE LEFT lock
231        await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE_LEFT);
232
233        // If we set a different lock and wait for it to be applied without ever having the
234        // listeners invoked, we assume they've been successfully removed
235        const desiredOrientations = [ScreenOrientation.Orientation.LANDSCAPE_LEFT];
236        const validOrientations = [
237          ScreenOrientation.Orientation.LANDSCAPE_LEFT,
238          ScreenOrientation.Orientation.PORTRAIT_UP,
239        ];
240        await applyAsync({ desiredOrientations, validOrientations });
241
242        // expect listeners to not have been called
243        t.expect(listenerWasCalled).toBe(false);
244      });
245
246      /*
247      This test fails about half the time on CI with the error that it expected false to be true.
248      This means that the check that subscription2Called is true fails.
249      It may be a problem with the removeOrientationChangeListener implementation since
250      this is the only test that calls that function on an external subscription while another is active.
251
252      t.it('Register some listeners and remove a subset', async () => {
253        // Register for screen orientation changes
254        let subscription1Called = false;
255        let subscription2Called = false;
256
257        const subscription1 = ScreenOrientation.addOrientationChangeListener(() => {
258          subscription1Called = true;
259        });
260
261        ScreenOrientation.addOrientationChangeListener(() => {
262          subscription2Called = true;
263        });
264
265        // remove subscription1 ONLY
266        ScreenOrientation.removeOrientationChangeListener(subscription1);
267
268        // set the screen orientation to LANDSCAPE LEFT lock
269        await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE_LEFT);
270
271        // If we set a different lock and wait for it to be applied without ever having the
272        // listeners invoked, we assume they've been successfully removed
273        const desiredOrientations = [ScreenOrientation.Orientation.LANDSCAPE_LEFT];
274        const validOrientations = [
275          ScreenOrientation.Orientation.LANDSCAPE_LEFT,
276          ScreenOrientation.Orientation.PORTRAIT_UP,
277        ];
278        await applyAsync({ desiredOrientations, validOrientations });
279
280        // expect subscription1 to NOT have been called
281        t.expect(subscription1Called).toBe(false);
282
283        // expect subscription2 to have been called
284        t.expect(subscription2Called).toBe(true);
285      });
286      */
287
288      t.it('supports accepted orientation locks', async () => {
289        // orientation locks that we should be able to apply
290        const acceptedLocks = [
291          ScreenOrientation.OrientationLock.DEFAULT,
292          ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT,
293        ];
294
295        for (const lock of acceptedLocks) {
296          const supported = await ScreenOrientation.supportsOrientationLockAsync(lock);
297          t.expect(supported).toBe(true);
298        }
299      });
300
301      t.it("doesn't support unsupported orientation locks", async () => {
302        // This is not a lock policy that we can apply
303        const unsupportedLock = ScreenOrientation.OrientationLock.OTHER;
304        const supported = await ScreenOrientation.supportsOrientationLockAsync(unsupportedLock);
305        t.expect(supported).toBe(false);
306      });
307
308      t.it('throws an error when asked for non-lock values', async () => {
309        // Expect non-lock values to throw an error
310        const notLocks = ['FOO', 99, -1];
311        for (const notLock of notLocks) {
312          let hasError = false;
313          try {
314            await ScreenOrientation.supportsOrientationLockAsync(notLock);
315          } catch {
316            hasError = true;
317          }
318          t.expect(hasError).toBe(true);
319        }
320      });
321    });
322  });
323}
324