From b8beb37f50b0bfec6949c843b71e18466ad6a1dc Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Wed, 11 May 2022 19:41:06 +0300 Subject: [PATCH] feat(config): drop support for the all-in-one configuration format BREAKING: please migrate to the new { apps, devices, configurations } schema that Detox has been already using for more than a year. --- .editorconfig | 4 + detox/index.d.ts | 125 +++--- detox/local-cli/test.test.js | 40 +- detox/src/Detox.test.js | 11 +- .../configuration/oldschema/package.json | 15 + .../configuration/packagejson/package.json | 7 +- .../configuration/priority/.detoxrc.js | 3 + .../configuration/priority/detox-config.json | 5 +- .../configuration/priority/package.json | 19 +- detox/src/configuration/composeAppsConfig.js | 73 +--- .../configuration/composeAppsConfig.test.js | 150 ++------ .../configuration/composeArtifactsConfig.js | 2 +- .../configuration/composeBehaviorConfig.js | 2 +- .../src/configuration/composeDeviceConfig.js | 33 +- .../configuration/composeDeviceConfig.test.js | 84 ++--- .../src/configuration/composeRunnerConfig.js | 2 +- .../src/configuration/composeSessionConfig.js | 2 +- detox/src/configuration/index.js | 6 +- detox/src/configuration/index.test.js | 23 +- detox/src/errors/DetoxConfigErrorComposer.js | 89 +++-- .../errors/DetoxConfigErrorComposer.test.js | 111 +++--- .../DetoxConfigErrorComposer.test.js.snap | 356 ++++++++++++------ detox/test/e2e/detox.config.js | 6 +- docs/Guide.ThirdPartyDrivers.md | 9 +- 24 files changed, 606 insertions(+), 571 deletions(-) create mode 100644 detox/src/configuration/__mocks__/configuration/oldschema/package.json diff --git a/.editorconfig b/.editorconfig index 030620a096..42a36eb6dc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,6 +14,10 @@ trim_trailing_whitespace = true indent_size = 2 tab_width = 2 +[detox/index.d.ts] +indent_size = 4 +tab_width = 4 + [{*.cjs,*.js}] indent_size = 2 tab_width = 2 diff --git a/detox/index.d.ts b/detox/index.d.ts index cca4290175..2c825ed409 100644 --- a/detox/index.d.ts +++ b/detox/index.d.ts @@ -30,7 +30,7 @@ declare global { namespace Detox { // region DetoxConfig - interface DetoxConfig { + interface DetoxConfig extends DetoxConfigurationCommon { /** * @example extends: './relative/detox.config' * @example extends: '@my-org/detox-preset' @@ -51,15 +51,18 @@ declare global { * @example specs: 'detoxE2E' */ specs?: string; - artifacts?: DetoxArtifactsConfig; - behavior?: DetoxBehaviorConfig; - session?: DetoxSessionConfig; apps?: Record; devices?: Record; selectedConfiguration?: string; configurations: Record; } + type DetoxConfigurationCommon = { + artifacts?: false | DetoxArtifactsConfig; + behavior?: DetoxBehaviorConfig; + session?: DetoxSessionConfig; + }; + interface DetoxArtifactsConfig { rootDir?: string; pathBuilder?: string; @@ -107,7 +110,7 @@ declare global { sessionId?: string; } - type DetoxAppConfig = (DetoxIosAppConfig | DetoxAndroidAppConfig) & { + type DetoxAppConfig = (DetoxBuiltInAppConfig | DetoxCustomAppConfig) & { /** * App name to use with device.selectApp(appName) calls. * Can be omitted if you have a single app under the test. @@ -119,8 +122,6 @@ declare global { type DetoxDeviceConfig = DetoxBuiltInDeviceConfig | DetoxCustomDriverConfig; - type DetoxConfiguration = DetoxPlainConfiguration | DetoxAliasedConfiguration; - interface DetoxLogArtifactsPluginConfig { enabled?: boolean; keepOnlyFailedTestsArtifacts?: boolean; @@ -164,6 +165,8 @@ declare global { enabled?: boolean; } + type DetoxBuiltInAppConfig = (DetoxIosAppConfig | DetoxAndroidAppConfig); + interface DetoxIosAppConfig { type: 'ios.app'; binaryPath: string; @@ -181,27 +184,17 @@ declare global { launchArgs?: Record; } - interface _DetoxAppConfigFragment { - binaryPath: string; - bundleId?: string; - build?: string; - testBinaryPath?: string; - launchArgs?: Record; + interface DetoxCustomAppConfig { + type: string; + + [prop: string]: unknown; } type DetoxBuiltInDeviceConfig = - | DetoxIosSimulatorDriverConfig - | DetoxAttachedAndroidDriverConfig - | DetoxAndroidEmulatorDriverConfig - | DetoxGenymotionCloudDriverConfig; - - type DetoxPlainConfiguration = DetoxConfigurationOverrides & ( - | (DetoxIosSimulatorDriverConfig & _DetoxAppConfigFragment) - | (DetoxAttachedAndroidDriverConfig & _DetoxAppConfigFragment) - | (DetoxAndroidEmulatorDriverConfig & _DetoxAppConfigFragment) - | (DetoxGenymotionCloudDriverConfig & _DetoxAppConfigFragment) - | (DetoxCustomDriverConfig) - ); + | DetoxIosSimulatorDriverConfig + | DetoxAttachedAndroidDriverConfig + | DetoxAndroidEmulatorDriverConfig + | DetoxGenymotionCloudDriverConfig; interface DetoxIosSimulatorDriverConfig { type: 'ios.simulator'; @@ -251,30 +244,25 @@ declare global { type DetoxKnownDeviceType = DetoxBuiltInDeviceConfig['type']; - type DetoxConfigurationOverrides = { - artifacts?: false | DetoxArtifactsConfig; - behavior?: DetoxBehaviorConfig; - session?: DetoxSessionConfig; - }; - - type DetoxAliasedConfiguration = - | DetoxAliasedConfigurationSingleApp - | DetoxAliasedConfigurationMultiApps; + type DetoxConfiguration = DetoxConfigurationCommon & ( + | DetoxConfigurationSingleApp + | DetoxConfigurationMultiApps + ); - interface DetoxAliasedConfigurationSingleApp { - type?: never; + interface DetoxConfigurationSingleApp { device: DetoxAliasedDevice; - app: string | DetoxAppConfig; + app: DetoxAliasedApp; } - interface DetoxAliasedConfigurationMultiApps { - type?: never; + interface DetoxConfigurationMultiApps { device: DetoxAliasedDevice; - apps: string[]; + apps: DetoxAliasedApp[]; } type DetoxAliasedDevice = string | DetoxDeviceConfig; + type DetoxAliasedApp = string | DetoxAppConfig; + // endregion DetoxConfig // Detox exports all methods from detox global and all of the global constants. @@ -458,13 +446,13 @@ declare global { */ launchApp(config?: DeviceLaunchAppConfig): Promise; - /** - * Relaunch the app. Convenience method that calls {@link Device#launchApp} - * with { newInstance: true } override. - * - * @param config - * @see Device#launchApp - */ + /** + * Relaunch the app. Convenience method that calls {@link Device#launchApp} + * with { newInstance: true } override. + * + * @param config + * @see Device#launchApp + */ relaunchApp(config?: Omit): Promise; /** @@ -492,6 +480,7 @@ declare global { * @see AppLaunchArgs */ appLaunchArgs: AppLaunchArgs; + /** * Terminate the app. * @@ -856,11 +845,13 @@ declare global { * @example await element(by.text('Product').and(by.id('product_name')); */ and(by: NativeMatcher): NativeMatcher; + /** * Find an element by a matcher with a parent matcher * @example await element(by.id('Grandson883').withAncestor(by.id('Son883'))); */ withAncestor(parentBy: NativeMatcher): NativeMatcher; + /** * Find an element by a matcher with a child matcher * @example await element(by.id('Son883').withDescendant(by.id('Grandson883'))); @@ -874,6 +865,7 @@ declare global { interface ExpectFacade { (element: NativeElement): Expect; + (webElement: WebElement): WebExpect; } @@ -968,6 +960,7 @@ declare global { * @example await expect(element(by.id('switch'))).toHaveToggleValue(true); */ toHaveToggleValue(value: boolean): R; + /** * Expect components like a Switch to have a value ('0' for off, '1' for on). * @example await expect(element(by.id('UniqueId533'))).toHaveValue('0'); @@ -1004,6 +997,7 @@ declare global { * @example await waitFor(element(by.text('Text5'))).toBeVisible().whileElement(by.id('ScrollView630')).scroll(50, 'down'); */ whileElement(by: NativeMatcher): NativeElement & WaitFor; + // TODO: not sure about & WaitFor - check if we can chain whileElement multiple times } @@ -1029,6 +1023,7 @@ declare global { */ longPressAndDrag(duration: number, normalizedPositionX: number, normalizedPositionY: number, targetElement: NativeElement, normalizedTargetPositionX: number, normalizedTargetPositionY: number, speed: Speed, holdDuration: number): Promise; + /** * Simulate multiple taps on an element. * @param times number of times to tap @@ -1084,10 +1079,10 @@ declare global { * @example await element(by.id('scrollView')).scroll(100, 'up'); */ scroll( - pixels: number, - direction: Direction, - startPositionX?: number, - startPositionY?: number, + pixels: number, + direction: Direction, + startPositionX?: number, + startPositionY?: number ): Promise; /** @@ -1095,7 +1090,7 @@ declare global { * @example await element(by.id('scrollView')).scrollToIndex(10); */ scrollToIndex( - index: Number + index: Number ): Promise; /** @@ -1184,7 +1179,7 @@ declare global { * // * on failure, to: /✗ Menu items should have Logout/tap on menu.png * }); */ - takeScreenshot(name: string): Promise; + takeScreenshot(name: string): Promise; /** * Gets the native (OS-dependent) attributes of the element. @@ -1202,7 +1197,7 @@ declare global { * jestExpect(attributes.width).toHaveValue(100); * }) */ - getAttributes(): Promise; + getAttributes(): Promise; } interface WebExpect> { @@ -1218,7 +1213,7 @@ declare global { * @example * await expect(web.element(by.web.id('UniqueId205'))).toHaveText('ExactText'); */ - toHaveText(text: string): R + toHaveText(text: string): R; /** * Expect the view to exist in the webview DOM tree. @@ -1239,56 +1234,56 @@ declare global { } interface WebElementActions { - tap(): Promise + tap(): Promise; /** * @param text to type * @param isContentEditable whether its a ContentEditable element, default is false. */ - typeText(text: string, isContentEditable: boolean): Promise + typeText(text: string, isContentEditable: boolean): Promise; /** * At the moment not working on content-editable * @param text to replace with the old content. */ - replaceText(text: string): Promise + replaceText(text: string): Promise; /** * At the moment not working on content-editable */ - clearText(): Promise + clearText(): Promise; /** * scrolling to the view, the element top position will be at the top of the screen. */ - scrollToView(): Promise + scrollToView(): Promise; /** * Gets the input content */ - getText(): Promise + getText(): Promise; /** * Calls the focus function on the element */ - focus(): Promise + focus(): Promise; /** * Selects all the input content, works on ContentEditable at the moment. */ - selectAllText(): Promise + selectAllText(): Promise; /** * Moves the input cursor / caret to the end of the content, works on ContentEditable at the moment. */ - moveCursorToEnd(): Promise + moveCursorToEnd(): Promise; /** * Running a script on the element * @param script a method that accept the element as its first arg * @example function foo(element) { console.log(element); } */ - runScript(script: string): Promise + runScript(script: string): Promise; /** * Running a script on the element that accept args diff --git a/detox/local-cli/test.test.js b/detox/local-cli/test.test.js index 5896ddf1da..80f1ce862d 100644 --- a/detox/local-cli/test.test.js +++ b/detox/local-cli/test.test.js @@ -35,9 +35,11 @@ describe('CLI', () => { detoxConfig = { configurations: { single: { - type: 'ios.simulator', - device: 'iPhone X', - binaryPath: 'path/to/app', + device: { + type: 'ios.simulator', + device: 'iPhone X' + }, + apps: [], }, }, }; @@ -87,7 +89,7 @@ describe('CLI', () => { describe('given no extra args (iOS)', () => { beforeEach(async () => { - singleConfig().type = 'ios.simulator'; + singleConfig().device.type = 'ios.simulator'; await run(); }); @@ -108,7 +110,7 @@ describe('CLI', () => { describe('given no extra args (Android)', () => { beforeEach(async () => { - singleConfig().type = 'android.emulator'; + singleConfig().device.type = 'android.emulator'; await run(); }); @@ -130,10 +132,10 @@ describe('CLI', () => { test.each([['-c'], ['--configuration']])( '%s should provide inverted --testNamePattern that configuration (jest)', async (__configuration) => { - detoxConfig.configurations.iosTest = { ...detoxConfig.configurations.single }; - detoxConfig.configurations.iosTest.type = 'ios.simulator'; - detoxConfig.configurations.androidTest = { ...detoxConfig.configurations.single }; - detoxConfig.configurations.androidTest.type = 'android.emulator'; + detoxConfig.configurations.iosTest = _.cloneDeep(detoxConfig.configurations.single); + detoxConfig.configurations.iosTest.device.type = 'ios.simulator'; + detoxConfig.configurations.androidTest = _.cloneDeep(detoxConfig.configurations.single); + detoxConfig.configurations.androidTest.device.type = 'android.emulator'; await run(`${__configuration} androidTest`); expect(cliCall(0).command).toContain(`--testNamePattern ${quote('^((?!:ios:).)*$')}`); @@ -289,37 +291,37 @@ describe('CLI', () => { }); test.each([['-w'], ['--workers']])('%s should not warn anything for iOS', async (__workers) => { - singleConfig().type = 'ios.simulator'; + singleConfig().device.type = 'ios.simulator'; await run(`${__workers} 2`); expect(logger.warn).not.toHaveBeenCalled(); }); test.each([['-w'], ['--workers']])('%s should not put readOnlyEmu environment variable for iOS', async (__workers) => { - singleConfig().type = 'ios.simulator'; + singleConfig().device.type = 'ios.simulator'; await run(`${__workers} 2`); expect(cliCall().env).not.toHaveProperty('DETOX_READ_ONLY_EMU'); }); test.each([['-w'], ['--workers']])('%s should not put readOnlyEmu environment variable for android.attached', async (__workers) => { - singleConfig().type = 'android.attached'; + singleConfig().device.type = 'android.attached'; await run(`${__workers} 2`); expect(cliCall().env).not.toHaveProperty('DETOX_READ_ONLY_EMU'); }); test.each([['-w'], ['--workers']])('%s should not put readOnlyEmu environment variable for android.emulator if there is a single worker', async (__workers) => { - singleConfig().type = 'android.emulator'; + singleConfig().device.type = 'android.emulator'; await run(`${__workers} 1`); expect(cliCall().env).not.toHaveProperty('DETOX_READ_ONLY_EMU'); }); test.each([['-w'], ['--workers']])('%s should put readOnlyEmu environment variable for Android if there are multiple workers', async (__workers) => { - singleConfig().type = 'android.emulator'; + singleConfig().device.type = 'android.emulator'; await run(`${__workers} 2`); expect(cliCall().env).toEqual(expect.objectContaining({ DETOX_READ_ONLY_EMU: true })); }); test('should omit --testNamePattern for custom platforms', async () => { - singleConfig().type = tempfile('.js', aCustomDriverModule()); + singleConfig().device.type = tempfile('.js', aCustomDriverModule()); await run(); expect(cliCall().command).not.toContain('--testNamePattern'); @@ -392,13 +394,13 @@ describe('CLI', () => { }); test('--force-adb-install should be ignored for iOS', async () => { - singleConfig().type = 'ios.simulator'; + singleConfig().device.type = 'ios.simulator'; await run(`--force-adb-install`); expect(cliCall().env).not.toHaveProperty('DETOX_FORCE_ADB_INSTALL'); }); test('--force-adb-install should be passed as environment variable', async () => { - singleConfig().type = 'android.emulator'; + singleConfig().device.type = 'android.emulator'; await run(`--force-adb-install`); expect(cliCall().env).toEqual(expect.objectContaining({ DETOX_FORCE_ADB_INSTALL: true, @@ -435,7 +437,7 @@ describe('CLI', () => { ['--use-custom-logger e2eFolder', / e2eFolder$/, { DETOX_USE_CUSTOM_LOGGER: true }], ['--force-adb-install e2eFolder', / e2eFolder$/, { DETOX_FORCE_ADB_INSTALL: true }], ])('"%s" should be disambigued correctly', async (command, commandMatcher, envMatcher) => { - singleConfig().type = 'android.emulator'; + singleConfig().device.type = 'android.emulator'; await run(command); expect(cliCall().command).toMatch(commandMatcher); @@ -473,7 +475,7 @@ describe('CLI', () => { describe.each([['ios.simulator'], ['android.emulator']])('for %s', (deviceType) => { beforeEach(() => { - Object.values(detoxConfig.configurations)[0].type = deviceType; + Object.values(detoxConfig.configurations)[0].device.type = deviceType; }); test('--keepLockFile should be suppress clearing the device lock file', async () => { diff --git a/detox/src/Detox.test.js b/detox/src/Detox.test.js index 5ffdea1d7a..003f9e5374 100644 --- a/detox/src/Detox.test.js +++ b/detox/src/Detox.test.js @@ -77,9 +77,14 @@ describe('Detox', () => { override: { configurations: { test: { - type: 'fake.device', - binaryPath: '/tmp/fake/path', - device: 'a device', + device: { + type: 'fake.device', + device: 'a device', + }, + app: { + type: 'fake.app', + binaryPath: '/tmp/fake/path', + }, }, }, }, diff --git a/detox/src/configuration/__mocks__/configuration/oldschema/package.json b/detox/src/configuration/__mocks__/configuration/oldschema/package.json new file mode 100644 index 0000000000..e45523de0a --- /dev/null +++ b/detox/src/configuration/__mocks__/configuration/oldschema/package.json @@ -0,0 +1,15 @@ +{ + "name": "oldschema", + "version": "1.0.0", + "dependencies": { + }, + "detox": { + "configurations": { + "single": { + "type": "ios.simulator", + "device": "iPhone 12", + "binaryPath": "/some/path/to.app" + } + } + } +} diff --git a/detox/src/configuration/__mocks__/configuration/packagejson/package.json b/detox/src/configuration/__mocks__/configuration/packagejson/package.json index a9ac39e671..3b8f16b2e8 100644 --- a/detox/src/configuration/__mocks__/configuration/packagejson/package.json +++ b/detox/src/configuration/__mocks__/configuration/packagejson/package.json @@ -8,8 +8,11 @@ }, "configurations": { "simple": { - "type": "android.attached", - "device": "Hello from package.json" + "device": { + "type": "android.attached", + "device": "Hello from package.json" + }, + "apps": [] } } } diff --git a/detox/src/configuration/__mocks__/configuration/priority/.detoxrc.js b/detox/src/configuration/__mocks__/configuration/priority/.detoxrc.js index a80d56056a..0580f07c04 100644 --- a/detox/src/configuration/__mocks__/configuration/priority/.detoxrc.js +++ b/detox/src/configuration/__mocks__/configuration/priority/.detoxrc.js @@ -1,8 +1,11 @@ module.exports = { configurations: { simple: { + device: { type: "android.attached", device: "Hello from .detoxrc", + }, + apps: [], }, }, }; diff --git a/detox/src/configuration/__mocks__/configuration/priority/detox-config.json b/detox/src/configuration/__mocks__/configuration/priority/detox-config.json index fb7d51b551..57cca2692f 100644 --- a/detox/src/configuration/__mocks__/configuration/priority/detox-config.json +++ b/detox/src/configuration/__mocks__/configuration/priority/detox-config.json @@ -1,8 +1,11 @@ { "configurations": { "simple": { + "device": { "type": "android.attached", "device": "Hello from detox-config.json" + }, + "apps": [] } } -} \ No newline at end of file +} diff --git a/detox/src/configuration/__mocks__/configuration/priority/package.json b/detox/src/configuration/__mocks__/configuration/priority/package.json index fcb9dfba01..71878b35a4 100644 --- a/detox/src/configuration/__mocks__/configuration/priority/package.json +++ b/detox/src/configuration/__mocks__/configuration/priority/package.json @@ -1,10 +1,13 @@ { - "detox": { - "configurations": { - "simple": { - "type": "android.attached", - "device": "Hello from package.json" - } - } + "detox": { + "configurations": { + "simple": { + "device": { + "type": "android.attached", + "device": "Hello from detox-config.json" + }, + "apps": [] + } } -} \ No newline at end of file + } +} diff --git a/detox/src/configuration/composeAppsConfig.js b/detox/src/configuration/composeAppsConfig.js index a5b6610244..4ad6a410a5 100644 --- a/detox/src/configuration/composeAppsConfig.js +++ b/detox/src/configuration/composeAppsConfig.js @@ -19,12 +19,7 @@ const CLI_PARSER_OPTIONS = { * @returns {Record} */ function composeAppsConfig(opts) { - const { localConfig } = opts; - - const appsConfig = localConfig.type - ? composeAppsConfigFromPlain(opts) - : composeAppsConfigFromAliased(opts); - + const appsConfig = composeAppsConfigFromAliased(opts); overrideAppLaunchArgs(appsConfig, opts.cliConfig); return appsConfig; @@ -35,64 +30,7 @@ function composeAppsConfig(opts) { * @param {string} opts.configurationName * @param {Detox.DetoxDeviceConfig} opts.deviceConfig * @param {Detox.DetoxConfig} opts.globalConfig - * @param {Detox.DetoxPlainConfiguration} opts.localConfig - * @returns {Record} - */ -function composeAppsConfigFromPlain(opts) { - const { errorComposer, localConfig } = opts; - - if (localConfig.app || localConfig.apps) { - throw errorComposer.oldSchemaHasAppAndApps(); - } - - /** @type {Detox.DetoxAppConfig} */ - let appConfig; - - switch (opts.deviceConfig.type) { - case 'android.attached': - case 'android.emulator': - case 'android.genycloud': - appConfig = { - type: 'android.apk', - binaryPath: localConfig.binaryPath, - bundleId: localConfig.bundleId, - build: localConfig.build, - testBinaryPath: localConfig.testBinaryPath, - launchArgs: localConfig.launchArgs, - }; break; - case 'ios.simulator': - appConfig = { - type: 'ios.app', - binaryPath: localConfig.binaryPath, - bundleId: localConfig.bundleId, - build: localConfig.build, - launchArgs: localConfig.launchArgs, - }; - break; - default: - appConfig = { - ...localConfig, - }; - } - - validateAppConfig({ - errorComposer, - appConfig, - deviceConfig: opts.deviceConfig, - appPath: ['configurations', opts.configurationName], - }); - - return { - default: _.omitBy(appConfig, _.isUndefined), - }; -} - -/** - * @param {DetoxConfigErrorComposer} opts.errorComposer - * @param {string} opts.configurationName - * @param {Detox.DetoxDeviceConfig} opts.deviceConfig - * @param {Detox.DetoxConfig} opts.globalConfig - * @param {Detox.DetoxAliasedConfiguration} opts.localConfig + * @param {Detox.DetoxConfiguration} opts.localConfig * @returns {Record} */ function composeAppsConfigFromAliased(opts) { @@ -100,8 +38,13 @@ function composeAppsConfigFromAliased(opts) { const result = {}; const { configurationName, errorComposer, deviceConfig, globalConfig, localConfig } = opts; + const isBuiltinDevice = Boolean(deviceAppTypes[deviceConfig.type]); if (localConfig.app == null && localConfig.apps == null) { - throw errorComposer.noAppIsDefined(deviceConfig.type); + if (isBuiltinDevice) { + throw errorComposer.noAppIsDefined(deviceConfig.type); + } else { + return result; + } } if (localConfig.app != null && localConfig.apps != null) { diff --git a/detox/src/configuration/composeAppsConfig.test.js b/detox/src/configuration/composeAppsConfig.test.js index 236e48946d..9ffc456744 100644 --- a/detox/src/configuration/composeAppsConfig.test.js +++ b/detox/src/configuration/composeAppsConfig.test.js @@ -48,101 +48,6 @@ describe('composeAppsConfig', () => { cliConfig, }); - describe('given a plain configuration', () => { - beforeEach(() => { - localConfig = { - type: 'ios.simulator', - device: 'Phone', - binaryPath: 'path/to/app', - bundleId: 'com.example.app', - build: 'echo OK', - launchArgs: { - hello: 'world', - } - }; - }); - - it.each([ - ['ios.simulator', 'ios.app'], - ['android.attached', 'android.apk'], - ['android.emulator', 'android.apk'], - ['android.genycloud', 'android.apk'], - ])('should infer type and app properties for %j', (deviceType, appType) => { - deviceConfig.type = deviceType; - expect(compose()).toEqual({ - default: { - ...localConfig, - type: appType, - device: undefined, - }, - }); - }); - - it('should take it as-is for unknown device type', () => { - deviceConfig.type = './customDriver'; - localConfig = { ...deviceConfig }; - expect(compose()).toEqual({ - default: localConfig - }); - }); - - it('should ignore mistyped Android properties for iOS app', () => { - deviceConfig.type = 'ios.simulator'; - localConfig.testBinaryPath = 'somePath'; - - const appConfig = compose().default; - expect(appConfig.testBinaryPath).toBe(undefined); - }); - - it('should include Android properties for Android app', () => { - deviceConfig.type = 'android.emulator'; - localConfig.testBinaryPath = 'somePath'; - - const appConfig = compose().default; - expect(appConfig.testBinaryPath).toBe('somePath'); - }); - - it.each([ - ['ios.simulator'], - ['android.attached'], - ['android.emulator'], - ['android.genycloud'], - ])('should ignore non-recognized properties for %j', (deviceType) => { - deviceConfig.type = deviceType; - localConfig.testBinaryPath2 = 'somePath'; - expect(compose().default.testBinaryPath2).toBe(undefined); - }); - - describe('.launchArgs', () => { - it('when it it is a string, should throw', () => { - localConfig.launchArgs = '-detoxAppArgument NO'; - expect(compose).toThrowError(errorComposer.malformedAppLaunchArgs(['configurations', configurationName])); - }); - - it('when it is an object with nullish properties, it should omit them', () => { - localConfig.launchArgs.nully = null; - localConfig.launchArgs.undefiny = undefined; - localConfig.launchArgs.aString = 'proveYourself'; - localConfig.launchArgs.anObject = { a: 1 }; - localConfig.launchArgs.anInteger = 2; - - expect(compose().default.launchArgs).toEqual({ - hello: 'world', - aString: 'proveYourself', - anInteger: 2, - anObject: { a: 1 }, - }); - }); - }); - - describe('given an unknown device type', () => { - it('should transfer the config as-is, for backward compatibility', () => { - deviceConfig.type = './myDriver'; - expect(compose()).toEqual({ default: localConfig }); - }); - }); - }); - describe('given a configuration with single app', () => { beforeEach(() => { deviceConfig.type = 'ios.simulator'; @@ -235,37 +140,29 @@ describe('composeAppsConfig', () => { }); }); - describe('unhappy scenarios:', () => { - describe('plain configuration:', () => { - beforeEach(() => { - localConfig = { - type: 'ios.simulator', - device: 'Phone', - binaryPath: 'path/to/app', - bundleId: 'com.example.app', - build: 'echo OK', - launchArgs: { - hello: 'world', - } - }; - }); + describe('given a configuration with no apps', () => { + beforeEach(() => { + delete localConfig.app; + delete localConfig.apps; + }); - it('should throw if the config has .app property defined', () => { - localConfig.app = 'myapp'; - expect(compose).toThrowError(errorComposer.oldSchemaHasAppAndApps()); + describe('when the device is powered by a custom driver', () => { + beforeEach(() => { + deviceConfig.type = './stub/driver'; }); - it('should throw if the config has .apps property defined', () => { - localConfig.apps = ['myapp']; - expect(compose).toThrowError(errorComposer.oldSchemaHasAppAndApps()); + it('should return an empty app config', () => { + expect(compose()).toEqual({}); }); }); + }); + describe('unhappy scenarios:', () => { describe('aliased configuration:', () => { beforeEach(() => { globalConfig.apps = { - example1: appWithAbsoluteBinaryPath, - example2: appWithRelativeBinaryPath, + example1: { ...appWithAbsoluteBinaryPath }, + example2: { ...appWithRelativeBinaryPath }, }; delete localConfig.type; @@ -273,8 +170,9 @@ describe('composeAppsConfig', () => { test.each([ ['ios.simulator'], + ['android.attached'], ['android.emulator'], - ['./stub/driver'], + ['android.genycloud'], ])('no app/apps is defined when device is %s', (deviceType) => { delete localConfig.app; delete localConfig.apps; @@ -361,6 +259,22 @@ describe('composeAppsConfig', () => { )); }); + test.each([ + ['ios.app', 'ios.simulator'], + ['android.apk', 'android.attached'], + ['android.apk', 'android.emulator'], + ['android.apk', 'android.genycloud'], + ])('known app (device type = %s) has malformed launchArgs', (appType, deviceType) => { + globalConfig.apps.example1.launchArgs = '-hello -world'; + globalConfig.apps.example1.type = appType; + deviceConfig.type = deviceType; + localConfig.app = 'example1'; + + expect(compose).toThrowError(errorComposer.malformedAppLaunchArgs( + ['apps', 'example1'] + )); + }); + test.each([ ['android.apk', 'ios.simulator'], ['ios.app', 'android.attached'], diff --git a/detox/src/configuration/composeArtifactsConfig.js b/detox/src/configuration/composeArtifactsConfig.js index d8d9b952c9..af2c0cd9c8 100644 --- a/detox/src/configuration/composeArtifactsConfig.js +++ b/detox/src/configuration/composeArtifactsConfig.js @@ -15,7 +15,7 @@ const resolveModuleFromPath = require('../utils/resolveModuleFromPath'); * @param {*} cliConfig * @param {string} configurationName * @param {Detox.DetoxConfig} globalConfig - * @param {Detox.DetoxConfigurationOverrides} localConfig + * @param {Detox.DetoxConfiguration} localConfig */ function composeArtifactsConfig({ cliConfig, diff --git a/detox/src/configuration/composeBehaviorConfig.js b/detox/src/configuration/composeBehaviorConfig.js index 37952dadb0..4c141c29fb 100644 --- a/detox/src/configuration/composeBehaviorConfig.js +++ b/detox/src/configuration/composeBehaviorConfig.js @@ -4,7 +4,7 @@ const _ = require('lodash'); /** * @param {*} cliConfig * @param {Detox.DetoxConfig} globalConfig - * @param {Detox.DetoxConfigurationOverrides} localConfig + * @param {Detox.DetoxConfiguration} localConfig */ function composeBehaviorConfig({ cliConfig, diff --git a/detox/src/configuration/composeDeviceConfig.js b/detox/src/configuration/composeDeviceConfig.js index b634abfcf6..e186d94353 100644 --- a/detox/src/configuration/composeDeviceConfig.js +++ b/detox/src/configuration/composeDeviceConfig.js @@ -12,13 +12,8 @@ const log = require('../utils/logger').child({ __filename }); * @returns {Detox.DetoxDeviceConfig} */ function composeDeviceConfig(opts) { - const { localConfig, cliConfig } = opts; - - const deviceConfig = localConfig.type - ? composeDeviceConfigFromPlain(opts) - : composeDeviceConfigFromAliased(opts); - - applyCLIOverrides(deviceConfig, cliConfig); + const deviceConfig = composeDeviceConfigFromAliased(opts); + applyCLIOverrides(deviceConfig, opts.cliConfig); deviceConfig.device = unpackDeviceQuery(deviceConfig); return deviceConfig; @@ -27,29 +22,7 @@ function composeDeviceConfig(opts) { /** * @param {DetoxConfigErrorComposer} opts.errorComposer * @param {Detox.DetoxConfig} opts.globalConfig - * @param {Detox.DetoxPlainConfiguration} opts.localConfig - * @returns {Detox.DetoxDeviceConfig} - */ -function composeDeviceConfigFromPlain(opts) { - const { errorComposer, localConfig } = opts; - - const type = localConfig.type; - const device = localConfig.device || localConfig.name; - const utilBinaryPaths = localConfig.utilBinaryPaths; - - const deviceConfig = type in EXPECTED_DEVICE_MATCHER_PROPS - ? _.omitBy({ type, device, utilBinaryPaths }, _.isUndefined) - : { ...localConfig }; - - validateDeviceConfig({ deviceConfig, errorComposer }); - - return deviceConfig; -} - -/** - * @param {DetoxConfigErrorComposer} opts.errorComposer - * @param {Detox.DetoxConfig} opts.globalConfig - * @param {Detox.DetoxAliasedConfiguration} opts.localConfig + * @param {Detox.DetoxConfiguration} opts.localConfig * @returns {Detox.DetoxDeviceConfig} */ function composeDeviceConfigFromAliased(opts) { diff --git a/detox/src/configuration/composeDeviceConfig.test.js b/detox/src/configuration/composeDeviceConfig.test.js index 02e4ea5938..bf379571c7 100644 --- a/detox/src/configuration/composeDeviceConfig.test.js +++ b/detox/src/configuration/composeDeviceConfig.test.js @@ -32,7 +32,7 @@ describe('composeDeviceConfig', () => { const givenConfigValidationSuccess = () => environmentFactory.validateConfig.mockReturnValue(undefined); const givenConfigValidationError = (error) => environmentFactory.validateConfig.mockImplementation(() => { throw error; }); - const KNOWN_CONFIGURATIONS = [['plain'], ['inline'], ['aliased']]; + const KNOWN_CONFIGURATIONS = [['inline'], ['aliased']]; const KNOWN_DEVICES = [ 'ios.simulator', @@ -46,7 +46,7 @@ describe('composeDeviceConfig', () => { /** * @param {'ios.simulator' | 'android.attached' | 'android.emulator' | 'android.genycloud' | './customDriver'} deviceType - * @param {'plain' | 'inline' | 'aliased' } configType + * @param {'inline' | 'aliased' } configType */ function setConfig(deviceType, configType = 'aliased') { const mixins = { @@ -100,10 +100,6 @@ describe('composeDeviceConfig', () => { deviceConfig = _.cloneDeep(deviceTemplates[deviceType] || deviceTemplates[undefined]); switch (configType) { - case 'plain': - Object.assign(localConfig, deviceConfig); - localConfig.binaryPath = deviceConfig.binaryPath || _.uniqueId('/path/to/app'); - break; case 'inline': localConfig.device = deviceConfig; break; @@ -140,58 +136,58 @@ describe('composeDeviceConfig', () => { describe('by config type', () => { describe.each(KNOWN_DEVICES)('given a device (%j)', (deviceType) => { - describe('plain', () => { - beforeEach(() => { - setConfig(deviceType, 'plain'); - - // NOTE: these properties are ignored for plain configurations - delete deviceConfig.bootArgs; - delete deviceConfig.forceAdbInstall; - delete deviceConfig.gpu; - delete deviceConfig.headless; - delete deviceConfig.readonly; - }); + describe('inlined', () => { + beforeEach(() => setConfig(deviceType, 'inline')); it('should extract type and device', () => expect(compose()).toEqual(deviceConfig)); - // region supported devices - if (deviceType === './customDriver') return; + describe('unhappy scenarios', () => { + test('should throw if device config is not found', () => { + delete localConfig.device; + expect(compose).toThrow(errorComposer.deviceConfigIsUndefined()); + }); - it('should have a fallback for known devices: .name -> .device', () => { - const expected = compose(); + test('should throw on no .type in device config', () => { + delete deviceConfig.type; + expect(compose).toThrow(errorComposer.missingDeviceType(undefined)); + }); + }); + }); - localConfig.name = localConfig.device; - delete localConfig.device; + describe('aliased', () => { + beforeEach(() => setConfig(deviceType, 'aliased')); - const actual = compose(); - expect(actual).toEqual(expected); - }); + it('should extract type and device', () => + expect(compose()).toEqual(deviceConfig)); - it('should extract type, utilBinaryPaths and unpack device query', () => { - localConfig.device = Object.values(deviceConfig.device).join(', '); + describe('unhappy scenarios', () => { + test('should throw if devices are not declared', () => { + globalConfig.devices = {}; + expect(compose).toThrow(errorComposer.thereAreNoDeviceConfigs(localConfig.device)); + }); + + test('should throw if device config is not found', () => { + localConfig.device = 'unknownDevice'; + expect(compose).toThrow(errorComposer.cantResolveDeviceAlias('unknownDevice')); + }); - expect(compose()).toEqual({ - type: deviceConfig.type, - device: deviceConfig.device, - utilBinaryPaths: deviceConfig.utilBinaryPaths, + test('should throw on no .type in device config', () => { + delete deviceConfig.type; + expect(compose).toThrow(errorComposer.missingDeviceType(localConfig.device)); }); }); - // endregion }); + }); + describe('given a custom device', () => { describe('inlined', () => { - beforeEach(() => setConfig(deviceType, 'inline')); + beforeEach(() => setConfig('./customDriver', 'inline')); it('should extract type and device', () => expect(compose()).toEqual(deviceConfig)); describe('unhappy scenarios', () => { - test('should throw if device config is not found', () => { - delete localConfig.device; - expect(compose).toThrow(errorComposer.deviceConfigIsUndefined()); - }); - test('should throw on no .type in device config', () => { delete deviceConfig.type; expect(compose).toThrow(errorComposer.missingDeviceType(undefined)); @@ -200,7 +196,7 @@ describe('composeDeviceConfig', () => { }); describe('aliased', () => { - beforeEach(() => setConfig(deviceType, 'aliased')); + beforeEach(() => setConfig('./customDriver', 'aliased')); it('should extract type and device', () => expect(compose()).toEqual(deviceConfig)); @@ -211,11 +207,6 @@ describe('composeDeviceConfig', () => { expect(compose).toThrow(errorComposer.thereAreNoDeviceConfigs(localConfig.device)); }); - test('should throw if device config is not found', () => { - localConfig.device = 'unknownDevice'; - expect(compose).toThrow(errorComposer.cantResolveDeviceAlias('unknownDevice')); - }); - test('should throw on no .type in device config', () => { delete deviceConfig.type; expect(compose).toThrow(errorComposer.missingDeviceType(localConfig.device)); @@ -489,9 +480,6 @@ describe('composeDeviceConfig', () => { )); }); - //region separate device config validation - if (configType === 'plain') return; - describe('.bootArgs validation', () => { test.each([ 'android.attached', diff --git a/detox/src/configuration/composeRunnerConfig.js b/detox/src/configuration/composeRunnerConfig.js index 68f5602500..fc5dc1f7a1 100644 --- a/detox/src/configuration/composeRunnerConfig.js +++ b/detox/src/configuration/composeRunnerConfig.js @@ -1,7 +1,7 @@ // @ts-nocheck /** * @param {Detox.DetoxConfig} globalConfig - * @param {Detox.DetoxConfigurationOverrides} localConfig + * @param {Detox.DetoxConfiguration} localConfig */ function composeRunnerConfig({ globalConfig, cliConfig }) { const testRunner = globalConfig.testRunner || 'jest'; diff --git a/detox/src/configuration/composeSessionConfig.js b/detox/src/configuration/composeSessionConfig.js index 913dd4f54f..3887a622ee 100644 --- a/detox/src/configuration/composeSessionConfig.js +++ b/detox/src/configuration/composeSessionConfig.js @@ -5,7 +5,7 @@ const uuid = require('../utils/uuid'); * @param {{ * cliConfig: Record; * globalConfig: Detox.DetoxConfig; - * localConfig: Detox.DetoxConfigurationOverrides; + * localConfig: Detox.DetoxConfiguration; * errorComposer: import('../errors/DetoxConfigErrorComposer'); * }} options */ diff --git a/detox/src/configuration/index.js b/detox/src/configuration/index.js index 4241174b2f..7362595ce3 100644 --- a/detox/src/configuration/index.js +++ b/detox/src/configuration/index.js @@ -20,9 +20,9 @@ const hooks = { async function composeDetoxConfig({ cwd = process.cwd(), argv = undefined, + errorComposer = new DetoxConfigErrorComposer(), override, }) { - const errorComposer = new DetoxConfigErrorComposer(); const cliConfig = collectCliConfig({ argv }); const findupResult = await loadExternalConfig({ errorComposer, @@ -57,6 +57,10 @@ async function composeDetoxConfig({ const localConfig = configurations[configurationName]; + if (localConfig['type']) { + throw errorComposer.configurationShouldNotUseLegacyFormat(); + } + const deviceConfig = composeDeviceConfig({ errorComposer, globalConfig, diff --git a/detox/src/configuration/index.test.js b/detox/src/configuration/index.test.js index 3c7074509c..9f2e37c491 100644 --- a/detox/src/configuration/index.test.js +++ b/detox/src/configuration/index.test.js @@ -43,6 +43,18 @@ describe('composeDetoxConfig', () => { })).rejects.toThrowError(errorComposer.noConfigurationSpecified()); }); + it('should throw an error if the local config has the old schema', async () => { + try { + await configuration.composeDetoxConfig({ + cwd: path.join(__dirname, '__mocks__/configuration/oldschema'), + errorComposer, + }); + } catch (e) { + // NOTE: we want errorComposer to be mutated, that's why we assert inside try-catch + expect(e).toEqual(errorComposer.configurationShouldNotUseLegacyFormat()); + } + }); + it('should return a complete Detox config merged with the file configuration', async () => { const config = await configuration.composeDetoxConfig({ cwd: path.join(__dirname, '__mocks__/configuration/packagejson'), @@ -68,9 +80,14 @@ describe('composeDetoxConfig', () => { }, configurations: { another: { - type: 'ios.simulator', - device: 'iPhone X', - binaryPath: 'path/to/app', + device: { + type: 'ios.simulator', + device: 'iPhone X' + }, + app: { + type: 'ios.app', + binaryPath: 'path/to/app', + }, }, }, } diff --git a/detox/src/errors/DetoxConfigErrorComposer.js b/detox/src/errors/DetoxConfigErrorComposer.js index f692ddd70f..696631a435 100644 --- a/detox/src/errors/DetoxConfigErrorComposer.js +++ b/detox/src/errors/DetoxConfigErrorComposer.js @@ -25,16 +25,6 @@ class DetoxConfigErrorComposer { _atPath() { return this.filepath ? ` at path:\n${this.filepath}` : '.'; } - - _inTheAppConfig() { - const { type } = this._getSelectedConfiguration(); - if (type) { - return `in configuration ${J(this.configurationName)}`; - } - - return `in the app config`; - } - _getSelectedConfiguration() { return _.get(this.contents, ['configurations', this.configurationName]); } @@ -64,9 +54,10 @@ class DetoxConfigErrorComposer { } _focusOnDeviceConfig(deviceAlias, postProcess = _.identity) { - const { type, device } = this._getSelectedConfiguration(); + const { device } = this._getSelectedConfiguration(); if (!deviceAlias) { - if (type || !device) { + // istanbul ignore next + if (!device) { return this._focusOnConfiguration(postProcess); } else { return this._focusOnConfiguration(c => { @@ -92,8 +83,7 @@ class DetoxConfigErrorComposer { if (alias) { return this.contents.devices[alias]; } else { - const config = this._getSelectedConfiguration(); - return config.type ? config : config.device; + return this._getSelectedConfiguration().device; } } @@ -225,6 +215,61 @@ Examine your Detox config${this._atPath()}`, }); } + configurationShouldNotUseLegacyFormat() { + const name = this.configurationName; + const localConfig = this._getSelectedConfiguration(); + /* istanbul ignore next */ + const deviceType = localConfig.type || ''; + const isAndroid = deviceType.startsWith('android.'); + const isIOS = deviceType.startsWith('ios.'); + const appName = 'myApp' + (isIOS ? '.ios' : '') + (isAndroid ? '.android' : ''); + const deviceName = isIOS ? 'simulator' : isAndroid ? 'emulator' : 'myDevice'; + const appType = isIOS ? 'ios.app' : isAndroid ? 'android.apk' : ''; + const binaryPath = isIOS || isAndroid ? localConfig.binaryPath : (localConfig.binaryPath || ''); + const deviceQuery = isIOS || isAndroid ? localConfig.device : (localConfig.device || ''); + + return new DetoxConfigError({ + message: `The ${J(name)} configuration utilizes a deprecated all-in-one schema, that is not supported ` + + `by the current version of Detox.`, + hint: `Remove the "type" property. A valid configuration is expected to have both the "device" and "app" aliases ` + + `pointing to the corresponding keys in the 'devices' and 'apps' config sections. For example:\n +{ + "apps": { +*-->${J(appName)}: { +| "type": ${J(appType)}, +| "binaryPath": ${J(binaryPath)}, +| }, +| }, +| "devices": { +|*->${J(deviceName)}: { +|| "type": ${J(deviceType)}, +|| "device": ${J(deviceQuery)} +|| }, +||}, +||"configurations": { +|| ${J(name)}: { +|| /* REMOVE (!) "type": ${J(deviceType)} */ +|*--- "device": ${J(deviceName)}, +*---- "app": ${J(appName)}, + ... + } + } +} +Examine your Detox config${this._atPath()}`, + debugInfo: { + apps: this.contents.apps + ? _.mapValues(this.contents.apps, _.constant({})) + : undefined, + devices: this.contents.devices + ? _.mapValues(this.contents.devices, _.constant({})) + : undefined, + + ...this._focusOnConfiguration(), + }, + inspectOptions: { depth: 2 } + }); + } + // endregion // region composeDeviceConfig @@ -456,7 +501,7 @@ Examine your Detox config${this._atPath()}`, malformedAppLaunchArgs(appPath) { return new DetoxConfigError({ - message: `Invalid type of "launchArgs" property ${this._inTheAppConfig()}.\nExpected an object:`, + message: `Invalid type of "launchArgs" property in the app config.\nExpected an object:`, debugInfo: this._focusOnAppConfig(appPath), inspectOptions: { depth: 4 }, }); @@ -464,7 +509,7 @@ Examine your Detox config${this._atPath()}`, missingAppBinaryPath(appPath) { return new DetoxConfigError({ - message: `Missing "binaryPath" property ${this._inTheAppConfig()}.\nExpected a string:`, + message: `Missing "binaryPath" property in the app config.\nExpected a string:`, debugInfo: this._focusOnAppConfig(appPath, this._ensureProperty('binaryPath')), inspectOptions: { depth: 4 }, }); @@ -538,18 +583,6 @@ Examine your Detox config${this._atPath()}`, }); } - oldSchemaHasAppAndApps() { - return new DetoxConfigError({ - message: `Your configuration ${J(this.configurationName)} appears to be in a legacy format, which can’t contain "app" or "apps".`, - hint: `Remove "type" property from configuration and use "device" property instead:\n` + - `a) "device": { "type": ${J(this._getSelectedConfiguration().type)}, ... }\n` + - `b) "device": "" // you should add that device configuration to "devices" with the same key` + - `\n\nCheck your Detox config${this._atPath()}`, - debugInfo: this._focusOnConfiguration(this._ensureProperty('type', 'device')), - inspectOptions: { depth: 2 }, - }); - } - ambiguousAppAndApps() { return new DetoxConfigError({ message: `You can't have both "app" and "apps" defined in the ${J(this.configurationName)} configuration.`, diff --git a/detox/src/errors/DetoxConfigErrorComposer.test.js b/detox/src/errors/DetoxConfigErrorComposer.test.js index 75461a972c..d31f79d32d 100644 --- a/detox/src/errors/DetoxConfigErrorComposer.test.js +++ b/detox/src/errors/DetoxConfigErrorComposer.test.js @@ -30,11 +30,6 @@ describe('DetoxConfigErrorComposer', () => { }, }, configurations: { - plain: { - type: 'android.emulator', - device: 'Pixel_3a_API_30_x86', - binaryPath: 'path/to/apk', - }, aliased: { device: 'aDevice', apps: ['someApp'], @@ -160,6 +155,61 @@ describe('DetoxConfigErrorComposer', () => { expect(build()).toMatchSnapshot(); }); }); + + describe('.configurationShouldNotUseLegacyFormat', () => { + describe.each([ + 'ios.simulator', + 'android.emulator', + 'android.genycloud', + ])('for %j device type', (deviceType) => { + beforeEach(() => { + build = () => builder.configurationShouldNotUseLegacyFormat(); + builder.setConfigurationName('legacy'); + config.configurations.legacy = { + type: deviceType, + device: `some-query(${deviceType})`, + binaryPath: '/path/' + deviceType + '.app', + }; + }); + + it('should create a helpful error', () => { + expect(build()).toMatchSnapshot(); + }); + }); + + describe('for custom driver type', () => { + beforeEach(() => { + build = () => builder.configurationShouldNotUseLegacyFormat(); + builder.setConfigurationName('legacy'); + config.configurations.legacy = { + type: './custom-driver', + webUrl: 'https://example.com', + }; + }); + + it('should create a helpful error', () => { + expect(build()).toMatchSnapshot(); + }); + }); + + describe('for missing global .apps and .devices', () => { + beforeEach(() => { + build = () => builder.configurationShouldNotUseLegacyFormat(); + builder.setConfigurationName('legacy'); + delete config.devices; + delete config.apps; + config.configurations.legacy = { + type: 'ios.simulator', + device: 'iPhone 12', + binaryPath: '/some/path', + }; + }); + + it('should create a helpful error', () => { + expect(build()).toMatchSnapshot(); + }); + }); + }); }); describe('(from composeDeviceConfig)', () => { @@ -194,17 +244,14 @@ describe('DetoxConfigErrorComposer', () => { ['headless', 'aliased', 'non-boolean'], ['readonly', 'inlined', 'non-boolean'], ['readonly', 'aliased', 'non-boolean'], - ['utilBinaryPaths', 'plain', 'invalid'], ['utilBinaryPaths', 'inlined', [NaN, 'valid']], ['utilBinaryPaths', 'aliased', [NaN, 'valid']], ])('(%j) should create an error for %s configuration', (propertyName, configurationType, invalidValue) => { builder.setConfigurationName(configurationType); const deviceAlias = configurationType === 'aliased' ? 'aDevice' : undefined; - const deviceConfig = configurationType === 'plain' - ? config.configurations[configurationType] - : configurationType === 'inlined' - ? config.configurations[configurationType].device - : config.devices.aDevice; + const deviceConfig = configurationType === 'inlined' + ? config.configurations[configurationType].device + : config.devices.aDevice; deviceConfig[propertyName] = invalidValue; expect(builder.malformedDeviceProperty(deviceAlias, propertyName)).toMatchSnapshot(); @@ -227,17 +274,14 @@ describe('DetoxConfigErrorComposer', () => { ['headless', 'aliased', true], ['readonly', 'inlined', false], ['readonly', 'aliased', false], - ['utilBinaryPaths', 'plain', []], ['utilBinaryPaths', 'inlined', []], ['utilBinaryPaths', 'aliased', []], ])('(%j) should create an error for %s configuration', (propertyName, configurationType, invalidValue) => { builder.setConfigurationName(configurationType); const deviceAlias = configurationType === 'aliased' ? 'aDevice' : undefined; - const deviceConfig = configurationType === 'plain' - ? config.configurations[configurationType] - : configurationType === 'inlined' - ? config.configurations[configurationType].device - : config.devices.aDevice; + const deviceConfig = configurationType === 'inlined' + ? config.configurations[configurationType].device + : config.devices.aDevice; deviceConfig[propertyName] = invalidValue; expect(builder.unsupportedDeviceProperty(deviceAlias, propertyName)).toMatchSnapshot(); @@ -254,7 +298,8 @@ describe('DetoxConfigErrorComposer', () => { }); it('should produce a helpful error', () => { - builder.setConfigurationName('plain'); + builder.setConfigurationName('aliased'); + delete config.configurations.aliased.device; expect(build()).toMatchSnapshot(); }); }); @@ -306,11 +351,6 @@ describe('DetoxConfigErrorComposer', () => { build = (alias) => builder.missingDeviceMatcherProperties(alias, ['foo', 'bar']); }); - it('should work with plain configurations', () => { - builder.setConfigurationName('plain'); - expect(build()).toMatchSnapshot(); - }); - it('should work with inlined configurations', () => { builder.setConfigurationName('inlined'); expect(build()).toMatchSnapshot(); @@ -360,12 +400,6 @@ describe('DetoxConfigErrorComposer', () => { build = (appPath) => builder.malformedAppLaunchArgs(appPath); }); - it('should work with plain configurations', () => { - config.configurations.plain.launchArgs = 'invalid'; - builder.setConfigurationName('plain'); - expect(build(['configurations', 'plain'])).toMatchSnapshot(); - }); - it('should work with inlined configurations', () => { config.configurations.inlinedMulti.apps[0].launchArgs = 'invalid'; builder.setConfigurationName('inlinedMulti'); @@ -384,12 +418,6 @@ describe('DetoxConfigErrorComposer', () => { build = (appPath) => builder.missingAppBinaryPath(appPath); }); - it('should create an error for plain configuration', () => { - builder.setConfigurationName('plain'); - delete config.configurations.plain.binaryPath; - expect(build(['configurations', 'plain'])).toMatchSnapshot(); - }); - it('should create an error for aliased configuration', () => { builder.setConfigurationName('aliased'); delete config.apps.someApp.binaryPath; @@ -522,19 +550,6 @@ describe('DetoxConfigErrorComposer', () => { }); }); - describe('.oldSchemaHasAppAndApps', () => { - beforeEach(() => { - build = () => builder.oldSchemaHasAppAndApps(); - }); - - it('should create an error for ambigous old/new configuration if it has .apps', () => { - builder.setConfigurationName('plain'); - config.configurations.plain.app = 'my-app'; - - expect(build()).toMatchSnapshot(); - }); - }); - describe('.ambiguousAppAndApps', () => { beforeEach(() => { build = () => builder.ambiguousAppAndApps(); diff --git a/detox/src/errors/__snapshots__/DetoxConfigErrorComposer.test.js.snap b/detox/src/errors/__snapshots__/DetoxConfigErrorComposer.test.js.snap index d45745ca78..9911598476 100644 --- a/detox/src/errors/__snapshots__/DetoxConfigErrorComposer.test.js.snap +++ b/detox/src/errors/__snapshots__/DetoxConfigErrorComposer.test.js.snap @@ -267,22 +267,6 @@ Expected an object: }] `; -exports[`DetoxConfigErrorComposer (from composeAppsConfig) .malformedAppLaunchArgs should work with plain configurations 1`] = ` -[DetoxConfigError: Invalid type of "launchArgs" property in configuration "plain". -Expected an object: - -{ - configurations: { - plain: { - type: 'android.emulator', - device: 'Pixel_3a_API_30_x86', - binaryPath: 'path/to/apk', - launchArgs: 'invalid' - } - } -}] -`; - exports[`DetoxConfigErrorComposer (from composeAppsConfig) .missingAppBinaryPath should create an error for aliased configuration 1`] = ` [DetoxConfigError: Missing "binaryPath" property in the app config. Expected a string: @@ -328,21 +312,6 @@ Expected a string: }] `; -exports[`DetoxConfigErrorComposer (from composeAppsConfig) .missingAppBinaryPath should create an error for plain configuration 1`] = ` -[DetoxConfigError: Missing "binaryPath" property in configuration "plain". -Expected a string: - -{ - configurations: { - plain: { - type: 'android.emulator', - device: 'Pixel_3a_API_30_x86', - binaryPath: undefined - } - } -}] -`; - exports[`DetoxConfigErrorComposer (from composeAppsConfig) .multipleAppsConfigArrayTypo should create an error for aliased configuration 1`] = ` [DetoxConfigError: Invalid type of the "app" property in the selected configuration "aliased". @@ -495,28 +464,6 @@ Examine your Detox config at path: /home/detox/myproject/.detoxrc.json] `; -exports[`DetoxConfigErrorComposer (from composeAppsConfig) .oldSchemaHasAppAndApps should create an error for ambigous old/new configuration if it has .apps 1`] = ` -[DetoxConfigError: Your configuration "plain" appears to be in a legacy format, which can’t contain "app" or "apps". - -HINT: Remove "type" property from configuration and use "device" property instead: -a) "device": { "type": "android.emulator", ... } -b) "device": "" // you should add that device configuration to "devices" with the same key - -Check your Detox config at path: -/home/detox/myproject/.detoxrc.json - -{ - configurations: { - plain: { - type: 'android.emulator', - device: 'Pixel_3a_API_30_x86', - binaryPath: 'path/to/apk', - app: 'my-app' - } - } -}] -`; - exports[`DetoxConfigErrorComposer (from composeAppsConfig) .thereAreNoAppConfigs should create an error for aliased configuration 1`] = ` [DetoxConfigError: Cannot use app alias "someApp" since there is no "apps" config in Detox config at path: /home/detox/myproject/.detoxrc.json @@ -555,7 +502,7 @@ Check your configuration "aliased": `; exports[`DetoxConfigErrorComposer (from composeDeviceConfig) .deviceConfigIsUndefined should produce a helpful error 1`] = ` -[DetoxConfigError: Missing "device" property in the selected configuration "plain": +[DetoxConfigError: Missing "device" property in the selected configuration "aliased": HINT: It should be an alias to the device config, or the device config itself, e.g.: { @@ -567,7 +514,7 @@ HINT: It should be an alias to the device config, or the device config itself, e | } | }, | "configurations": { -| "plain": { +| "aliased": { *---- "device": "myDevice", // or { type: 'ios.simulator', ... } ... }, @@ -880,25 +827,6 @@ HINT: Check that in your Detox config at path: }] `; -exports[`DetoxConfigErrorComposer (from composeDeviceConfig) .malformedDeviceProperty ("utilBinaryPaths") should create an error for plain configuration 1`] = ` -[DetoxConfigError: Invalid type of "utilBinaryPaths" inside the device configuration. -Expected an array of strings. - -HINT: Check that in your Detox config at path: -/home/detox/myproject/.detoxrc.json - -{ - configurations: { - plain: { - type: 'android.emulator', - device: 'Pixel_3a_API_30_x86', - binaryPath: 'path/to/apk', - utilBinaryPaths: 'invalid' - } - } -}] -`; - exports[`DetoxConfigErrorComposer (from composeDeviceConfig) .malformedDeviceProperty should throw on an unknown argument 1`] = ` "Composing .malformedDeviceProperty(unknown) is not implemented Please report this issue on our GitHub tracker: @@ -957,30 +885,6 @@ Check that in your Detox config at path: }] `; -exports[`DetoxConfigErrorComposer (from composeDeviceConfig) .missingDeviceMatcherProperties should work with plain configurations 1`] = ` -[DetoxConfigError: Invalid or empty "device" matcher inside the device config. - -HINT: It should have the device query to run on, e.g.: - -{ - "type": "android.emulator", - "device": { "foo": ... } - // or { "bar": ... } -} -Check that in your Detox config at path: -/home/detox/myproject/.detoxrc.json - -{ - configurations: { - plain: { - type: 'android.emulator', - device: 'Pixel_3a_API_30_x86', - binaryPath: 'path/to/apk' - } - } -}] -`; - exports[`DetoxConfigErrorComposer (from composeDeviceConfig) .missingDeviceType should create an error for aliased configuration 1`] = ` [DetoxConfigError: Missing "type" inside the device configuration. @@ -1327,29 +1231,6 @@ Please fix your Detox config at path: }] `; -exports[`DetoxConfigErrorComposer (from composeDeviceConfig) .unsupportedDeviceProperty ("utilBinaryPaths") should create an error for plain configuration 1`] = ` -[DetoxConfigError: The current device type "android.emulator" does not support "utilBinaryPaths" property. - -HINT: You can use this property only with the following device types: -* android.attached -* android.emulator -* android.genycloud - -Please fix your Detox config at path: -/home/detox/myproject/.detoxrc.json - -{ - configurations: { - plain: { - type: 'android.emulator', - device: 'Pixel_3a_API_30_x86', - binaryPath: 'path/to/apk', - utilBinaryPaths: [] - } - } -}] -`; - exports[`DetoxConfigErrorComposer (from composeDeviceConfig) .unsupportedDeviceProperty should throw on an unknown argument 1`] = ` "Composing .unsupportedDeviceProperty(unknown) is not implemented Please report this issue on our GitHub tracker: @@ -1536,7 +1417,6 @@ exports[`DetoxConfigErrorComposer (from configuration/index) .cantChooseConfigur /etc/detox/config.js HINT: Use --configuration to choose one of the following: -* plain * aliased * inlined * inlinedMulti] @@ -1573,7 +1453,6 @@ Examine your Detox config at path: { configurations: { empty: {}, - plain: [Object], aliased: [Object], inlined: [Object], inlinedMulti: [Object] @@ -1581,6 +1460,236 @@ Examine your Detox config at path: }] `; +exports[`DetoxConfigErrorComposer (from configuration/index) .configurationShouldNotUseLegacyFormat for "android.emulator" device type should create a helpful error 1`] = ` +[DetoxConfigError: The "legacy" configuration utilizes a deprecated all-in-one schema, that is not supported by the current version of Detox. + +HINT: Remove the "type" property. A valid configuration is expected to have both the "device" and "app" aliases pointing to the corresponding keys in the 'devices' and 'apps' config sections. For example: + +{ + "apps": { +*-->"myApp.android": { +| "type": "android.apk", +| "binaryPath": "/path/android.emulator.app", +| }, +| }, +| "devices": { +|*->"emulator": { +|| "type": "android.emulator", +|| "device": "some-query(android.emulator)" +|| }, +||}, +||"configurations": { +|| "legacy": { +|| /* REMOVE (!) "type": "android.emulator" */ +|*--- "device": "emulator", +*---- "app": "myApp.android", + ... + } + } +} +Examine your Detox config at path: +/home/detox/myproject/.detoxrc.json + +{ + apps: { + someApp: {} + }, + devices: { + aDevice: {} + }, + configurations: { + legacy: { + type: 'android.emulator', + device: 'some-query(android.emulator)', + binaryPath: '/path/android.emulator.app' + } + } +}] +`; + +exports[`DetoxConfigErrorComposer (from configuration/index) .configurationShouldNotUseLegacyFormat for "android.genycloud" device type should create a helpful error 1`] = ` +[DetoxConfigError: The "legacy" configuration utilizes a deprecated all-in-one schema, that is not supported by the current version of Detox. + +HINT: Remove the "type" property. A valid configuration is expected to have both the "device" and "app" aliases pointing to the corresponding keys in the 'devices' and 'apps' config sections. For example: + +{ + "apps": { +*-->"myApp.android": { +| "type": "android.apk", +| "binaryPath": "/path/android.genycloud.app", +| }, +| }, +| "devices": { +|*->"emulator": { +|| "type": "android.genycloud", +|| "device": "some-query(android.genycloud)" +|| }, +||}, +||"configurations": { +|| "legacy": { +|| /* REMOVE (!) "type": "android.genycloud" */ +|*--- "device": "emulator", +*---- "app": "myApp.android", + ... + } + } +} +Examine your Detox config at path: +/home/detox/myproject/.detoxrc.json + +{ + apps: { + someApp: {} + }, + devices: { + aDevice: {} + }, + configurations: { + legacy: { + type: 'android.genycloud', + device: 'some-query(android.genycloud)', + binaryPath: '/path/android.genycloud.app' + } + } +}] +`; + +exports[`DetoxConfigErrorComposer (from configuration/index) .configurationShouldNotUseLegacyFormat for "ios.simulator" device type should create a helpful error 1`] = ` +[DetoxConfigError: The "legacy" configuration utilizes a deprecated all-in-one schema, that is not supported by the current version of Detox. + +HINT: Remove the "type" property. A valid configuration is expected to have both the "device" and "app" aliases pointing to the corresponding keys in the 'devices' and 'apps' config sections. For example: + +{ + "apps": { +*-->"myApp.ios": { +| "type": "ios.app", +| "binaryPath": "/path/ios.simulator.app", +| }, +| }, +| "devices": { +|*->"simulator": { +|| "type": "ios.simulator", +|| "device": "some-query(ios.simulator)" +|| }, +||}, +||"configurations": { +|| "legacy": { +|| /* REMOVE (!) "type": "ios.simulator" */ +|*--- "device": "simulator", +*---- "app": "myApp.ios", + ... + } + } +} +Examine your Detox config at path: +/home/detox/myproject/.detoxrc.json + +{ + apps: { + someApp: {} + }, + devices: { + aDevice: {} + }, + configurations: { + legacy: { + type: 'ios.simulator', + device: 'some-query(ios.simulator)', + binaryPath: '/path/ios.simulator.app' + } + } +}] +`; + +exports[`DetoxConfigErrorComposer (from configuration/index) .configurationShouldNotUseLegacyFormat for custom driver type should create a helpful error 1`] = ` +[DetoxConfigError: The "legacy" configuration utilizes a deprecated all-in-one schema, that is not supported by the current version of Detox. + +HINT: Remove the "type" property. A valid configuration is expected to have both the "device" and "app" aliases pointing to the corresponding keys in the 'devices' and 'apps' config sections. For example: + +{ + "apps": { +*-->"myApp": { +| "type": "", +| "binaryPath": "", +| }, +| }, +| "devices": { +|*->"myDevice": { +|| "type": "./custom-driver", +|| "device": "" +|| }, +||}, +||"configurations": { +|| "legacy": { +|| /* REMOVE (!) "type": "./custom-driver" */ +|*--- "device": "myDevice", +*---- "app": "myApp", + ... + } + } +} +Examine your Detox config at path: +/home/detox/myproject/.detoxrc.json + +{ + apps: { + someApp: {} + }, + devices: { + aDevice: {} + }, + configurations: { + legacy: { + type: './custom-driver', + webUrl: 'https://example.com' + } + } +}] +`; + +exports[`DetoxConfigErrorComposer (from configuration/index) .configurationShouldNotUseLegacyFormat for missing global .apps and .devices should create a helpful error 1`] = ` +[DetoxConfigError: The "legacy" configuration utilizes a deprecated all-in-one schema, that is not supported by the current version of Detox. + +HINT: Remove the "type" property. A valid configuration is expected to have both the "device" and "app" aliases pointing to the corresponding keys in the 'devices' and 'apps' config sections. For example: + +{ + "apps": { +*-->"myApp.ios": { +| "type": "ios.app", +| "binaryPath": "/some/path", +| }, +| }, +| "devices": { +|*->"simulator": { +|| "type": "ios.simulator", +|| "device": "iPhone 12" +|| }, +||}, +||"configurations": { +|| "legacy": { +|| /* REMOVE (!) "type": "ios.simulator" */ +|*--- "device": "simulator", +*---- "app": "myApp.ios", + ... + } + } +} +Examine your Detox config at path: +/home/detox/myproject/.detoxrc.json + +{ + apps: undefined, + devices: undefined, + configurations: { + legacy: { + type: 'ios.simulator', + device: 'iPhone 12', + binaryPath: '/some/path' + } + } +}] +`; + exports[`DetoxConfigErrorComposer (from configuration/index) .failedToReadConfiguration should create a generic error, if I/O error is unknown 1`] = ` [DetoxConfigError: An error occurred while trying to load Detox config from: /home/detox/myproject/.detoxrc.json] @@ -1627,7 +1736,6 @@ exports[`DetoxConfigErrorComposer (from configuration/index) .noConfigurationWit /home/detox/myproject/.detoxrc.json HINT: Below are the configurations Detox was able to find: -* plain * aliased * inlined * inlinedMulti] diff --git a/detox/test/e2e/detox.config.js b/detox/test/e2e/detox.config.js index 3a7e9119bc..b0da816d68 100644 --- a/detox/test/e2e/detox.config.js +++ b/detox/test/e2e/detox.config.js @@ -200,10 +200,12 @@ const config = { apps: ['android.release', 'android.release.withArgs'], }, 'stub': { - type: './integration/stub', - name: 'integration-stub', device: { + type: './integration/stub', integ: 'stub' + }, + app: { + name: 'example' } } } diff --git a/docs/Guide.ThirdPartyDrivers.md b/docs/Guide.ThirdPartyDrivers.md index aea175a2de..d0fdfbb80c 100644 --- a/docs/Guide.ThirdPartyDrivers.md +++ b/docs/Guide.ThirdPartyDrivers.md @@ -15,7 +15,10 @@ For example, the following configuration uses the "ios.simulator" driver. "ios.sim": { "type": "ios.simulator", "device": "...", - "binaryPath": "bin/YourApp.app" + "app": { + "type": "ios.app", + "binaryPath": "bin/YourApp.app" + } } } ``` @@ -37,7 +40,9 @@ Overall the setup for any third party driver is fairly simple. ```diff + "thirdparty.driver.config": { + "type": "detox-driver-package", - + "binaryPath": "bin/YourApp.app", + + "app": { + + "binaryPath": "bin/YourApp.app", + + } + } ```