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