diff --git a/detox/src/devices/Device.js b/detox/src/devices/Device.js index 825afff155..fb44341103 100644 --- a/detox/src/devices/Device.js +++ b/detox/src/devices/Device.js @@ -83,7 +83,7 @@ class Device { } } - const processId = await this.deviceDriver.launchApp(this._deviceId, _bundleId, this._prepareLaunchArgs(baseLaunchArgs)); + const processId = await this.deviceDriver.launchApp(this._deviceId, _bundleId, this._prepareLaunchArgs(baseLaunchArgs), params.languageAndLocale); this._processes[_bundleId] = processId; await this.deviceDriver.waitUntilReady(); diff --git a/detox/src/devices/Device.test.js b/detox/src/devices/Device.test.js index f5b754cedd..aeb116a4e1 100644 --- a/detox/src/devices/Device.test.js +++ b/detox/src/devices/Device.test.js @@ -96,7 +96,22 @@ describe('Device', () => { expect(device.deviceDriver.launchApp).toHaveBeenCalledWith(device._deviceId, device._bundleId, - {"-detoxServer": "ws://localhost:8099", "-detoxSessionId": "test"}); + {"-detoxServer": "ws://localhost:8099", "-detoxSessionId": "test"}, undefined); + }); + + it('launchApp({languageAndLocale}) should launch app with a specific language/locale', async () => { + device = validDevice(); + + const languageAndLocale = { + language: 'es-MX', + locale: 'es-MX' + }; + + await device.launchApp({languageAndLocale}); + + expect(device.deviceDriver.launchApp).toHaveBeenCalledWith(device._deviceId, + device._bundleId, + {"-detoxServer": "ws://localhost:8099", "-detoxSessionId": "test"}, languageAndLocale); }); it(`relaunchApp()`, async () => { @@ -107,7 +122,7 @@ describe('Device', () => { expect(device.deviceDriver.terminate).toHaveBeenCalled(); expect(device.deviceDriver.launchApp).toHaveBeenCalledWith(device._deviceId, device._bundleId, - {"-detoxServer": "ws://localhost:8099", "-detoxSessionId": "test"}); + {"-detoxServer": "ws://localhost:8099", "-detoxSessionId": "test"}, undefined); }); it(`relaunchApp({newInstance: false}) should not terminate the app before launch`, async () => { @@ -144,7 +159,7 @@ describe('Device', () => { expect(device.deviceDriver.installApp).toHaveBeenCalled(); expect(device.deviceDriver.launchApp).toHaveBeenCalledWith(device._deviceId, device._bundleId, - {"-detoxServer": "ws://localhost:8099", "-detoxSessionId": "test"}); + {"-detoxServer": "ws://localhost:8099", "-detoxSessionId": "test"}, undefined); }); it(`relaunchApp() without delete when reuse is enabled should not uninstall and install`, async () => { @@ -158,7 +173,7 @@ describe('Device', () => { expect(device.deviceDriver.installApp).not.toHaveBeenCalled(); expect(device.deviceDriver.launchApp).toHaveBeenCalledWith(device._deviceId, device._bundleId, - {"-detoxServer": "ws://localhost:8099", "-detoxSessionId": "test"}); + {"-detoxServer": "ws://localhost:8099", "-detoxSessionId": "test"}, undefined); }); it(`relaunchApp() with url should send the url as a param in launchParams`, async () => { @@ -167,7 +182,7 @@ describe('Device', () => { expect(device.deviceDriver.launchApp).toHaveBeenCalledWith(device._deviceId, device._bundleId, - {"-detoxServer": "ws://localhost:8099", "-detoxSessionId": "test", "-detoxURLOverride": "scheme://some.url"}); + {"-detoxServer": "ws://localhost:8099", "-detoxSessionId": "test", "-detoxURLOverride": "scheme://some.url"}, undefined); }); it(`relaunchApp() with url should send the url as a param in launchParams`, async () => { @@ -179,7 +194,7 @@ describe('Device', () => { { "-detoxServer": "ws://localhost:8099", "-detoxSessionId": "test", "-detoxURLOverride": "scheme://some.url", "-detoxSourceAppOverride": "sourceAppBundleId" - }); + }, undefined); }); it(`launchApp() with disableTouchIndicators should send a boolean switch as a param in launchParams`, async () => { @@ -188,7 +203,7 @@ describe('Device', () => { expect(device.deviceDriver.launchApp).toHaveBeenCalledWith(device._deviceId, device._bundleId, - {"-detoxServer": "ws://localhost:8099", "-detoxSessionId": "test", "-detoxDisableTouchIndicators": true}); + {"-detoxServer": "ws://localhost:8099", "-detoxSessionId": "test", "-detoxDisableTouchIndicators": true}, undefined); }); it(`relaunchApp() with userNofitication should send the userNotification as a param in launchParams`, async () => { @@ -200,7 +215,7 @@ describe('Device', () => { expect(device.deviceDriver.launchApp).toHaveBeenCalledWith(device._deviceId, device._bundleId, - {"-detoxServer": "ws://localhost:8099", "-detoxSessionId": "test", "-detoxUserNotificationDataURL": "url"}); + {"-detoxServer": "ws://localhost:8099", "-detoxSessionId": "test", "-detoxUserNotificationDataURL": "url"}, undefined); }); it(`relaunchApp() with url and userNofitication should throw`, async () => { @@ -228,7 +243,7 @@ describe('Device', () => { expect(device.deviceDriver.launchApp).toHaveBeenCalledWith(device._deviceId, device._bundleId, - {"-detoxServer": "ws://localhost:8099", "-detoxSessionId": "test", "-arg1": "1", "-arg2": 2}); + {"-detoxServer": "ws://localhost:8099", "-detoxSessionId": "test", "-arg1": "1", "-arg2": 2}, undefined); }); it(`sendToHome() should pass to device driver`, async () => { diff --git a/detox/src/devices/drivers/AndroidDriver.js b/detox/src/devices/drivers/AndroidDriver.js index 32af22b7d7..29fae59b7e 100644 --- a/detox/src/devices/drivers/AndroidDriver.js +++ b/detox/src/devices/drivers/AndroidDriver.js @@ -83,7 +83,7 @@ class AndroidDriver extends DeviceDriverBase { } } - async launchApp(deviceId, bundleId, launchArgs) { + async launchApp(deviceId, bundleId, launchArgs, languageAndLocale) { await this.emitter.emit('beforeLaunchApp', { deviceId, bundleId, launchArgs }); if (!this.instrumentationProcess) { diff --git a/detox/src/devices/drivers/SimulatorDriver.js b/detox/src/devices/drivers/SimulatorDriver.js index 7960655222..dbb7d7862c 100644 --- a/detox/src/devices/drivers/SimulatorDriver.js +++ b/detox/src/devices/drivers/SimulatorDriver.js @@ -84,9 +84,9 @@ class SimulatorDriver extends IosDriver { await this._applesimutils.uninstall(deviceId, bundleId); } - async launchApp(deviceId, bundleId, launchArgs) { + async launchApp(deviceId, bundleId, launchArgs, languageAndLocale) { await this.emitter.emit('beforeLaunchApp', {bundleId, deviceId, launchArgs}); - const pid = await this._applesimutils.launch(deviceId, bundleId, launchArgs); + const pid = await this._applesimutils.launch(deviceId, bundleId, launchArgs, languageAndLocale); await this.emitter.emit('launchApp', {bundleId, deviceId, launchArgs, pid}); return pid; diff --git a/detox/src/devices/ios/AppleSimUtils.js b/detox/src/devices/ios/AppleSimUtils.js index db62c95915..810c6f91f6 100644 --- a/detox/src/devices/ios/AppleSimUtils.js +++ b/detox/src/devices/ios/AppleSimUtils.js @@ -121,12 +121,12 @@ class AppleSimUtils { } } - async launch(udid, bundleId, launchArgs) { + async launch(udid, bundleId, launchArgs, languageAndLocale) { const frameworkPath = await environment.getFrameworkPath(); const logsInfo = new LogsInfo(udid); const args = this._joinLaunchArgs(launchArgs); - const result = await this._launchMagically(frameworkPath, logsInfo, udid, bundleId, args); + const result = await this._launchMagically(frameworkPath, logsInfo, udid, bundleId, args, languageAndLocale); return this._parseLaunchId(result); } @@ -250,17 +250,25 @@ class AppleSimUtils { return _.map(launchArgs, (v, k) => `${k} ${v}`).join(' ').trim(); } - async _launchMagically(frameworkPath, logsInfo, udid, bundleId, args) { + async _launchMagically(frameworkPath, logsInfo, udid, bundleId, args, languageAndLocale) { const statusLogs = { trying: `Launching ${bundleId}...`, successful: `${bundleId} launched. The stdout and stderr logs were recreated, you can watch them with:\n` + ` tail -F ${logsInfo.absJoined}` }; - const launchBin = `/bin/cat /dev/null >${logsInfo.absStdout} 2>${logsInfo.absStderr} && ` + + let launchBin = `/bin/cat /dev/null >${logsInfo.absStdout} 2>${logsInfo.absStderr} && ` + `SIMCTL_CHILD_DYLD_INSERT_LIBRARIES="${frameworkPath}/Detox" ` + `/usr/bin/xcrun simctl launch --stdout=${logsInfo.simStdout} --stderr=${logsInfo.simStderr} ` + - `${udid} ${bundleId} --args ${args}`; + `${udid} ${bundleId} --args ${args}`;; + + if (!!languageAndLocale && !!languageAndLocale.language) { + launchBin += ` -AppleLanguages "(${languageAndLocale.language})"`; + } + + if (!!languageAndLocale && !!languageAndLocale.locale) { + launchBin += ` -AppleLocale ${languageAndLocale.locale}`; + } return await exec.execWithRetriesAndLogs(launchBin, undefined, statusLogs, 1); } diff --git a/detox/src/devices/ios/AppleSimUtils.test.js b/detox/src/devices/ios/AppleSimUtils.test.js index d5cb389782..6f436125a3 100644 --- a/detox/src/devices/ios/AppleSimUtils.test.js +++ b/detox/src/devices/ios/AppleSimUtils.test.js @@ -317,6 +317,15 @@ describe('AppleSimUtils', () => { expect(exec.execWithRetriesAndLogs.mock.calls).toMatchSnapshot(); }); + it('appends language and locale flags', async () => { + const languageAndLocale = { + language: "es-MS", + locale: "en-US" + }; + await uut.launch('udid', 'theBundleId', undefined, languageAndLocale); + expect(exec.execWithRetriesAndLogs.mock.calls).toMatchSnapshot(); + }); + it('concats args', async () => { await uut.launch('udid', 'theBundleId', { 'foo': 'bar', 'bob': 'yourUncle' }); expect(exec.execWithRetriesAndLogs.mock.calls).toMatchSnapshot(); @@ -344,6 +353,7 @@ describe('AppleSimUtils', () => { const result = await uut.launch('udid', 'theBundleId'); expect(result).toEqual(12345); }); + }); describe('sendToHome', () => { diff --git a/detox/src/devices/ios/__snapshots__/AppleSimUtils.test.js.snap b/detox/src/devices/ios/__snapshots__/AppleSimUtils.test.js.snap index e38bf52977..3291f4da7e 100644 --- a/detox/src/devices/ios/__snapshots__/AppleSimUtils.test.js.snap +++ b/detox/src/devices/ios/__snapshots__/AppleSimUtils.test.js.snap @@ -132,6 +132,21 @@ Array [ ] `; +exports[`AppleSimUtils launch appends language and locale flags 1`] = ` +Array [ + Array [ + "/bin/cat /dev/null >/Users/detox/Library/Developer/CoreSimulator/Devices/udid/data/tmp/detox.last_launch_app_log.out 2>/Users/detox/Library/Developer/CoreSimulator/Devices/udid/data/tmp/detox.last_launch_app_log.err && SIMCTL_CHILD_DYLD_INSERT_LIBRARIES=\\"undefined/Detox\\" /usr/bin/xcrun simctl launch --stdout=/tmp/detox.last_launch_app_log.out --stderr=/tmp/detox.last_launch_app_log.err udid theBundleId --args -AppleLanguages \\"(es-MS)\\" -AppleLocale en-US", + undefined, + Object { + "successful": "theBundleId launched. The stdout and stderr logs were recreated, you can watch them with: + tail -F /Users/detox/Library/Developer/CoreSimulator/Devices/udid/data/tmp/detox.last_launch_app_log.{out,err}", + "trying": "Launching theBundleId...", + }, + 1, + ], +] +`; + exports[`AppleSimUtils launch asks environment for frameworkPath 1`] = ` Array [ Array [ diff --git a/detox/test/e2e/06.device.test.js b/detox/test/e2e/06.device.test.js index ed460c7f87..4d189049ee 100644 --- a/detox/test/e2e/06.device.test.js +++ b/detox/test/e2e/06.device.test.js @@ -39,6 +39,29 @@ describe('Device', () => { await expect(element(by.text('Hello!!!'))).toBeVisible(); }); + // // Passing on iOS, not implemented on Android + // it('launchApp in a different language', async () => { + // let languageAndLocale = { + // language: "es-MX", + // locale: "es-MX" + // }; + + // await device.launchApp({newInstance: true, languageAndLocale}); + // await element(by.text('Language')).tap(); + // await expect(element(by.text(`Current locale: ${languageAndLocale.locale}`))).toBeVisible(); + // await expect(element(by.text(`Current language: ${languageAndLocale.language}`))).toBeVisible(); + + // languageAndLocale = { + // language: "en-US", + // locale: "en-US" + // }; + + // await device.launchApp({newInstance: true, languageAndLocale}); + // await element(by.text('Language')).tap(); + // await expect(element(by.text(`Current locale: ${languageAndLocale.locale}`))).toBeVisible(); + // await expect(element(by.text(`Current language: ${languageAndLocale.language}`))).toBeVisible(); + // }); + it('resetContentAndSettings() + install() + relaunch() - should tap successfully', async () => { await device.resetContentAndSettings(); await device.installApp(); diff --git a/detox/test/src/Screens/LanguageScreen.js b/detox/test/src/Screens/LanguageScreen.js new file mode 100644 index 0000000000..88e6256d2c --- /dev/null +++ b/detox/test/src/Screens/LanguageScreen.js @@ -0,0 +1,27 @@ +import React, { Component } from 'react'; +import { Text, View, NativeModules, Platform } from 'react-native'; +import _ from 'lodash'; + +export default class LanguageScreen extends Component { + render() { + + const locale = Platform.select({ + ios: () => NativeModules.SettingsManager.settings.AppleLocale, + android: () => NativeModules.I18nManager.localeIdentifier + })(); + + const language = Platform.select({ + ios: () => _.take(NativeModules.SettingsManager.settings.AppleLanguages, 1), + android: () => 'Unavailable' + })(); + + return ( + + Current locale: {locale} + + Current language: {language} + + + ); + } +} diff --git a/detox/test/src/Screens/index.js b/detox/test/src/Screens/index.js index 8548420b1c..01906d2300 100644 --- a/detox/test/src/Screens/index.js +++ b/detox/test/src/Screens/index.js @@ -12,7 +12,8 @@ import NetworkScreen from './NetworkScreen'; import AnimationsScreen from './AnimationsScreen'; import LocationScreen from './LocationScreen'; import ShakeScreen from './ShakeScreen'; -import DatePickerScreen from './DatePickerScreen' +import DatePickerScreen from './DatePickerScreen'; +import LanguageScreen from './LanguageScreen'; export { SanityScreen, @@ -29,5 +30,6 @@ export { AnimationsScreen, LocationScreen, ShakeScreen, - DatePickerScreen + DatePickerScreen, + LanguageScreen }; diff --git a/detox/test/src/app.js b/detox/test/src/app.js index cdb9201845..2e43897d19 100644 --- a/detox/test/src/app.js +++ b/detox/test/src/app.js @@ -78,6 +78,7 @@ class example extends Component { Choose a test + {this.renderScreenButton('Language', Screens.LanguageScreen)} {this.renderScreenButton('Sanity', Screens.SanityScreen)} {this.renderScreenButton('Matchers', Screens.MatchersScreen)} {this.renderScreenButton('Actions', Screens.ActionsScreen)} diff --git a/docs/APIRef.DeviceObjectAPI.md b/docs/APIRef.DeviceObjectAPI.md index 6396bae06f..509393bdfe 100644 --- a/docs/APIRef.DeviceObjectAPI.md +++ b/docs/APIRef.DeviceObjectAPI.md @@ -135,6 +135,48 @@ Disable touch indicators on iOS. await device.launchApp({disableTouchIndicators: true}); ``` +##### 9. Launch with a specific language (iOS only) +Launch the app with a specific system language + +Information about accepted values can be found [here](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPInternational/LanguageandLocaleIDs/LanguageandLocaleIDs.html). + +```js +await device.launchApp({ + languageAndLocale: { + language: "es-MX", + locale: "es-MX" + } +}); +``` + +With this API, you can run sets of e2e tests per language. For example: +```js +['es-MX', 'fr-FR', 'pt-BR'].forEach(locale => { + describe(`Test suite in ${locale}`, () => { + + beforeAll(async () => { + await device.launchApp({ + newInstance: true, + languageAndLocale: { + language: locale, + locale + } + }); + }); + + + it('Test A', () => { + + }) + + it('Test B', () => { + + }) + + }); +}); +``` + ### `device.relaunchApp(params)` **Deprecated** Use `device.launchApp(params)` instead. This method is now calling `launchApp({newInstance: true})` for backwards compatibility, it will be removed in Detox 6.X.X.
Kill and relaunch the app defined in the current [`configuration`](APIRef.Configuration.md).