From 9386700469e39c30e224fa51048fe12d85a2c2b1 Mon Sep 17 00:00:00 2001 From: wixmobile Date: Thu, 26 Oct 2023 01:54:53 -0700 Subject: [PATCH 001/185] Publish 20.13.2 [ci skip] --- detox/package.json | 2 +- detox/test/package.json | 4 ++-- examples/demo-native-android/package.json | 4 ++-- examples/demo-native-ios/package.json | 4 ++-- examples/demo-plugin/package.json | 4 ++-- examples/demo-react-native-detox-instruments/package.json | 4 ++-- examples/demo-react-native/package.json | 4 ++-- generation/package.json | 2 +- lerna.json | 2 +- package.json | 2 +- 10 files changed, 16 insertions(+), 16 deletions(-) diff --git a/detox/package.json b/detox/package.json index c8102f4a6b..d3efb81998 100644 --- a/detox/package.json +++ b/detox/package.json @@ -1,7 +1,7 @@ { "name": "detox", "description": "E2E tests and automation for mobile", - "version": "20.13.1", + "version": "20.13.2", "bin": { "detox": "local-cli/cli.js" }, diff --git a/detox/test/package.json b/detox/test/package.json index 8d0e369991..415ce7695b 100644 --- a/detox/test/package.json +++ b/detox/test/package.json @@ -1,6 +1,6 @@ { "name": "detox-test", - "version": "20.13.1", + "version": "20.13.2", "private": true, "engines": { "node": ">=14.5.0" @@ -56,7 +56,7 @@ "@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/parser": "^5.4.0", "cross-env": "^7.0.3", - "detox": "^20.13.1", + "detox": "^20.13.2", "eslint": "^8.41.0", "eslint-plugin-unicorn": "^47.0.0", "execa": "^5.1.1", diff --git a/examples/demo-native-android/package.json b/examples/demo-native-android/package.json index 046718f670..4d515361cd 100644 --- a/examples/demo-native-android/package.json +++ b/examples/demo-native-android/package.json @@ -1,13 +1,13 @@ { "name": "detox-demo-native-android", - "version": "20.13.1", + "version": "20.13.2", "private": true, "scripts": { "packager": "react-native start", "detox-server": "detox run-server" }, "devDependencies": { - "detox": "^20.13.1" + "detox": "^20.13.2" }, "detox": {} } diff --git a/examples/demo-native-ios/package.json b/examples/demo-native-ios/package.json index c1718a5f73..9bfd8a1607 100644 --- a/examples/demo-native-ios/package.json +++ b/examples/demo-native-ios/package.json @@ -1,9 +1,9 @@ { "name": "detox-demo-native-ios", - "version": "20.13.1", + "version": "20.13.2", "private": true, "devDependencies": { - "detox": "^20.13.1" + "detox": "^20.13.2" }, "detox": { "specs": "", diff --git a/examples/demo-plugin/package.json b/examples/demo-plugin/package.json index 34ac752fa0..7d0272b057 100644 --- a/examples/demo-plugin/package.json +++ b/examples/demo-plugin/package.json @@ -1,12 +1,12 @@ { "name": "demo-plugin", - "version": "20.13.1", + "version": "20.13.2", "private": true, "scripts": { "test:plugin": "detox test --configuration plugin -l verbose" }, "devDependencies": { - "detox": "^20.13.1", + "detox": "^20.13.2", "jest": "^28.0.0" }, "detox": { diff --git a/examples/demo-react-native-detox-instruments/package.json b/examples/demo-react-native-detox-instruments/package.json index fba4ef847e..6e45bedd56 100644 --- a/examples/demo-react-native-detox-instruments/package.json +++ b/examples/demo-react-native-detox-instruments/package.json @@ -1,10 +1,10 @@ { "name": "demo-react-native-detox-instruments", - "version": "20.13.1", + "version": "20.13.2", "private": true, "scripts": {}, "devDependencies": { - "detox": "^20.13.1" + "detox": "^20.13.2" }, "detox": { "configurations": { diff --git a/examples/demo-react-native/package.json b/examples/demo-react-native/package.json index b51e913f15..0766665eac 100644 --- a/examples/demo-react-native/package.json +++ b/examples/demo-react-native/package.json @@ -1,6 +1,6 @@ { "name": "example", - "version": "20.13.1", + "version": "20.13.2", "private": true, "scripts": { "start": "react-native start", @@ -32,7 +32,7 @@ "@types/fs-extra": "^9.0.13", "@types/jest": "^29.2.1", "@types/react": "^18.0.24", - "detox": "^20.13.1", + "detox": "^20.13.2", "fs-extra": "^9.1.0", "jest": "^29.2.1", "ts-jest": "^29.0.3", diff --git a/generation/package.json b/generation/package.json index ba7f05f2da..4e33b736ae 100644 --- a/generation/package.json +++ b/generation/package.json @@ -1,6 +1,6 @@ { "name": "generation", - "version": "20.13.1", + "version": "20.13.2", "description": "Generate wrapper code for native dependencies", "main": "index.js", "private": true, diff --git a/lerna.json b/lerna.json index af6cefbc97..572bb45d3b 100644 --- a/lerna.json +++ b/lerna.json @@ -13,7 +13,7 @@ "generation", "." ], - "version": "20.13.1", + "version": "20.13.2", "npmClient": "npm", "command": { "publish": { diff --git a/package.json b/package.json index e654038ed7..00faee33dd 100644 --- a/package.json +++ b/package.json @@ -54,5 +54,5 @@ "shell-utils": "1.x.x", "unified": "^10.1.0" }, - "version": "20.13.1" + "version": "20.13.2" } From 38486138ba4a6f528e17c401657f62ddfe437523 Mon Sep 17 00:00:00 2001 From: wixmobile Date: Thu, 26 Oct 2023 01:55:58 -0700 Subject: [PATCH 002/185] Publish docs version 20.x --- .../version-20.x/api/actions.md | 4 ++++ .../version-20.x/api/webviews.md | 20 ++++++++++--------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/website/versioned_docs/version-20.x/api/actions.md b/website/versioned_docs/version-20.x/api/actions.md index 75cb77a04d..c249c0aa67 100644 --- a/website/versioned_docs/version-20.x/api/actions.md +++ b/website/versioned_docs/version-20.x/api/actions.md @@ -317,6 +317,10 @@ If the value for a given attribute is null or cannot be otherwise computed, the If the query matches multiple elements, the attributes of all matched elements is returned as an array of objects under the `elements` key. ```js +// import jestExpect from 'expect'; +const jestExpect = require('expect').default; + +// ... const attributes = await element(by.text('Tap Me')).getAttributes(); jestExpect(attributes.text).toBe('Tap Me'); diff --git a/website/versioned_docs/version-20.x/api/webviews.md b/website/versioned_docs/version-20.x/api/webviews.md index 98ddadcb0f..24c5681d57 100644 --- a/website/versioned_docs/version-20.x/api/webviews.md +++ b/website/versioned_docs/version-20.x/api/webviews.md @@ -78,7 +78,7 @@ web.element(by.web.cssSelector('#cssSelector')); ### `by.web.name(name)` -Match elements with the specified name. +Match form input elements with the specified [`name` attribute][name]. ```js web.element(by.web.name('name')); @@ -321,21 +321,21 @@ await expect(web.element(by.web.id('identifier'))).not.toHaveText('Hello World!' [web view expectations]: webviews.md#expectations -[`by.web.id()`]: webviews.md#byidid +[`by.web.id()`]: webviews.md#bywebidid -[`by.web.className()`]: webviews.md#byclassnameclassname +[`by.web.className()`]: webviews.md#bywebclassnameclassname -[`by.web.cssSelector()`]: webviews.md#bycssselectorcssselector +[`by.web.cssSelector()`]: webviews.md#bywebcssselectorcssselector -[`by.web.name()`]: webviews.md#byname +[`by.web.name()`]: webviews.md#bywebnamename -[`by.web.xpath()`]: webviews.md#byxpathxpath +[`by.web.xpath()`]: webviews.md#bywebxpathxpath -[`by.web.href()`]: webviews.md#byhrefhref +[`by.web.href()`]: webviews.md#bywebhrefhref -[`by.web.hrefContains()`]: webviews.md#byhrefcontainshref +[`by.web.hrefContains()`]: webviews.md#bywebhrefcontainshref -[`by.web.tag()`]: webviews.md#bytagtag +[`by.web.tag()`]: webviews.md#bywebtagtag [`atIndex()`]: webviews.md#atindexindex @@ -359,6 +359,8 @@ await expect(web.element(by.web.id('identifier'))).not.toHaveText('Hello World!' [`moveCursorToEnd()`]: webviews.md#movecursortoend +[name]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input#name + [`runScript()`]: webviews.md#runscriptscript [`runScriptWithArgs()`]: webviews.md#runscriptwithargsscript-args From fa5dcf858961a0cf11301b2bcfea98d8f47bbc07 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Thu, 26 Oct 2023 13:11:17 +0300 Subject: [PATCH 003/185] refactor: extract global typings (#4243) --- detox/.eslintignore | 1 + detox/detox.d.ts | 1823 +++++++++++++++++++++++++ detox/globals.d.ts | 23 + detox/index.d.ts | 1843 +------------------------- detox/src/android/core/WebElement.js | 2 +- detox/tsconfig.json | 3 + 6 files changed, 1853 insertions(+), 1842 deletions(-) create mode 100644 detox/detox.d.ts create mode 100644 detox/globals.d.ts diff --git a/detox/.eslintignore b/detox/.eslintignore index 435d2f7ce2..1f41a35cbc 100644 --- a/detox/.eslintignore +++ b/detox/.eslintignore @@ -1,3 +1,4 @@ +*.d.ts /src/android/espressoapi/**/*.js /coverage /ios diff --git a/detox/detox.d.ts b/detox/detox.d.ts new file mode 100644 index 0000000000..316493416f --- /dev/null +++ b/detox/detox.d.ts @@ -0,0 +1,1823 @@ +// TypeScript definitions for Detox +// Original authors (from DefinitelyTyped): +// * Jane Smith +// * Tareq El-Masri +// * Steve Chun +// * Hammad Jutt +// * pera +// * Max Komarychev +// * Dor Ben Baruch + +import { BunyanDebugStreamOptions } from 'bunyan-debug-stream'; + +declare global { + namespace Detox { + //#region DetoxConfig + + interface DetoxConfig extends DetoxConfigurationCommon { + /** + * @example extends: './relative/detox.config' + * @example extends: '@my-org/detox-preset' + */ + extends?: string; + + apps?: Record; + devices?: Record; + selectedConfiguration?: string; + configurations: Record; + } + + type DetoxConfigurationCommon = { + artifacts?: false | DetoxArtifactsConfig; + behavior?: DetoxBehaviorConfig; + logger?: DetoxLoggerConfig; + session?: DetoxSessionConfig; + testRunner?: DetoxTestRunnerConfig; + }; + + interface DetoxArtifactsConfig { + rootDir?: string; + pathBuilder?: string; + plugins?: { + log?: 'none' | 'failing' | 'all' | DetoxLogArtifactsPluginConfig; + screenshot?: 'none' | 'manual' | 'failing' | 'all' | DetoxScreenshotArtifactsPluginConfig; + video?: 'none' | 'failing' | 'all' | DetoxVideoArtifactsPluginConfig; + instruments?: 'none' | 'all' | DetoxInstrumentsArtifactsPluginConfig; + uiHierarchy?: 'disabled' | 'enabled' | DetoxUIHierarchyArtifactsPluginConfig; + + [pluginId: string]: unknown; + }; + } + + interface DetoxBehaviorConfig { + init?: { + /** + * By default, Detox exports `device`, `expect`, `element`, `by` and `waitFor` + * as global variables. If you want to control their initialization manually, + * set this property to `false`. + * + * This is useful when during E2E tests you also need to run regular expectations + * in Node.js. Jest's `expect` for instance, will not be overridden by Detox when + * this option is used. + */ + exposeGlobals?: boolean; + /** + * By default, Detox will uninstall and install the app upon initialization. + * If you wish to reuse the existing app for a faster run, set the property to + * `false`. + */ + reinstallApp?: boolean; + /** + * When false, `detox test` command always deletes the shared lock file on start, + * assuming it had been left from the previous, already finished test session. + * The lock file contains information about busy and free devices and ensures + * no device can be used simultaneously by multiple test workers. + * + * Setting it to **true** might be useful when if you need to run multiple + * `detox test` commands in parallel, e.g. test a few configurations at once. + * + * @default false + */ + keepLockFile?: boolean; + }; + launchApp?: 'auto' | 'manual'; + cleanup?: { + shutdownDevice?: boolean; + }; + } + + type _DetoxLoggerOptions = Omit; + + interface DetoxLoggerConfig { + /** + * Log level filters the messages printed to your terminal, + * and it does not affect the logs written to the artifacts. + * + * Use `info` by default. + * Use `error` or warn when you want to make the output as silent as possible. + * Use `debug` to control what generally is happening under the hood. + * Use `trace` when troubleshooting specific issues. + * + * @default 'info' + */ + level?: DetoxLogLevel; + /** + * When enabled, hijacks all the console methods (console.log, console.warn, etc) + * so that the messages printed via them are formatted and saved as Detox logs. + * + * @default true + */ + overrideConsole?: boolean; + /** + * Since Detox is using + * {@link https://www.npmjs.com/package/bunyan-debug-stream bunyan-debug-stream} + * for printing logs, all its options are exposed for sake of simplicity + * of customization. + * + * The only exception is {@link BunyanDebugStreamOptions#out} option, + * which is always set to `process.stdout`. + * + * You can also pass a callback function to override the logger config + * programmatically, e.g. depending on the selected log level. + * + * @see {@link BunyanDebugStreamOptions} + */ + options?: _DetoxLoggerOptions | ((config: Partial) => _DetoxLoggerOptions); + } + + interface DetoxSessionConfig { + autoStart?: boolean; + debugSynchronization?: number; + server?: string; + sessionId?: string; + } + + interface DetoxTestRunnerConfig { + args?: { + /** + * The command to use for runner: 'jest', 'nyc jest', + */ + $0: string; + /** + * The positional arguments to pass to the runner. + */ + _?: string[]; + /** + * Any other properties recognized by test runner + */ + [prop: string]: unknown; + }; + + /** + * This is an add-on section used by our Jest integration code (but not Detox core itself). + * In other words, if you’re implementing (or using) a custom integration with some other test runner, feel free to define a section for yourself (e.g. `testRunner.mocha`) + */ + jest?: { + /** + * Environment setup timeout + * + * As a part of the environment setup, Detox boots the device and installs the apps. + * If that takes longer than the specified value, the entire test suite will be considered as failed, e.g.: + * ```plain text + * FAIL e2e/starter.test.js + * ● Test suite failed to run + * + * Exceeded timeout of 300000ms while setting up Detox environment + * ``` + * + * The default value is 5 minutes. + * + * @default 300000 + * @see {@link https://jestjs.io/docs/configuration/#testenvironment-string} + */ + setupTimeout?: number | undefined; + /** + * Environemnt teardown timeout + * + * If the environment teardown takes longer than the specified value, Detox will throw a timeout error. + * The default value is half a minute. + * + * @default 30000 (30 seconds) + * @see {@link https://jestjs.io/docs/configuration/#testenvironment-string} + */ + teardownTimeout?: number | undefined; + /** + * Jest provides an API to re-run individual failed tests: `jest.retryTimes(count)`. + * When Detox detects the use of this API, it suppresses its own CLI retry mechanism controlled via `detox test … --retries ` or {@link DetoxTestRunnerConfig#retries}. + * The motivation is simple – activating the both mechanisms is apt to increase your test duration dramatically, if your tests are flaky. + * If you wish nevertheless to use both the mechanisms simultaneously, set it to `true`. + * + * @default false + * @see {@link https://jestjs.io/docs/29.0/jest-object#jestretrytimesnumretries-options} + */ + retryAfterCircusRetries?: boolean; + /** + * By default, Jest prints the test names and their status (_passed_ or _failed_) at the very end of the test session. + * When enabled, it makes Detox to print messages like these each time the new test starts and ends: + * ```plain text + * 18:03:36.258 detox[40125] i Sanity: should have welcome screen + * 18:03:37.495 detox[40125] i Sanity: should have welcome screen [OK] + * 18:03:37.496 detox[40125] i Sanity: should show hello screen after tap + * 18:03:38.928 detox[40125] i Sanity: should show hello screen after tap [OK] + * 18:03:38.929 detox[40125] i Sanity: should show world screen after tap + * 18:03:40.351 detox[40125] i Sanity: should show world screen after tap [OK] + * ``` + * By default, it is enabled automatically in test sessions with a single worker. + * And vice versa, if multiple tests are executed concurrently, Detox turns it off to avoid confusion in the log. + * Use boolean values, `true` or `false`, to turn off the automatic choice. + * + * @default undefined + */ + reportSpecs?: boolean | undefined; + /** + * In the environment setup phase, Detox boots the device and installs the apps. + * This flag tells Detox to print messages like these every time the device gets assigned to a specific suite: + * + * ```plain text + * 18:03:29.869 detox[40125] i starter.test.js is assigned to 4EC84833-C7EA-4CA3-A6E9-5C30A29EA596 (iPhone 12 Pro Max) + * ``` + * + * @default true + */ + reportWorkerAssign?: boolean | undefined; + }; + /** + * Retries count. Zero means a single attempt to run tests. + */ + retries?: number; + /** + * When true, tells Detox CLI to cancel next retrying if it gets + * at least one report about a permanent test suite failure. + * Has no effect, if {@link DetoxTestRunnerConfig#retries} is + * undefined or set to zero. + * + * @default false + * @see {DetoxInternals.DetoxTestFileReport#isPermanentFailure} + */ + bail?: boolean; + /** + * Custom handler to process --inspect-brk CLI flag. + * Use it when you rely on another test runner than Jest to mutate the config. + */ + inspectBrk?: (config: DetoxTestRunnerConfig) => void; + /** + * Forward environment variables to the spawned test runner + * accordingly to the given CLI argument overrides. + * + * If false, Detox CLI will be only printing a hint message on + * how to start the test runner using environment variables, + * in case when a user wants to avoid using Detox CLI. + * + * @default false + */ + forwardEnv?: boolean; + } + + 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. + * + * @see Device#selectApp + */ + name?: string; + }; + + type DetoxDeviceConfig = DetoxBuiltInDeviceConfig | DetoxCustomDriverConfig; + + interface DetoxLogArtifactsPluginConfig { + enabled?: boolean; + keepOnlyFailedTestsArtifacts?: boolean; + } + + interface DetoxScreenshotArtifactsPluginConfig { + enabled?: boolean; + keepOnlyFailedTestsArtifacts?: boolean; + shouldTakeAutomaticSnapshots?: boolean; + takeWhen?: { + testStart?: boolean; + testFailure?: boolean; + testDone?: boolean; + appNotReady?: boolean; + }; + } + + interface DetoxVideoArtifactsPluginConfig { + enabled?: boolean; + keepOnlyFailedTestsArtifacts?: boolean; + android?: Partial<{ + size: [number, number]; + bitRate: number; + timeLimit: number; + verbose: boolean; + }>; + simulator?: Partial<{ + codec: string; + }>; + } + + interface DetoxInstrumentsArtifactsPluginConfig { + enabled?: boolean; + } + + interface DetoxUIHierarchyArtifactsPluginConfig { + enabled?: boolean; + } + + type DetoxBuiltInAppConfig = (DetoxIosAppConfig | DetoxAndroidAppConfig); + + interface DetoxIosAppConfig { + type: 'ios.app'; + binaryPath: string; + bundleId?: string; + build?: string; + start?: string; + launchArgs?: Record; + } + + interface DetoxAndroidAppConfig { + type: 'android.apk'; + binaryPath: string; + bundleId?: string; + build?: string; + start?: string; + testBinaryPath?: string; + launchArgs?: Record; + /** + * TCP ports to `adb reverse` upon the installation. + * E.g. 8081 - to be able to access React Native packager in Debug mode. + * + * @example [8081] + */ + reversePorts?: number[]; + } + + interface DetoxCustomAppConfig { + type: string; + + [prop: string]: unknown; + } + + type DetoxBuiltInDeviceConfig = + | DetoxIosSimulatorDriverConfig + | DetoxAttachedAndroidDriverConfig + | DetoxAndroidEmulatorDriverConfig + | DetoxGenymotionCloudDriverConfig; + + interface DetoxIosSimulatorDriverConfig { + type: 'ios.simulator'; + device: string | Partial; + bootArgs?: string; + } + + interface DetoxSharedAndroidDriverConfig { + forceAdbInstall?: boolean; + utilBinaryPaths?: string[]; + } + + interface DetoxAttachedAndroidDriverConfig extends DetoxSharedAndroidDriverConfig { + type: 'android.attached'; + device: string | { adbName: string }; + } + + interface DetoxAndroidEmulatorDriverConfig extends DetoxSharedAndroidDriverConfig { + type: 'android.emulator'; + device: string | { avdName: string }; + bootArgs?: string; + gpuMode?: 'auto' | 'host' | 'swiftshader_indirect' | 'angle_indirect' | 'guest' | 'off'; + headless?: boolean; + /** + * @default true + */ + readonly?: boolean; + } + + interface DetoxGenymotionCloudDriverConfig extends DetoxSharedAndroidDriverConfig { + type: 'android.genycloud'; + device: string | { recipeUUID: string; } | { recipeName: string; }; + } + + interface DetoxCustomDriverConfig { + type: string; + + [prop: string]: unknown; + } + + interface IosSimulatorQuery { + id: string; + type: string; + name: string; + os: string; + } + + type DetoxConfiguration = DetoxConfigurationCommon & ( + | DetoxConfigurationSingleApp + | DetoxConfigurationMultiApps + ); + + interface DetoxConfigurationSingleApp { + device: DetoxAliasedDevice; + app: DetoxAliasedApp; + } + + interface DetoxConfigurationMultiApps { + device: DetoxAliasedDevice; + apps: DetoxAliasedApp[]; + } + + type DetoxAliasedDevice = string | DetoxDeviceConfig; + + type DetoxAliasedApp = string | DetoxAppConfig; + + //#endregion + + interface DetoxExportWrapper { + readonly device: Device; + + readonly element: ElementFacade; + + readonly waitFor: WaitForFacade; + + readonly expect: ExpectFacade; + + readonly by: ByFacade; + + readonly web: WebFacade; + + readonly DetoxConstants: { + userNotificationTriggers: { + push: 'push'; + calendar: 'calendar'; + timeInterval: 'timeInterval'; + location: 'location'; + }; + userActivityTypes: { + searchableItem: string; + browsingWeb: string; + }, + searchableItemActivityIdentifier: string; + }; + + /** + * Detox logger instance. Can be used for saving user logs to the general log file. + */ + readonly log: Logger; + + /** + * @deprecated + * + * Deprecated - use {@link Detox.Logger#trace} + * Detox tracer instance. Can be used for building timelines in Google Event Tracing format. + */ + readonly trace: { + /** @deprecated */ + readonly startSection: (name: string) => void; + /** @deprecated */ + readonly endSection: (name: string) => void; + }; + + /** + * Trace a single call, with a given name and arguments. + * + * @deprecated + * @param sectionName The name of the section to trace. + * @param promiseOrFunction Promise or a function that provides a promise. + * @param args Optional arguments to pass to the trace. + * @returns The returned value of the traced call. + * @see https://wix.github.io/Detox/docs/19.x/api/detox-object-api/#detoxtracecall + */ + readonly traceCall: (event: string, action: () => Promise, args?: Record) => Promise; + } + + interface Logger { + readonly level: DetoxLogLevel; + + readonly fatal: _LogMethod; + readonly error: _LogMethod; + readonly warn: _LogMethod; + readonly info: _LogMethod; + readonly debug: _LogMethod; + readonly trace: _LogMethod; + + child(context?: Partial): Logger; + } + + /** @internal */ + interface _LogMethod extends _LogMethodSignature { + readonly begin: _LogMethodSignature; + readonly complete: _CompleteMethodSignature; + readonly end: _LogMethodSignature; + } + + /** @internal */ + interface _LogMethodSignature { + (...args: unknown[]): void + (event: LogEvent, ...args: unknown[]): void; + } + + /** @internal */ + interface _CompleteMethodSignature { + (message: string, action: T | (() => T)): T; + (event: LogEvent, message: string, action: T | (() => T)): T; + } + + type LogEvent = { + /** Use when there's a risk of logging several parallel duration events. */ + id?: string | number; + /** Optional. Event categories (tags) to facilitate filtering. */ + cat?: string | string[]; + /** Optional. Color name (applicable in Google Chrome Trace Format) */ + cname?: string; + + /** Reserved property. Process ID. */ + pid?: never; + /** Reserved property. Thread ID. */ + tid?: never; + /** Reserved property. Timestamp. */ + ts?: never; + /** Reserved property. Event phase. */ + ph?: never; + + [customProperty: string]: unknown; + }; + + type DetoxLogLevel = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace'; + + type Point2D = { + x: number, + y: number, + } + + /** + * A construct allowing for the querying and modification of user arguments passed to an app upon launch by Detox. + * + * @see AppLaunchArgs#modify + * @see AppLaunchArgs#reset + * @see AppLaunchArgs#get + */ + interface AppLaunchArgs { + /** + * Shared (global) arguments that are not specific to a particular application. + * Selecting another app does not reset them, yet they still can be overridden + * by configuring app-specific launch args. + * @see Device#selectApp + * @see AppLaunchArgs + */ + readonly shared: ScopedAppLaunchArgs; + + /** + * Modify the launch-arguments via a modifier object, according to the following logic: + * - Non-nullish modifier properties would set a new value or override the previous value of + * existing properties with the same name. + * - Modifier properties set to either `undefined` or `null` would delete the corresponding property + * if it existed. + * These custom app launch arguments get erased whenever you select a different application. + * If you need to share them between all the applications, use {@link AppLaunchArgs#shared} property. + * Note: app-specific launch args have a priority over shared ones. + * + * @param modifier The modifier object. + * @example + * // With current launch arguments set to: + * // { + * // mockServerPort: 1234, + * // mockServerCredentials: 'user@test.com:12345678', + * // } + * device.appLaunchArgs.modify({ + * mockServerPort: 4321, + * mockServerCredentials: null, + * mockServerToken: 'abcdef', + * }); + * await device.launchApp(); + * // ==> launch-arguments become: + * // { + * // mockServerPort: 4321, + * // mockServerToken: 'abcdef', + * // } + */ + modify(modifier: object): this; + + /** + * Reset all app-specific launch arguments (back to an empty object). + * If you need to reset the shared launch args, use {@link AppLaunchArgs#shared}. + */ + reset(): this; + + /** + * Get all currently set launch arguments (including shared ones). + * @returns An object containing all launch-arguments. + * Note: mutating the values inside the result object is pointless, as it is immutable. + */ + get(): object; + } + + /** + * Shared (global) arguments that are not specific to a particular application. + */ + interface ScopedAppLaunchArgs { + /** @see AppLaunchArgs#modify */ + modify(modifier: object): this; + + /** @see AppLaunchArgs#reset */ + reset(): this; + + /** @see AppLaunchArgs#get */ + get(): object; + } + + type DigitWithoutZero = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; + type Digit = 0 | DigitWithoutZero; + type BatteryLevel = `${Digit}` | `${DigitWithoutZero}${Digit}` | "100"; + + interface Device { + /** + * Holds the environment-unique ID of the device, namely, the adb ID on Android (e.g. emulator-5554) and the Mac-global simulator UDID on iOS - + * as used by simctl (e.g. AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE). + */ + id: string; + /** + * Holds a descriptive name of the device. Example: emulator-5554 (Pixel_API_29) + */ + name: string; + + /** + * Select the current app (relevant only to multi-app configs) by its name. + * After execution, all app-specific device methods will target the selected app. + * + * @see DetoxAppConfig#name + * @example + * await device.selectApp('passenger'); + * await device.launchApp(); // passenger + * // ... run tests for the passenger app + * await device.uninstallApp(); // passenger + * await device.selectApp('driver'); + * await device.installApp(); // driver + * await device.launchApp(); // driver + * // ... run tests for the driver app + * await device.terminateApp(); // driver + */ + selectApp(app: string): Promise; + + /** + * Launch the app. + * + *

For info regarding launch arguments, refer to the [dedicated guide](https://wix.github.io/Detox/docs/api/launch-args). + * + * @example + * // Terminate the app and launch it again. If set to false, the simulator will try to bring app from background, + * // if the app isn't running, it will launch a new instance. default is false + * await device.launchApp({newInstance: true}); + * @example + * // Grant or deny runtime permissions for your application. + * await device.launchApp({permissions: {calendar: 'YES'}}); + * @example + * // Mock opening the app from URL to test your app's deep link handling mechanism. + * await device.launchApp({url: url}); + * @example + * // Start the app with some custom arguments. + * await device.launchApp({ + * launchArgs: {arg1: 1, arg2: "2"}, + * }); + */ + launchApp(config?: DeviceLaunchAppConfig): Promise; + + /** + * Relaunch the app. Convenience method that calls {@link Device#launchApp} + * with { newInstance: true } override. + * + * @deprecated + * @param config + * @see Device#launchApp + */ + relaunchApp(config?: DeviceLaunchAppConfig): Promise; + + /** + * Access the user-defined launch-arguments predefined through static scopes such as the Detox configuration file and + * command-line arguments. This access allows - through dedicated methods, for both value-querying and + * modification (see {@link AppLaunchArgs}). + * Refer to the [dedicated guide](https://wix.github.io/Detox/docs/api/launch-args) for complete details. + * + * @example + * // With Detox being preconfigured statically to use these arguments in app launch: + * // { + * // mockServerPort: 1234, + * // } + * // The following code would result in these arguments eventually passed into the launched app: + * // { + * // mockServerPort: 4321, + * // mockServerToken: 'uvwxyz', + * // } + * device.appLaunchArgs.modify({ + * mockServerPort: 4321, + * mockServerToken: 'abcdef', + * }); + * await device.launchApp({ launchArgs: { mockServerToken: 'uvwxyz' } }); + * + * @see AppLaunchArgs + */ + appLaunchArgs: AppLaunchArgs; + + /** + * Terminate the app. + * + * @example + * // By default, terminateApp() with no params will terminate the app + * await device.terminateApp(); + * @example + * // To terminate another app, specify its bundle id + * await device.terminateApp('other.bundle.id'); + */ + terminateApp(bundle?: string): Promise; + + /** + * Send application to background by bringing com.apple.springboard to the foreground. + * Combining sendToHome() with launchApp({newInstance: false}) will simulate app coming back from background. + * @example + * await device.sendToHome(); + * await device.launchApp({newInstance: false}); + */ + sendToHome(): Promise; + + /** + * If this is a React Native app, reload the React Native JS bundle. This action is much faster than device.launchApp(), and can be used if you just need to reset your React Native logic. + * + * @example await device.reloadReactNative() + */ + reloadReactNative(): Promise; + + /** + * By default, installApp() with no params will install the app file defined in the current configuration. + * To install another app, specify its path + * @example await device.installApp(); + * @example await device.installApp('path/to/other/app'); + */ + installApp(path?: any): Promise; + + /** + * By default, uninstallApp() with no params will uninstall the app defined in the current configuration. + * To uninstall another app, specify its bundle id + * @example await device.installApp('other.bundle.id'); + */ + uninstallApp(bundle?: string): Promise; + + /** + * Mock opening the app from URL. sourceApp is an optional parameter to specify source application bundle id. + */ + openURL(url: { url: string; sourceApp?: string }): Promise; + + /** + * Mock handling of received user notification when app is in foreground. + */ + sendUserNotification(...params: any[]): Promise; + + /** + * Mock handling of received user activity when app is in foreground. + */ + sendUserActivity(...params: any[]): Promise; + + /** + * Takes "portrait" or "landscape" and rotates the device to the given orientation. Currently only available in the iOS Simulator. + */ + setOrientation(orientation: Orientation): Promise; + + /** + * Sets the simulator/emulator location to the given latitude and longitude. + * + *

On iOS `setLocation` is dependent on [fbsimctl](https://github.com/facebook/idb/tree/4b7929480c3c0f158f33f78a5b802c1d0e7030d2/fbsimctl) + * which [is now deprecated](https://github.com/wix/Detox/issues/1371). + * If `fbsimctl` is not installed, the command will fail, asking for it to be installed. + * + *

On Android `setLocation` will work with both Android Emulator (bundled with Android development tools) and Genymotion. + * The correct permissions must be set in your app manifest. + * + * @example await device.setLocation(32.0853, 34.7818); + */ + setLocation(lat: number, lon: number): Promise; + + /** + * (iOS only) Override simulator’s status bar. + * @platform iOS + * @param {config} config status bar configuration. + * @example + * await device.setStatusBar({ + * time: "12:34", + * // Set the date or time to a fixed value. + * // If the string is a valid ISO date string it will also set the date on relevant devices. + * dataNetwork: "wifi", + * // If specified must be one of 'hide', 'wifi', '3g', '4g', 'lte', 'lte-a', 'lte+', '5g', '5g+', '5g-uwb', or '5g-uc'. + * wifiMode: "failed", + * // If specified must be one of 'searching', 'failed', or 'active'. + * wifiBars: "2", + * // If specified must be 0-3. + * cellularMode: "searching", + * // If specified must be one of 'notSupported', 'searching', 'failed', or 'active'. + * cellularBars: "3", + * // If specified must be 0-4. + * operatorName: "A1", + * // Set the cellular operator/carrier name. Use '' for the empty string. + * batteryState: "charging", + * // If specified must be one of 'charging', 'charged', or 'discharging'. + * batteryLevel: "50", + * // If specified must be 0-100. + * }); + */ + setStatusBar(config: { + time?: string, + dataNetwork?: "hide" | "wifi" | "3g" | "4g" | "lte" | "lte-a" | "lte+" | "5g" | "5g+" | "5g-uwb" | "5g-uc", + wifiMode?: "searching" |"failed" | "active", + wifiBars?: "0" | "1" | "2" | "3", + cellularMode?: "notSupported" | "searching" | "failed" | "active", + cellularBars?: "0" | "1" | "2" | "3" | "4", + operatorName?: string; + batteryState?: "charging" | "charged" | "discharging", + batteryLevel?: BatteryLevel, + }): Promise; + + /** + * Disable network synchronization mechanism on preferred endpoints. Useful if you want to on skip over synchronizing on certain URLs. + * + * @example await device.setURLBlacklist(['.*127.0.0.1.*']); + */ + setURLBlacklist(urls: string[]): Promise; + + /** + * Temporarily disable synchronization (idle/busy monitoring) with the app - namely, stop waiting for the app to go idle before moving forward in the test execution. + * + *

This API is useful for cases where test assertions must be made in an area of your application where it is okay for it to ever remain partly *busy* (e.g. due to an + * endlessly repeating on-screen animation). However, using it inherently suggests that you are likely to resort to applying `sleep()`'s in your test code - testing + * that area, **which is not recommended and can never be 100% stable. + * **Therefore, as a rule of thumb, test code running "inside" a sync-disabled mode must be reduced to the bare minimum. + * + *

Note: Synchronization is enabled by default, and it gets **reenabled on every launch of a new instance of the app.** + * + * @example await device.disableSynchronization(); + */ + disableSynchronization(): Promise; + + /** + * Reenable synchronization (idle/busy monitoring) with the app - namely, resume waiting for the app to go idle before moving forward in the test execution, after a + * previous disabling of it through a call to `device.disableSynchronization()`. + * + *

Warning: Making this call would resume synchronization **instantly**, having its returned promise only resolve when the app becomes idle again. + * In other words, this **must only be called after you navigate back to "the safe zone", where the app should be able to eventually become idle again**, or it would + * remain suspended "forever" (i.e. until a safeguard time-out expires). + * + * @example await device.enableSynchronization(); + */ + enableSynchronization(): Promise; + + /** + * Resets the Simulator to clean state (like the Simulator > Reset Content and Settings... menu item), especially removing previously set permissions. + * + * @example await device.resetContentAndSettings(); + */ + resetContentAndSettings(): Promise; + + /** + * Returns the current device, ios or android. + * + * @example + * if (device.getPlatform() === 'ios') { + * await expect(loopSwitch).toHaveValue('1'); + * } + */ + getPlatform(): 'ios' | 'android'; + + /** + * Takes a screenshot on the device and schedules putting it in the artifacts folder upon completion of the current test. + * @param name for the screenshot artifact + * @returns a temporary path to the screenshot. + * @example + * test('Menu items should have logout', async () => { + * const tempPath = await device.takeScreenshot('tap on menu'); + * // The temporary path will remain valid until the test completion. + * // Afterwards, the screenshot will be moved, e.g.: + * // * on success, to: /✓ Menu items should have Logout/tap on menu.png + * // * on failure, to: /✗ Menu items should have Logout/tap on menu.png + * }); + */ + takeScreenshot(name: string): Promise; + + /** + * (iOS only) Saves a view hierarchy snapshot (*.viewhierarchy) of the currently opened application + * to a temporary folder and schedules putting it to the artifacts folder upon the completion of + * the current test. The file can be opened later in Xcode 12.0 and above. + * @see https://developer.apple.com/documentation/xcode-release-notes/xcode-12-release-notes#:~:text=57933113 + * @param [name="capture"] optional name for the *.viewhierarchy artifact + * @returns a temporary path to the captured view hierarchy snapshot. + * @example + * test('Menu items should have logout', async () => { + * await device.captureViewHierarchy('myElements'); + * // The temporary path will remain valid until the test completion. + * // Afterwards, the artifact will be moved, e.g.: + * // * on success, to: /✓ Menu items should have Logout/myElements.viewhierarchy + * // * on failure, to: /✗ Menu items should have Logout/myElements.viewhierarchy + * }); + */ + captureViewHierarchy(name?: string): Promise; + + /** + * Simulate shake (iOS Only) + */ + shake(): Promise; + + /** + * Toggles device enrollment in biometric auth (TouchID or FaceID) (iOS Only) + * @example await device.setBiometricEnrollment(true); + * @example await device.setBiometricEnrollment(false); + */ + setBiometricEnrollment(enabled: boolean): Promise; + + /** + * Simulates the success of a face match via FaceID (iOS Only) + */ + matchFace(): Promise; + + /** + * Simulates the failure of a face match via FaceID (iOS Only) + */ + unmatchFace(): Promise; + + /** + * Simulates the success of a finger match via TouchID (iOS Only) + */ + matchFinger(): Promise; + + /** + * Simulates the failure of a finger match via TouchID (iOS Only) + */ + unmatchFinger(): Promise; + + /** + * Clears the simulator keychain (iOS Only) + */ + clearKeychain(): Promise; + + /** + * Simulate press back button (Android Only) + * @example await device.pressBack(); + */ + pressBack(): Promise; + + /** + * (Android Only) + * Exposes UiAutomator's UiDevice API (https://developer.android.com/reference/android/support/test/uiautomator/UiDevice). + * This is not a part of the official Detox API, + * it may break and change whenever an update to UiDevice or UiAutomator gradle dependencies ('androidx.test.uiautomator:uiautomator') is introduced. + * UIDevice's autogenerated code reference: https://github.com/wix/Detox/blob/master/detox/src/android/espressoapi/UIDevice.js + */ + getUiDevice(): Promise; + + /** + * (Android Only) + * Runs `adb reverse tcp:PORT tcp:PORT` for the current device + * to enable network requests forwarding on localhost:PORT (computer<->device). + * For more information, see {@link https://www.reddit.com/r/reactnative/comments/5etpqw/what_do_you_call_what_adb_reverse_is_doing|here}. + * This is a no-op when running on iOS. + */ + reverseTcpPort(port: number): Promise; + + /** + * (Android Only) + * Runs `adb reverse --remove tcp:PORT tcp:PORT` for the current device + * to disable network requests forwarding on localhost:PORT (computer<->device). + * For more information, see {@link https://www.reddit.com/r/reactnative/comments/5etpqw/what_do_you_call_what_adb_reverse_is_doing|here}. + * This is a no-op when running on iOS. + */ + unreverseTcpPort(port: number): Promise; + } + + /** + * @deprecated + */ + type DetoxAny = NativeElement & WaitFor; + + interface ElementFacade { + (by: NativeMatcher): IndexableNativeElement; + } + + interface IndexableNativeElement extends NativeElement { + /** + * Choose from multiple elements matching the same matcher using index + * @example await element(by.text('Product')).atIndex(2).tap(); + */ + atIndex(index: number): NativeElement; + } + + interface NativeElement extends NativeElementActions { + } + + interface ByFacade { + /** + * by.id will match an id that is given to the view via testID prop. + * @example + * // In a React Native component add testID like so: + * + * // Then match with by.id: + * await element(by.id('tap_me')); + * await element(by.id(/^tap_[a-z]+$/)); + */ + id(id: string | RegExp): NativeMatcher; + + /** + * Find an element by text, useful for text fields, buttons. + * @example + * await element(by.text('Tap Me')); + * await element(by.text(/^Tap .*$/)); + */ + text(text: string | RegExp): NativeMatcher; + + /** + * Find an element by accessibilityLabel on iOS, or by contentDescription on Android. + * @example + * await element(by.label('Welcome')); + * await element(by.label(/[a-z]+/i)); + */ + label(label: string | RegExp): NativeMatcher; + + /** + * Find an element by native view type. + * @example await element(by.type('RCTImageView')); + */ + type(nativeViewType: string): NativeMatcher; + + /** + * Find an element with an accessibility trait. (iOS only) + * @example await element(by.traits(['button'])); + */ + traits(traits: string[]): NativeMatcher; + + /** + * Collection of web matchers + */ + readonly web: ByWebFacade; + } + + interface ByWebFacade { + /** + * Find an element on the DOM tree by its id + * @param id + * @example + * web.element(by.web.id('testingh1')) + */ + id(id: string): WebMatcher; + + /** + * Find an element on the DOM tree by its CSS class + * @param className + * @example + * web.element(by.web.className('a')) + */ + className(className: string): WebMatcher; + + /** + * Find an element on the DOM tree matching the given CSS selector + * @param cssSelector + * @example + * web.element(by.web.cssSelector('#cssSelector')) + */ + cssSelector(cssSelector: string): WebMatcher; + + /** + * Find an element on the DOM tree by its "name" attribute + * @param name + * @example + * web.element(by.web.name('sec_input')) + */ + name(name: string): WebMatcher; + + /** + * Find an element on the DOM tree by its XPath + * @param xpath + * @example + * web.element(by.web.xpath('//*[@id="testingh1-1"]')) + */ + xpath(xpath: string): WebMatcher; + + /** + * Find an element on the DOM tree by its link text (href content) + * @param linkText + * @example + * web.element(by.web.href('disney.com')) + */ + href(linkText: string): WebMatcher; + + /** + * Find an element on the DOM tree by its partial link text (href content) + * @param linkTextFragment + * @example + * web.element(by.web.hrefContains('disney')) + */ + hrefContains(linkTextFragment: string): WebMatcher; + + /** + * Find an element on the DOM tree by its tag name + * @param tag + * @example + * web.element(by.web.tag('mark')) + */ + tag(tagName: string): WebMatcher; + } + + interface NativeMatcher { + /** + * Find an element satisfying all the matchers + * @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'))); + */ + withDescendant(childBy: NativeMatcher): NativeMatcher; + } + + interface WebMatcher { + __web__: any; // prevent type coersion + } + + interface ExpectFacade { + (element: NativeElement): Expect; + + (webElement: WebElement): WebExpect; + } + + interface WebViewElement { + element(webMatcher: WebMatcher): IndexableWebElement; + } + + interface WebFacade extends WebViewElement { + /** + * Gets the webview element as a testing element. + * @param matcher a simple view matcher for the webview element in th UI hierarchy. + * If there is only ONE webview element in the UI hierarchy, its NOT a must to supply it. + * If there are MORE then one webview element in the UI hierarchy you MUST supply are view matcher. + */ + (matcher?: NativeMatcher): WebViewElement; + } + + interface Expect> { + + /** + * Expect the view to be at least N% visible. If no number is provided then defaults to 75%. Negating this + * expectation with a `not` expects the view's visible area to be smaller than N%. + * @param pct optional integer ranging from 1 to 100, indicating how much percent of the view should be + * visible to the user to be accepted. + * @example await expect(element(by.id('mainTitle'))).toBeVisible(35); + */ + toBeVisible(pct?: number): R; + + /** + * Negate the expectation. + * @example await expect(element(by.id('cancelButton'))).not.toBeVisible(); + */ + not: this; + + /** + * Expect the view to not be visible. + * @example await expect(element(by.id('cancelButton'))).toBeNotVisible(); + * @deprecated Use `.not.toBeVisible()` instead. + */ + toBeNotVisible(): R; + + /** + * Expect the view to exist in the UI hierarchy. + * @example await expect(element(by.id('okButton'))).toExist(); + */ + toExist(): R; + + /** + * Expect the view to not exist in the UI hierarchy. + * @example await expect(element(by.id('cancelButton'))).toNotExist(); + * @deprecated Use `.not.toExist()` instead. + */ + toNotExist(): R; + + /** + * Expect the view to be focused. + * @example await expect(element(by.id('emailInput'))).toBeFocused(); + */ + toBeFocused(): R; + + /** + * Expect the view not to be focused. + * @example await expect(element(by.id('passwordInput'))).toBeNotFocused(); + * @deprecated Use `.not.toBeFocused()` instead. + */ + toBeNotFocused(): R; + + /** + * In React Native apps, expect UI component of type to have text. + * In native iOS apps, expect UI elements of type UIButton, UILabel, UITextField or UITextViewIn to have inputText with text. + * @example await expect(element(by.id('mainTitle'))).toHaveText('Welcome back!); + */ + toHaveText(text: string): R; + + /** + * Expects a specific accessibilityLabel, as specified via the `accessibilityLabel` prop in React Native. + * On the native side (in both React Native and pure-native apps), that is equivalent to `accessibilityLabel` + * on iOS and contentDescription on Android. Refer to Detox's documentation in order to learn about caveats + * with accessibility-labels in React Native apps. + * @example await expect(element(by.id('submitButton'))).toHaveLabel('Submit'); + */ + toHaveLabel(label: string): R; + + /** + * In React Native apps, expect UI component to have testID with that id. + * In native iOS apps, expect UI element to have accessibilityIdentifier with that id. + * @example await expect(element(by.text('Submit'))).toHaveId('submitButton'); + */ + toHaveId(id: string): R; + + /** + * Expects a toggle-able element (e.g. a Switch or a Check-Box) to be on/checked or off/unchecked. + * As a reference, in react-native, this is the equivalent switch component. + * @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('temperatureDial'))).toHaveValue('25'); + */ + toHaveValue(value: any): R; + + /** + * Expect Slider to have a position (0 - 1). + * Can have an optional tolerance to take into account rounding issues on ios + * @example await expect(element(by.id('SliderId'))).toHavePosition(0.75); + * @example await expect(element(by.id('SliderId'))).toHavePosition(0.74, 0.1); + */ + toHaveSliderPosition(position: number, tolerance?: number): Promise; + } + + interface WaitForFacade { + /** + * This API polls using the given expectation continuously until the expectation is met. Use manual synchronization with waitFor only as a last resort. + * NOTE: Every waitFor call must set a timeout using withTimeout(). Calling waitFor without setting a timeout will do nothing. + * @example await waitFor(element(by.id('bigButton'))).toExist().withTimeout(2000); + */ + (element: NativeElement): Expect; + } + + interface WaitFor { + /** + * Waits for the condition to be met until the specified time (millis) have elapsed. + * @example await waitFor(element(by.id('bigButton'))).toExist().withTimeout(2000); + */ + withTimeout(millis: number): Promise; + + /** + * Performs the action repeatedly on the element until an expectation is met + * @example await waitFor(element(by.text('Item #5'))).toBeVisible().whileElement(by.id('itemsList')).scroll(50, 'down'); + */ + whileElement(by: NativeMatcher): NativeElement & WaitFor; + + // TODO: not sure about & WaitFor - check if we can chain whileElement multiple times + } + + interface NativeElementActions { + /** + * Simulate tap on an element + * @param point relative coordinates to the matched element (the element size could changes on different devices or even when changing the device font size) + * @example await element(by.id('tappable')).tap(); + * @example await element(by.id('tappable')).tap({ x:5, y:10 }); + */ + tap(point?: Point2D): Promise; + + /** + * Simulate long press on an element + * @param duration (iOS only) custom press duration time, in milliseconds. Optional (default is 1000ms). + * @example await element(by.id('tappable')).longPress(); + */ + longPress(duration?: number): Promise; + + /** + * Simulate long press on an element and then drag it to the position of the target element. (iOS Only) + * @example await element(by.id('draggable')).longPressAndDrag(2000, NaN, NaN, element(by.id('target')), NaN, NaN, 'fast', 0); + */ + 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 + * @example await element(by.id('tappable')).multiTap(3); + */ + multiTap(times: number): Promise; + + /** + * Simulate tap at a specific point on an element. + * Note: The point coordinates are relative to the matched element and the element size could changes on different devices or even when changing the device font size. + * @example await element(by.id('tappable')).tapAtPoint({ x:5, y:10 }); + * @deprecated Use `.tap()` instead. + */ + tapAtPoint(point: Point2D): Promise; + + /** + * Use the builtin keyboard to type text into a text field. + * @example await element(by.id('textField')).typeText('passcode'); + */ + typeText(text: string): Promise; + + /** + * Paste text into a text field. + * @example await element(by.id('textField')).replaceText('passcode again'); + */ + replaceText(text: string): Promise; + + /** + * Clear text from a text field. + * @example await element(by.id('textField')).clearText(); + */ + clearText(): Promise; + + /** + * Taps the backspace key on the built-in keyboard. + * @example await element(by.id('textField')).tapBackspaceKey(); + */ + tapBackspaceKey(): Promise; + + /** + * Taps the return key on the built-in keyboard. + * @example await element(by.id('textField')).tapReturnKey(); + */ + tapReturnKey(): Promise; + + /** + * Scrolls a given amount of pixels in the provided direction, starting from the provided start positions. + * @param pixels - independent device pixels + * @param direction - left/right/up/down + * @param startPositionX - the X starting scroll position, in percentage; valid input: `[0.0, 1.0]`, `NaN`; default: `NaN`—choose the best value automatically + * @param startPositionY - the Y starting scroll position, in percentage; valid input: `[0.0, 1.0]`, `NaN`; default: `NaN`—choose the best value automatically + * @example await element(by.id('scrollView')).scroll(100, 'down', NaN, 0.85); + * @example await element(by.id('scrollView')).scroll(100, 'up'); + */ + scroll( + pixels: number, + direction: Direction, + startPositionX?: number, + startPositionY?: number + ): Promise; + + /** + * Scroll to index. + * @example await element(by.id('scrollView')).scrollToIndex(10); + */ + scrollToIndex( + index: Number + ): Promise; + + /** + * Scroll to edge. + * @example await element(by.id('scrollView')).scrollTo('bottom'); + * @example await element(by.id('scrollView')).scrollTo('top'); + */ + scrollTo(edge: Direction): Promise; + + /** + * Adjust slider to position. + * @example await element(by.id('slider')).adjustSliderToPosition(0.75); + */ + adjustSliderToPosition(newPosition: number): Promise; + + /** + * Swipes in the provided direction at the provided speed, started from percentage. + * @param speed default: `fast` + * @param percentage screen percentage to swipe; valid input: `[0.0, 1.0]` + * @param optional normalizedStartingPointX X coordinate of swipe starting point, relative to the view width; valid input: `[0.0, 1.0]` + * @param normalizedStartingPointY Y coordinate of swipe starting point, relative to the view height; valid input: `[0.0, 1.0]` + * @example await element(by.id('scrollView')).swipe('down'); + * @example await element(by.id('scrollView')).swipe('down', 'fast'); + * @example await element(by.id('scrollView')).swipe('down', 'fast', 0.5); + * @example await element(by.id('scrollView')).swipe('down', 'fast', 0.5, 0.2); + * @example await element(by.id('scrollView')).swipe('down', 'fast', 0.5, 0.2, 0.5); + */ + swipe(direction: Direction, speed?: Speed, percentage?: number, normalizedStartingPointX?: number, normalizedStartingPointY?: number): Promise; + + /** + * Sets a picker view’s column to the given value. This function supports both date pickers and general picker views. (iOS Only) + * Note: When working with date pickers, you should always set an explicit locale when launching your app in order to prevent flakiness from different date and time styles. + * See [here](https://wix.github.io/Detox/docs/api/device-object-api#9-launch-with-a-specific-language-ios-only) for more information. + * + * @param column number of datepicker column (starts from 0) + * @param value string value in set column (must be correct) + * @example + * await expect(element(by.type('UIPickerView'))).toBeVisible(); + * await element(by.type('UIPickerView')).setColumnToValue(1,"6"); + * await element(by.type('UIPickerView')).setColumnToValue(2,"34"); + */ + setColumnToValue(column: number, value: string): Promise; + + /** + * Sets the date of a date-picker according to the specified date-string and format. + * @param dateString Textual representation of a date (e.g. '2023/01/01'). Should be in coherence with the format specified by `dateFormat`. + * @param dateFormat Format of `dateString`: Generally either 'ISO8601' or an explicitly specified format (e.g. 'yyyy/MM/dd'); It should + * follow the rules of NSDateFormatter for iOS and DateTimeFormatter for Android. + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString + * @example + * await element(by.id('datePicker')).setDatePickerDate('2023-01-01T00:00:00Z', 'ISO8601'); + * await element(by.id('datePicker')).setDatePickerDate(new Date().toISOString(), 'ISO8601'); + * await element(by.id('datePicker')).setDatePickerDate('2023/01/01', 'yyyy/MM/dd'); + */ + setDatePickerDate(dateString: string, dateFormat: string): Promise; + + /** + * Triggers a given [accessibility action]{@link https://reactnative.dev/docs/accessibility#accessibility-actions}. + * @param actionName - name of the accessibility action + * @example await element(by.id('view')).performAccessibilityAction('activate'); + */ + performAccessibilityAction(actionName: string): Promise + + /** + * Pinches in the given direction with speed and angle. (iOS only) + * @param angle value in radiant, default is `0` + * @example + * await expect(element(by.id('PinchableScrollView'))).toBeVisible(); + * await element(by.id('PinchableScrollView')).pinchWithAngle('outward', 'slow', 0); + * @deprecated Use `.pinch()` instead. + */ + pinchWithAngle(direction: PinchDirection, speed: Speed, angle: number): Promise; + + /** + * Pinches with the given scale, speed, and angle. (iOS only) + * @param speed default is `fast` + * @param angle value in radiant, default is `0` + * @example + * await element(by.id('PinchableScrollView')).pinch(1.1); + * await element(by.id('PinchableScrollView')).pinch(2.0); + * await element(by.id('PinchableScrollView')).pinch(0.001); + */ + pinch(scale: number, speed?: Speed, angle?: number): Promise; + + /** + * Takes a screenshot of the element and schedules putting it in the artifacts folder upon completion of the current test. + * For more information, see {@link https://wix.github.io/Detox/docs/api/screenshots#element-level-screenshots} + * @param {string} name for the screenshot artifact + * @returns {Promise} a temporary path to the screenshot. + * @example + * test('Menu items should have logout', async () => { + * const imagePath = await element(by.id('menuRoot')).takeScreenshot('tap on menu'); + * // The temporary path will remain valid until the test completion. + * // Afterwards, the screenshot will be moved, e.g.: + * // * on success, to: /✓ Menu items should have Logout/tap on menu.png + * // * on failure, to: /✗ Menu items should have Logout/tap on menu.png + * }); + */ + takeScreenshot(name: string): Promise; + + /** + * Retrieves the OS-dependent attributes of an element. + * If there are multiple matches, it returns an array of attributes for all matched elements. + * For detailed information, refer to {@link https://wix.github.io/Detox/docs/api/actions-on-element/#getattributes} + * + * @example + * test('Get the attributes for my text element', async () => { + * const attributes = await element(by.id('myText')).getAttributes() + * const jestExpect = require('expect'); + * // 'visible' attribute available on both iOS and Android + * jestExpect(attributes.visible).toBe(true); + * // 'activationPoint' attribute available on iOS only + * jestExpect(attributes.activationPoint.x).toHaveValue(50); + * // 'width' attribute available on Android only + * jestExpect(attributes.width).toHaveValue(100); + * }) + */ + getAttributes(): Promise; + } + + interface WebExpect> { + /** + * Negate the expectation. + * @example await expect(web.element(by.web.id('sessionTimeout'))).not.toExist(); + */ + not: this; + + /** + * Expect the element content to have the `text` supplied + * @param text expected to be on the element content + * @example + * await expect(web.element(by.web.id('checkoutButton'))).toHaveText('Proceed to check out'); + */ + toHaveText(text: string): R; + + /** + * Expect the view to exist in the webview DOM tree. + * @example await expect(web.element(by.web.id('submitButton'))).toExist(); + */ + toExist(): R; + } + + interface IndexableWebElement extends WebElement { + /** + * Choose from multiple elements matching the same matcher using index + * @example await web.element(by.web.hrefContains('Details')).atIndex(2).tap(); + */ + atIndex(index: number): WebElement; + } + + interface WebElement extends WebElementActions { + } + + interface WebElementActions { + tap(): Promise; + + /** + * @param text to type + * @param isContentEditable whether its a ContentEditable element, default is false. + */ + 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; + + /** + * At the moment not working on content-editable + */ + clearText(): Promise; + + /** + * scrolling to the view, the element top position will be at the top of the screen. + */ + scrollToView(): Promise; + + /** + * Gets the input content + */ + getText(): Promise; + + /** + * Calls the focus function on the element + */ + focus(): Promise; + + /** + * Selects all the input content, works on ContentEditable at the moment. + */ + selectAllText(): Promise; + + /** + * Moves the input cursor / caret to the end of the content, works on ContentEditable at the moment. + */ + moveCursorToEnd(): Promise; + + /** + * Running a JavaScript function on the element. + * The first argument to the function will be the element itself. + * The rest of the arguments will be forwarded to the JavaScript function as is. + * + * @param script a callback function in stringified form, or a plain function reference + * without closures, bindings etc. that will be converted to a string. + * @param args optional args to pass to the script + * + * @example + * await webElement.runScript('(el) => el.click()'); + * await webElement.runScript(function setText(element, text) { + * element.textContent = text; + * }, ['Custom Title']); + */ + runScript(script: string, args?: unknown[]): Promise; + runScript(script: (...args: any[]) => F, args?: unknown[]): Promise; + + /** + * Gets the current page url + */ + getCurrentUrl(): Promise; + + /** + * Gets the current page title + */ + getTitle(): Promise; + } + + type Direction = 'left' | 'right' | 'top' | 'bottom' | 'up' | 'down'; + + type PinchDirection = 'outward' | 'inward' + + type Orientation = 'portrait' | 'landscape'; + + type Speed = 'fast' | 'slow'; + + interface LanguageAndLocale { + language?: string; + locale?: string; + } + + /** + * Source for string definitions is https://github.com/wix/AppleSimulatorUtils + */ + interface DevicePermissions { + location?: LocationPermission; + notifications?: NotificationsPermission; + calendar?: CalendarPermission; + camera?: CameraPermission; + contacts?: ContactsPermission; + health?: HealthPermission; + homekit?: HomekitPermission; + medialibrary?: MediaLibraryPermission; + microphone?: MicrophonePermission; + motion?: MotionPermission; + photos?: PhotosPermission; + reminders?: RemindersPermission; + siri?: SiriPermission; + speech?: SpeechPermission; + faceid?: FaceIDPermission; + userTracking?: UserTrackingPermission; + } + + type LocationPermission = 'always' | 'inuse' | 'never' | 'unset'; + type PermissionState = 'YES' | 'NO' | 'unset'; + type CameraPermission = PermissionState; + type ContactsPermission = PermissionState; + type CalendarPermission = PermissionState; + type HealthPermission = PermissionState; + type HomekitPermission = PermissionState; + type MediaLibraryPermission = PermissionState; + type MicrophonePermission = PermissionState; + type MotionPermission = PermissionState; + type PhotosPermission = PermissionState; + type RemindersPermission = PermissionState; + type SiriPermission = PermissionState; + type SpeechPermission = PermissionState; + type NotificationsPermission = PermissionState; + type FaceIDPermission = PermissionState; + type UserTrackingPermission = PermissionState; + + interface DeviceLaunchAppConfig { + /** + * Restart the app + * Terminate the app and launch it again. If set to false, the simulator will try to bring app from background, if the app isn't running, it will launch a new instance. default is false + */ + newInstance?: boolean; + /** + * Set runtime permissions + * Grant or deny runtime permissions for your application. + */ + permissions?: DevicePermissions; + /** + * Launch from URL + * Mock opening the app from URL to test your app's deep link handling mechanism. + */ + url?: any; + /** + * Launch with user notifications + */ + userNotification?: any; + /** + * Launch with user activity + */ + userActivity?: any; + /** + * Launch into a fresh installation + * A flag that enables relaunching into a fresh installation of the app (it will uninstall and install the binary again), default is false. + */ + delete?: boolean; + /** + * Arguments to pass-through into the app. + * Refer to the [dedicated guide](https://wix.github.io/Detox/docs/api/launch-args) for complete details. + */ + launchArgs?: Record; + /** + * Launch config for specifying the native language and locale + */ + languageAndLocale?: LanguageAndLocale; + } + + // Element Attributes Shared Among iOS and Android + interface ElementAttributes { + /** + * Whether or not the element is enabled for user interaction. + */ + enabled: boolean; + /** + * The identifier of the element. Matches accessibilityIdentifier on iOS, and the main view tag, on Android - both commonly holding the component's test ID in React Native apps. + */ + identifier: string; + /** + * Whether the element is visible. On iOS, visibility is calculated for the activation point. On Android, the attribute directly holds the value returned by View.getLocalVisibleRect()). + */ + visible: boolean; + /** + * The text value of any textual element. + */ + text?: string; + /** + * The label of the element. Largely matches accessibilityLabel for ios, and contentDescription for android. + * Refer to Detox's documentation (`toHaveLabel()` subsection) in order to learn about caveats associated with + * this property in React Native apps. + */ + label?: string; + /** + * The placeholder text value of the element. Matches hint on android. + */ + placeholder?: string; + /** + * The value of the element, where applicable. + * Matches accessibilityValue, on iOS. + * For example: the position of a slider, or whether a checkbox has been marked (Android). + */ + value?: unknown; + } + + interface IosElementAttributeFrame { + y: number; + x: number; + width: number; + height: number; + } + + interface IosElementAttributeInsets { + right: number; + top: number; + left: number; + bottom: number; + } + + // iOS Specific Attributes + interface IosElementAttributes extends ElementAttributes { + /** + * The [activation point]{@link https://developer.apple.com/documentation/objectivec/nsobject/1615179-accessibilityactivationpoint} of the element, in element coordinate space. + */ + activationPoint: Point2D; + /** + * The activation point of the element, in normalized percentage ([0.0, 1.0]). + */ + normalizedActivationPoint: Point2D; + /** + * Whether the element is hittable at the activation point. + */ + hittable: boolean; + /** + * The frame of the element, in screen coordinate space. + */ + frame: IosElementAttributeFrame; + /** + * The frame of the element, in container coordinate space. + */ + elementFrame: IosElementAttributeFrame; + /** + * The bounds of the element, in element coordinate space. + */ + elementBounds: IosElementAttributeFrame; + /** + * The safe area insets of the element, in element coordinate space. + */ + safeAreaInsets: IosElementAttributeInsets; + /** + * The safe area bounds of the element, in element coordinate space. + */ + elementSafeBounds: IosElementAttributeFrame; + /** + * The date of the element (if it is a date picker). + */ + date?: string; + /** + * The normalized slider position (if it is a slider). + */ + normalizedSliderPosition?: number; + /** + * The content offset (if it is a scroll view). + */ + contentOffset?: Point2D; + /** + * The content inset (if it is a scroll view). + */ + contentInset?: IosElementAttributeInsets; + /** + * The adjusted content inset (if it is a scroll view). + */ + adjustedContentInset?: IosElementAttributeInsets; + /** + * @example "" + */ + layer: string; + } + + // Android Specific Attributes + interface AndroidElementAttributes extends ElementAttributes { + /** + * The OS visibility type associated with the element: visible, invisible or gone. + */ + visibility: 'visible' | 'invisible' | 'gone'; + /** + * Width of the element, in pixels. + */ + width: number; + /** + * Height of the element, in pixels. + */ + height: number; + /** + * Elevation of the element. + */ + elevation: number; + /** + * Alpha value for the element. + */ + alpha: number; + /** + * Whether the element is the one currently in focus. + */ + focused: boolean; + /** + * The text size for the text element. + */ + textSize?: number; + /** + * The length of the text element (character count). + */ + length?: number; + } + } +} + +export = Detox; diff --git a/detox/globals.d.ts b/detox/globals.d.ts new file mode 100644 index 0000000000..f58f993fbb --- /dev/null +++ b/detox/globals.d.ts @@ -0,0 +1,23 @@ +import Detox = require('./detox'); + +declare global { + const detox: Detox.DetoxExportWrapper; + const device: Detox.DetoxExportWrapper['device']; + const element: Detox.DetoxExportWrapper['element']; + const waitFor: Detox.DetoxExportWrapper['waitFor']; + const expect: Detox.DetoxExportWrapper['expect']; + const by: Detox.DetoxExportWrapper['by']; + const web: Detox.DetoxExportWrapper['web']; + + namespace NodeJS { + interface Global { + detox: Detox.DetoxExportWrapper; + device: Detox.DetoxExportWrapper['device']; + element: Detox.DetoxExportWrapper['element']; + waitFor: Detox.DetoxExportWrapper['waitFor']; + expect: Detox.DetoxExportWrapper['expect']; + by: Detox.DetoxExportWrapper['by']; + web: Detox.DetoxExportWrapper['web']; + } + } +} diff --git a/detox/index.d.ts b/detox/index.d.ts index 68b5705899..9137e82552 100644 --- a/detox/index.d.ts +++ b/detox/index.d.ts @@ -1,1844 +1,5 @@ -// TypeScript definitions for Detox -// Original authors (from DefinitelyTyped): -// * Jane Smith -// * Tareq El-Masri -// * Steve Chun -// * Hammad Jutt -// * pera -// * Max Komarychev -// * Dor Ben Baruch - -import { BunyanDebugStreamOptions } from 'bunyan-debug-stream'; - -declare global { - const detox: Detox.DetoxExportWrapper; - const device: Detox.DetoxExportWrapper['device']; - const element: Detox.DetoxExportWrapper['element']; - const waitFor: Detox.DetoxExportWrapper['waitFor']; - const expect: Detox.DetoxExportWrapper['expect']; - const by: Detox.DetoxExportWrapper['by']; - const web: Detox.DetoxExportWrapper['web']; - - namespace NodeJS { - interface Global { - detox: Detox.DetoxExportWrapper; - device: Detox.DetoxExportWrapper['device']; - element: Detox.DetoxExportWrapper['element']; - waitFor: Detox.DetoxExportWrapper['waitFor']; - expect: Detox.DetoxExportWrapper['expect']; - by: Detox.DetoxExportWrapper['by']; - web: Detox.DetoxExportWrapper['web']; - } - } - - namespace Detox { - //#region DetoxConfig - - interface DetoxConfig extends DetoxConfigurationCommon { - /** - * @example extends: './relative/detox.config' - * @example extends: '@my-org/detox-preset' - */ - extends?: string; - - apps?: Record; - devices?: Record; - selectedConfiguration?: string; - configurations: Record; - } - - type DetoxConfigurationCommon = { - artifacts?: false | DetoxArtifactsConfig; - behavior?: DetoxBehaviorConfig; - logger?: DetoxLoggerConfig; - session?: DetoxSessionConfig; - testRunner?: DetoxTestRunnerConfig; - }; - - interface DetoxArtifactsConfig { - rootDir?: string; - pathBuilder?: string; - plugins?: { - log?: 'none' | 'failing' | 'all' | DetoxLogArtifactsPluginConfig; - screenshot?: 'none' | 'manual' | 'failing' | 'all' | DetoxScreenshotArtifactsPluginConfig; - video?: 'none' | 'failing' | 'all' | DetoxVideoArtifactsPluginConfig; - instruments?: 'none' | 'all' | DetoxInstrumentsArtifactsPluginConfig; - uiHierarchy?: 'disabled' | 'enabled' | DetoxUIHierarchyArtifactsPluginConfig; - - [pluginId: string]: unknown; - }; - } - - interface DetoxBehaviorConfig { - init?: { - /** - * By default, Detox exports `device`, `expect`, `element`, `by` and `waitFor` - * as global variables. If you want to control their initialization manually, - * set this property to `false`. - * - * This is useful when during E2E tests you also need to run regular expectations - * in Node.js. Jest's `expect` for instance, will not be overridden by Detox when - * this option is used. - */ - exposeGlobals?: boolean; - /** - * By default, Detox will uninstall and install the app upon initialization. - * If you wish to reuse the existing app for a faster run, set the property to - * `false`. - */ - reinstallApp?: boolean; - /** - * When false, `detox test` command always deletes the shared lock file on start, - * assuming it had been left from the previous, already finished test session. - * The lock file contains information about busy and free devices and ensures - * no device can be used simultaneously by multiple test workers. - * - * Setting it to **true** might be useful when if you need to run multiple - * `detox test` commands in parallel, e.g. test a few configurations at once. - * - * @default false - */ - keepLockFile?: boolean; - }; - launchApp?: 'auto' | 'manual'; - cleanup?: { - shutdownDevice?: boolean; - }; - } - - type _DetoxLoggerOptions = Omit; - - interface DetoxLoggerConfig { - /** - * Log level filters the messages printed to your terminal, - * and it does not affect the logs written to the artifacts. - * - * Use `info` by default. - * Use `error` or warn when you want to make the output as silent as possible. - * Use `debug` to control what generally is happening under the hood. - * Use `trace` when troubleshooting specific issues. - * - * @default 'info' - */ - level?: DetoxLogLevel; - /** - * When enabled, hijacks all the console methods (console.log, console.warn, etc) - * so that the messages printed via them are formatted and saved as Detox logs. - * - * @default true - */ - overrideConsole?: boolean; - /** - * Since Detox is using - * {@link https://www.npmjs.com/package/bunyan-debug-stream bunyan-debug-stream} - * for printing logs, all its options are exposed for sake of simplicity - * of customization. - * - * The only exception is {@link BunyanDebugStreamOptions#out} option, - * which is always set to `process.stdout`. - * - * You can also pass a callback function to override the logger config - * programmatically, e.g. depending on the selected log level. - * - * @see {@link BunyanDebugStreamOptions} - */ - options?: _DetoxLoggerOptions | ((config: Partial) => _DetoxLoggerOptions); - } - - interface DetoxSessionConfig { - autoStart?: boolean; - debugSynchronization?: number; - server?: string; - sessionId?: string; - } - - interface DetoxTestRunnerConfig { - args?: { - /** - * The command to use for runner: 'jest', 'nyc jest', - */ - $0: string; - /** - * The positional arguments to pass to the runner. - */ - _?: string[]; - /** - * Any other properties recognized by test runner - */ - [prop: string]: unknown; - }; - - /** - * This is an add-on section used by our Jest integration code (but not Detox core itself). - * In other words, if you’re implementing (or using) a custom integration with some other test runner, feel free to define a section for yourself (e.g. `testRunner.mocha`) - */ - jest?: { - /** - * Environment setup timeout - * - * As a part of the environment setup, Detox boots the device and installs the apps. - * If that takes longer than the specified value, the entire test suite will be considered as failed, e.g.: - * ```plain text - * FAIL e2e/starter.test.js - * ● Test suite failed to run - * - * Exceeded timeout of 300000ms while setting up Detox environment - * ``` - * - * The default value is 5 minutes. - * - * @default 300000 - * @see {@link https://jestjs.io/docs/configuration/#testenvironment-string} - */ - setupTimeout?: number | undefined; - /** - * Environemnt teardown timeout - * - * If the environment teardown takes longer than the specified value, Detox will throw a timeout error. - * The default value is half a minute. - * - * @default 30000 (30 seconds) - * @see {@link https://jestjs.io/docs/configuration/#testenvironment-string} - */ - teardownTimeout?: number | undefined; - /** - * Jest provides an API to re-run individual failed tests: `jest.retryTimes(count)`. - * When Detox detects the use of this API, it suppresses its own CLI retry mechanism controlled via `detox test … --retries ` or {@link DetoxTestRunnerConfig#retries}. - * The motivation is simple – activating the both mechanisms is apt to increase your test duration dramatically, if your tests are flaky. - * If you wish nevertheless to use both the mechanisms simultaneously, set it to `true`. - * - * @default false - * @see {@link https://jestjs.io/docs/29.0/jest-object#jestretrytimesnumretries-options} - */ - retryAfterCircusRetries?: boolean; - /** - * By default, Jest prints the test names and their status (_passed_ or _failed_) at the very end of the test session. - * When enabled, it makes Detox to print messages like these each time the new test starts and ends: - * ```plain text - * 18:03:36.258 detox[40125] i Sanity: should have welcome screen - * 18:03:37.495 detox[40125] i Sanity: should have welcome screen [OK] - * 18:03:37.496 detox[40125] i Sanity: should show hello screen after tap - * 18:03:38.928 detox[40125] i Sanity: should show hello screen after tap [OK] - * 18:03:38.929 detox[40125] i Sanity: should show world screen after tap - * 18:03:40.351 detox[40125] i Sanity: should show world screen after tap [OK] - * ``` - * By default, it is enabled automatically in test sessions with a single worker. - * And vice versa, if multiple tests are executed concurrently, Detox turns it off to avoid confusion in the log. - * Use boolean values, `true` or `false`, to turn off the automatic choice. - * - * @default undefined - */ - reportSpecs?: boolean | undefined; - /** - * In the environment setup phase, Detox boots the device and installs the apps. - * This flag tells Detox to print messages like these every time the device gets assigned to a specific suite: - * - * ```plain text - * 18:03:29.869 detox[40125] i starter.test.js is assigned to 4EC84833-C7EA-4CA3-A6E9-5C30A29EA596 (iPhone 12 Pro Max) - * ``` - * - * @default true - */ - reportWorkerAssign?: boolean | undefined; - }; - /** - * Retries count. Zero means a single attempt to run tests. - */ - retries?: number; - /** - * When true, tells Detox CLI to cancel next retrying if it gets - * at least one report about a permanent test suite failure. - * Has no effect, if {@link DetoxTestRunnerConfig#retries} is - * undefined or set to zero. - * - * @default false - * @see {DetoxInternals.DetoxTestFileReport#isPermanentFailure} - */ - bail?: boolean; - /** - * Custom handler to process --inspect-brk CLI flag. - * Use it when you rely on another test runner than Jest to mutate the config. - */ - inspectBrk?: (config: DetoxTestRunnerConfig) => void; - /** - * Forward environment variables to the spawned test runner - * accordingly to the given CLI argument overrides. - * - * If false, Detox CLI will be only printing a hint message on - * how to start the test runner using environment variables, - * in case when a user wants to avoid using Detox CLI. - * - * @default false - */ - forwardEnv?: boolean; - } - - 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. - * - * @see Device#selectApp - */ - name?: string; - }; - - type DetoxDeviceConfig = DetoxBuiltInDeviceConfig | DetoxCustomDriverConfig; - - interface DetoxLogArtifactsPluginConfig { - enabled?: boolean; - keepOnlyFailedTestsArtifacts?: boolean; - } - - interface DetoxScreenshotArtifactsPluginConfig { - enabled?: boolean; - keepOnlyFailedTestsArtifacts?: boolean; - shouldTakeAutomaticSnapshots?: boolean; - takeWhen?: { - testStart?: boolean; - testFailure?: boolean; - testDone?: boolean; - appNotReady?: boolean; - }; - } - - interface DetoxVideoArtifactsPluginConfig { - enabled?: boolean; - keepOnlyFailedTestsArtifacts?: boolean; - android?: Partial<{ - size: [number, number]; - bitRate: number; - timeLimit: number; - verbose: boolean; - }>; - simulator?: Partial<{ - codec: string; - }>; - } - - interface DetoxInstrumentsArtifactsPluginConfig { - enabled?: boolean; - } - - interface DetoxUIHierarchyArtifactsPluginConfig { - enabled?: boolean; - } - - type DetoxBuiltInAppConfig = (DetoxIosAppConfig | DetoxAndroidAppConfig); - - interface DetoxIosAppConfig { - type: 'ios.app'; - binaryPath: string; - bundleId?: string; - build?: string; - start?: string; - launchArgs?: Record; - } - - interface DetoxAndroidAppConfig { - type: 'android.apk'; - binaryPath: string; - bundleId?: string; - build?: string; - start?: string; - testBinaryPath?: string; - launchArgs?: Record; - /** - * TCP ports to `adb reverse` upon the installation. - * E.g. 8081 - to be able to access React Native packager in Debug mode. - * - * @example [8081] - */ - reversePorts?: number[]; - } - - interface DetoxCustomAppConfig { - type: string; - - [prop: string]: unknown; - } - - type DetoxBuiltInDeviceConfig = - | DetoxIosSimulatorDriverConfig - | DetoxAttachedAndroidDriverConfig - | DetoxAndroidEmulatorDriverConfig - | DetoxGenymotionCloudDriverConfig; - - interface DetoxIosSimulatorDriverConfig { - type: 'ios.simulator'; - device: string | Partial; - bootArgs?: string; - } - - interface DetoxSharedAndroidDriverConfig { - forceAdbInstall?: boolean; - utilBinaryPaths?: string[]; - } - - interface DetoxAttachedAndroidDriverConfig extends DetoxSharedAndroidDriverConfig { - type: 'android.attached'; - device: string | { adbName: string }; - } - - interface DetoxAndroidEmulatorDriverConfig extends DetoxSharedAndroidDriverConfig { - type: 'android.emulator'; - device: string | { avdName: string }; - bootArgs?: string; - gpuMode?: 'auto' | 'host' | 'swiftshader_indirect' | 'angle_indirect' | 'guest' | 'off'; - headless?: boolean; - /** - * @default true - */ - readonly?: boolean; - } - - interface DetoxGenymotionCloudDriverConfig extends DetoxSharedAndroidDriverConfig { - type: 'android.genycloud'; - device: string | { recipeUUID: string; } | { recipeName: string; }; - } - - interface DetoxCustomDriverConfig { - type: string; - - [prop: string]: unknown; - } - - interface IosSimulatorQuery { - id: string; - type: string; - name: string; - os: string; - } - - type DetoxConfiguration = DetoxConfigurationCommon & ( - | DetoxConfigurationSingleApp - | DetoxConfigurationMultiApps - ); - - interface DetoxConfigurationSingleApp { - device: DetoxAliasedDevice; - app: DetoxAliasedApp; - } - - interface DetoxConfigurationMultiApps { - device: DetoxAliasedDevice; - apps: DetoxAliasedApp[]; - } - - type DetoxAliasedDevice = string | DetoxDeviceConfig; - - type DetoxAliasedApp = string | DetoxAppConfig; - - //#endregion - - interface DetoxExportWrapper { - readonly device: Device; - - readonly element: ElementFacade; - - readonly waitFor: WaitForFacade; - - readonly expect: ExpectFacade; - - readonly by: ByFacade; - - readonly web: WebFacade; - - readonly DetoxConstants: { - userNotificationTriggers: { - push: 'push'; - calendar: 'calendar'; - timeInterval: 'timeInterval'; - location: 'location'; - }; - userActivityTypes: { - searchableItem: string; - browsingWeb: string; - }, - searchableItemActivityIdentifier: string; - }; - - /** - * Detox logger instance. Can be used for saving user logs to the general log file. - */ - readonly log: Logger; - - /** - * @deprecated - * - * Deprecated - use {@link Detox.Logger#trace} - * Detox tracer instance. Can be used for building timelines in Google Event Tracing format. - */ - readonly trace: { - /** @deprecated */ - readonly startSection: (name: string) => void; - /** @deprecated */ - readonly endSection: (name: string) => void; - }; - - /** - * Trace a single call, with a given name and arguments. - * - * @deprecated - * @param sectionName The name of the section to trace. - * @param promiseOrFunction Promise or a function that provides a promise. - * @param args Optional arguments to pass to the trace. - * @returns The returned value of the traced call. - * @see https://wix.github.io/Detox/docs/19.x/api/detox-object-api/#detoxtracecall - */ - readonly traceCall: (event: string, action: () => Promise, args?: Record) => Promise; - } - - interface Logger { - readonly level: DetoxLogLevel; - - readonly fatal: _LogMethod; - readonly error: _LogMethod; - readonly warn: _LogMethod; - readonly info: _LogMethod; - readonly debug: _LogMethod; - readonly trace: _LogMethod; - - child(context?: Partial): Logger; - } - - /** @internal */ - interface _LogMethod extends _LogMethodSignature { - readonly begin: _LogMethodSignature; - readonly complete: _CompleteMethodSignature; - readonly end: _LogMethodSignature; - } - - /** @internal */ - interface _LogMethodSignature { - (...args: unknown[]): void - (event: LogEvent, ...args: unknown[]): void; - } - - /** @internal */ - interface _CompleteMethodSignature { - (message: string, action: T | (() => T)): T; - (event: LogEvent, message: string, action: T | (() => T)): T; - } - - type LogEvent = { - /** Use when there's a risk of logging several parallel duration events. */ - id?: string | number; - /** Optional. Event categories (tags) to facilitate filtering. */ - cat?: string | string[]; - /** Optional. Color name (applicable in Google Chrome Trace Format) */ - cname?: string; - - /** Reserved property. Process ID. */ - pid?: never; - /** Reserved property. Thread ID. */ - tid?: never; - /** Reserved property. Timestamp. */ - ts?: never; - /** Reserved property. Event phase. */ - ph?: never; - - [customProperty: string]: unknown; - }; - - type DetoxLogLevel = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace'; - - type Point2D = { - x: number, - y: number, - } - - /** - * A construct allowing for the querying and modification of user arguments passed to an app upon launch by Detox. - * - * @see AppLaunchArgs#modify - * @see AppLaunchArgs#reset - * @see AppLaunchArgs#get - */ - interface AppLaunchArgs { - /** - * Shared (global) arguments that are not specific to a particular application. - * Selecting another app does not reset them, yet they still can be overridden - * by configuring app-specific launch args. - * @see Device#selectApp - * @see AppLaunchArgs - */ - readonly shared: ScopedAppLaunchArgs; - - /** - * Modify the launch-arguments via a modifier object, according to the following logic: - * - Non-nullish modifier properties would set a new value or override the previous value of - * existing properties with the same name. - * - Modifier properties set to either `undefined` or `null` would delete the corresponding property - * if it existed. - * These custom app launch arguments get erased whenever you select a different application. - * If you need to share them between all the applications, use {@link AppLaunchArgs#shared} property. - * Note: app-specific launch args have a priority over shared ones. - * - * @param modifier The modifier object. - * @example - * // With current launch arguments set to: - * // { - * // mockServerPort: 1234, - * // mockServerCredentials: 'user@test.com:12345678', - * // } - * device.appLaunchArgs.modify({ - * mockServerPort: 4321, - * mockServerCredentials: null, - * mockServerToken: 'abcdef', - * }); - * await device.launchApp(); - * // ==> launch-arguments become: - * // { - * // mockServerPort: 4321, - * // mockServerToken: 'abcdef', - * // } - */ - modify(modifier: object): this; - - /** - * Reset all app-specific launch arguments (back to an empty object). - * If you need to reset the shared launch args, use {@link AppLaunchArgs#shared}. - */ - reset(): this; - - /** - * Get all currently set launch arguments (including shared ones). - * @returns An object containing all launch-arguments. - * Note: mutating the values inside the result object is pointless, as it is immutable. - */ - get(): object; - } - - /** - * Shared (global) arguments that are not specific to a particular application. - */ - interface ScopedAppLaunchArgs { - /** @see AppLaunchArgs#modify */ - modify(modifier: object): this; - - /** @see AppLaunchArgs#reset */ - reset(): this; - - /** @see AppLaunchArgs#get */ - get(): object; - } - - type DigitWithoutZero = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; - type Digit = 0 | DigitWithoutZero; - type BatteryLevel = `${Digit}` | `${DigitWithoutZero}${Digit}` | "100"; - - interface Device { - /** - * Holds the environment-unique ID of the device, namely, the adb ID on Android (e.g. emulator-5554) and the Mac-global simulator UDID on iOS - - * as used by simctl (e.g. AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE). - */ - id: string; - /** - * Holds a descriptive name of the device. Example: emulator-5554 (Pixel_API_29) - */ - name: string; - - /** - * Select the current app (relevant only to multi-app configs) by its name. - * After execution, all app-specific device methods will target the selected app. - * - * @see DetoxAppConfig#name - * @example - * await device.selectApp('passenger'); - * await device.launchApp(); // passenger - * // ... run tests for the passenger app - * await device.uninstallApp(); // passenger - * await device.selectApp('driver'); - * await device.installApp(); // driver - * await device.launchApp(); // driver - * // ... run tests for the driver app - * await device.terminateApp(); // driver - */ - selectApp(app: string): Promise; - - /** - * Launch the app. - * - *

For info regarding launch arguments, refer to the [dedicated guide](https://wix.github.io/Detox/docs/api/launch-args). - * - * @example - * // Terminate the app and launch it again. If set to false, the simulator will try to bring app from background, - * // if the app isn't running, it will launch a new instance. default is false - * await device.launchApp({newInstance: true}); - * @example - * // Grant or deny runtime permissions for your application. - * await device.launchApp({permissions: {calendar: 'YES'}}); - * @example - * // Mock opening the app from URL to test your app's deep link handling mechanism. - * await device.launchApp({url: url}); - * @example - * // Start the app with some custom arguments. - * await device.launchApp({ - * launchArgs: {arg1: 1, arg2: "2"}, - * }); - */ - launchApp(config?: DeviceLaunchAppConfig): Promise; - - /** - * Relaunch the app. Convenience method that calls {@link Device#launchApp} - * with { newInstance: true } override. - * - * @deprecated - * @param config - * @see Device#launchApp - */ - relaunchApp(config?: DeviceLaunchAppConfig): Promise; - - /** - * Access the user-defined launch-arguments predefined through static scopes such as the Detox configuration file and - * command-line arguments. This access allows - through dedicated methods, for both value-querying and - * modification (see {@link AppLaunchArgs}). - * Refer to the [dedicated guide](https://wix.github.io/Detox/docs/api/launch-args) for complete details. - * - * @example - * // With Detox being preconfigured statically to use these arguments in app launch: - * // { - * // mockServerPort: 1234, - * // } - * // The following code would result in these arguments eventually passed into the launched app: - * // { - * // mockServerPort: 4321, - * // mockServerToken: 'uvwxyz', - * // } - * device.appLaunchArgs.modify({ - * mockServerPort: 4321, - * mockServerToken: 'abcdef', - * }); - * await device.launchApp({ launchArgs: { mockServerToken: 'uvwxyz' } }}; - * - * @see AppLaunchArgs - */ - appLaunchArgs: AppLaunchArgs; - - /** - * Terminate the app. - * - * @example - * // By default, terminateApp() with no params will terminate the app - * await device.terminateApp(); - * @example - * // To terminate another app, specify its bundle id - * await device.terminateApp('other.bundle.id'); - */ - terminateApp(bundle?: string): Promise; - - /** - * Send application to background by bringing com.apple.springboard to the foreground. - * Combining sendToHome() with launchApp({newInstance: false}) will simulate app coming back from background. - * @example - * await device.sendToHome(); - * await device.launchApp({newInstance: false}); - */ - sendToHome(): Promise; - - /** - * If this is a React Native app, reload the React Native JS bundle. This action is much faster than device.launchApp(), and can be used if you just need to reset your React Native logic. - * - * @example await device.reloadReactNative() - */ - reloadReactNative(): Promise; - - /** - * By default, installApp() with no params will install the app file defined in the current configuration. - * To install another app, specify its path - * @example await device.installApp(); - * @example await device.installApp('path/to/other/app'); - */ - installApp(path?: any): Promise; - - /** - * By default, uninstallApp() with no params will uninstall the app defined in the current configuration. - * To uninstall another app, specify its bundle id - * @example await device.installApp('other.bundle.id'); - */ - uninstallApp(bundle?: string): Promise; - - /** - * Mock opening the app from URL. sourceApp is an optional parameter to specify source application bundle id. - */ - openURL(url: { url: string; sourceApp?: string }): Promise; - - /** - * Mock handling of received user notification when app is in foreground. - */ - sendUserNotification(...params: any[]): Promise; - - /** - * Mock handling of received user activity when app is in foreground. - */ - sendUserActivity(...params: any[]): Promise; - - /** - * Takes "portrait" or "landscape" and rotates the device to the given orientation. Currently only available in the iOS Simulator. - */ - setOrientation(orientation: Orientation): Promise; - - /** - * Sets the simulator/emulator location to the given latitude and longitude. - * - *

On iOS `setLocation` is dependent on [fbsimctl](https://github.com/facebook/idb/tree/4b7929480c3c0f158f33f78a5b802c1d0e7030d2/fbsimctl) - * which [is now deprecated](https://github.com/wix/Detox/issues/1371). - * If `fbsimctl` is not installed, the command will fail, asking for it to be installed. - * - *

On Android `setLocation` will work with both Android Emulator (bundled with Android development tools) and Genymotion. - * The correct permissions must be set in your app manifest. - * - * @example await device.setLocation(32.0853, 34.7818); - */ - setLocation(lat: number, lon: number): Promise; - - /** - * (iOS only) Override simulator’s status bar. - * @platform iOS - * @param {config} config status bar configuration. - * @example - * await device.setStatusBar({ - * time: "12:34", - * // Set the date or time to a fixed value. - * // If the string is a valid ISO date string it will also set the date on relevant devices. - * dataNetwork: "wifi", - * // If specified must be one of 'hide', 'wifi', '3g', '4g', 'lte', 'lte-a', 'lte+', '5g', '5g+', '5g-uwb', or '5g-uc'. - * wifiMode: "failed", - * // If specified must be one of 'searching', 'failed', or 'active'. - * wifiBars: "2", - * // If specified must be 0-3. - * cellularMode: "searching", - * // If specified must be one of 'notSupported', 'searching', 'failed', or 'active'. - * cellularBars: "3", - * // If specified must be 0-4. - * operatorName: "A1", - * // Set the cellular operator/carrier name. Use '' for the empty string. - * batteryState: "charging", - * // If specified must be one of 'charging', 'charged', or 'discharging'. - * batteryLevel: "50", - * // If specified must be 0-100. - * }); - */ - setStatusBar(config: { - time?: string, - dataNetwork?: "hide" | "wifi" | "3g" | "4g" | "lte" | "lte-a" | "lte+" | "5g" | "5g+" | "5g-uwb" | "5g-uc", - wifiMode?: "searching" |"failed" | "active", - wifiBars?: "0" | "1" | "2" | "3", - cellularMode?: "notSupported" | "searching" | "failed" | "active", - cellularBars?: "0" | "1" | "2" | "3" | "4", - operatorName?: string; - batteryState?: "charging" | "charged" | "discharging", - batteryLevel?: BatteryLevel, - }): Promise; - - /** - * Disable network synchronization mechanism on preferred endpoints. Useful if you want to on skip over synchronizing on certain URLs. - * - * @example await device.setURLBlacklist(['.*127.0.0.1.*']); - */ - setURLBlacklist(urls: string[]): Promise; - - /** - * Temporarily disable synchronization (idle/busy monitoring) with the app - namely, stop waiting for the app to go idle before moving forward in the test execution. - * - *

This API is useful for cases where test assertions must be made in an area of your application where it is okay for it to ever remain partly *busy* (e.g. due to an - * endlessly repeating on-screen animation). However, using it inherently suggests that you are likely to resort to applying `sleep()`'s in your test code - testing - * that area, **which is not recommended and can never be 100% stable. - * **Therefore, as a rule of thumb, test code running "inside" a sync-disabled mode must be reduced to the bare minimum. - * - *

Note: Synchronization is enabled by default, and it gets **reenabled on every launch of a new instance of the app.** - * - * @example await device.disableSynchronization(); - */ - disableSynchronization(): Promise; - - /** - * Reenable synchronization (idle/busy monitoring) with the app - namely, resume waiting for the app to go idle before moving forward in the test execution, after a - * previous disabling of it through a call to `device.disableSynchronization()`. - * - *

Warning: Making this call would resume synchronization **instantly**, having its returned promise only resolve when the app becomes idle again. - * In other words, this **must only be called after you navigate back to "the safe zone", where the app should be able to eventually become idle again**, or it would - * remain suspended "forever" (i.e. until a safeguard time-out expires). - * - * @example await device.enableSynchronization(); - */ - enableSynchronization(): Promise; - - /** - * Resets the Simulator to clean state (like the Simulator > Reset Content and Settings... menu item), especially removing previously set permissions. - * - * @example await device.resetContentAndSettings(); - */ - resetContentAndSettings(): Promise; - - /** - * Returns the current device, ios or android. - * - * @example - * if (device.getPlatform() === 'ios') { - * await expect(loopSwitch).toHaveValue('1'); - * } - */ - getPlatform(): 'ios' | 'android'; - - /** - * Takes a screenshot on the device and schedules putting it in the artifacts folder upon completion of the current test. - * @param name for the screenshot artifact - * @returns a temporary path to the screenshot. - * @example - * test('Menu items should have logout', async () => { - * const tempPath = await device.takeScreenshot('tap on menu'); - * // The temporary path will remain valid until the test completion. - * // Afterwards, the screenshot will be moved, e.g.: - * // * on success, to: /✓ Menu items should have Logout/tap on menu.png - * // * on failure, to: /✗ Menu items should have Logout/tap on menu.png - * }); - */ - takeScreenshot(name: string): Promise; - - /** - * (iOS only) Saves a view hierarchy snapshot (*.viewhierarchy) of the currently opened application - * to a temporary folder and schedules putting it to the artifacts folder upon the completion of - * the current test. The file can be opened later in Xcode 12.0 and above. - * @see https://developer.apple.com/documentation/xcode-release-notes/xcode-12-release-notes#:~:text=57933113 - * @param [name="capture"] optional name for the *.viewhierarchy artifact - * @returns a temporary path to the captured view hierarchy snapshot. - * @example - * test('Menu items should have logout', async () => { - * await device.captureViewHierarchy('myElements'); - * // The temporary path will remain valid until the test completion. - * // Afterwards, the artifact will be moved, e.g.: - * // * on success, to: /✓ Menu items should have Logout/myElements.viewhierarchy - * // * on failure, to: /✗ Menu items should have Logout/myElements.viewhierarchy - * }); - */ - captureViewHierarchy(name?: string): Promise; - - /** - * Simulate shake (iOS Only) - */ - shake(): Promise; - - /** - * Toggles device enrollment in biometric auth (TouchID or FaceID) (iOS Only) - * @example await device.setBiometricEnrollment(true); - * @example await device.setBiometricEnrollment(false); - */ - setBiometricEnrollment(enabled: boolean): Promise; - - /** - * Simulates the success of a face match via FaceID (iOS Only) - */ - matchFace(): Promise; - - /** - * Simulates the failure of a face match via FaceID (iOS Only) - */ - unmatchFace(): Promise; - - /** - * Simulates the success of a finger match via TouchID (iOS Only) - */ - matchFinger(): Promise; - - /** - * Simulates the failure of a finger match via TouchID (iOS Only) - */ - unmatchFinger(): Promise; - - /** - * Clears the simulator keychain (iOS Only) - */ - clearKeychain(): Promise; - - /** - * Simulate press back button (Android Only) - * @example await device.pressBack(); - */ - pressBack(): Promise; - - /** - * (Android Only) - * Exposes UiAutomator's UiDevice API (https://developer.android.com/reference/android/support/test/uiautomator/UiDevice). - * This is not a part of the official Detox API, - * it may break and change whenever an update to UiDevice or UiAutomator gradle dependencies ('androidx.test.uiautomator:uiautomator') is introduced. - * UIDevice's autogenerated code reference: https://github.com/wix/Detox/blob/master/detox/src/android/espressoapi/UIDevice.js - */ - getUiDevice(): Promise; - - /** - * (Android Only) - * Runs `adb reverse tcp:PORT tcp:PORT` for the current device - * to enable network requests forwarding on localhost:PORT (computer<->device). - * For more information, see {@link https://www.reddit.com/r/reactnative/comments/5etpqw/what_do_you_call_what_adb_reverse_is_doing|here}. - * This is a no-op when running on iOS. - */ - reverseTcpPort(port: number): Promise; - - /** - * (Android Only) - * Runs `adb reverse --remove tcp:PORT tcp:PORT` for the current device - * to disable network requests forwarding on localhost:PORT (computer<->device). - * For more information, see {@link https://www.reddit.com/r/reactnative/comments/5etpqw/what_do_you_call_what_adb_reverse_is_doing|here}. - * This is a no-op when running on iOS. - */ - unreverseTcpPort(port: number): Promise; - } - - /** - * @deprecated - */ - type DetoxAny = NativeElement & WaitFor; - - interface ElementFacade { - (by: NativeMatcher): IndexableNativeElement; - } - - interface IndexableNativeElement extends NativeElement { - /** - * Choose from multiple elements matching the same matcher using index - * @example await element(by.text('Product')).atIndex(2).tap(); - */ - atIndex(index: number): NativeElement; - } - - interface NativeElement extends NativeElementActions { - } - - interface ByFacade { - /** - * by.id will match an id that is given to the view via testID prop. - * @example - * // In a React Native component add testID like so: - * - * // Then match with by.id: - * await element(by.id('tap_me')); - * await element(by.id(/^tap_[a-z]+$/)); - */ - id(id: string | RegExp): NativeMatcher; - - /** - * Find an element by text, useful for text fields, buttons. - * @example - * await element(by.text('Tap Me')); - * await element(by.text(/^Tap .*$/)); - */ - text(text: string | RegExp): NativeMatcher; - - /** - * Find an element by accessibilityLabel on iOS, or by contentDescription on Android. - * @example - * await element(by.label('Welcome')); - * await element(by.label(/[a-z]+/i)); - */ - label(label: string | RegExp): NativeMatcher; - - /** - * Find an element by native view type. - * @example await element(by.type('RCTImageView')); - */ - type(nativeViewType: string): NativeMatcher; - - /** - * Find an element with an accessibility trait. (iOS only) - * @example await element(by.traits(['button'])); - */ - traits(traits: string[]): NativeMatcher; - - /** - * Collection of web matchers - */ - readonly web: ByWebFacade; - } - - interface ByWebFacade { - /** - * Find an element on the DOM tree by its id - * @param id - * @example - * web.element(by.web.id('testingh1')) - */ - id(id: string): WebMatcher; - - /** - * Find an element on the DOM tree by its CSS class - * @param className - * @example - * web.element(by.web.className('a')) - */ - className(className: string): WebMatcher; - - /** - * Find an element on the DOM tree matching the given CSS selector - * @param cssSelector - * @example - * web.element(by.web.cssSelector('#cssSelector')) - */ - cssSelector(cssSelector: string): WebMatcher; - - /** - * Find an element on the DOM tree by its "name" attribute - * @param name - * @example - * web.element(by.web.name('sec_input')) - */ - name(name: string): WebMatcher; - - /** - * Find an element on the DOM tree by its XPath - * @param xpath - * @example - * web.element(by.web.xpath('//*[@id="testingh1-1"]')) - */ - xpath(xpath: string): WebMatcher; - - /** - * Find an element on the DOM tree by its link text (href content) - * @param linkText - * @example - * web.element(by.web.href('disney.com')) - */ - href(linkText: string): WebMatcher; - - /** - * Find an element on the DOM tree by its partial link text (href content) - * @param linkTextFragment - * @example - * web.element(by.web.hrefContains('disney')) - */ - hrefContains(linkTextFragment: string): WebMatcher; - - /** - * Find an element on the DOM tree by its tag name - * @param tag - * @example - * web.element(by.web.tag('mark')) - */ - tag(tagName: string): WebMatcher; - } - - interface NativeMatcher { - /** - * Find an element satisfying all the matchers - * @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'))); - */ - withDescendant(childBy: NativeMatcher): NativeMatcher; - } - - interface WebMatcher { - __web__: any; // prevent type coersion - } - - interface ExpectFacade { - (element: NativeElement): Expect; - - (webElement: WebElement): WebExpect; - } - - interface WebViewElement { - element(webMatcher: WebMatcher): IndexableWebElement; - } - - interface WebFacade extends WebViewElement { - /** - * Gets the webview element as a testing element. - * @param matcher a simple view matcher for the webview element in th UI hierarchy. - * If there is only ONE webview element in the UI hierarchy, its NOT a must to supply it. - * If there are MORE then one webview element in the UI hierarchy you MUST supply are view matcher. - */ - (matcher?: NativeMatcher): WebViewElement; - } - - interface Expect> { - - /** - * Expect the view to be at least N% visible. If no number is provided then defaults to 75%. Negating this - * expectation with a `not` expects the view's visible area to be smaller than N%. - * @param pct optional integer ranging from 1 to 100, indicating how much percent of the view should be - * visible to the user to be accepted. - * @example await expect(element(by.id('mainTitle'))).toBeVisible(35); - */ - toBeVisible(pct?: number): R; - - /** - * Negate the expectation. - * @example await expect(element(by.id('cancelButton'))).not.toBeVisible(); - */ - not: this; - - /** - * Expect the view to not be visible. - * @example await expect(element(by.id('cancelButton'))).toBeNotVisible(); - * @deprecated Use `.not.toBeVisible()` instead. - */ - toBeNotVisible(): R; - - /** - * Expect the view to exist in the UI hierarchy. - * @example await expect(element(by.id('okButton'))).toExist(); - */ - toExist(): R; - - /** - * Expect the view to not exist in the UI hierarchy. - * @example await expect(element(by.id('cancelButton'))).toNotExist(); - * @deprecated Use `.not.toExist()` instead. - */ - toNotExist(): R; - - /** - * Expect the view to be focused. - * @example await expect(element(by.id('emailInput'))).toBeFocused(); - */ - toBeFocused(): R; - - /** - * Expect the view not to be focused. - * @example await expect(element(by.id('passwordInput'))).toBeNotFocused(); - * @deprecated Use `.not.toBeFocused()` instead. - */ - toBeNotFocused(): R; - - /** - * In React Native apps, expect UI component of type to have text. - * In native iOS apps, expect UI elements of type UIButton, UILabel, UITextField or UITextViewIn to have inputText with text. - * @example await expect(element(by.id('mainTitle'))).toHaveText('Welcome back!); - */ - toHaveText(text: string): R; - - /** - * Expects a specific accessibilityLabel, as specified via the `accessibilityLabel` prop in React Native. - * On the native side (in both React Native and pure-native apps), that is equivalent to `accessibilityLabel` - * on iOS and contentDescription on Android. Refer to Detox's documentation in order to learn about caveats - * with accessibility-labels in React Native apps. - * @example await expect(element(by.id('submitButton'))).toHaveLabel('Submit'); - */ - toHaveLabel(label: string): R; - - /** - * In React Native apps, expect UI component to have testID with that id. - * In native iOS apps, expect UI element to have accessibilityIdentifier with that id. - * @example await expect(element(by.text('Submit'))).toHaveId('submitButton'); - */ - toHaveId(id: string): R; - - /** - * Expects a toggle-able element (e.g. a Switch or a Check-Box) to be on/checked or off/unchecked. - * As a reference, in react-native, this is the equivalent switch component. - * @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('temperatureDial'))).toHaveValue('25'); - */ - toHaveValue(value: any): R; - - /** - * Expect Slider to have a position (0 - 1). - * Can have an optional tolerance to take into account rounding issues on ios - * @example await expect(element(by.id('SliderId'))).toHavePosition(0.75); - * @example await expect(element(by.id('SliderId'))).toHavePosition(0.74, 0.1); - */ - toHaveSliderPosition(position: number, tolerance?: number): Promise; - } - - interface WaitForFacade { - /** - * This API polls using the given expectation continuously until the expectation is met. Use manual synchronization with waitFor only as a last resort. - * NOTE: Every waitFor call must set a timeout using withTimeout(). Calling waitFor without setting a timeout will do nothing. - * @example await waitFor(element(by.id('bigButton'))).toExist().withTimeout(2000); - */ - (element: NativeElement): Expect; - } - - interface WaitFor { - /** - * Waits for the condition to be met until the specified time (millis) have elapsed. - * @example await waitFor(element(by.id('bigButton'))).toExist().withTimeout(2000); - */ - withTimeout(millis: number): Promise; - - /** - * Performs the action repeatedly on the element until an expectation is met - * @example await waitFor(element(by.text('Item #5'))).toBeVisible().whileElement(by.id('itemsList')).scroll(50, 'down'); - */ - whileElement(by: NativeMatcher): NativeElement & WaitFor; - - // TODO: not sure about & WaitFor - check if we can chain whileElement multiple times - } - - interface NativeElementActions { - /** - * Simulate tap on an element - * @param point relative coordinates to the matched element (the element size could changes on different devices or even when changing the device font size) - * @example await element(by.id('tappable')).tap(); - * @example await element(by.id('tappable')).tap({ x:5, y:10 }); - */ - tap(point?: Point2D): Promise; - - /** - * Simulate long press on an element - * @param duration (iOS only) custom press duration time, in milliseconds. Optional (default is 1000ms). - * @example await element(by.id('tappable')).longPress(); - */ - longPress(duration?: number): Promise; - - /** - * Simulate long press on an element and then drag it to the position of the target element. (iOS Only) - * @example await element(by.id('draggable')).longPressAndDrag(2000, NaN, NaN, element(by.id('target')), NaN, NaN, 'fast', 0); - */ - 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 - * @example await element(by.id('tappable')).multiTap(3); - */ - multiTap(times: number): Promise; - - /** - * Simulate tap at a specific point on an element. - * Note: The point coordinates are relative to the matched element and the element size could changes on different devices or even when changing the device font size. - * @example await element(by.id('tappable')).tapAtPoint({ x:5, y:10 }); - * @deprecated Use `.tap()` instead. - */ - tapAtPoint(point: Point2D): Promise; - - /** - * Use the builtin keyboard to type text into a text field. - * @example await element(by.id('textField')).typeText('passcode'); - */ - typeText(text: string): Promise; - - /** - * Paste text into a text field. - * @example await element(by.id('textField')).replaceText('passcode again'); - */ - replaceText(text: string): Promise; - - /** - * Clear text from a text field. - * @example await element(by.id('textField')).clearText(); - */ - clearText(): Promise; - - /** - * Taps the backspace key on the built-in keyboard. - * @example await element(by.id('textField')).tapBackspaceKey(); - */ - tapBackspaceKey(): Promise; - - /** - * Taps the return key on the built-in keyboard. - * @example await element(by.id('textField')).tapReturnKey(); - */ - tapReturnKey(): Promise; - - /** - * Scrolls a given amount of pixels in the provided direction, starting from the provided start positions. - * @param pixels - independent device pixels - * @param direction - left/right/up/down - * @param startPositionX - the X starting scroll position, in percentage; valid input: `[0.0, 1.0]`, `NaN`; default: `NaN`—choose the best value automatically - * @param startPositionY - the Y starting scroll position, in percentage; valid input: `[0.0, 1.0]`, `NaN`; default: `NaN`—choose the best value automatically - * @example await element(by.id('scrollView')).scroll(100, 'down', NaN, 0.85); - * @example await element(by.id('scrollView')).scroll(100, 'up'); - */ - scroll( - pixels: number, - direction: Direction, - startPositionX?: number, - startPositionY?: number - ): Promise; - - /** - * Scroll to index. - * @example await element(by.id('scrollView')).scrollToIndex(10); - */ - scrollToIndex( - index: Number - ): Promise; - - /** - * Scroll to edge. - * @example await element(by.id('scrollView')).scrollTo('bottom'); - * @example await element(by.id('scrollView')).scrollTo('top'); - */ - scrollTo(edge: Direction): Promise; - - /** - * Adjust slider to position. - * @example await element(by.id('slider')).adjustSliderToPosition(0.75); - */ - adjustSliderToPosition(newPosition: number): Promise; - - /** - * Swipes in the provided direction at the provided speed, started from percentage. - * @param speed default: `fast` - * @param percentage screen percentage to swipe; valid input: `[0.0, 1.0]` - * @param optional normalizedStartingPointX X coordinate of swipe starting point, relative to the view width; valid input: `[0.0, 1.0]` - * @param normalizedStartingPointY Y coordinate of swipe starting point, relative to the view height; valid input: `[0.0, 1.0]` - * @example await element(by.id('scrollView')).swipe('down'); - * @example await element(by.id('scrollView')).swipe('down', 'fast'); - * @example await element(by.id('scrollView')).swipe('down', 'fast', 0.5); - * @example await element(by.id('scrollView')).swipe('down', 'fast', 0.5, 0.2); - * @example await element(by.id('scrollView')).swipe('down', 'fast', 0.5, 0.2, 0.5); - */ - swipe(direction: Direction, speed?: Speed, percentage?: number, normalizedStartingPointX?: number, normalizedStartingPointY?: number): Promise; - - /** - * Sets a picker view’s column to the given value. This function supports both date pickers and general picker views. (iOS Only) - * Note: When working with date pickers, you should always set an explicit locale when launching your app in order to prevent flakiness from different date and time styles. - * See [here](https://wix.github.io/Detox/docs/api/device-object-api#9-launch-with-a-specific-language-ios-only) for more information. - * - * @param column number of datepicker column (starts from 0) - * @param value string value in set column (must be correct) - * @example - * await expect(element(by.type('UIPickerView'))).toBeVisible(); - * await element(by.type('UIPickerView')).setColumnToValue(1,"6"); - * await element(by.type('UIPickerView')).setColumnToValue(2,"34"); - */ - setColumnToValue(column: number, value: string): Promise; - - /** - * Sets the date of a date-picker according to the specified date-string and format. - * @param dateString Textual representation of a date (e.g. '2023/01/01'). Should be in coherence with the format specified by `dateFormat`. - * @param dateFormat Format of `dateString`: Generally either 'ISO8601' or an explicitly specified format (e.g. 'yyyy/MM/dd'); It should - * follow the rules of NSDateFormatter for iOS and DateTimeFormatter for Android. - * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString - * @example - * await element(by.id('datePicker')).setDatePickerDate('2023-01-01T00:00:00Z', 'ISO8601'); - * await element(by.id('datePicker')).setDatePickerDate(new Date().toISOString(), 'ISO8601'); - * await element(by.id('datePicker')).setDatePickerDate('2023/01/01', 'yyyy/MM/dd'); - */ - setDatePickerDate(dateString: string, dateFormat: string): Promise; - - /** - * Triggers a given [accessibility action]{@link https://reactnative.dev/docs/accessibility#accessibility-actions}. - * @param actionName - name of the accessibility action - * @example await element(by.id('view')).performAccessibilityAction('activate'); - */ - performAccessibilityAction(actionName: string): Promise - - /** - * Pinches in the given direction with speed and angle. (iOS only) - * @param angle value in radiant, default is `0` - * @example - * await expect(element(by.id('PinchableScrollView'))).toBeVisible(); - * await element(by.id('PinchableScrollView')).pinchWithAngle('outward', 'slow', 0); - * @deprecated Use `.pinch()` instead. - */ - pinchWithAngle(direction: PinchDirection, speed: Speed, angle: number): Promise; - - /** - * Pinches with the given scale, speed, and angle. (iOS only) - * @param speed default is `fast` - * @param angle value in radiant, default is `0` - * @example - * await element(by.id('PinchableScrollView')).pinch(1.1); - * await element(by.id('PinchableScrollView')).pinch(2.0); - * await element(by.id('PinchableScrollView')).pinch(0.001); - */ - pinch(scale: number, speed?: Speed, angle?: number): Promise; - - /** - * Takes a screenshot of the element and schedules putting it in the artifacts folder upon completion of the current test. - * For more information, see {@link https://wix.github.io/Detox/docs/api/screenshots#element-level-screenshots} - * @param {string} name for the screenshot artifact - * @returns {Promise} a temporary path to the screenshot. - * @example - * test('Menu items should have logout', async () => { - * const imagePath = await element(by.id('menuRoot')).takeScreenshot('tap on menu'); - * // The temporary path will remain valid until the test completion. - * // Afterwards, the screenshot will be moved, e.g.: - * // * on success, to: /✓ Menu items should have Logout/tap on menu.png - * // * on failure, to: /✗ Menu items should have Logout/tap on menu.png - * }); - */ - takeScreenshot(name: string): Promise; - - /** - * Retrieves the OS-dependent attributes of an element. - * If there are multiple matches, it returns an array of attributes for all matched elements. - * For detailed information, refer to {@link https://wix.github.io/Detox/docs/api/actions-on-element/#getattributes} - * - * @example - * test('Get the attributes for my text element', async () => { - * const attributes = await element(by.id('myText')).getAttributes() - * const jestExpect = require('expect'); - * // 'visible' attribute available on both iOS and Android - * jestExpect(attributes.visible).toBe(true); - * // 'activationPoint' attribute available on iOS only - * jestExpect(attributes.activationPoint.x).toHaveValue(50); - * // 'width' attribute available on Android only - * jestExpect(attributes.width).toHaveValue(100); - * }) - */ - getAttributes(): Promise; - } - - interface WebExpect> { - /** - * Negate the expectation. - * @example await expect(web.element(by.web.id('sessionTimeout'))).not.toExist(); - */ - not: this; - - /** - * Expect the element content to have the `text` supplied - * @param text expected to be on the element content - * @example - * await expect(web.element(by.web.id('checkoutButton'))).toHaveText('Proceed to check out'); - */ - toHaveText(text: string): R; - - /** - * Expect the view to exist in the webview DOM tree. - * @example await expect(web.element(by.web.id('submitButton'))).toExist(); - */ - toExist(): R; - } - - interface IndexableWebElement extends WebElement { - /** - * Choose from multiple elements matching the same matcher using index - * @example await web.element(by.web.hrefContains('Details')).atIndex(2).tap(); - */ - atIndex(index: number): WebElement; - } - - interface WebElement extends WebElementActions { - } - - interface WebElementActions { - tap(): Promise; - - /** - * @param text to type - * @param isContentEditable whether its a ContentEditable element, default is false. - */ - 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; - - /** - * At the moment not working on content-editable - */ - clearText(): Promise; - - /** - * scrolling to the view, the element top position will be at the top of the screen. - */ - scrollToView(): Promise; - - /** - * Gets the input content - */ - getText(): Promise; - - /** - * Calls the focus function on the element - */ - focus(): Promise; - - /** - * Selects all the input content, works on ContentEditable at the moment. - */ - selectAllText(): Promise; - - /** - * Moves the input cursor / caret to the end of the content, works on ContentEditable at the moment. - */ - moveCursorToEnd(): Promise; - - /** - * Running a JavaScript function on the element. - * The first argument to the function will be the element itself. - * The rest of the arguments will be forwarded to the JavaScript function as is. - * - * @param script a callback function in stringified form, or a plain function reference - * without closures, bindings etc. that will be converted to a string. - * @param args optional args to pass to the script - * - * @example - * await webElement.runScript('(el) => el.click()'); - * await webElement.runScript(function setText(element, text) { - * element.textContent = text; - * }, ['Custom Title']); - */ - runScript(script: string, args?: unknown[]): Promise; - runScript(script: (...args: any[]) => F, args?: unknown[]): Promise; - - /** - * Gets the current page url - */ - getCurrentUrl(): Promise; - - /** - * Gets the current page title - */ - getTitle(): Promise; - } - - type Direction = 'left' | 'right' | 'top' | 'bottom' | 'up' | 'down'; - - type PinchDirection = 'outward' | 'inward' - - type Orientation = 'portrait' | 'landscape'; - - type Speed = 'fast' | 'slow'; - - interface LanguageAndLocale { - language?: string; - locale?: string; - } - - /** - * Source for string definitions is https://github.com/wix/AppleSimulatorUtils - */ - interface DevicePermissions { - location?: LocationPermission; - notifications?: NotificationsPermission; - calendar?: CalendarPermission; - camera?: CameraPermission; - contacts?: ContactsPermission; - health?: HealthPermission; - homekit?: HomekitPermission; - medialibrary?: MediaLibraryPermission; - microphone?: MicrophonePermission; - motion?: MotionPermission; - photos?: PhotosPermission; - reminders?: RemindersPermission; - siri?: SiriPermission; - speech?: SpeechPermission; - faceid?: FaceIDPermission; - userTracking?: UserTrackingPermission; - } - - type LocationPermission = 'always' | 'inuse' | 'never' | 'unset'; - type PermissionState = 'YES' | 'NO' | 'unset'; - type CameraPermission = PermissionState; - type ContactsPermission = PermissionState; - type CalendarPermission = PermissionState; - type HealthPermission = PermissionState; - type HomekitPermission = PermissionState; - type MediaLibraryPermission = PermissionState; - type MicrophonePermission = PermissionState; - type MotionPermission = PermissionState; - type PhotosPermission = PermissionState; - type RemindersPermission = PermissionState; - type SiriPermission = PermissionState; - type SpeechPermission = PermissionState; - type NotificationsPermission = PermissionState; - type FaceIDPermission = PermissionState; - type UserTrackingPermission = PermissionState; - - interface DeviceLaunchAppConfig { - /** - * Restart the app - * Terminate the app and launch it again. If set to false, the simulator will try to bring app from background, if the app isn't running, it will launch a new instance. default is false - */ - newInstance?: boolean; - /** - * Set runtime permissions - * Grant or deny runtime permissions for your application. - */ - permissions?: DevicePermissions; - /** - * Launch from URL - * Mock opening the app from URL to test your app's deep link handling mechanism. - */ - url?: any; - /** - * Launch with user notifications - */ - userNotification?: any; - /** - * Launch with user activity - */ - userActivity?: any; - /** - * Launch into a fresh installation - * A flag that enables relaunching into a fresh installation of the app (it will uninstall and install the binary again), default is false. - */ - delete?: boolean; - /** - * Arguments to pass-through into the app. - * Refer to the [dedicated guide](https://wix.github.io/Detox/docs/api/launch-args) for complete details. - */ - launchArgs?: Record; - /** - * Launch config for specifying the native language and locale - */ - languageAndLocale?: LanguageAndLocale; - } - - // Element Attributes Shared Among iOS and Android - interface ElementAttributes { - /** - * Whether or not the element is enabled for user interaction. - */ - enabled: boolean; - /** - * The identifier of the element. Matches accessibilityIdentifier on iOS, and the main view tag, on Android - both commonly holding the component's test ID in React Native apps. - */ - identifier: string; - /** - * Whether the element is visible. On iOS, visibility is calculated for the activation point. On Android, the attribute directly holds the value returned by View.getLocalVisibleRect()). - */ - visible: boolean; - /** - * The text value of any textual element. - */ - text?: string; - /** - * The label of the element. Largely matches accessibilityLabel for ios, and contentDescription for android. - * Refer to Detox's documentation (`toHaveLabel()` subsection) in order to learn about caveats associated with - * this property in React Native apps. - */ - label?: string; - /** - * The placeholder text value of the element. Matches hint on android. - */ - placeholder?: string; - /** - * The value of the element, where applicable. - * Matches accessibilityValue, on iOS. - * For example: the position of a slider, or whether a checkbox has been marked (Android). - */ - value?: unknown; - } - - interface IosElementAttributeFrame { - y: number; - x: number; - width: number; - height: number; - } - - interface IosElementAttributeInsets { - right: number; - top: number; - left: number; - bottom: number; - } - - // iOS Specific Attributes - interface IosElementAttributes extends ElementAttributes { - /** - * The [activation point]{@link https://developer.apple.com/documentation/objectivec/nsobject/1615179-accessibilityactivationpoint} of the element, in element coordinate space. - */ - activationPoint: Point2D; - /** - * The activation point of the element, in normalized percentage ([0.0, 1.0]). - */ - normalizedActivationPoint: Point2D; - /** - * Whether the element is hittable at the activation point. - */ - hittable: boolean; - /** - * The frame of the element, in screen coordinate space. - */ - frame: IosElementAttributeFrame; - /** - * The frame of the element, in container coordinate space. - */ - elementFrame: IosElementAttributeFrame; - /** - * The bounds of the element, in element coordinate space. - */ - elementBounds: IosElementAttributeFrame; - /** - * The safe area insets of the element, in element coordinate space. - */ - safeAreaInsets: IosElementAttributeInsets; - /** - * The safe area bounds of the element, in element coordinate space. - */ - elementSafeBounds: IosElementAttributeFrame; - /** - * The date of the element (if it is a date picker). - */ - date?: string; - /** - * The normalized slider position (if it is a slider). - */ - normalizedSliderPosition?: number; - /** - * The content offset (if it is a scroll view). - */ - contentOffset?: Point2D; - /** - * The content inset (if it is a scroll view). - */ - contentInset?: IosElementAttributeInsets; - /** - * The adjusted content inset (if it is a scroll view). - */ - adjustedContentInset?: IosElementAttributeInsets; - /** - * @example "" - */ - layer: string; - } - - // Android Specific Attributes - interface AndroidElementAttributes extends ElementAttributes { - /** - * The OS visibility type associated with the element: visible, invisible or gone. - */ - visibility: 'visible' | 'invisible' | 'gone'; - /** - * Width of the element, in pixels. - */ - width: number; - /** - * Height of the element, in pixels. - */ - height: number; - /** - * Elevation of the element. - */ - elevation: number; - /** - * Alpha value for the element. - */ - alpha: number; - /** - * Whether the element is the one currently in focus. - */ - focused: boolean; - /** - * The text size for the text element. - */ - textSize?: number; - /** - * The length of the text element (character count). - */ - length?: number; - } - } -} +/// +/// declare const detox: Detox.DetoxExportWrapper; export = detox; diff --git a/detox/src/android/core/WebElement.js b/detox/src/android/core/WebElement.js index deed912c3a..fd2d00bdf3 100644 --- a/detox/src/android/core/WebElement.js +++ b/detox/src/android/core/WebElement.js @@ -120,7 +120,7 @@ class WebViewElement { }); } - throw new DetoxRuntimeError(`element() argument is invalid, expected a web matcher, but got ${typeof element}`); + throw new DetoxRuntimeError(`element() argument is invalid, expected a web matcher, but got ${typeof webMatcher}`); } } diff --git a/detox/tsconfig.json b/detox/tsconfig.json index 65ac4d5898..d1e0ba1c3b 100644 --- a/detox/tsconfig.json +++ b/detox/tsconfig.json @@ -15,6 +15,9 @@ }, "rootDir": ".", "include": [ + "detox.d.ts", + "globals.d.ts", + "index.d.ts", "**/*.js", "**/*.ts" ], From bc0a70129c69efa1fc6962b712787418d6f3f1f6 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Thu, 26 Oct 2023 15:00:42 +0300 Subject: [PATCH 004/185] docs(website): fix headers in light mode (#4247) --- .../components/Homepage/Header/HomepageHeader.module.scss | 2 ++ website/src/components/Showcase/Hero.module.scss | 2 ++ website/src/css/_root.scss | 5 +++-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/website/src/components/Homepage/Header/HomepageHeader.module.scss b/website/src/components/Homepage/Header/HomepageHeader.module.scss index 65ef4c6b85..dbcdee8bd2 100644 --- a/website/src/components/Homepage/Header/HomepageHeader.module.scss +++ b/website/src/components/Homepage/Header/HomepageHeader.module.scss @@ -1,6 +1,8 @@ @import '@site/src/css/fonts'; .heroBanner { + --ifm-heading-color: #fff; + width: 100%; height: calc(366px + 22vw); padding: 54px 10px; diff --git a/website/src/components/Showcase/Hero.module.scss b/website/src/components/Showcase/Hero.module.scss index 747ba861df..5fe32c0357 100644 --- a/website/src/components/Showcase/Hero.module.scss +++ b/website/src/components/Showcase/Hero.module.scss @@ -1,6 +1,8 @@ @import '@site/src/css/fonts'; .heroBanner { + --ifm-heading-color: #fff; + height: 420px; width: auto; color: white; diff --git a/website/src/css/_root.scss b/website/src/css/_root.scss index 6ee993843e..504c218a52 100644 --- a/website/src/css/_root.scss +++ b/website/src/css/_root.scss @@ -28,7 +28,7 @@ --ifm-code-font-size: 95%; - --ifm-heading-color: #fff; + --ifm-button-background-color: #fff; --ifm-color-gray-900: #12161B; } @@ -36,6 +36,7 @@ &[data-theme='light'] { @include define-detox-light-colors(); --docusaurus-highlighted-code-line-bg: #d9e5f6; + --ifm-heading-color: #131419; } &[data-theme='dark'] { @@ -43,6 +44,6 @@ --docusaurus-highlighted-code-line-bg: #414458; --ifm-background-color: var(--detox-color-a1); --ifm-footer-background-color: var(--detox-color-a1); - + --ifm-heading-color: #fff; } } \ No newline at end of file From 89c14ef3e5e67461f4bd3d42c4b41364cbee32ca Mon Sep 17 00:00:00 2001 From: Mike Hardy Date: Thu, 26 Oct 2023 07:26:49 -0500 Subject: [PATCH 005/185] fix(android): add namespace to build.gradle for android gradle plugin 8 compatibility (#4229) --- detox/android/detox/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/detox/android/detox/build.gradle b/detox/android/detox/build.gradle index 69cf1a0ea7..3a8efdf957 100644 --- a/detox/android/detox/build.gradle +++ b/detox/android/detox/build.gradle @@ -21,6 +21,10 @@ def _rnNativeArtifact = rnInfo.isRN71OrHigher : 'com.facebook.react:react-native:+' android { + def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')[0].toInteger() + if (agpVersion >= 7) { + namespace "com.wix.detox" + } compileSdkVersion _compileSdkVersion buildToolsVersion _buildToolsVersion From e2ed93644a41a3d0d26d597fe2d33ec1841572a9 Mon Sep 17 00:00:00 2001 From: wixmobile Date: Fri, 27 Oct 2023 00:35:02 -0700 Subject: [PATCH 006/185] Publish 20.13.3 [ci skip] --- detox/package.json | 2 +- detox/test/package.json | 4 ++-- examples/demo-native-android/package.json | 4 ++-- examples/demo-native-ios/package.json | 4 ++-- examples/demo-plugin/package.json | 4 ++-- examples/demo-react-native-detox-instruments/package.json | 4 ++-- examples/demo-react-native/package.json | 4 ++-- lerna.json | 2 +- package.json | 2 +- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/detox/package.json b/detox/package.json index d3efb81998..e3ba698e52 100644 --- a/detox/package.json +++ b/detox/package.json @@ -1,7 +1,7 @@ { "name": "detox", "description": "E2E tests and automation for mobile", - "version": "20.13.2", + "version": "20.13.3", "bin": { "detox": "local-cli/cli.js" }, diff --git a/detox/test/package.json b/detox/test/package.json index 415ce7695b..b70bedccc1 100644 --- a/detox/test/package.json +++ b/detox/test/package.json @@ -1,6 +1,6 @@ { "name": "detox-test", - "version": "20.13.2", + "version": "20.13.3", "private": true, "engines": { "node": ">=14.5.0" @@ -56,7 +56,7 @@ "@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/parser": "^5.4.0", "cross-env": "^7.0.3", - "detox": "^20.13.2", + "detox": "^20.13.3", "eslint": "^8.41.0", "eslint-plugin-unicorn": "^47.0.0", "execa": "^5.1.1", diff --git a/examples/demo-native-android/package.json b/examples/demo-native-android/package.json index 4d515361cd..f9abdea043 100644 --- a/examples/demo-native-android/package.json +++ b/examples/demo-native-android/package.json @@ -1,13 +1,13 @@ { "name": "detox-demo-native-android", - "version": "20.13.2", + "version": "20.13.3", "private": true, "scripts": { "packager": "react-native start", "detox-server": "detox run-server" }, "devDependencies": { - "detox": "^20.13.2" + "detox": "^20.13.3" }, "detox": {} } diff --git a/examples/demo-native-ios/package.json b/examples/demo-native-ios/package.json index 9bfd8a1607..501363ad7d 100644 --- a/examples/demo-native-ios/package.json +++ b/examples/demo-native-ios/package.json @@ -1,9 +1,9 @@ { "name": "detox-demo-native-ios", - "version": "20.13.2", + "version": "20.13.3", "private": true, "devDependencies": { - "detox": "^20.13.2" + "detox": "^20.13.3" }, "detox": { "specs": "", diff --git a/examples/demo-plugin/package.json b/examples/demo-plugin/package.json index 7d0272b057..a37a773e80 100644 --- a/examples/demo-plugin/package.json +++ b/examples/demo-plugin/package.json @@ -1,12 +1,12 @@ { "name": "demo-plugin", - "version": "20.13.2", + "version": "20.13.3", "private": true, "scripts": { "test:plugin": "detox test --configuration plugin -l verbose" }, "devDependencies": { - "detox": "^20.13.2", + "detox": "^20.13.3", "jest": "^28.0.0" }, "detox": { diff --git a/examples/demo-react-native-detox-instruments/package.json b/examples/demo-react-native-detox-instruments/package.json index 6e45bedd56..3a230f4a04 100644 --- a/examples/demo-react-native-detox-instruments/package.json +++ b/examples/demo-react-native-detox-instruments/package.json @@ -1,10 +1,10 @@ { "name": "demo-react-native-detox-instruments", - "version": "20.13.2", + "version": "20.13.3", "private": true, "scripts": {}, "devDependencies": { - "detox": "^20.13.2" + "detox": "^20.13.3" }, "detox": { "configurations": { diff --git a/examples/demo-react-native/package.json b/examples/demo-react-native/package.json index 0766665eac..0ac3f3f115 100644 --- a/examples/demo-react-native/package.json +++ b/examples/demo-react-native/package.json @@ -1,6 +1,6 @@ { "name": "example", - "version": "20.13.2", + "version": "20.13.3", "private": true, "scripts": { "start": "react-native start", @@ -32,7 +32,7 @@ "@types/fs-extra": "^9.0.13", "@types/jest": "^29.2.1", "@types/react": "^18.0.24", - "detox": "^20.13.2", + "detox": "^20.13.3", "fs-extra": "^9.1.0", "jest": "^29.2.1", "ts-jest": "^29.0.3", diff --git a/lerna.json b/lerna.json index 572bb45d3b..3d71b47171 100644 --- a/lerna.json +++ b/lerna.json @@ -13,7 +13,7 @@ "generation", "." ], - "version": "20.13.2", + "version": "20.13.3", "npmClient": "npm", "command": { "publish": { diff --git a/package.json b/package.json index 00faee33dd..f56e8d8217 100644 --- a/package.json +++ b/package.json @@ -54,5 +54,5 @@ "shell-utils": "1.x.x", "unified": "^10.1.0" }, - "version": "20.13.2" + "version": "20.13.3" } From 500b525376450d479c2d4bf3dbe8abbbc240fc3b Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Mon, 30 Oct 2023 09:13:12 +0200 Subject: [PATCH 007/185] fix(android): AGP8 support (#4254) --- detox/android/detox/build.gradle | 8 ++++---- detox/package.json | 2 +- detox/test/package.json | 4 ++-- examples/demo-native-android/package.json | 4 ++-- examples/demo-native-ios/package.json | 4 ++-- examples/demo-plugin/package.json | 4 ++-- examples/demo-react-native-detox-instruments/package.json | 4 ++-- examples/demo-react-native/package.json | 4 ++-- lerna.json | 2 +- package.json | 2 +- 10 files changed, 19 insertions(+), 19 deletions(-) diff --git a/detox/android/detox/build.gradle b/detox/android/detox/build.gradle index 3a8efdf957..7ebad1f876 100644 --- a/detox/android/detox/build.gradle +++ b/detox/android/detox/build.gradle @@ -25,8 +25,8 @@ android { if (agpVersion >= 7) { namespace "com.wix.detox" } - compileSdkVersion _compileSdkVersion - buildToolsVersion _buildToolsVersion + compileSdk _compileSdkVersion + buildToolsVersion = _buildToolsVersion defaultConfig { minSdkVersion _minSdkVersion @@ -37,7 +37,7 @@ android { consumerProguardFiles 'proguard-rules.pro' } - flavorDimensions 'detox' + flavorDimensions = ['detox'] productFlavors { full { dimension 'detox' @@ -52,7 +52,7 @@ android { unitTests.returnDefaultValues = true unitTests.all { t -> reports { - html.enabled true + html.required = true } testLogging { events "passed", "skipped", "failed", "standardOut", "standardError" diff --git a/detox/package.json b/detox/package.json index e3ba698e52..448878bd64 100644 --- a/detox/package.json +++ b/detox/package.json @@ -1,7 +1,7 @@ { "name": "detox", "description": "E2E tests and automation for mobile", - "version": "20.13.3", + "version": "20.13.4-smoke.0", "bin": { "detox": "local-cli/cli.js" }, diff --git a/detox/test/package.json b/detox/test/package.json index b70bedccc1..acef04610c 100644 --- a/detox/test/package.json +++ b/detox/test/package.json @@ -1,6 +1,6 @@ { "name": "detox-test", - "version": "20.13.3", + "version": "20.13.4-smoke.0", "private": true, "engines": { "node": ">=14.5.0" @@ -56,7 +56,7 @@ "@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/parser": "^5.4.0", "cross-env": "^7.0.3", - "detox": "^20.13.3", + "detox": "^20.13.4-smoke.0", "eslint": "^8.41.0", "eslint-plugin-unicorn": "^47.0.0", "execa": "^5.1.1", diff --git a/examples/demo-native-android/package.json b/examples/demo-native-android/package.json index f9abdea043..b9344ccaf9 100644 --- a/examples/demo-native-android/package.json +++ b/examples/demo-native-android/package.json @@ -1,13 +1,13 @@ { "name": "detox-demo-native-android", - "version": "20.13.3", + "version": "20.13.4-smoke.0", "private": true, "scripts": { "packager": "react-native start", "detox-server": "detox run-server" }, "devDependencies": { - "detox": "^20.13.3" + "detox": "^20.13.4-smoke.0" }, "detox": {} } diff --git a/examples/demo-native-ios/package.json b/examples/demo-native-ios/package.json index 501363ad7d..8332e6fd7c 100644 --- a/examples/demo-native-ios/package.json +++ b/examples/demo-native-ios/package.json @@ -1,9 +1,9 @@ { "name": "detox-demo-native-ios", - "version": "20.13.3", + "version": "20.13.4-smoke.0", "private": true, "devDependencies": { - "detox": "^20.13.3" + "detox": "^20.13.4-smoke.0" }, "detox": { "specs": "", diff --git a/examples/demo-plugin/package.json b/examples/demo-plugin/package.json index a37a773e80..1e6d1d05d9 100644 --- a/examples/demo-plugin/package.json +++ b/examples/demo-plugin/package.json @@ -1,12 +1,12 @@ { "name": "demo-plugin", - "version": "20.13.3", + "version": "20.13.4-smoke.0", "private": true, "scripts": { "test:plugin": "detox test --configuration plugin -l verbose" }, "devDependencies": { - "detox": "^20.13.3", + "detox": "^20.13.4-smoke.0", "jest": "^28.0.0" }, "detox": { diff --git a/examples/demo-react-native-detox-instruments/package.json b/examples/demo-react-native-detox-instruments/package.json index 3a230f4a04..fcc2f45263 100644 --- a/examples/demo-react-native-detox-instruments/package.json +++ b/examples/demo-react-native-detox-instruments/package.json @@ -1,10 +1,10 @@ { "name": "demo-react-native-detox-instruments", - "version": "20.13.3", + "version": "20.13.4-smoke.0", "private": true, "scripts": {}, "devDependencies": { - "detox": "^20.13.3" + "detox": "^20.13.4-smoke.0" }, "detox": { "configurations": { diff --git a/examples/demo-react-native/package.json b/examples/demo-react-native/package.json index 0ac3f3f115..f400d67f9a 100644 --- a/examples/demo-react-native/package.json +++ b/examples/demo-react-native/package.json @@ -1,6 +1,6 @@ { "name": "example", - "version": "20.13.3", + "version": "20.13.4-smoke.0", "private": true, "scripts": { "start": "react-native start", @@ -32,7 +32,7 @@ "@types/fs-extra": "^9.0.13", "@types/jest": "^29.2.1", "@types/react": "^18.0.24", - "detox": "^20.13.3", + "detox": "^20.13.4-smoke.0", "fs-extra": "^9.1.0", "jest": "^29.2.1", "ts-jest": "^29.0.3", diff --git a/lerna.json b/lerna.json index 3d71b47171..8109cc07ea 100644 --- a/lerna.json +++ b/lerna.json @@ -13,7 +13,7 @@ "generation", "." ], - "version": "20.13.3", + "version": "20.13.4-smoke.0", "npmClient": "npm", "command": { "publish": { diff --git a/package.json b/package.json index f56e8d8217..33376c20b2 100644 --- a/package.json +++ b/package.json @@ -54,5 +54,5 @@ "shell-utils": "1.x.x", "unified": "^10.1.0" }, - "version": "20.13.3" + "version": "20.13.4-smoke.0" } From 940ecfc4b69fdb85b9afd6ff9ff5c27661ff2332 Mon Sep 17 00:00:00 2001 From: Dyva Pandhu Date: Mon, 30 Oct 2023 15:04:45 +0700 Subject: [PATCH 008/185] docs(website): fix outdated sdk and avd manager directories (#4256) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: “dyva” <“dyva@zero-one-group.com“> --- docs/introduction/preparing-for-ci.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/introduction/preparing-for-ci.md b/docs/introduction/preparing-for-ci.md index adcfcaee82..dff392115a 100644 --- a/docs/introduction/preparing-for-ci.md +++ b/docs/introduction/preparing-for-ci.md @@ -172,9 +172,9 @@ detox_e2e: - mkdir -p /root/.android && touch /root/.android/repositories.cfg # The Dockerimage provides two paths for sdkmanager and avdmanager, which the defaults are from $ANDROID_HOME/cmdline-tools # That is not compatible with the one that Detox is using ($ANDROID_HOME/tools/bin) - - echo yes | $ANDROID_HOME/tools/bin/sdkmanager --channel=0 --verbose "system-images;android-27;default;x86_64" + - echo yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --channel=0 --verbose "system-images;android-27;default;x86_64" "emulator" # Nexus 6P, API 27, XXXHDPI - - echo no | $ANDROID_HOME/tools/bin/avdmanager --verbose create avd --force --name "Nexus6P" --package "system-images;android-27;default;x86_64" --sdcard 200M --device 11 + - echo no | $ANDROID_HOME/cmdline-tools/latest/bin/avdmanager --verbose create avd --force --name "Nexus6P" --package "system-images;android-27;default;x86_64" --sdcard 200M --device 11 - adb start-server script: - npx detox build -c android.emu.release.ci From 2d7b4a243e6962c8b6103e6b440810e5536f962f Mon Sep 17 00:00:00 2001 From: Asaf Korem <55082339+asafkorem@users.noreply.github.com> Date: Mon, 30 Oct 2023 12:54:55 +0200 Subject: [PATCH 009/185] fix(iOS): update DetoxSync, fix sync issue. (#4258) Addresses synchronization problem reported on: https://github.com/wix/Detox/issues/4194 Bypass perform-selector sync in a unique scenario stemming from react-native-gesture-handler. This is a temporary solution, specifically addressing this case. A comprehensive fix is in the pipeline, but for the moment, this resolves the isolated occurrence. --- detox/ios/DetoxSync | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/detox/ios/DetoxSync b/detox/ios/DetoxSync index 526f2507e2..92d71f7d0d 160000 --- a/detox/ios/DetoxSync +++ b/detox/ios/DetoxSync @@ -1 +1 @@ -Subproject commit 526f2507e2e41c744e8286a83fe9325e2b4bda8d +Subproject commit 92d71f7d0d6caf32872161bacf12b1a175eafa05 From 47815d3f57d896369460cbfb44fb0947a97e2f77 Mon Sep 17 00:00:00 2001 From: wixmobile Date: Mon, 30 Oct 2023 04:09:40 -0700 Subject: [PATCH 010/185] Publish 20.13.4 [ci skip] --- detox/package.json | 2 +- detox/test/package.json | 4 ++-- examples/demo-native-android/package.json | 4 ++-- examples/demo-native-ios/package.json | 4 ++-- examples/demo-plugin/package.json | 4 ++-- examples/demo-react-native-detox-instruments/package.json | 4 ++-- examples/demo-react-native/package.json | 4 ++-- lerna.json | 2 +- package.json | 2 +- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/detox/package.json b/detox/package.json index 448878bd64..6a315e7e90 100644 --- a/detox/package.json +++ b/detox/package.json @@ -1,7 +1,7 @@ { "name": "detox", "description": "E2E tests and automation for mobile", - "version": "20.13.4-smoke.0", + "version": "20.13.4", "bin": { "detox": "local-cli/cli.js" }, diff --git a/detox/test/package.json b/detox/test/package.json index acef04610c..495ba670e1 100644 --- a/detox/test/package.json +++ b/detox/test/package.json @@ -1,6 +1,6 @@ { "name": "detox-test", - "version": "20.13.4-smoke.0", + "version": "20.13.4", "private": true, "engines": { "node": ">=14.5.0" @@ -56,7 +56,7 @@ "@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/parser": "^5.4.0", "cross-env": "^7.0.3", - "detox": "^20.13.4-smoke.0", + "detox": "^20.13.4", "eslint": "^8.41.0", "eslint-plugin-unicorn": "^47.0.0", "execa": "^5.1.1", diff --git a/examples/demo-native-android/package.json b/examples/demo-native-android/package.json index b9344ccaf9..f65ba08c83 100644 --- a/examples/demo-native-android/package.json +++ b/examples/demo-native-android/package.json @@ -1,13 +1,13 @@ { "name": "detox-demo-native-android", - "version": "20.13.4-smoke.0", + "version": "20.13.4", "private": true, "scripts": { "packager": "react-native start", "detox-server": "detox run-server" }, "devDependencies": { - "detox": "^20.13.4-smoke.0" + "detox": "^20.13.4" }, "detox": {} } diff --git a/examples/demo-native-ios/package.json b/examples/demo-native-ios/package.json index 8332e6fd7c..bf31a49ab9 100644 --- a/examples/demo-native-ios/package.json +++ b/examples/demo-native-ios/package.json @@ -1,9 +1,9 @@ { "name": "detox-demo-native-ios", - "version": "20.13.4-smoke.0", + "version": "20.13.4", "private": true, "devDependencies": { - "detox": "^20.13.4-smoke.0" + "detox": "^20.13.4" }, "detox": { "specs": "", diff --git a/examples/demo-plugin/package.json b/examples/demo-plugin/package.json index 1e6d1d05d9..faa73bfd21 100644 --- a/examples/demo-plugin/package.json +++ b/examples/demo-plugin/package.json @@ -1,12 +1,12 @@ { "name": "demo-plugin", - "version": "20.13.4-smoke.0", + "version": "20.13.4", "private": true, "scripts": { "test:plugin": "detox test --configuration plugin -l verbose" }, "devDependencies": { - "detox": "^20.13.4-smoke.0", + "detox": "^20.13.4", "jest": "^28.0.0" }, "detox": { diff --git a/examples/demo-react-native-detox-instruments/package.json b/examples/demo-react-native-detox-instruments/package.json index fcc2f45263..bc99c809db 100644 --- a/examples/demo-react-native-detox-instruments/package.json +++ b/examples/demo-react-native-detox-instruments/package.json @@ -1,10 +1,10 @@ { "name": "demo-react-native-detox-instruments", - "version": "20.13.4-smoke.0", + "version": "20.13.4", "private": true, "scripts": {}, "devDependencies": { - "detox": "^20.13.4-smoke.0" + "detox": "^20.13.4" }, "detox": { "configurations": { diff --git a/examples/demo-react-native/package.json b/examples/demo-react-native/package.json index f400d67f9a..ed0850ef53 100644 --- a/examples/demo-react-native/package.json +++ b/examples/demo-react-native/package.json @@ -1,6 +1,6 @@ { "name": "example", - "version": "20.13.4-smoke.0", + "version": "20.13.4", "private": true, "scripts": { "start": "react-native start", @@ -32,7 +32,7 @@ "@types/fs-extra": "^9.0.13", "@types/jest": "^29.2.1", "@types/react": "^18.0.24", - "detox": "^20.13.4-smoke.0", + "detox": "^20.13.4", "fs-extra": "^9.1.0", "jest": "^29.2.1", "ts-jest": "^29.0.3", diff --git a/lerna.json b/lerna.json index 8109cc07ea..cad50b1501 100644 --- a/lerna.json +++ b/lerna.json @@ -13,7 +13,7 @@ "generation", "." ], - "version": "20.13.4-smoke.0", + "version": "20.13.4", "npmClient": "npm", "command": { "publish": { diff --git a/package.json b/package.json index 33376c20b2..091c2fe8e4 100644 --- a/package.json +++ b/package.json @@ -54,5 +54,5 @@ "shell-utils": "1.x.x", "unified": "^10.1.0" }, - "version": "20.13.4-smoke.0" + "version": "20.13.4" } From a38e7368d12b203bde82506536ca2d003094b772 Mon Sep 17 00:00:00 2001 From: wixmobile Date: Mon, 30 Oct 2023 04:10:45 -0700 Subject: [PATCH 011/185] Publish docs version 20.x --- .../version-20.x/introduction/preparing-for-ci.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/versioned_docs/version-20.x/introduction/preparing-for-ci.md b/website/versioned_docs/version-20.x/introduction/preparing-for-ci.md index adcfcaee82..dff392115a 100644 --- a/website/versioned_docs/version-20.x/introduction/preparing-for-ci.md +++ b/website/versioned_docs/version-20.x/introduction/preparing-for-ci.md @@ -172,9 +172,9 @@ detox_e2e: - mkdir -p /root/.android && touch /root/.android/repositories.cfg # The Dockerimage provides two paths for sdkmanager and avdmanager, which the defaults are from $ANDROID_HOME/cmdline-tools # That is not compatible with the one that Detox is using ($ANDROID_HOME/tools/bin) - - echo yes | $ANDROID_HOME/tools/bin/sdkmanager --channel=0 --verbose "system-images;android-27;default;x86_64" + - echo yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --channel=0 --verbose "system-images;android-27;default;x86_64" "emulator" # Nexus 6P, API 27, XXXHDPI - - echo no | $ANDROID_HOME/tools/bin/avdmanager --verbose create avd --force --name "Nexus6P" --package "system-images;android-27;default;x86_64" --sdcard 200M --device 11 + - echo no | $ANDROID_HOME/cmdline-tools/latest/bin/avdmanager --verbose create avd --force --name "Nexus6P" --package "system-images;android-27;default;x86_64" --sdcard 200M --device 11 - adb start-server script: - npx detox build -c android.emu.release.ci From 5ffe90d4c8bdb7b4241c761304b3f7c28bda4f72 Mon Sep 17 00:00:00 2001 From: Asaf Korem Date: Tue, 31 Oct 2023 13:22:23 +0200 Subject: [PATCH 012/185] fix(iOS): fix race condition with reanimated. Update DetoxSync to latest commit. Resolves https://github.com/wix/Detox/issues/4253. See fix description here: https://github.com/wix-incubator/DetoxSync/pull/70 --- detox/ios/DetoxSync | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/detox/ios/DetoxSync b/detox/ios/DetoxSync index 92d71f7d0d..5b26331610 160000 --- a/detox/ios/DetoxSync +++ b/detox/ios/DetoxSync @@ -1 +1 @@ -Subproject commit 92d71f7d0d6caf32872161bacf12b1a175eafa05 +Subproject commit 5b26331610dc358fc48894023b5d172c238e54b8 From 81db382567a147b0c597d39a519c6ca32d4d97df Mon Sep 17 00:00:00 2001 From: wixmobile Date: Tue, 31 Oct 2023 05:33:25 -0700 Subject: [PATCH 013/185] Publish 20.13.5 [ci skip] --- detox/package.json | 2 +- detox/test/package.json | 4 ++-- examples/demo-native-android/package.json | 4 ++-- examples/demo-native-ios/package.json | 4 ++-- examples/demo-plugin/package.json | 4 ++-- examples/demo-react-native-detox-instruments/package.json | 4 ++-- examples/demo-react-native/package.json | 4 ++-- lerna.json | 2 +- package.json | 2 +- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/detox/package.json b/detox/package.json index 6a315e7e90..a76c616bcc 100644 --- a/detox/package.json +++ b/detox/package.json @@ -1,7 +1,7 @@ { "name": "detox", "description": "E2E tests and automation for mobile", - "version": "20.13.4", + "version": "20.13.5", "bin": { "detox": "local-cli/cli.js" }, diff --git a/detox/test/package.json b/detox/test/package.json index 495ba670e1..4325ea6570 100644 --- a/detox/test/package.json +++ b/detox/test/package.json @@ -1,6 +1,6 @@ { "name": "detox-test", - "version": "20.13.4", + "version": "20.13.5", "private": true, "engines": { "node": ">=14.5.0" @@ -56,7 +56,7 @@ "@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/parser": "^5.4.0", "cross-env": "^7.0.3", - "detox": "^20.13.4", + "detox": "^20.13.5", "eslint": "^8.41.0", "eslint-plugin-unicorn": "^47.0.0", "execa": "^5.1.1", diff --git a/examples/demo-native-android/package.json b/examples/demo-native-android/package.json index f65ba08c83..581f293e37 100644 --- a/examples/demo-native-android/package.json +++ b/examples/demo-native-android/package.json @@ -1,13 +1,13 @@ { "name": "detox-demo-native-android", - "version": "20.13.4", + "version": "20.13.5", "private": true, "scripts": { "packager": "react-native start", "detox-server": "detox run-server" }, "devDependencies": { - "detox": "^20.13.4" + "detox": "^20.13.5" }, "detox": {} } diff --git a/examples/demo-native-ios/package.json b/examples/demo-native-ios/package.json index bf31a49ab9..c2aa0383d4 100644 --- a/examples/demo-native-ios/package.json +++ b/examples/demo-native-ios/package.json @@ -1,9 +1,9 @@ { "name": "detox-demo-native-ios", - "version": "20.13.4", + "version": "20.13.5", "private": true, "devDependencies": { - "detox": "^20.13.4" + "detox": "^20.13.5" }, "detox": { "specs": "", diff --git a/examples/demo-plugin/package.json b/examples/demo-plugin/package.json index faa73bfd21..13f004b4eb 100644 --- a/examples/demo-plugin/package.json +++ b/examples/demo-plugin/package.json @@ -1,12 +1,12 @@ { "name": "demo-plugin", - "version": "20.13.4", + "version": "20.13.5", "private": true, "scripts": { "test:plugin": "detox test --configuration plugin -l verbose" }, "devDependencies": { - "detox": "^20.13.4", + "detox": "^20.13.5", "jest": "^28.0.0" }, "detox": { diff --git a/examples/demo-react-native-detox-instruments/package.json b/examples/demo-react-native-detox-instruments/package.json index bc99c809db..a47b77d555 100644 --- a/examples/demo-react-native-detox-instruments/package.json +++ b/examples/demo-react-native-detox-instruments/package.json @@ -1,10 +1,10 @@ { "name": "demo-react-native-detox-instruments", - "version": "20.13.4", + "version": "20.13.5", "private": true, "scripts": {}, "devDependencies": { - "detox": "^20.13.4" + "detox": "^20.13.5" }, "detox": { "configurations": { diff --git a/examples/demo-react-native/package.json b/examples/demo-react-native/package.json index ed0850ef53..409d1f255d 100644 --- a/examples/demo-react-native/package.json +++ b/examples/demo-react-native/package.json @@ -1,6 +1,6 @@ { "name": "example", - "version": "20.13.4", + "version": "20.13.5", "private": true, "scripts": { "start": "react-native start", @@ -32,7 +32,7 @@ "@types/fs-extra": "^9.0.13", "@types/jest": "^29.2.1", "@types/react": "^18.0.24", - "detox": "^20.13.4", + "detox": "^20.13.5", "fs-extra": "^9.1.0", "jest": "^29.2.1", "ts-jest": "^29.0.3", diff --git a/lerna.json b/lerna.json index cad50b1501..e5ebf3fbb3 100644 --- a/lerna.json +++ b/lerna.json @@ -13,7 +13,7 @@ "generation", "." ], - "version": "20.13.4", + "version": "20.13.5", "npmClient": "npm", "command": { "publish": { diff --git a/package.json b/package.json index 091c2fe8e4..0c99489c75 100644 --- a/package.json +++ b/package.json @@ -54,5 +54,5 @@ "shell-utils": "1.x.x", "unified": "^10.1.0" }, - "version": "20.13.4" + "version": "20.13.5" } From 6e87dc13826341dba21ed0a732e5b57efa08e7b5 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Wed, 1 Nov 2023 13:46:53 +0200 Subject: [PATCH 014/185] feat(cli): detached runner (#4260) --- detox/detox.d.ts | 7 ++++ detox/internals.d.ts | 3 +- detox/local-cli/test.test.js | 40 +++++++++++++++++++ .../testCommand/TestRunnerCommand.js | 29 ++++++++++++-- detox/local-cli/utils/interruptListeners.js | 15 +++++++ detox/runners/jest/testEnvironment/index.js | 8 +++- .../src/configuration/composeRunnerConfig.js | 4 +- .../configuration/composeRunnerConfig.test.js | 8 ++++ detox/src/ipc/IPCClient.js | 4 +- detox/src/ipc/IPCServer.js | 8 ++-- detox/src/ipc/ipc.test.js | 34 +++++++++++++++- detox/src/realms/DetoxContext.js | 2 +- detox/src/realms/DetoxPrimaryContext.js | 4 +- detox/src/realms/DetoxSecondaryContext.js | 4 +- detox/test/e2e/detox.config.js | 4 +- detox/test/integration/jest.config.js | 2 + docs/config/testRunner.mdx | 10 +++++ 17 files changed, 167 insertions(+), 19 deletions(-) create mode 100644 detox/local-cli/utils/interruptListeners.js diff --git a/detox/detox.d.ts b/detox/detox.d.ts index 316493416f..785321e455 100644 --- a/detox/detox.d.ts +++ b/detox/detox.d.ts @@ -235,6 +235,13 @@ declare global { * @see {DetoxInternals.DetoxTestFileReport#isPermanentFailure} */ bail?: boolean; + /** + * When true, tells `detox test` to spawn the test runner in a detached mode. + * This is useful in CI environments, where you want to intercept SIGINT and SIGTERM signals to gracefully shut down the test runner and the device. + * Instead of passing the kill signal to the child process (the test runner), Detox will send an emergency shutdown request to all the workers, and then it will wait for them to finish. + * @default false + */ + detached?: boolean; /** * Custom handler to process --inspect-brk CLI flag. * Use it when you rely on another test runner than Jest to mutate the config. diff --git a/detox/internals.d.ts b/detox/internals.d.ts index c0a2fecad8..9496b5ec9e 100644 --- a/detox/internals.d.ts +++ b/detox/internals.d.ts @@ -116,8 +116,9 @@ declare global { /** * Workaround for Jest exiting abruptly in --bail mode. * Makes sure that all workers and their test environments are properly torn down. + * @param [permanent] - forbids further retries */ - unsafe_conductEarlyTeardown(): Promise; + unsafe_conductEarlyTeardown(permanent?: boolean): Promise; /** * Reports to Detox CLI about passed and failed test files. * The failed test files might be re-run again if diff --git a/detox/local-cli/test.test.js b/detox/local-cli/test.test.js index c29ce59959..4c86cfca1f 100644 --- a/detox/local-cli/test.test.js +++ b/detox/local-cli/test.test.js @@ -5,6 +5,7 @@ if (process.platform === 'win32') { jest.mock('../src/logger/DetoxLogger'); jest.mock('./utils/jestInternals'); +jest.mock('./utils/interruptListeners'); const cp = require('child_process'); const cpSpawn = cp.spawn; @@ -18,6 +19,8 @@ const { buildMockCommand, callCli } = require('../__tests__/helpers'); const { DEVICE_LAUNCH_ARGS_DEPRECATION } = require('./testCommand/warnings'); +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + describe('CLI', () => { let _env; let logger; @@ -143,6 +146,38 @@ describe('CLI', () => { }); }); + describe('detached runner', () => { + beforeEach(() => { + detoxConfig.testRunner.detached = true; + }); + + test('should be able to run as you would normally expect', async () => { + await run(); + expect(_.last(cliCall().argv)).toEqual('e2e/config.json'); + }); + + test('should intercept SIGINT and SIGTERM', async () => { + const { subscribe, unsubscribe } = jest.requireMock('./utils/interruptListeners'); + const simulateSIGINT = () => subscribe.mock.calls[0][0](); + + mockExitCode(1); + mockLongRun(2000); + + await Promise.all([ + run('--retries 2').catch(_.noop), + sleep(1000).then(() => { + simulateSIGINT(); + simulateSIGINT(); + expect(unsubscribe).not.toHaveBeenCalled(); + }), + ]); + + expect(unsubscribe).toHaveBeenCalled(); + expect(cliCall(0)).not.toBe(null); + expect(cliCall(1)).toBe(null); + }); + }); + test('should use testRunner.args._ as default specs', async () => { detoxConfig.testRunner.args._ = ['e2e/sanity']; await run(); @@ -620,4 +655,9 @@ describe('CLI', () => { mockExecutable.options.exitCode = code; detoxConfig.testRunner.args.$0 = mockExecutable.cmd; } + + function mockLongRun(ms) { + mockExecutable.options.sleep = ms; + detoxConfig.testRunner.args.$0 = mockExecutable.cmd; + } }); diff --git a/detox/local-cli/testCommand/TestRunnerCommand.js b/detox/local-cli/testCommand/TestRunnerCommand.js index fcf4534732..e24fb64dbe 100644 --- a/detox/local-cli/testCommand/TestRunnerCommand.js +++ b/detox/local-cli/testCommand/TestRunnerCommand.js @@ -12,6 +12,7 @@ const { escapeSpaces, useForwardSlashes } = require('../../src/utils/shellUtils' const sleep = require('../../src/utils/sleep'); const AppStartCommand = require('../startCommand/AppStartCommand'); const { markErrorAsLogged } = require('../utils/cliErrorHandling'); +const interruptListeners = require('../utils/interruptListeners'); const TestRunnerError = require('./TestRunnerError'); @@ -28,10 +29,12 @@ class TestRunnerCommand { const appsConfig = opts.config.apps; this._argv = runnerConfig.args; + this._detached = runnerConfig.detached; this._retries = runnerConfig.retries; this._envHint = this._buildEnvHint(opts.env); this._startCommands = this._prepareStartCommands(appsConfig, cliConfig); this._envFwd = {}; + this._terminating = false; if (runnerConfig.forwardEnv) { this._envFwd = this._buildEnvOverride(cliConfig, deviceConfig); @@ -59,16 +62,20 @@ class TestRunnerCommand { } catch (e) { launchError = e; + if (this._terminating) { + runsLeft = 0; + } + const failedTestFiles = detox.session.testResults.filter(r => !r.success); const { bail } = detox.config.testRunner; if (bail && failedTestFiles.some(r => r.isPermanentFailure)) { - throw e; + runsLeft = 0; } const testFilesToRetry = failedTestFiles.filter(r => !r.isPermanentFailure).map(r => r.testFilePath); - if (_.isEmpty(testFilesToRetry)) { - throw e; + if (testFilesToRetry.length === 0) { + runsLeft = 0; } if (--runsLeft > 0) { @@ -143,6 +150,15 @@ class TestRunnerCommand { }, _.isUndefined); } + _onTerminate = () => { + if (this._terminating) { + return; + } + + this._terminating = true; + return detox.unsafe_conductEarlyTeardown(true); + }; + async _spawnTestRunner() { const fullCommand = this._buildSpawnArguments().map(escapeSpaces); const fullCommandWithHint = printEnvironmentVariables(this._envHint) + fullCommand.join(' '); @@ -153,6 +169,7 @@ class TestRunnerCommand { cp.spawn(fullCommand[0], fullCommand.slice(1), { shell: true, stdio: 'inherit', + detached: this._detached, env: _({}) .assign(process.env) .assign(this._envFwd) @@ -162,6 +179,8 @@ class TestRunnerCommand { }) .on('error', /* istanbul ignore next */ (err) => reject(err)) .on('exit', (code, signal) => { + interruptListeners.unsubscribe(this._onTerminate); + if (code === 0) { log.trace.end({ success: true }); resolve(); @@ -175,6 +194,10 @@ class TestRunnerCommand { reject(markErrorAsLogged(error)); } }); + + if (this._detached) { + interruptListeners.subscribe(this._onTerminate); + } }); } diff --git a/detox/local-cli/utils/interruptListeners.js b/detox/local-cli/utils/interruptListeners.js new file mode 100644 index 0000000000..d59ad52b46 --- /dev/null +++ b/detox/local-cli/utils/interruptListeners.js @@ -0,0 +1,15 @@ +function subscribe(listener) { + process.on('SIGINT', listener); + process.on('SIGTERM', listener); +} + +function unsubscribe(listener) { + process.removeListener('SIGINT', listener); + process.removeListener('SIGTERM', listener); +} + +module.exports = { + subscribe, + unsubscribe, +}; + diff --git a/detox/runners/jest/testEnvironment/index.js b/detox/runners/jest/testEnvironment/index.js index cddafe5e85..9418a65ed7 100644 --- a/detox/runners/jest/testEnvironment/index.js +++ b/detox/runners/jest/testEnvironment/index.js @@ -73,7 +73,9 @@ class DetoxCircusEnvironment extends NodeEnvironment { // @ts-expect-error TS2425 async handleTestEvent(event, state) { if (detox.session.unsafe_earlyTeardown) { - throw new Error('Detox halted test execution due to an early teardown request'); + if (event.name === 'test_fn_start' || event.name === 'hook_start') { + throw new Error('Detox halted test execution due to an early teardown request'); + } } this._timer.schedule(state.testTimeout != null ? state.testTimeout : this.setupTimeout); @@ -107,6 +109,10 @@ class DetoxCircusEnvironment extends NodeEnvironment { * @protected */ async initDetox() { + if (detox.session.unsafe_earlyTeardown) { + throw new Error('Detox halted test execution due to an early teardown request'); + } + const opts = { global: this.global, workerId: `w${process.env.JEST_WORKER_ID}`, diff --git a/detox/src/configuration/composeRunnerConfig.js b/detox/src/configuration/composeRunnerConfig.js index a367467521..2c3394e81f 100644 --- a/detox/src/configuration/composeRunnerConfig.js +++ b/detox/src/configuration/composeRunnerConfig.js @@ -32,6 +32,7 @@ function composeRunnerConfig(opts) { retries: 0, inspectBrk: inspectBrkHookDefault, forwardEnv: false, + detached: false, bail: false, jest: { setupTimeout: 300000, @@ -56,8 +57,9 @@ function composeRunnerConfig(opts) { if (typeof merged.inspectBrk === 'function') { if (cliConfig.inspectBrk) { - merged.retries = 0; + merged.detached = false; merged.forwardEnv = true; + merged.retries = 0; merged.inspectBrk(merged); } diff --git a/detox/src/configuration/composeRunnerConfig.test.js b/detox/src/configuration/composeRunnerConfig.test.js index 958fe04aa8..84bfc54503 100644 --- a/detox/src/configuration/composeRunnerConfig.test.js +++ b/detox/src/configuration/composeRunnerConfig.test.js @@ -46,6 +46,7 @@ describe('composeRunnerConfig', () => { }, retries: 0, bail: false, + detached: false, forwardEnv: false, }); }); @@ -60,6 +61,7 @@ describe('composeRunnerConfig', () => { }, bail: true, retries: 1, + detached: true, forwardEnv: true, }; @@ -77,6 +79,7 @@ describe('composeRunnerConfig', () => { }, bail: true, retries: 1, + detached: true, forwardEnv: true, }); }); @@ -92,6 +95,7 @@ describe('composeRunnerConfig', () => { }, bail: true, retries: 1, + detached: true, forwardEnv: true, }; @@ -109,6 +113,7 @@ describe('composeRunnerConfig', () => { }, bail: true, retries: 1, + detached: true, forwardEnv: true, }); }); @@ -222,6 +227,7 @@ describe('composeRunnerConfig', () => { reportSpecs: true, }, bail: true, + detached: true, retries: 1, }; @@ -236,6 +242,7 @@ describe('composeRunnerConfig', () => { reportSpecs: false, }, bail: false, + detached: false, retries: 3, }; @@ -256,6 +263,7 @@ describe('composeRunnerConfig', () => { reportWorkerAssign: true, }, bail: false, + detached: false, retries: 3, forwardEnv: false, }); diff --git a/detox/src/ipc/IPCClient.js b/detox/src/ipc/IPCClient.js index 5033d1804f..ca55543610 100644 --- a/detox/src/ipc/IPCClient.js +++ b/detox/src/ipc/IPCClient.js @@ -86,8 +86,8 @@ class IPCClient { this._sessionState.patch(sessionState); } - async conductEarlyTeardown() { - const sessionState = await this._emit('conductEarlyTeardown', {}); + async conductEarlyTeardown({ permanent }) { + const sessionState = await this._emit('conductEarlyTeardown', { permanent }); this._sessionState.patch(sessionState); } diff --git a/detox/src/ipc/IPCServer.js b/detox/src/ipc/IPCServer.js index 1fba85ac27..51f4e2f10e 100644 --- a/detox/src/ipc/IPCServer.js +++ b/detox/src/ipc/IPCServer.js @@ -73,6 +73,7 @@ class IPCServer { this._ipc.server.emit(socket, 'registerContextDone', { testResults: this._sessionState.testResults, testSessionIndex: this._sessionState.testSessionIndex, + unsafe_earlyTeardown: this._sessionState.unsafe_earlyTeardown, }); } @@ -90,10 +91,11 @@ class IPCServer { } } - onConductEarlyTeardown(_data = null, socket = null) { - // Note that we don't save `unsafe_earlyTeardown` in the primary session state - // because it's transient and needed only to make the workers quit early. + onConductEarlyTeardown({ permanent }, socket = null) { const newState = { unsafe_earlyTeardown: true }; + if (permanent) { + Object.assign(this._sessionState, newState); + } if (socket) { this._ipc.server.emit(socket, 'conductEarlyTeardownDone', newState); diff --git a/detox/src/ipc/ipc.test.js b/detox/src/ipc/ipc.test.js index 4414935ffe..29f70758b0 100644 --- a/detox/src/ipc/ipc.test.js +++ b/detox/src/ipc/ipc.test.js @@ -129,6 +129,36 @@ describe('IPC', () => { }); }); + describe('conductEarlyTeardown', () => { + beforeEach(() => ipcServer.init()); + + describe('(permanent)', () => { + beforeEach(() => ipcServer.onConductEarlyTeardown({ permanent: true })); + + it('should change the session state', async () => { + expect(ipcServer.sessionState.unsafe_earlyTeardown).toEqual(true); + }); + + it('should pass the session state to the client', async () => { + await ipcClient1.init(); + expect(ipcClient1.sessionState.unsafe_earlyTeardown).toEqual(true); + }); + }); + + describe('(transient)', () => { + beforeEach(() => ipcServer.onConductEarlyTeardown({ permanent: false })); + + it('should not change the session state', async () => { + expect(ipcServer.sessionState.unsafe_earlyTeardown).toBe(undefined); + }); + + it('should not pass the session state to the client', async () => { + await ipcClient1.init(); + expect(ipcClient1.sessionState.unsafe_earlyTeardown).toBe(undefined); + }); + }); + }); + describe('dispose()', () => { it('should resolve if there are no connected clients', async () => { await ipcServer.init(); @@ -278,7 +308,7 @@ describe('IPC', () => { expect(ipcClient1.sessionState).toEqual(expect.objectContaining({ unsafe_earlyTeardown: undefined })); expect(ipcClient2.sessionState).toEqual(expect.objectContaining({ unsafe_earlyTeardown: undefined })); - await ipcClient1.conductEarlyTeardown(); + await ipcClient1.conductEarlyTeardown({ permanent: false }); expect(ipcClient1.sessionState).toEqual(expect.objectContaining({ unsafe_earlyTeardown: true })); await sleep(10); // broadcasting might happen with a delay expect(ipcClient2.sessionState).toEqual(expect.objectContaining({ unsafe_earlyTeardown: true })); @@ -287,7 +317,7 @@ describe('IPC', () => { it('should broadcast early teardown in all connected clients (from server)', async () => { expect(ipcClient1.sessionState).toEqual(expect.objectContaining({ unsafe_earlyTeardown: undefined })); expect(ipcClient2.sessionState).toEqual(expect.objectContaining({ unsafe_earlyTeardown: undefined })); - await ipcServer.onConductEarlyTeardown(); + await ipcServer.onConductEarlyTeardown({ permanent: false }); await sleep(10); // broadcasting might happen with a delay expect(ipcClient1.sessionState).toEqual(expect.objectContaining({ unsafe_earlyTeardown: true })); expect(ipcClient2.sessionState).toEqual(expect.objectContaining({ unsafe_earlyTeardown: true })); diff --git a/detox/src/realms/DetoxContext.js b/detox/src/realms/DetoxContext.js index 28d2f6d870..67c6d6ea40 100644 --- a/detox/src/realms/DetoxContext.js +++ b/detox/src/realms/DetoxContext.js @@ -102,7 +102,7 @@ class DetoxContext { /** @abstract */ [symbols.reportTestResults](_testResults) {} /** @abstract */ - [symbols.conductEarlyTeardown]() {} + [symbols.conductEarlyTeardown](_permanent) {} /** * @abstract * @param {Partial} _opts diff --git a/detox/src/realms/DetoxPrimaryContext.js b/detox/src/realms/DetoxPrimaryContext.js index bec924eda2..feb061fdb9 100644 --- a/detox/src/realms/DetoxPrimaryContext.js +++ b/detox/src/realms/DetoxPrimaryContext.js @@ -51,9 +51,9 @@ class DetoxPrimaryContext extends DetoxContext { } } - [symbols.conductEarlyTeardown] = async () => { + [symbols.conductEarlyTeardown] = async (permanent = false) => { if (this[_ipcServer]) { - await this[_ipcServer].onConductEarlyTeardown(); + await this[_ipcServer].onConductEarlyTeardown({ permanent }); } }; diff --git a/detox/src/realms/DetoxSecondaryContext.js b/detox/src/realms/DetoxSecondaryContext.js index 0a74e9a25a..108daf29ce 100644 --- a/detox/src/realms/DetoxSecondaryContext.js +++ b/detox/src/realms/DetoxSecondaryContext.js @@ -33,9 +33,9 @@ class DetoxSecondaryContext extends DetoxContext { } } - [symbols.conductEarlyTeardown] = async () => { + [symbols.conductEarlyTeardown] = async (permanent = false) => { if (this[_ipcClient]) { - await this[_ipcClient].conductEarlyTeardown(); + await this[_ipcClient].conductEarlyTeardown({ permanent }); } else { throw new DetoxInternalError('Detected an attempt to report early teardown using a non-initialized context.'); } diff --git a/detox/test/e2e/detox.config.js b/detox/test/e2e/detox.config.js index bd6a5a4c57..84a2a367b0 100644 --- a/detox/test/e2e/detox.config.js +++ b/detox/test/e2e/detox.config.js @@ -12,8 +12,10 @@ const config = { args: { $0: 'nyc jest', config: 'e2e/jest.config.js', - _: ['e2e/'] + forceExit: process.env.CI ? true : undefined, + _: ['e2e/'], }, + detached: !!process.env.CI, retries: process.env.CI ? 1 : undefined, jest: { setupTimeout: +`${process.env.DETOX_JEST_SETUP_TIMEOUT || 300000}`, diff --git a/detox/test/integration/jest.config.js b/detox/test/integration/jest.config.js index 680626e0f2..019df7af49 100644 --- a/detox/test/integration/jest.config.js +++ b/detox/test/integration/jest.config.js @@ -1,3 +1,5 @@ +process.env.CI = ''; // disable CI-specific behavior for integration tests + module.exports = { "maxWorkers": 1, "testMatch": ["/*.test.js"], diff --git a/docs/config/testRunner.mdx b/docs/config/testRunner.mdx index ca7638d5f4..524f5d2c1d 100644 --- a/docs/config/testRunner.mdx +++ b/docs/config/testRunner.mdx @@ -142,6 +142,16 @@ Default: `false`. When true, tells `detox test` to cancel next retrying if it gets at least one report about a [permanent test suite failure](../api/internals.mdx#reporting-test-results). Has no effect, if [`testRunner.retries`] is undefined or set to zero. +### `testRunner.detached` \[boolean] + +Default: `false`. + +When true, tells `detox test` to spawn the test runner in a detached mode. + +This is useful in CI environments, where you want to intercept SIGINT and SIGTERM signals to gracefully shut down the test runner and the device. + +Instead of passing the kill signal to the child process (the test runner), Detox will send an emergency shutdown request to all the workers, and then it will wait for them to finish. + ### `testRunner.forwardEnv` \[boolean] Default: `false`. From 8e7af5e57aceed397e5e85a6ef66872fa04bf31b Mon Sep 17 00:00:00 2001 From: Artem Vasylenko <103461364+IrbisKronos@users.noreply.github.com> Date: Wed, 1 Nov 2023 19:15:33 +0200 Subject: [PATCH 015/185] docs(website): home page styling (#4246) --- .../components/Homepage/Features/Features.js | 11 ++-- .../Homepage/Features/Features.module.scss | 52 +++++++++++++++ .../Homepage/Features/Features.scss | 9 --- .../Homepage/Features/HomepageFeatures.js | 5 +- .../Features/HomepageFeatures.module.scss | 32 ++++++---- .../Header/HomepageHeader.module.scss | 21 ++++--- .../Homepage/SubHeader/HomepageSubHeader.js | 53 +++++++++------- .../SubHeader/HomepageSubHeader.module.scss | 63 +++++++++++-------- website/src/css/_fonts.scss | 3 +- website/src/css/_root.scss | 2 +- .../static/img/homepage/cross-platform.svg | 8 +-- website/static/img/homepage/sync.svg | 13 ++-- 12 files changed, 173 insertions(+), 99 deletions(-) create mode 100644 website/src/components/Homepage/Features/Features.module.scss delete mode 100644 website/src/components/Homepage/Features/Features.scss diff --git a/website/src/components/Homepage/Features/Features.js b/website/src/components/Homepage/Features/Features.js index f6f5af8fa2..c92c363abd 100644 --- a/website/src/components/Homepage/Features/Features.js +++ b/website/src/components/Homepage/Features/Features.js @@ -1,13 +1,14 @@ import React from 'react'; -import './Features.scss'; +import styles from './Features.module.scss'; +import clsx from 'clsx'; const Features = ({ Svg, title, description }) => { return ( -

- +
+
-

{title}

-

{description}

+

{title}

+

{description}

); diff --git a/website/src/components/Homepage/Features/Features.module.scss b/website/src/components/Homepage/Features/Features.module.scss new file mode 100644 index 0000000000..481c49c129 --- /dev/null +++ b/website/src/components/Homepage/Features/Features.module.scss @@ -0,0 +1,52 @@ +@import '@site/src/css/fonts'; + +.featuresGap { + --ifm-col-width: calc(1 / 3 * 100%); + margin-bottom: 91px; +} + +.featureImage { + height: 60px; + width: 60px; + margin-bottom: 18px; +} + +.featureTitle { + @include h5; + margin-bottom: 14px; + letter-spacing: -0.2px; +} + +.featureText { + @include t2; + max-width: 256px; +} + +@media only screen and (max-width: 996px) { + .featuresGap { + --ifm-col-width: 50%; + } +} + +@media only screen and (max-width: 500px) { + .featuresGap { + --ifm-col-width: 100%; + + margin-bottom: 29px; + padding-left: 25px; + } + + .featureImage { + height: 45px; + width: 45px; + margin-bottom: 7px; + } + + .featureTitle { + margin-bottom: 11px; + } + + .featureText { + max-width: 290px; + } +} \ No newline at end of file diff --git a/website/src/components/Homepage/Features/Features.scss b/website/src/components/Homepage/Features/Features.scss deleted file mode 100644 index 3700d7fe91..0000000000 --- a/website/src/components/Homepage/Features/Features.scss +++ /dev/null @@ -1,9 +0,0 @@ -.feature-image { - height: 60px; - width: 60px; - margin-bottom: 10px; -} - -.features-gap { - margin-bottom: 100px; -} diff --git a/website/src/components/Homepage/Features/HomepageFeatures.js b/website/src/components/Homepage/Features/HomepageFeatures.js index a4e934c1f2..4180453910 100644 --- a/website/src/components/Homepage/Features/HomepageFeatures.js +++ b/website/src/components/Homepage/Features/HomepageFeatures.js @@ -1,15 +1,16 @@ import React from 'react'; import styles from './HomepageFeatures.module.scss'; +import clsx from 'clsx'; import FeatureList from './FeatureList'; import Features from './Features'; const HomepageFeatures = () => { return (
-
+
-

Detox benefits

+

Detox benefits

diff --git a/website/src/components/Homepage/Features/HomepageFeatures.module.scss b/website/src/components/Homepage/Features/HomepageFeatures.module.scss index 6a87dd3cd0..2438182016 100644 --- a/website/src/components/Homepage/Features/HomepageFeatures.module.scss +++ b/website/src/components/Homepage/Features/HomepageFeatures.module.scss @@ -1,15 +1,23 @@ +@import '@site/src/css/fonts'; + .benefitsHeader { - font-family: 'Nunito Sans', sans-serif; - font-style: normal; - font-weight: 700; - font-size: 58px; - line-height: 70px; - color: #131419; - margin-top: 125px; - margin-bottom: 125px; - margin-left: -5px; -} + margin-bottom: 24px; -[data-theme='dark'] .benefitsHeader { - color: #ffffff; + @media only screen and (max-width: 1200px) { + --ifm-spacing-horizontal: 1.625rem; + } } + +.benefitsTitle { + @include h2; + color: var(--ifm-heading-color); + margin-top: 136px; + margin-bottom: 104px; + margin-left: -5px; + + @media only screen and (max-width: 500px) { + font-size: 40px; + margin-top: 47px; + margin-bottom: 30px; + } +} \ No newline at end of file diff --git a/website/src/components/Homepage/Header/HomepageHeader.module.scss b/website/src/components/Homepage/Header/HomepageHeader.module.scss index dbcdee8bd2..aa4f035276 100644 --- a/website/src/components/Homepage/Header/HomepageHeader.module.scss +++ b/website/src/components/Homepage/Header/HomepageHeader.module.scss @@ -4,8 +4,8 @@ --ifm-heading-color: #fff; width: 100%; - height: calc(366px + 22vw); - padding: 54px 10px; + height: calc(357px + 23vw); + padding-top: 54px; min-height: 450px; max-height: 725px; overflow: hidden; @@ -24,8 +24,13 @@ background-size: 1100px; } - @media only screen and (max-width: 870px) { - padding: 32px 9px; + @media only screen and (max-width: 890px) { + padding-top: 32px; + padding-bottom: 32px; + } + + @media only screen and (max-width: 1200px) { + --ifm-spacing-horizontal: 1.625rem; } @media only screen and (min-width: 1600px) { @@ -42,7 +47,7 @@ letter-spacing: -6px; margin-left: -10px; - @media only screen and (max-width: 870px) { + @media only screen and (max-width: 890px) { font-size: 72px; line-height: normal; letter-spacing: -3.6px; @@ -52,14 +57,13 @@ .tagline { @include t1; - max-width: 450px; min-height: 60px; margin-top: -30px; margin-bottom: 32px; letter-spacing: 0.1px; - @media only screen and (max-width: 870px) { + @media only screen and (max-width: 890px) { margin-top: -22px; margin-bottom: 21px; } @@ -71,8 +75,7 @@ .linkText { @include t2; - - font-family: 'Poppins'; + font-size: 17px; color: var(--ifm-color-gray-900); background-color: var(--ifm-button-background-color); min-width: 250px; diff --git a/website/src/components/Homepage/SubHeader/HomepageSubHeader.js b/website/src/components/Homepage/SubHeader/HomepageSubHeader.js index 66c4d2ee93..b8cee7cb8b 100644 --- a/website/src/components/Homepage/SubHeader/HomepageSubHeader.js +++ b/website/src/components/Homepage/SubHeader/HomepageSubHeader.js @@ -1,32 +1,37 @@ import React from 'react'; +import clsx from 'clsx'; import styles from './HomepageSubHeader.module.scss'; const HomepageSubHeader = () => { - return ( -
- ); }; export default HomepageSubHeader; diff --git a/website/src/components/Homepage/SubHeader/HomepageSubHeader.module.scss b/website/src/components/Homepage/SubHeader/HomepageSubHeader.module.scss index f2de7e07f5..80c44f329c 100644 --- a/website/src/components/Homepage/SubHeader/HomepageSubHeader.module.scss +++ b/website/src/components/Homepage/SubHeader/HomepageSubHeader.module.scss @@ -1,41 +1,52 @@ -.subHeader { +@import '@site/src/css/fonts'; + +.subHeaderWrapper { background-color: #171b20; - color: var(--primary-dark-a-0, #FFF); - font-family: 'Nunito Sans', sans-serif; - font-style: normal; - font-weight: 300; - font-size: 24px; width: 100%; + color: #fff; + + @media only screen and (max-width: 1200px) { + --ifm-spacing-horizontal: 1.625rem; + } +} + +.subHeader { min-height: 85px; display: flex; - flex-wrap: wrap; - justify-content: space-around; - padding: 0 var(--ifm-spacing-horizontal); + justify-content: space-between; } .callToAction { - color: var(--primary-dark-a-0, #FFF); + @include h5; + font-weight: 300; + line-height: 35px; + color: #fff; display: flex; align-items: center; margin-right: 10px; flex-wrap: nowrap; - @media only screen and (max-width: 600px) { + @media only screen and (max-width: 570px) { font-size: 18px; - line-height: 24px; + line-height: 25px; } @media only screen and (max-width: 480px) { max-width: 140px; - margin-top: 20px; + margin-top: 16px; align-items: normal; } + + @media only screen and (max-width: 370px) { + letter-spacing: -1px; + } } .socialNetworks { display: flex; align-items: center; margin: 20px 0; + @include t2; } .discordButton { @@ -58,6 +69,7 @@ display: inline-block; background-color: #1E9EEE; height: 45px; + line-height: 45px; width: 199px; border-radius: 10px; margin-right: 10px; @@ -77,8 +89,7 @@ .twitterFollowText, .twitterFollowId { - color: var(--primary-dark-a-0, #FFF); - font-size: 16px; + color: #fff; } .twitterFollowText { @@ -93,10 +104,8 @@ align-items: center; justify-content: center; border-radius: 5px; - background: var(--primary-dark-a-4, #313439); - color: var(--primary-dark-a-0, #FFF); - font-size: 16px; - line-height: 25px; + background: #313439; + color: #fff; @media only screen and (max-width: 1100px) { display: none; @@ -119,10 +128,8 @@ align-items: center; justify-content: center; border-radius: 5px; - background: var(--primary-dark-a-4, #313439); - color: var(--primary-dark-a-0, #FFF); - font-size: 16px; - line-height: 25px; + background: #313439; + color: #fff; @media only screen and (max-width: 950px) { display: none; @@ -155,9 +162,9 @@ } } -@media only screen and (max-width: 870px) { +@media only screen and (max-width: 890px) { .discordButton { - margin-right: 5px; + margin-right: 10px; } .twitterButton { @@ -170,9 +177,11 @@ } } -@media only screen and (max-width: 750px) { +@media only screen and (max-width: 780px) { .subHeader { - padding: 10px var(--ifm-spacing-horizontal); + padding-top: 10px; + padding-bottom: 10px; + } .discordButton, diff --git a/website/src/css/_fonts.scss b/website/src/css/_fonts.scss index 907fb177ee..d1fd21662c 100644 --- a/website/src/css/_fonts.scss +++ b/website/src/css/_fonts.scss @@ -1,5 +1,4 @@ @import url('https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@300;700&display=swap'); -@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;600&display=swap'); @mixin h1 { font: 80px/96px 'Nunito Sans', sans-serif; @@ -35,4 +34,4 @@ @mixin t3 { font: 400 13px/20px 'Nunito Sans', sans-serif; -} +} \ No newline at end of file diff --git a/website/src/css/_root.scss b/website/src/css/_root.scss index 504c218a52..09a0736cb0 100644 --- a/website/src/css/_root.scss +++ b/website/src/css/_root.scss @@ -36,7 +36,7 @@ &[data-theme='light'] { @include define-detox-light-colors(); --docusaurus-highlighted-code-line-bg: #d9e5f6; - --ifm-heading-color: #131419; + --ifm-heading-color: #12161B; } &[data-theme='dark'] { diff --git a/website/static/img/homepage/cross-platform.svg b/website/static/img/homepage/cross-platform.svg index 9d55ded847..46a816b92f 100644 --- a/website/static/img/homepage/cross-platform.svg +++ b/website/static/img/homepage/cross-platform.svg @@ -1,8 +1,8 @@ - - - + + + - + diff --git a/website/static/img/homepage/sync.svg b/website/static/img/homepage/sync.svg index 6f51539f00..07f7b98cfa 100644 --- a/website/static/img/homepage/sync.svg +++ b/website/static/img/homepage/sync.svg @@ -1,10 +1,15 @@ - - - + + + + + - + + + + From 18d8fad1d565b4960d7b094b45f8f69c713b5af1 Mon Sep 17 00:00:00 2001 From: Artem Vasylenko <103461364+IrbisKronos@users.noreply.github.com> Date: Fri, 3 Nov 2023 09:18:24 +0200 Subject: [PATCH 016/185] docs(website): navbar and fonts --- .../CustomBanner/StandWithUkraine.js | 2 +- .../CustomBanner/StandWithUkraine.module.scss | 11 ++++- website/src/css/_navbar.scss | 42 +++++++++++-------- website/src/css/_root.scss | 9 +++- .../homepage/flag_of_ukraine_with_waves.svg | 1 + 5 files changed, 45 insertions(+), 20 deletions(-) create mode 100644 website/static/img/homepage/flag_of_ukraine_with_waves.svg diff --git a/website/src/components/CustomBanner/StandWithUkraine.js b/website/src/components/CustomBanner/StandWithUkraine.js index a6e6bf3ef7..357ba51879 100644 --- a/website/src/components/CustomBanner/StandWithUkraine.js +++ b/website/src/components/CustomBanner/StandWithUkraine.js @@ -6,7 +6,7 @@ const StandWithUkraine = (props) => { return (
- 🇺🇦 This project is created with substantial contributions from our Ukrainian colleagues.{' '} + This project is created with substantial contributions from our Ukrainian colleagues.{' '} #StandWithUkraine
diff --git a/website/src/components/CustomBanner/StandWithUkraine.module.scss b/website/src/components/CustomBanner/StandWithUkraine.module.scss index d851162fcd..1cf06e5536 100644 --- a/website/src/components/CustomBanner/StandWithUkraine.module.scss +++ b/website/src/components/CustomBanner/StandWithUkraine.module.scss @@ -17,6 +17,15 @@ text-decoration: none; } +.uaFlag { + background-image: url(@site/static/img/homepage/flag_of_ukraine_with_waves.svg); + background-repeat: no-repeat; + display: inline-block; + height: 13px; + width: 18px; +} + + .hashtag { color: #ffd600; } @@ -25,4 +34,4 @@ .banner { height: 56px; } -} +} \ No newline at end of file diff --git a/website/src/css/_navbar.scss b/website/src/css/_navbar.scss index f71d585e1b..43a8ee8dd7 100644 --- a/website/src/css/_navbar.scss +++ b/website/src/css/_navbar.scss @@ -1,9 +1,16 @@ +@import '@site/src/css/fonts'; + [data-theme='dark'] .navbar { background-color: #12161b; } +.navbar__title { + @include t2b; + font-weight: 700; +} + .navbar__link { - margin-right: 10px; + margin-right: 20px; font-weight: 300; } @@ -18,6 +25,10 @@ font-weight: 300; } +.navbar__brand { + margin-right: 30px; +} + [data-theme='dark'] .navbar__item { padding: 5px; font-weight: 300; @@ -28,7 +39,7 @@ /* Restyle logo & hamburger button */ .navbar { - height: 70px; + height: 60px; &__items { &--right { @@ -38,7 +49,7 @@ &__logo { height: 26px; - margin: 0 10px 0 8px; + margin: 0 10px 0 0; } &__toggle { @@ -55,32 +66,29 @@ } } +@media (max-width: 996px) { + .navbar__toggle { + display: none; + } +} + + @media (max-width: 768px) { .navbar__toggle { + display: inherit; position: absolute; margin: 0; - top: 15px; + top: 10px; right: 25px; } } /* Restyle search button */ -@media (max-width: 768px) { - button.DocSearch-Button { - background-color: var(--detox-color-a3); - height: 40px; - border-radius: 10px; - padding: 0 10px; - } - - .DocSearch-Search-Icon { - opacity: 0.6; - } +.DocSearch-Button { + font-family: inherit; } -/* Restyle search button */ - @media (max-width: 768px) { button.DocSearch-Button { background-color: var(--detox-color-a3); diff --git a/website/src/css/_root.scss b/website/src/css/_root.scss index 09a0736cb0..8714d1f3ea 100644 --- a/website/src/css/_root.scss +++ b/website/src/css/_root.scss @@ -31,6 +31,8 @@ --ifm-button-background-color: #fff; --ifm-color-gray-900: #12161B; + + --ifm-font-family-base: 'Nunito Sans', sans-serif; } &[data-theme='light'] { @@ -46,4 +48,9 @@ --ifm-footer-background-color: var(--detox-color-a1); --ifm-heading-color: #fff; } -} \ No newline at end of file +} + +html { + -webkit-font-smoothing: initial; + text-rendering: initial; +} diff --git a/website/static/img/homepage/flag_of_ukraine_with_waves.svg b/website/static/img/homepage/flag_of_ukraine_with_waves.svg new file mode 100644 index 0000000000..0e595e87f9 --- /dev/null +++ b/website/static/img/homepage/flag_of_ukraine_with_waves.svg @@ -0,0 +1 @@ + \ No newline at end of file From b323ad185b5a43d715c917af9166d722849788ad Mon Sep 17 00:00:00 2001 From: Asaf Korem Date: Sun, 25 Jun 2023 21:56:20 +0300 Subject: [PATCH 017/185] chore(website): remove unused images. --- .../static/img/tutorial/docsVersionDropdown.png | Bin 25102 -> 0 bytes website/static/img/tutorial/localeDropdown.png | Bin 30020 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 website/static/img/tutorial/docsVersionDropdown.png delete mode 100644 website/static/img/tutorial/localeDropdown.png diff --git a/website/static/img/tutorial/docsVersionDropdown.png b/website/static/img/tutorial/docsVersionDropdown.png deleted file mode 100644 index ff1cbe68893d205dc0a6821bfa74d8e7d25c09cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25102 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 Js!v|N^*`%a2QvTw diff --git a/website/static/img/tutorial/localeDropdown.png b/website/static/img/tutorial/localeDropdown.png deleted file mode 100644 index d7163f96752499e2e39e771d4d5e9e42c68677aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30020 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 From babc83adc3599e638241de0bbfee5a440a91be18 Mon Sep 17 00:00:00 2001 From: Asaf Korem Date: Mon, 26 Jun 2023 14:26:44 +0300 Subject: [PATCH 018/185] docs(README): update example test with updated APIs. `toNotExist` is deprecated. --- README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 66f18d2648..650fa7b651 100644 --- a/README.md +++ b/README.md @@ -24,15 +24,19 @@ This is a test for a login screen, it runs on a device/simulator like an actual ```js describe('Login flow', () => { - it('should login successfully', async () => { + beforeEach(async () => { await device.reloadReactNative(); + }); + it('should login successfully', async () => { await element(by.id('email')).typeText('john@example.com'); await element(by.id('password')).typeText('123456'); - await element(by.text('Login')).tap(); - await expect(element(by.text('Welcome'))).toBeVisible(); - await expect(element(by.id('email'))).toNotExist(); + const loginButton = element(by.text('Login')); + await loginButton.tap(); + + await expect(loginButton).not.toExist(); + await expect(element(by.label('Welcome'))).toBeVisible(); }); }); ``` From 20004fed7cb479753a079f955c01cda7c693f6f8 Mon Sep 17 00:00:00 2001 From: Asaf Korem Date: Tue, 27 Jun 2023 17:12:46 +0300 Subject: [PATCH 019/185] docs(getting-started): extract env setup guide and add overview. --- docs/introduction/environment-setup.md | 83 +++++++++++++++++++ docs/introduction/getting-started.mdx | 108 ++++++++++--------------- website/sidebars.js | 1 + 3 files changed, 128 insertions(+), 64 deletions(-) create mode 100644 docs/introduction/environment-setup.md diff --git a/docs/introduction/environment-setup.md b/docs/introduction/environment-setup.md new file mode 100644 index 0000000000..c54972b505 --- /dev/null +++ b/docs/introduction/environment-setup.md @@ -0,0 +1,83 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +import IntroRN from './partials/_getting-started-rn.md'; +import IntroExpo from './partials/_getting-started-expo.md'; +import IntroOther from './partials/_getting-started-other.mdx'; + +# Environment Setup + +The _Introduction section_ walks you through setting up Detox in your project, one step at a time. + +You will find that some steps are longer than the others: some are a couple of paragraphs, while the others look like a dedicated multistep guide. +Bear with us - it is all necessary, and once set up, it is easy to move forward with writing tests at a high pace. + +Please select type of your mobile application before you start the tutorial: + + + + + + + + + + + + + +## React Native CLI Quickstart + +Your first step would be to complete the +[📚 React Native CLI Quickstart Guide](https://reactnative.dev/docs/next/environment-setup). + +![](../img/rn-env.png) + +

+ Open the link above and switch there to React Native CLI Quickstart tab to see + the interactive tutorial for Development OS and Target OS of your choice. +

+ +

+ Follow all + the steps (yes, even if you have a native app!) and + make sure you can create and run React Native apps on virtual testing devices. +

+ +![](../img/rn-env-done.png) + +## Detox Prerequisites + +To complete the environment setup, you still have 2 steps ahead: installing `detox-cli` and `applesimutils`. + +### 1. Command Line Tools (`detox-cli`) + +`detox-cli` is merely a script that passes commands through to +a local Detox executable located at `node_modules/.bin/detox`, +making it easier to operate Detox from the command line, e.g. `detox test -c ios.sim.debug`. + +It should be installed globally, enabling usage of the command line tools outside your npm scripts: + +```bash npm2yarn +npm install detox-cli --global +``` + +On the other hand, if you are fine with running Detox commands with `npx detox ...`, you may skip installing `detox-cli`. + +### 2. [MacOS Only] `applesimutils` + +This tool is **required** by Detox to work with iOS simulators. The recommended way to install +[`applesimutils`](https://github.com/wix/AppleSimulatorUtils) is via [Homebrew](https://brew.sh): + +```bash +brew tap wix/brew +brew install applesimutils +``` + +:::info Note + +Make sure to periodically update your version of `applesimutils` to the latest version. + +::: + +Now, when your environment is ready, let's move to the next section, [Project Setup](project-setup.mdx). diff --git a/docs/introduction/getting-started.mdx b/docs/introduction/getting-started.mdx index 7f8d906004..263d3bc163 100644 --- a/docs/introduction/getting-started.mdx +++ b/docs/introduction/getting-started.mdx @@ -1,85 +1,65 @@ -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; +# Getting Started -import IntroRN from './partials/_getting-started-rn.md'; -import IntroExpo from './partials/_getting-started-expo.md'; -import IntroOther from './partials/_getting-started-other.mdx'; +## Before You Start -# Environment Setup +### Overview of Detox -**Welcome to Detox!** +Detox is an open-source end-to-end (E2E) testing framework for React Native mobile applications. +Our main goal is to enable the testing of any end-to-end flow in the app, with maximum velocity and zero flakiness. +Detox tests your mobile app while it's running on a real device or simulator, simulating a real user's interactions. -The _Introduction section_ walks you through setting up Detox in your project, one step at a time. +![Detox overview](../img/Detox.gif) -You will find that some steps are longer than the others: some are a couple of paragraphs, while the others look like a dedicated multistep guide. -Bear with us - it is all necessary, and once set up, it is easy to move forward with writing tests at a high pace. +### What Does a Detox Test Look Like? -Please select type of your mobile application before you start the tutorial: +This is a test for a login screen, it runs on a device or simulator like an actual user: - - - - - - - - - - - +```js +describe('Login flow', () => { + beforeEach(async () => { + await device.reloadReactNative(); + }); -## React Native CLI Quickstart + it('should login successfully', async () => { + await element(by.id('email')).typeText('john@example.com'); + await element(by.id('password')).typeText('123456'); -Your first step would be to complete the -[📚 React Native CLI Quickstart Guide](https://reactnative.dev/docs/next/environment-setup). + const loginButton = element(by.text('Login')); + await loginButton.tap(); -![](../img/rn-env.png) - -

- Open the link above and switch there to React Native CLI Quickstart tab to see - the interactive tutorial for Development OS and Target OS of your choice. -

- -

- Follow all - the steps (yes, even if you have a native app!) and - make sure you can create and run React Native apps on virtual testing devices. -

- -![](../img/rn-env-done.png) - -## Detox Prerequisites - -To complete the environment setup, you still have 2 steps ahead: installing `detox-cli` and `applesimutils`. - -### 1. Command Line Tools (`detox-cli`) + await expect(loginButton).not.toExist(); + await expect(element(by.label('Welcome'))).toBeVisible(); + }); +}); +``` -`detox-cli` is merely a script that passes commands through to -a local Detox executable located at `node_modules/.bin/detox`, -making it easier to operate Detox from the command line, e.g. `detox test -c ios.sim.debug`. +### Gray box testing -It should be installed globally, enabling usage of the command line tools outside your npm scripts: +One of the most challenging aspects of automated testing on mobile is end-to-end testing. +UI testing frameworks are often not fully-deterministic, since they are built with a "black box" approach, where the test runner has no access to the internals of the app under test, and is unaware of asynchronous operations that take place in the app (such as network requests, animations, etc.). +This makes it difficult to write reliable E2E tests, and often leads to flaky tests that are hard to debug and maintain. -```bash npm2yarn -npm install detox-cli --global -``` +To address this issue, Detox adopts a "gray box" testing approach, by having access to the internals of the app under test. +This allows for more control and predictability in the testing process. For more information, you can read [how Detox works]. -On the other hand, if you are fine with running Detox commands with `npx detox ...`, you may skip installing `detox-cli`. +## Start with Detox -### 2. [MacOS Only] `applesimutils` +### Follow the Setup Guides -This tool is **required** by Detox to work with iOS simulators. The recommended way to install -[`applesimutils`](https://github.com/wix/AppleSimulatorUtils) is via [Homebrew](https://brew.sh): +The following guides in this Introduction section will help you get started with Detox, one step at a time, from setting up your environment to writing and running your first test. -```bash -brew tap wix/brew -brew install applesimutils -``` +### Join the Community 🤝 -:::info Note +As an open-source project, Detox encourages and welcomes contributions from the community. +Whether you're a developer, tester, or just someone who is passionate about making mobile development better, there's a place for you in the Detox community. +By contributing to Detox, you'll be helping to improve the reliability and user experience of mobile apps, and you'll be part of a team that is dedicated to making mobile development faster and more efficient. +We welcome all types of contributions, from code and documentation, to bug reports, questions and feature requests. -Make sure to periodically update your version of `applesimutils` to the latest version. +So, if you're interested to be part of the community, we encourage you to get involved by reading our [contributing guidelines], and join our [Discord server]. +We also have a [Twitter account] where we share updates about the project. -::: -Now, when your environment is ready, let's move to the next section, [Project Setup](project-setup.mdx). +[how Detox works]: ../articles/how-detox-works.md +[contributing guidelines]: contributing.md +[Discord server]: https://discord.gg/CkD5QKheF5 +[Twitter account]: https://twitter.com/detoxe2e diff --git a/website/sidebars.js b/website/sidebars.js index 304ff9c89e..5cd44eeeea 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -19,6 +19,7 @@ const sidebars = { label: 'Introduction', items: [ 'introduction/getting-started', + 'introduction/environment-setup', 'introduction/project-setup', 'introduction/your-first-test', 'introduction/debugging', From 6b6faa5f7d3dd92739a92fb2ba96d07fc3dd5096 Mon Sep 17 00:00:00 2001 From: Asaf Korem Date: Tue, 27 Jun 2023 17:13:35 +0300 Subject: [PATCH 020/185] docs: create contribute section. use symlink to route the old contribute guide to the new one --- docs/contributing/overview.md | 197 ++++++++++++++++++++++++++++++++++ website/docusaurus.config.js | 6 ++ website/sidebars.js | 18 ++-- 3 files changed, 214 insertions(+), 7 deletions(-) create mode 100644 docs/contributing/overview.md diff --git a/docs/contributing/overview.md b/docs/contributing/overview.md new file mode 100644 index 0000000000..f37d8c92ae --- /dev/null +++ b/docs/contributing/overview.md @@ -0,0 +1,197 @@ +# Contributing Overview + + + +This guide is about contributing to our codebase. + +We don’t have any special guidelines - just some setup walk-through! + +### Environment + +First, complete our [Getting Started](../introduction/getting-started.mdx) guide. + +#### Install the monorepo management tool, `lerna` + +```bash npm2yarn +npm install lerna@3.x.x --global +``` + +For all the internal projects (detox, detox-cli, demos, test) `lerna` will create symbolic links in `node_modules` instead of `npm` copying the content of the projects. This way, any change you do on any code is there immediately. There is no need to update node modules or copy files between projects. + +#### Install common React Native dependencies + +React-Native CLI: + +```bash npm2yarn +npm install react-native-cli --global +``` + +Watchman: + +```bash +brew install watchman +``` + +#### xcpretty + +You must also have `xcpretty` installed: + +```bash +gem install xcpretty +``` + +### Detox + +#### Clone Detox and Submodules + +```bash +git clone git@github.com:wix/Detox.git +cd detox +git submodule update --init --recursive +``` + +(this makes sure all git submodule dependencies have been properly checked out) + +#### Installing and Linking Internal Projects + +```bash +lerna bootstrap +``` + +#### Building and Testing + +##### Automatically + +`scripts/ci.ios.sh` and `scripts/ci.android.sh` are the scripts Detox runs in CI, they will run `lerna bootstrap`, unit tests, and E2E tests. Make sure these scripts pass before submitting a PR, this is exactly what Detox is going to run in CI. + +##### Manually + +The following steps can be run manually in order to build / test the various components of Detox. + +###### 1. Unit Tests and Lint + +```bash +lerna run test +``` + +Detox JS code is 100% test covered and is set to break the build if coverage gets below, so make sure you run unit tests (`lerna run test`) locally before pushing. + +Alternatively, to run only the JS tests, run the following from the `detox/detox` directory: + +```bash npm2yarn +npm run unit +``` + +or + +```bash npm2yarn +npm run unit:watch +``` + +After running the tests, _Jest_ will create a coverage report you can examine: + +```bash +cd detox +open coverage/lcov-report/index.html +``` + +###### 2. Running Detox E2E Coverage Tests + +Detox has a suite of end-to-end tests to test its own API while developing (and for regression); We maintain a special application that is "tested" against Detox’s API, but essentially, it’s the API that is tested, not the app. + +To run the tests, you must first build the native code and then run based on your target of choice (Android / iOS): + +- **iOS:** + + ```bash npm2yarn + cd detox/test + npm run build:ios + npm run e2e:ios + ``` + +- **Android:** + + ```bash npm2yarn + cd detox/test + npm run build:android + npm run e2e:android + ``` + +FYI Android test project includes two flavors: + +- `fromBin` - (**standard use case**) utilizes the precompiled `.aar` from `node_modules` just like a standard RN project. +- `fromSource` - compiles the project with RN sources from `node_modules`, this is useful when developing and debugging Espresso idle resource. + [Here](https://github.com/facebook/react-native/wiki/Building-from-source#android) are the prerequisites to compiling React Native from source. + +Each build can be triggered separately by running its Gradle assembling task (under `detox/test/android/`): + +```bash +./gradlew assembleFromSourceDebug +``` + +or: + +```bash +./gradlew assembleFromBinDebug +``` + +To run from Android Studio, React Native’s `react.gradle` script may require `node` to be in path. +On MacOS, environment variables can be exported to desktop applications by adding the following to your `.bashrc`/`.zshrc`: + +```bash +launchctl setenv PATH $PATH +``` + +###### 3. Android Native Unit-Tests + +Under `detox/android`: + +```bash +./gradlew testFullRelease +``` + +### Detox - Example Projects + +This is in fact a monorepo that also sports some example projects (for usage reference), alongside the main test project: + +- `examples/demo-react-native-jest`: Demonstrate usage of Detox in a React Native app project. +- `examples/demo-native-ios`: Demonstrates usage of Detox in a pure-native iOS app. +- `examples/demo-native-android` (broken): Demonstrates usage of Detox in a pure-native Android app. +- `examples/demo-pure-native-android`: Demonstrates usage of the _pure_ [Detox-Native](https://github.com/wix/Detox/tree/master/detox-native/README.md) project +- more... + +**In order to run E2E tests associated with any of these projects, refer to the [project-specific](https://github.com/wix/Detox/tree/master/examples) READMEs.** + +### Detox Documentation Website + +The [documentation website](https://wix.github.io/Detox) is built using [Docusaurus](https://docusaurus.io/). + +To run the website locally, run the following commands: + +```bash npm2yarn +cd website +npm install +npm start +``` + +#### Updating the Website + +To update a specific page, edit the corresponding markdown file in `docs/`. To add a new page, create a new markdown file in `docs/` and add a link to it in `website/sidebars.json`. + +##### Website Deployment + +While changes to the website are published automatically on every commit to `master` under the `Next` version, tagging and locking docs to a specific version is done automatically on every Detox release. + +In case you want to update the docs for a specific version, you can change the related files and code under `website/versioned_docs/version-/` and `website/versioned_sidebars/version--sidebars.json`. + +##### Update Old Versions + +To update a specific version with the latest changes: + +1. Remove the version from `versions.json`. +1. Run `npm run docusaurus docs:version `. + +##### Inspect Documentation Style Changes + +This [demo page](../demo.mdx) serves as a visual representation of the styling and formatting of our documentation. +If you are changing the stylesheets, check with it to prevent undesirable visual regressions. diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 5daf1f85ea..937406ea3f 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -82,6 +82,12 @@ const config = { position: 'left', label: 'API' }, + { + type: 'doc', + docId: 'contributing', + position: 'left', + label: 'Contribute' + }, { to: 'blog', label: 'Blog', diff --git a/website/sidebars.js b/website/sidebars.js index 5cd44eeeea..199a9897b0 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -61,12 +61,7 @@ const sidebars = { { type: 'doc', id: 'guide/migration', - }, - { - type: 'doc', - label: 'Contributing', - id: 'contributing' - }, + } ], apiSidebar: [ { @@ -122,7 +117,16 @@ const sidebars = { 'articles/third-party-drivers', ] }, - ] + ], + contributeSidebar: [ + { + type: 'category', + label: 'Contribute', + items: [ + 'contributing', + ] + }, + ], }; module.exports = sidebars; From 248f46abf1e8298738ca527996f5498576dfde7e Mon Sep 17 00:00:00 2001 From: Asaf Korem Date: Sun, 5 Nov 2023 12:28:50 +0200 Subject: [PATCH 021/185] docs: add code-of-conduct. --- docs/contributing/code-of-conduct.md | 143 +++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 docs/contributing/code-of-conduct.md diff --git a/docs/contributing/code-of-conduct.md b/docs/contributing/code-of-conduct.md new file mode 100644 index 0000000000..30ac992a4d --- /dev/null +++ b/docs/contributing/code-of-conduct.md @@ -0,0 +1,143 @@ +# Code of Conduct + +:::note Attribution + +This Code of Conduct is adapted from the [Contributor Covenant], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +::: + +Our community is dedicated to providing a safe and inclusive environment for all contributors. +To ensure this, we have adopted the **[Contributor Covenant]** code of conduct. +This widely recognized and respected code of conduct sets clear guidelines for behavior, and it's easy for everyone to understand and follow. + +By participating in the Detox community, you agree to abide by the terms of this code of conduct. +We take violations of this code of conduct seriously, and we will not tolerate any form of harassment or discrimination. + +If you have any questions or concerns, please reach out to the project maintainers. + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at [our Discord server]. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +[Contributor Covenant]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations +[our Discord server]: https://discord.gg/CkD5QKheF5 From 51bb3bc1c6952780418826b423f4edce5d9035eb Mon Sep 17 00:00:00 2001 From: Asaf Korem Date: Sun, 5 Nov 2023 12:32:22 +0200 Subject: [PATCH 022/185] docs: add reporting bugs. --- docs/contributing/reporting-bugs.md | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 docs/contributing/reporting-bugs.md diff --git a/docs/contributing/reporting-bugs.md b/docs/contributing/reporting-bugs.md new file mode 100644 index 0000000000..03cfc60af2 --- /dev/null +++ b/docs/contributing/reporting-bugs.md @@ -0,0 +1,30 @@ +# Reporting Bugs + +Encountering a bug? Your detailed report is key for us to identify and rectify the issue. + +Before reporting a bug, please review the following guidelines. + +## Search for Existing Issues + +Before creating a new issue, search for [existing issues] to see if the bug has already been reported. +If you find an existing issue, add a comment to it with any additional information you have. + +## Create a Bug Report + +If you can’t find an existing issue, create a new one. When creating a bug report, follow these guidelines: + +1. **Follow the Template:** While [creating a bug report on GitHub], adhere to the provided template for creating a clear and concise report. +2. **Reproduction Steps:** Providing a reproduction example is essential. +3. **Artifacts:** Include screenshots, logs, visibility artifacts, and any other relevant information. Be ready to provide more information upon request. +4. **Detailed Description:** Avoid creating issues with brief descriptions or general error messages. Providing a thorough description and context is vital. + +:::tip Tip + +Creating a minimal code reproduction is crucial as it helps other contributors and maintainers understand and investigate the issue better. +**Issues without a reproduction are less likely to be addressed**. + +::: + + +[existing issues]: https://github.com/wix/Detox/issues +[creating a bug report on GitHub]: https://github.com/wix/Detox/issues/new/choose From 59f9bdb7f06f6a8f680fcb14e8059f74565dfd11 Mon Sep 17 00:00:00 2001 From: Asaf Korem Date: Sun, 5 Nov 2023 12:33:19 +0200 Subject: [PATCH 023/185] docs: add code contributing section. --- docs/contributing/code/example-projects.md | 10 ++++ docs/contributing/code/overview.md | 15 ++++++ .../code/reviewing-pull-requests.md | 37 +++++++++++++++ .../code/setting-up-the-dev-environment.md | 47 +++++++++++++++++++ .../code/submitting-pull-requests.md | 46 ++++++++++++++++++ 5 files changed, 155 insertions(+) create mode 100644 docs/contributing/code/example-projects.md create mode 100644 docs/contributing/code/overview.md create mode 100644 docs/contributing/code/reviewing-pull-requests.md create mode 100644 docs/contributing/code/setting-up-the-dev-environment.md create mode 100644 docs/contributing/code/submitting-pull-requests.md diff --git a/docs/contributing/code/example-projects.md b/docs/contributing/code/example-projects.md new file mode 100644 index 0000000000..42cd0afc5e --- /dev/null +++ b/docs/contributing/code/example-projects.md @@ -0,0 +1,10 @@ +# Example Projects + +Explore various example projects hosted in this monorepo to understand real-world usage and testing with Detox. + +- `examples/demo-react-native-jest`: Demonstrates Detox usage in a React Native app project. +- `examples/demo-native-ios`: Illustrates Detox usage in a pure-native iOS app. +- `examples/demo-native-android` (broken): Demonstrates Detox usage in a pure-native Android app. +- `examples/demo-pure-native-android`: Highlights the _pure_ [Detox-Native](https://github.com/wix/Detox/tree/master/detox-native/README.md) project. + +To run end-to-end tests associated with these projects, refer to the [project-specific](https://github.com/wix/Detox/tree/master/examples) READMEs. diff --git a/docs/contributing/code/overview.md b/docs/contributing/code/overview.md new file mode 100644 index 0000000000..dd8e638aa5 --- /dev/null +++ b/docs/contributing/code/overview.md @@ -0,0 +1,15 @@ +# Code Changes Overview + +Welcome to the code changes section! As a contributor, it's essential to understand the project's goals and adhere to its code of conduct. Before contributing, please review any existing issues related to your work, ensure your code is well-documented, and has adequate test coverage. It's also important that your code is compatible with the project's supported platforms and their versions. + +Our collaborative workflow is simple: +1. **Identify an Issue:** If not exists already, create an issue for new features or bug reports, outlining your proposal or the identified problem. +2. **Propose a Solution:** Open a pull request with a proposed solution to the issue. On complex issues, it's recommended to discuss your approach with the community and maintainers before submitting a PR. +3. **Engage in Review:** A collaborator will review your pull request. Reviews from other contributors are also encouraged. +4. **Merge and Release:** After the review, a collaborator will merge your contribution, typically releasing it in the next version of the project. + +We use [GitHub] for managing pull requests, conducting code reviews, and tracking issues. + +The code review process is central to our collaboration. Every contribution must go through a review before merging to maintain the quality of our codebase. As a contributor, being willing to discuss your work, respond to feedback, and work with the community is key to improving the project and creating a positive environment for all contributors. + +[GitHub]: https://github.com/wix/Detox diff --git a/docs/contributing/code/reviewing-pull-requests.md b/docs/contributing/code/reviewing-pull-requests.md new file mode 100644 index 0000000000..faf304dd7f --- /dev/null +++ b/docs/contributing/code/reviewing-pull-requests.md @@ -0,0 +1,37 @@ +# Review a Pull Request + +Reviewing a PR is an important part of the contribution process. It ensures the quality of the codebase and provides an opportunity for the community to collaborate and learn from each other. + +:::note +Reviewers should refer to the [Submitting Pull Requests] page to understand the guidelines the contributor followed when creating the PR. This will provide context and ensure a thorough review process. +::: + +## Understand the Changes +- Ensure alignment with the project’s coding standards and overall architecture. +- Read the provided description and any linked issues for context. + +## Provide Constructive Feedback + +Community members can provide valuable feedback on PRs, especially if they have experience with the feature or area of the codebase. + +- Be respectful and courteous. +- Offer code snippet suggestions if something could be done better. +- Request changes if necessary and provide clear, constructive feedback. + +## Review the Code + +Experienced developers are encouraged to review PRs and provide feedback. This is a great way to learn more about the project and collaborate with the community. + +You can review the code by following these requirements: +- Alignment with coding standards and architectural guidelines. +- Adequate test coverage (unit, integration, and e2e). +- Ensure the documentation reflects the changes in the PR, if necessary. Refer to the [Documentation Changes] page for guidelines. +- Meaningful and semantically correct commit messages. + +## Merging Pull Requests +Every PR requires approval from a maintainer before it can be merged. + +However, we encourage the community to review and provide feedback on PRs. Helping with the review process not only improves the quality of the code changes, but also helps the maintainers by reducing the workload. + +[Documentation Changes]: ../documentation.md +[Submitting Pull Requests]: ./submitting-pull-requests.md diff --git a/docs/contributing/code/setting-up-the-dev-environment.md b/docs/contributing/code/setting-up-the-dev-environment.md new file mode 100644 index 0000000000..2c2ae8e737 --- /dev/null +++ b/docs/contributing/code/setting-up-the-dev-environment.md @@ -0,0 +1,47 @@ +# Setting up the Development Environment + +This document guides you through setting up your development environment to start contributing to our codebase. + +:::important Prerequisites + +Please complete our [Introduction](introduction/getting-started.mdx) guides before proceeding. This ensures you have the necessary tools and dependencies installed. + +::: + +## Setting Up The Monorepo Management + +1. Install the monorepo management tool, `lerna`: + ```bash npm2yarn + npm install lerna@3.x.x --global + ``` + +2. Clone the repository and navigate to the project directory: + ```bash + git clone git@github.com:wix/Detox.git + cd detox + git submodule update --init --recursive + ``` + +3. From the project's root directory, install and link the internal projects: + ```bash + lerna bootstrap + ``` + +## Installing Common Dependencies + +- React-Native CLI: + ```bash npm2yarn + npm install react-native-cli --global + ``` +- Watchman: + ```bash + brew install watchman + ``` +- xcpretty: + ```bash + gem install xcpretty + ``` + +## Building and Testing + +Refer to the scripts `scripts/ci.ios.sh` and `scripts/ci.android.sh` to understand the build and test process. Ensure these scripts pass before submitting a pull request. diff --git a/docs/contributing/code/submitting-pull-requests.md b/docs/contributing/code/submitting-pull-requests.md new file mode 100644 index 0000000000..d23f7089f5 --- /dev/null +++ b/docs/contributing/code/submitting-pull-requests.md @@ -0,0 +1,46 @@ +# Submitting Pull Requests + +:::note + +Before creating a PR, it's recommended to consult with the Detox collaborators. Request a design review or assistance with planning the tests to ensure alignment with project goals. + +Contact us on our [Discord Server] or open an issue on GitHub. + +::: + +## Fork the Repository +- Fork the repository to your own GitHub account. +- Create a new branch from the `master` branch for your work. + + +## Test Your Changes +- **Unit Testing:** Write unit tests for your code, usually located within the same directory as the unit under test. +- **End-to-End (E2E) Testing:** + - Our E2E tests are located under the `detox/test/e2e` directory, with each test file named after the feature it tests. These tests are testing a real React Native app (`detox/test`). + - Running the test can be done using `detox build` and `detox test` commands, as described in the `package.json` of the project. + - After changes to the native implementation of Detox (or `DetoxSync`), rebuild the framework before running the tests locally: + ```bash + detox rebuild-framework-cache + ``` +- **Integration Testing:** Usually, integration tests are not required. If there are special cases, perform integration tests under `detox/test/integration`. + +## Modify Documentation +If your changes affect the public API, update the documentation accordingly to reflect your changes. +Refer to the [Documentation Changes] page for guidelines. + +## Commit Message Guidelines +Write descriptive, meaningful commit messages that follow the **Conventional Commits** format, specifying the type of change, the scope, and a concise description. For example: + ```plaintext + fix(iOS): resolve crash on scrolling in iOS 17.0 + feat(Android): add new API for setting the device locale + chore(tests): update unit tests for new utility function + ``` +More details on good commit messages can be found [here](https://www.conventionalcommits.org/en/v1.0.0/). + +## Open the Pull Request +- Fill in the provided PR template fields on GitHub. +- Provide a clear description of your changes and any necessary context. + + +[Documentation Changes]: ../documentation.md +[Discord Server]: https://discord.gg/CkD5QKheF5 From 0ec1a7c715808e83c03945a21aa3365e35b040f7 Mon Sep 17 00:00:00 2001 From: Asaf Korem Date: Sun, 5 Nov 2023 12:33:31 +0200 Subject: [PATCH 024/185] docs: add Q&A docs section. --- .../questions/answering-questions.md | 26 ++++++++++++++ .../questions/asking-questions.md | 35 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 docs/contributing/questions/answering-questions.md create mode 100644 docs/contributing/questions/asking-questions.md diff --git a/docs/contributing/questions/answering-questions.md b/docs/contributing/questions/answering-questions.md new file mode 100644 index 0000000000..be4c7108ba --- /dev/null +++ b/docs/contributing/questions/answering-questions.md @@ -0,0 +1,26 @@ +# Answering Questions + +Contributing answers is an invaluable part of supporting the Detox community. By sharing your knowledge and experience, you help others learn and grow, and you also contribute to enhancing Detox for the entire community. + +## Different Ways to Answer + +Answering a question is not limited to providing a solution in the comments. +Sometimes, you may not have a solution, but you can still contribute by: + +- **Guide towards a solution**: If unsure about an answer, provide guidance on where to look, including relevant documentation or resources. +- **Request more information**: Ask for more detail if required to better address the question. For example, you may ask for code snippets, screenshots, or reproducible examples. +- **Refer to existing answers**: Link an already answered question if applicable. + +## Guidelines for Writing Clear Answers + +When answering questions, follow these guidelines to ensure your answers are clear and actionable: + +- **Be specific**: Try to pinpoint what exact information you need to effectively answer the question. General requests such as "Please share more details" are not very helpful. +- **Maintain respect**: Ensure your communication is always polite and contextual. When asking for more information, it's better to explain why it's needed so that the person asking understands the relevance. +- **Completeness**: Make sure to provide a detailed answer that includes all relevant information, such as code snippets, screenshots, and examples. This practice helps others understand your answer and apply it to their use case. + +:::tip In a Nutshell + +When you're answering a question, remember to be specific, clear, and respectful. By providing detailed and well-thought out response, you will play an important role in contributing to and enhancing the knowledge of our community. + +::: diff --git a/docs/contributing/questions/asking-questions.md b/docs/contributing/questions/asking-questions.md new file mode 100644 index 0000000000..178619c438 --- /dev/null +++ b/docs/contributing/questions/asking-questions.md @@ -0,0 +1,35 @@ +# Asking Questions + +Posting queries on public forums increases visibility and chances of getting an answer, and it also helps others with similar issues. However, it's essential to ask questions the right way to get the right answers. + +In case you were able to resolve your issue, consider sharing your solution with the community. By doing so, you contribute to enhancing Detox for the entire community. + + +## Before Asking + +In case you're facing an issue, consider the following steps before posting a question: + +- **Suspect a bug?** Refer to our [Reporting Bugs] guide to determine if you've encountered a Detox bug and how to report it. +- **Search** for existing answers that may have addressed your question already. [Below](#where-to-ask), we've listed the right platforms to ask (and search) questions. +- Review our **documentation** thoroughly for any related information. Use the search bar on the top bar to streamline your search. + +If an existing answer doesn't fully address your question, feel free to ask a follow-up in the same thread if comment-enabled or create a new question, linking back to the original one if possible. + +## Where to Ask + +If you can't find an answer to your question, you can ask it in the following ways: + +- **Discord**: Join our dedicated Discord server [here][Discord] to seek community assistance. +- **Stack Overflow**: Tag your question with ["Detox" on Stack Overflow] to reach the right audience. +- **GitHub**: Raise an issue on our [Detox repository]. + +:::tip Pro Tip + +When posting a question, ensure to include all relevant information, such as code snippets, screenshots, and examples. This practice helps others understand your query and provide comprehensive answers. + +::: + +[Reporting Bugs]: ../reporting-bugs.md +[Discord]: https://discord.gg/CkD5QKheF5 +[Detox repository]: https://github.com/wix/Detox/issues +["Detox" on Stack Overflow]: https://stackoverflow.com/questions/tagged/detox From c16d42528559564b5b953afebe12a86ab3dd00a4 Mon Sep 17 00:00:00 2001 From: Asaf Korem Date: Sun, 5 Nov 2023 12:33:42 +0200 Subject: [PATCH 025/185] docs: add feature-requests docs. --- docs/contributing/feature-requests.md | 38 +++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 docs/contributing/feature-requests.md diff --git a/docs/contributing/feature-requests.md b/docs/contributing/feature-requests.md new file mode 100644 index 0000000000..36ca99f4b2 --- /dev/null +++ b/docs/contributing/feature-requests.md @@ -0,0 +1,38 @@ +--- +title: Feature Requests +--- + +# Feature Requests and Feedback + +Open dialogue and feedback, particularly feature requests, play an invaluable role in open-source communities like Detox. They provide insights into user preferences and requirements, aiding in the refinement and enhancement of the project. + +Detox project provides various platforms for feedback: [GitHub Issues], [GitHub Discussions], and [Discord]. + +Keep in mind that feedback is varied, including feature suggestions, improvements, identification of missing functionality, and support requests. Each type of feedback has a preferred method of submission. + +## Crafting Effective Feedback + +Follow these tips to deliver clear, actionable feedback: + +- **Constructive Criticism:** Frame your feedback in a positive light, understanding the perspective of the project maintainers. +- **Comprehensive Details:** Include as much relevant information as possible to help maintainers understand the context and use case. +- **Clarity and Respect:** Make your feedback specific, clear, and respectful, ensuring it is actionable for the maintainers. +- **Guidelines:** Adhere to the below suggestions for each type of feedback. + +## Feature Suggestions + +When suggesting a new feature, remember to provide thorough information, such as examples, screenshots, and code snippets, to communicate your idea effectively. + +## Crafting Effective Feature Requests + +Just as in general feedback, make sure your feature request is: + +- **Specific:** Clearly describe the feature you wish to see implemented. +- **Detailed:** Offer as much context as possible, including use cases, examples, and why this addition would benefit the community. +- **Respectful:** Remember, the project maintainers or contributors are not obligated to implement your request. Be polite and respectful in your request. + +By making your feedback as clear and detailed as possible, you'll greatly assist the maintainers in understanding and potentially implementing your suggestion. + +[GitHub Issues]: https://github.com/wix/Detox/issues +[GitHub Discussions]: https://github.com/wix/Detox/discussions +[Discord]: https://discord.gg/CkD5QKheF5 From bd91817a3fd7c4fb8f999f0ad197dca2826f3344 Mon Sep 17 00:00:00 2001 From: Asaf Korem Date: Sun, 5 Nov 2023 12:34:09 +0200 Subject: [PATCH 026/185] docs: add documentation-site docs. --- docs/contributing/documentation.md | 38 ++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 docs/contributing/documentation.md diff --git a/docs/contributing/documentation.md b/docs/contributing/documentation.md new file mode 100644 index 0000000000..6951077176 --- /dev/null +++ b/docs/contributing/documentation.md @@ -0,0 +1,38 @@ +--- +title: Documentation +--- + +# Enhancing Detox Documentation + +Contributions towards enhancing our documentation are highly valued in the Detox community. Clear, concise, and comprehensive documentation enables users to understand and utilize the project more effectively. We appreciate all contributions, from correcting typos to creating new documentation pages. + +## Getting Started + +Our [documentation website](https://wix.github.io/Detox) is developed using [Docusaurus](https://docusaurus.io/). To set up a local version of the website for editing, follow these steps: + +```bash npm2yarn +cd website +npm install +npm start +``` + +## Making Updates + +To modify an existing page, find the associated markdown file in the `docs/` directory and make the required changes. To introduce a new page, create a markdown file in `docs/` and link to it in `website/sidebars.json`. + +## Deploying Changes + +Automatic updates to the website occur with each commit to the `master` branch, with these changes reflected under the `Next` version. The process of tagging and locking documentation to a specific version is automated and coincides with each Detox release. + +If there's a need to update the documentation of a particular version, modify the associated files and code located under `website/versioned_docs/version-/` and `website/versioned_sidebars/version--sidebars.json`. + +## Modifying Old Versions + +To update an older version with the latest changes: + +1. Remove the desired version from `versions.json`. +1. Execute `npm run docusaurus docs:version `. + +## Reviewing Style Modifications + +If you're making changes to the stylesheets, use this [demo page](../demo.mdx) to visually inspect the impact of your changes and avoid unintentional visual regressions. From 819dc30db5d3770408ef12695a6565c4e90a4ccf Mon Sep 17 00:00:00 2001 From: Asaf Korem Date: Sun, 5 Nov 2023 12:34:50 +0200 Subject: [PATCH 027/185] docs: change contributing.md to a category overview. --- docs/contributing.md | 219 ++++++---------------------------- docs/contributing/overview.md | 197 ------------------------------ 2 files changed, 34 insertions(+), 382 deletions(-) delete mode 100644 docs/contributing/overview.md diff --git a/docs/contributing.md b/docs/contributing.md index 9014072c5e..229aa4e1ec 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1,201 +1,50 @@ -# Contributing +# Contributing to Detox - +## Call for Contributions -This guide is about contributing to our codebase. +Detox, an open-source project, greatly values community involvement. Whether you're a mobile developer, QA specialist, or an open source enthusiast, your contribution could significantly enhance the reliability, user experience, and development process of mobile applications. -We don’t have any special guidelines - just some setup walk-through! +### Our Code of Conduct -### Environment +To ensure an inclusive, safe environment for all contributors, we adhere to the [Contributor Covenant code of conduct]. This well-recognized code provides clear guidelines for respectful behavior. -First, complete our [Getting Started](introduction/getting-started.mdx) guide. +## Ways to Contribute -#### Install the monorepo management tool, `lerna` +All contributions, regardless of size, can help shape Detox. We welcome: -```bash npm2yarn -npm install lerna@3.x.x --global -``` +- **[Questions]**: Your inquiries can help identify gaps in our documentation and benefit all users. +- **[Answers]**: Assisting other Detox users by providing guidance or responses to their questions. +- **[Bug Reports]**: Help us identify and resolve Detox issues. +- **[Feature Suggestions]**: Your ideas for new features can help us improve Detox according to user needs. +- **[Code Submissions]**: Directly contribute by fixing bugs, adding features, or improving our codebase. +- **[Code Reviews]**: Help maintain the quality of our codebase by reviewing others' contributions. +- **[Documentation Enhancements]**: Improve Detox documentation from user guides to API references. +- **Content Creation**: Share your knowledge of Detox through blog posts, tutorials, videos, and more. We're always excited to share your content on our [Twitter account] and [Discord server]. -For all the internal projects (detox, detox-cli, demos, test) `lerna` will create symbolic links in `node_modules` instead of `npm` copying the content of the projects. This way, any change you do on any code is there immediately. There is no need to update node modules or copy files between projects. +Your active participation helps build a vibrant community dedicated to improving mobile development. -#### Install common React Native dependencies +## Engaging in the Community -React-Native CLI: +Join our [Discord server] to discuss Detox, ask questions, and provide help. Follow our [Twitter account] for updates on the project. -```bash npm2yarn -npm install react-native-cli --global -``` +Becoming an active community member is a great way to start contributing to Detox. It provides familiarity with the project, helps identify opportunities to contribute, and allows guidance from other contributors. The more you engage, the more recognition your contributions receive and the greater impact you can make on the project's direction. -Watchman: +## Appreciation and Recognition -```bash -brew install watchman -``` +All our contributors deserve recognition. We express our gratitude by featuring them in our [Release Notes]. -#### xcpretty +### Core Contributors Program -You must also have `xcpretty` installed: +We plan to launch a "Core Contributor" program for individuals who have significantly contributed to Detox and shown deep understanding of the project while being active in the community. Core contributors will be invited to a private Discord channel for exclusive discussions and will receive a distinguished role on our Discord server. -```bash -gem install xcpretty -``` - -### Detox - -#### Clone Detox and Submodules - -```bash -git clone git@github.com:wix/Detox.git -cd detox -git submodule update --init --recursive -``` - -(this makes sure all git submodule dependencies have been properly checked out) - -#### Installing and Linking Internal Projects - -```bash -lerna bootstrap -``` - -#### Building and Testing - -##### Automatically - -`scripts/ci.ios.sh` and `scripts/ci.android.sh` are the scripts Detox runs in CI, they will run `lerna bootstrap`, unit tests, and E2E tests. Make sure these scripts pass before submitting a PR, this is exactly what Detox is going to run in CI. - -##### Manually - -The following steps can be run manually in order to build / test the various components of Detox. - -###### 1. Unit Tests and Lint - -```bash -lerna run test -``` - -Detox JS code is 100% test covered and is set to break the build if coverage gets below, so make sure you run unit tests (`lerna run test`) locally before pushing. - -Alternatively, to run only the JS tests, run the following from the `detox/detox` directory: - -```bash npm2yarn -npm run unit -``` - -or - -```bash npm2yarn -npm run unit:watch -``` - -After running the tests, _Jest_ will create a coverage report you can examine: - -```bash -cd detox -open coverage/lcov-report/index.html -``` - -###### 2. Running Detox E2E Coverage Tests - -Detox has a suite of end-to-end tests to test its own API while developing (and for regression); We maintain a special application that is "tested" against Detox’s API, but essentially, it’s the API that is tested, not the app. - -To run the tests, you must first build the native code and then run based on your target of choice (Android / iOS): - -- **iOS:** - - ```bash npm2yarn - cd detox/test - npm run build:ios - npm run e2e:ios - ``` - -- **Android:** - - ```bash npm2yarn - cd detox/test - npm run build:android - npm run e2e:android - ``` - -FYI Android test project includes two flavors: - -- `fromBin` - (**standard use case**) utilizes the precompiled `.aar` from `node_modules` just like a standard RN project. -- `fromSource` - compiles the project with RN sources from `node_modules`, this is useful when developing and debugging Espresso idle resource. - [Here](https://github.com/facebook/react-native/wiki/Building-from-source#android) are the prerequisites to compiling React Native from source. - -Each build can be triggered separately by running its Gradle assembling task (under `detox/test/android/`): - -```bash -./gradlew assembleFromSourceDebug -``` - -or: - -```bash -./gradlew assembleFromBinDebug -``` - -To run from Android Studio, React Native’s `react.gradle` script may require `node` to be in path. -On MacOS, environment variables can be exported to desktop applications by adding the following to your `.bashrc`/`.zshrc`: - -```bash -launchctl setenv PATH $PATH -``` - -###### 3. Android Native Unit-Tests - -Under `detox/`: - -```bash -npm run unit:android-release -``` - -:::caution Note -Due to limitations with Robolectric, this currently does not work with JDK 17. Best to use Java 11, as explained in the [Android environment setup guide](./guide/android-dev-env.md). -::: - -### Detox - Example Projects - -This is in fact a monorepo that also sports some example projects (for usage reference), alongside the main test project: - -- `examples/demo-react-native-jest`: Demonstrate usage of Detox in a React Native app project. -- `examples/demo-native-ios`: Demonstrates usage of Detox in a pure-native iOS app. -- `examples/demo-native-android` (broken): Demonstrates usage of Detox in a pure-native Android app. -- `examples/demo-pure-native-android`: Demonstrates usage of the _pure_ [Detox-Native](https://github.com/wix/Detox/tree/master/detox-native/README.md) project -- more... - -**In order to run E2E tests associated with any of these projects, refer to the [project-specific](https://github.com/wix/Detox/tree/master/examples) READMEs.** - -### Detox Documentation Website - -The [documentation website](https://wix.github.io/Detox) is built using [Docusaurus](https://docusaurus.io/). - -To run the website locally, run the following commands: - -```bash npm2yarn -cd website -npm install -npm start -``` - -#### Updating the Website - -To update a specific page, edit the corresponding markdown file in `docs/`. To add a new page, create a new markdown file in `docs/` and add a link to it in `website/sidebars.json`. - -##### Website Deployment - -While changes to the website are published automatically on every commit to `master` under the `Next` version, tagging and locking docs to a specific version is done automatically on every Detox release. - -In case you want to update the docs for a specific version, you can change the related files and code under `website/versioned_docs/version-/` and `website/versioned_sidebars/version--sidebars.json`. - -##### Update Old Versions - -To update a specific version with the latest changes: - -1. Remove the version from `versions.json`. -1. Run `npm run docusaurus docs:version `. - -##### Inspect Documentation Style Changes - -This [demo page](demo.mdx) serves as a visual representation of the styling and formatting of our documentation. -If you are changing the stylesheets, check with it to prevent undesirable visual regressions. +[Contributor Covenant code of conduct]: contributing/code-of-conduct.md +[Questions]: contributing/questions/asking-questions.md +[Answers]: contributing/questions/answering-questions.md +[Bug Reports]: contributing/reporting-bugs.md +[Feature Suggestions]: contributing/feature-requests.md +[Code Submissions]: contributing/code/submitting-pull-requests.md +[Code Reviews]: contributing/code/reviewing-pull-requests.md +[Documentation Enhancements]: contributing/documentation.md +[Discord server]: https://discord.gg/CkD5QKheF5 +[Twitter account]: https://twitter.com/detoxe2e +[Release Notes]: https://github.com/wix/Detox/releases diff --git a/docs/contributing/overview.md b/docs/contributing/overview.md deleted file mode 100644 index f37d8c92ae..0000000000 --- a/docs/contributing/overview.md +++ /dev/null @@ -1,197 +0,0 @@ -# Contributing Overview - - - -This guide is about contributing to our codebase. - -We don’t have any special guidelines - just some setup walk-through! - -### Environment - -First, complete our [Getting Started](../introduction/getting-started.mdx) guide. - -#### Install the monorepo management tool, `lerna` - -```bash npm2yarn -npm install lerna@3.x.x --global -``` - -For all the internal projects (detox, detox-cli, demos, test) `lerna` will create symbolic links in `node_modules` instead of `npm` copying the content of the projects. This way, any change you do on any code is there immediately. There is no need to update node modules or copy files between projects. - -#### Install common React Native dependencies - -React-Native CLI: - -```bash npm2yarn -npm install react-native-cli --global -``` - -Watchman: - -```bash -brew install watchman -``` - -#### xcpretty - -You must also have `xcpretty` installed: - -```bash -gem install xcpretty -``` - -### Detox - -#### Clone Detox and Submodules - -```bash -git clone git@github.com:wix/Detox.git -cd detox -git submodule update --init --recursive -``` - -(this makes sure all git submodule dependencies have been properly checked out) - -#### Installing and Linking Internal Projects - -```bash -lerna bootstrap -``` - -#### Building and Testing - -##### Automatically - -`scripts/ci.ios.sh` and `scripts/ci.android.sh` are the scripts Detox runs in CI, they will run `lerna bootstrap`, unit tests, and E2E tests. Make sure these scripts pass before submitting a PR, this is exactly what Detox is going to run in CI. - -##### Manually - -The following steps can be run manually in order to build / test the various components of Detox. - -###### 1. Unit Tests and Lint - -```bash -lerna run test -``` - -Detox JS code is 100% test covered and is set to break the build if coverage gets below, so make sure you run unit tests (`lerna run test`) locally before pushing. - -Alternatively, to run only the JS tests, run the following from the `detox/detox` directory: - -```bash npm2yarn -npm run unit -``` - -or - -```bash npm2yarn -npm run unit:watch -``` - -After running the tests, _Jest_ will create a coverage report you can examine: - -```bash -cd detox -open coverage/lcov-report/index.html -``` - -###### 2. Running Detox E2E Coverage Tests - -Detox has a suite of end-to-end tests to test its own API while developing (and for regression); We maintain a special application that is "tested" against Detox’s API, but essentially, it’s the API that is tested, not the app. - -To run the tests, you must first build the native code and then run based on your target of choice (Android / iOS): - -- **iOS:** - - ```bash npm2yarn - cd detox/test - npm run build:ios - npm run e2e:ios - ``` - -- **Android:** - - ```bash npm2yarn - cd detox/test - npm run build:android - npm run e2e:android - ``` - -FYI Android test project includes two flavors: - -- `fromBin` - (**standard use case**) utilizes the precompiled `.aar` from `node_modules` just like a standard RN project. -- `fromSource` - compiles the project with RN sources from `node_modules`, this is useful when developing and debugging Espresso idle resource. - [Here](https://github.com/facebook/react-native/wiki/Building-from-source#android) are the prerequisites to compiling React Native from source. - -Each build can be triggered separately by running its Gradle assembling task (under `detox/test/android/`): - -```bash -./gradlew assembleFromSourceDebug -``` - -or: - -```bash -./gradlew assembleFromBinDebug -``` - -To run from Android Studio, React Native’s `react.gradle` script may require `node` to be in path. -On MacOS, environment variables can be exported to desktop applications by adding the following to your `.bashrc`/`.zshrc`: - -```bash -launchctl setenv PATH $PATH -``` - -###### 3. Android Native Unit-Tests - -Under `detox/android`: - -```bash -./gradlew testFullRelease -``` - -### Detox - Example Projects - -This is in fact a monorepo that also sports some example projects (for usage reference), alongside the main test project: - -- `examples/demo-react-native-jest`: Demonstrate usage of Detox in a React Native app project. -- `examples/demo-native-ios`: Demonstrates usage of Detox in a pure-native iOS app. -- `examples/demo-native-android` (broken): Demonstrates usage of Detox in a pure-native Android app. -- `examples/demo-pure-native-android`: Demonstrates usage of the _pure_ [Detox-Native](https://github.com/wix/Detox/tree/master/detox-native/README.md) project -- more... - -**In order to run E2E tests associated with any of these projects, refer to the [project-specific](https://github.com/wix/Detox/tree/master/examples) READMEs.** - -### Detox Documentation Website - -The [documentation website](https://wix.github.io/Detox) is built using [Docusaurus](https://docusaurus.io/). - -To run the website locally, run the following commands: - -```bash npm2yarn -cd website -npm install -npm start -``` - -#### Updating the Website - -To update a specific page, edit the corresponding markdown file in `docs/`. To add a new page, create a new markdown file in `docs/` and add a link to it in `website/sidebars.json`. - -##### Website Deployment - -While changes to the website are published automatically on every commit to `master` under the `Next` version, tagging and locking docs to a specific version is done automatically on every Detox release. - -In case you want to update the docs for a specific version, you can change the related files and code under `website/versioned_docs/version-/` and `website/versioned_sidebars/version--sidebars.json`. - -##### Update Old Versions - -To update a specific version with the latest changes: - -1. Remove the version from `versions.json`. -1. Run `npm run docusaurus docs:version `. - -##### Inspect Documentation Style Changes - -This [demo page](../demo.mdx) serves as a visual representation of the styling and formatting of our documentation. -If you are changing the stylesheets, check with it to prevent undesirable visual regressions. From cb2a73000517b861b9a64e2d7e86baf324b9d618 Mon Sep 17 00:00:00 2001 From: Asaf Korem Date: Sun, 5 Nov 2023 12:35:05 +0200 Subject: [PATCH 028/185] docs: update contribution sidebar. --- website/sidebars.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/website/sidebars.js b/website/sidebars.js index 199a9897b0..8b8fe9edcf 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -119,13 +119,30 @@ const sidebars = { }, ], contributeSidebar: [ + 'contributing', { type: 'category', - label: 'Contribute', + label: 'Questions & Answers', items: [ - 'contributing', + 'contributing/questions/asking-questions', + 'contributing/questions/answering-questions', ] }, + 'contributing/reporting-bugs', + 'contributing/feature-requests', + { + type: 'category', + label: 'Code Changes', + items: [ + 'contributing/code/overview', + 'contributing/code/setting-up-the-dev-environment', + 'contributing/code/example-projects', + 'contributing/code/submitting-pull-requests', + 'contributing/code/reviewing-pull-requests', + ] + }, + 'contributing/documentation', + 'contributing/code-of-conduct' ], }; From 50c54a458d192c49b3540e6926c4afba2acf7b44 Mon Sep 17 00:00:00 2001 From: Asaf Korem Date: Sun, 5 Nov 2023 12:51:04 +0200 Subject: [PATCH 029/185] docs: make docs lint happy. --- docs/contributing/code-of-conduct.md | 22 ++++----- docs/contributing/code/overview.md | 1 + .../code/reviewing-pull-requests.md | 5 ++ .../code/setting-up-the-dev-environment.md | 48 +++++++++++-------- .../code/submitting-pull-requests.md | 27 +++++++---- docs/contributing/documentation.md | 9 ++-- docs/contributing/feature-requests.md | 8 +--- .../questions/asking-questions.md | 1 - docs/contributing/reporting-bugs.md | 1 - 9 files changed, 69 insertions(+), 53 deletions(-) diff --git a/docs/contributing/code-of-conduct.md b/docs/contributing/code-of-conduct.md index 30ac992a4d..c72d7bb1f9 100644 --- a/docs/contributing/code-of-conduct.md +++ b/docs/contributing/code-of-conduct.md @@ -16,7 +16,7 @@ For answers to common questions about this code of conduct, see the FAQ at ::: Our community is dedicated to providing a safe and inclusive environment for all contributors. -To ensure this, we have adopted the **[Contributor Covenant]** code of conduct. +To ensure this, we have adopted the **[Contributor Covenant]*- code of conduct. This widely recognized and respected code of conduct sets clear guidelines for behavior, and it's easy for everyone to understand and follow. By participating in the Detox community, you agree to abide by the terms of this code of conduct. @@ -41,23 +41,23 @@ diverse, inclusive, and healthy community. Examples of behavior that contributes to a positive environment for our community include: -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience -* Focusing on what is best not just for us as individuals, but for the overall +- Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: -* The use of sexualized language or imagery, and sexual attention or advances of +- The use of sexualized language or imagery, and sexual attention or advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email address, +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities diff --git a/docs/contributing/code/overview.md b/docs/contributing/code/overview.md index dd8e638aa5..875dcc10a1 100644 --- a/docs/contributing/code/overview.md +++ b/docs/contributing/code/overview.md @@ -3,6 +3,7 @@ Welcome to the code changes section! As a contributor, it's essential to understand the project's goals and adhere to its code of conduct. Before contributing, please review any existing issues related to your work, ensure your code is well-documented, and has adequate test coverage. It's also important that your code is compatible with the project's supported platforms and their versions. Our collaborative workflow is simple: + 1. **Identify an Issue:** If not exists already, create an issue for new features or bug reports, outlining your proposal or the identified problem. 2. **Propose a Solution:** Open a pull request with a proposed solution to the issue. On complex issues, it's recommended to discuss your approach with the community and maintainers before submitting a PR. 3. **Engage in Review:** A collaborator will review your pull request. Reviews from other contributors are also encouraged. diff --git a/docs/contributing/code/reviewing-pull-requests.md b/docs/contributing/code/reviewing-pull-requests.md index faf304dd7f..bebaf0cad4 100644 --- a/docs/contributing/code/reviewing-pull-requests.md +++ b/docs/contributing/code/reviewing-pull-requests.md @@ -3,10 +3,13 @@ Reviewing a PR is an important part of the contribution process. It ensures the quality of the codebase and provides an opportunity for the community to collaborate and learn from each other. :::note + Reviewers should refer to the [Submitting Pull Requests] page to understand the guidelines the contributor followed when creating the PR. This will provide context and ensure a thorough review process. + ::: ## Understand the Changes + - Ensure alignment with the project’s coding standards and overall architecture. - Read the provided description and any linked issues for context. @@ -23,12 +26,14 @@ Community members can provide valuable feedback on PRs, especially if they have Experienced developers are encouraged to review PRs and provide feedback. This is a great way to learn more about the project and collaborate with the community. You can review the code by following these requirements: + - Alignment with coding standards and architectural guidelines. - Adequate test coverage (unit, integration, and e2e). - Ensure the documentation reflects the changes in the PR, if necessary. Refer to the [Documentation Changes] page for guidelines. - Meaningful and semantically correct commit messages. ## Merging Pull Requests + Every PR requires approval from a maintainer before it can be merged. However, we encourage the community to review and provide feedback on PRs. Helping with the review process not only improves the quality of the code changes, but also helps the maintainers by reducing the workload. diff --git a/docs/contributing/code/setting-up-the-dev-environment.md b/docs/contributing/code/setting-up-the-dev-environment.md index 2c2ae8e737..fa0553865c 100644 --- a/docs/contributing/code/setting-up-the-dev-environment.md +++ b/docs/contributing/code/setting-up-the-dev-environment.md @@ -11,36 +11,44 @@ Please complete our [Introduction](introduction/getting-started.mdx) guides befo ## Setting Up The Monorepo Management 1. Install the monorepo management tool, `lerna`: - ```bash npm2yarn - npm install lerna@3.x.x --global - ``` + +```bash npm2yarn +npm install lerna@3.x.x --global +``` 2. Clone the repository and navigate to the project directory: - ```bash - git clone git@github.com:wix/Detox.git - cd detox - git submodule update --init --recursive - ``` + +```bash +git clone git@github.com:wix/Detox.git +cd detox +git submodule update --init --recursive +``` 3. From the project's root directory, install and link the internal projects: - ```bash - lerna bootstrap - ``` + +```bash +lerna bootstrap +``` ## Installing Common Dependencies - React-Native CLI: - ```bash npm2yarn - npm install react-native-cli --global - ``` + +```bash npm2yarn +npm install react-native-cli --global +``` + - Watchman: - ```bash - brew install watchman - ``` +- +```bash +brew install watchman +``` + - xcpretty: - ```bash - gem install xcpretty - ``` + +```bash +gem install xcpretty +``` ## Building and Testing diff --git a/docs/contributing/code/submitting-pull-requests.md b/docs/contributing/code/submitting-pull-requests.md index d23f7089f5..b16e224ea0 100644 --- a/docs/contributing/code/submitting-pull-requests.md +++ b/docs/contributing/code/submitting-pull-requests.md @@ -9,35 +9,44 @@ Contact us on our [Discord Server] or open an issue on GitHub. ::: ## Fork the Repository + - Fork the repository to your own GitHub account. - Create a new branch from the `master` branch for your work. ## Test Your Changes + - **Unit Testing:** Write unit tests for your code, usually located within the same directory as the unit under test. - **End-to-End (E2E) Testing:** - Our E2E tests are located under the `detox/test/e2e` directory, with each test file named after the feature it tests. These tests are testing a real React Native app (`detox/test`). - Running the test can be done using `detox build` and `detox test` commands, as described in the `package.json` of the project. - - After changes to the native implementation of Detox (or `DetoxSync`), rebuild the framework before running the tests locally: - ```bash - detox rebuild-framework-cache - ``` +- After changes to the native implementation of Detox (or `DetoxSync`), rebuild the framework before running the tests locally: + +```bash +detox rebuild-framework-cache +``` + - **Integration Testing:** Usually, integration tests are not required. If there are special cases, perform integration tests under `detox/test/integration`. ## Modify Documentation + If your changes affect the public API, update the documentation accordingly to reflect your changes. Refer to the [Documentation Changes] page for guidelines. ## Commit Message Guidelines + Write descriptive, meaningful commit messages that follow the **Conventional Commits** format, specifying the type of change, the scope, and a concise description. For example: - ```plaintext - fix(iOS): resolve crash on scrolling in iOS 17.0 - feat(Android): add new API for setting the device locale - chore(tests): update unit tests for new utility function - ``` + +```plaintext +fix(iOS): resolve crash on scrolling in iOS 17.0 +feat(Android): add new API for setting the device locale +chore(tests): update unit tests for new utility function +``` + More details on good commit messages can be found [here](https://www.conventionalcommits.org/en/v1.0.0/). ## Open the Pull Request + - Fill in the provided PR template fields on GitHub. - Provide a clear description of your changes and any necessary context. diff --git a/docs/contributing/documentation.md b/docs/contributing/documentation.md index 6951077176..2298660b6a 100644 --- a/docs/contributing/documentation.md +++ b/docs/contributing/documentation.md @@ -1,10 +1,9 @@ ---- -title: Documentation ---- +# Documentation Site -# Enhancing Detox Documentation +Contributions towards enhancing our documentation are highly valued in the Detox community. +Clear, concise, and comprehensive documentation enables users to understand and utilize the project more effectively. -Contributions towards enhancing our documentation are highly valued in the Detox community. Clear, concise, and comprehensive documentation enables users to understand and utilize the project more effectively. We appreciate all contributions, from correcting typos to creating new documentation pages. +We appreciate all contributions, from correcting typos to creating new documentation pages. ## Getting Started diff --git a/docs/contributing/feature-requests.md b/docs/contributing/feature-requests.md index 36ca99f4b2..9dd8ac09e1 100644 --- a/docs/contributing/feature-requests.md +++ b/docs/contributing/feature-requests.md @@ -1,8 +1,4 @@ ---- -title: Feature Requests ---- - -# Feature Requests and Feedback +# Feedback and Suggestions Open dialogue and feedback, particularly feature requests, play an invaluable role in open-source communities like Detox. They provide insights into user preferences and requirements, aiding in the refinement and enhancement of the project. @@ -23,7 +19,7 @@ Follow these tips to deliver clear, actionable feedback: When suggesting a new feature, remember to provide thorough information, such as examples, screenshots, and code snippets, to communicate your idea effectively. -## Crafting Effective Feature Requests +## Crafting Effective Feature Suggestions Just as in general feedback, make sure your feature request is: diff --git a/docs/contributing/questions/asking-questions.md b/docs/contributing/questions/asking-questions.md index 178619c438..4115f795ad 100644 --- a/docs/contributing/questions/asking-questions.md +++ b/docs/contributing/questions/asking-questions.md @@ -4,7 +4,6 @@ Posting queries on public forums increases visibility and chances of getting an In case you were able to resolve your issue, consider sharing your solution with the community. By doing so, you contribute to enhancing Detox for the entire community. - ## Before Asking In case you're facing an issue, consider the following steps before posting a question: diff --git a/docs/contributing/reporting-bugs.md b/docs/contributing/reporting-bugs.md index 03cfc60af2..b8e3ae7f7d 100644 --- a/docs/contributing/reporting-bugs.md +++ b/docs/contributing/reporting-bugs.md @@ -25,6 +25,5 @@ Creating a minimal code reproduction is crucial as it helps other contributors a ::: - [existing issues]: https://github.com/wix/Detox/issues [creating a bug report on GitHub]: https://github.com/wix/Detox/issues/new/choose From 5aeae37a86d44875a9719c95a0aa995d17192b4e Mon Sep 17 00:00:00 2001 From: Asaf Korem Date: Sun, 5 Nov 2023 12:57:16 +0200 Subject: [PATCH 030/185] docs: make docs-lint happy. --- .../code/setting-up-the-dev-environment.md | 14 +++++++------- docs/contributing/code/submitting-pull-requests.md | 2 -- docs/introduction/environment-setup.md | 8 ++++---- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/docs/contributing/code/setting-up-the-dev-environment.md b/docs/contributing/code/setting-up-the-dev-environment.md index fa0553865c..ddf16ba3d7 100644 --- a/docs/contributing/code/setting-up-the-dev-environment.md +++ b/docs/contributing/code/setting-up-the-dev-environment.md @@ -10,13 +10,13 @@ Please complete our [Introduction](introduction/getting-started.mdx) guides befo ## Setting Up The Monorepo Management -1. Install the monorepo management tool, `lerna`: +Install the monorepo management tool, `lerna`: ```bash npm2yarn npm install lerna@3.x.x --global ``` -2. Clone the repository and navigate to the project directory: +Clone the repository and navigate to the project directory: ```bash git clone git@github.com:wix/Detox.git @@ -24,7 +24,7 @@ cd detox git submodule update --init --recursive ``` -3. From the project's root directory, install and link the internal projects: +From the project's root directory, install and link the internal projects: ```bash lerna bootstrap @@ -32,19 +32,19 @@ lerna bootstrap ## Installing Common Dependencies -- React-Native CLI: +#### React-Native CLI ```bash npm2yarn npm install react-native-cli --global ``` -- Watchman: -- +#### Watchman + ```bash brew install watchman ``` -- xcpretty: +#### xcpretty ```bash gem install xcpretty diff --git a/docs/contributing/code/submitting-pull-requests.md b/docs/contributing/code/submitting-pull-requests.md index b16e224ea0..ae4ad076ff 100644 --- a/docs/contributing/code/submitting-pull-requests.md +++ b/docs/contributing/code/submitting-pull-requests.md @@ -13,7 +13,6 @@ Contact us on our [Discord Server] or open an issue on GitHub. - Fork the repository to your own GitHub account. - Create a new branch from the `master` branch for your work. - ## Test Your Changes - **Unit Testing:** Write unit tests for your code, usually located within the same directory as the unit under test. @@ -50,6 +49,5 @@ More details on good commit messages can be found [here](https://www.conventiona - Fill in the provided PR template fields on GitHub. - Provide a clear description of your changes and any necessary context. - [Documentation Changes]: ../documentation.md [Discord Server]: https://discord.gg/CkD5QKheF5 diff --git a/docs/introduction/environment-setup.md b/docs/introduction/environment-setup.md index c54972b505..88039bb58d 100644 --- a/docs/introduction/environment-setup.md +++ b/docs/introduction/environment-setup.md @@ -1,3 +1,5 @@ +# Environment Setup + import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; @@ -5,8 +7,6 @@ import IntroRN from './partials/_getting-started-rn.md'; import IntroExpo from './partials/_getting-started-expo.md'; import IntroOther from './partials/_getting-started-other.mdx'; -# Environment Setup - The _Introduction section_ walks you through setting up Detox in your project, one step at a time. You will find that some steps are longer than the others: some are a couple of paragraphs, while the others look like a dedicated multistep guide. @@ -31,7 +31,7 @@ Please select type of your mobile application before you start the tutorial: Your first step would be to complete the [📚 React Native CLI Quickstart Guide](https://reactnative.dev/docs/next/environment-setup). -![](../img/rn-env.png) +![RN Env](../img/rn-env.png)

Open the link above and switch there to React Native CLI Quickstart tab to see @@ -44,7 +44,7 @@ Your first step would be to complete the make sure you can create and run React Native apps on virtual testing devices.

-![](../img/rn-env-done.png) +![RN Env Done](../img/rn-env-done.png) ## Detox Prerequisites From d4fd5bf5677bd0ca741b5b2fb420fae3dd5d9876 Mon Sep 17 00:00:00 2001 From: Asaf Korem Date: Sun, 5 Nov 2023 12:59:10 +0200 Subject: [PATCH 031/185] docs: make doc-link happy. --- docs/contributing/code/setting-up-the-dev-environment.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/contributing/code/setting-up-the-dev-environment.md b/docs/contributing/code/setting-up-the-dev-environment.md index ddf16ba3d7..a72dc305b4 100644 --- a/docs/contributing/code/setting-up-the-dev-environment.md +++ b/docs/contributing/code/setting-up-the-dev-environment.md @@ -32,19 +32,19 @@ lerna bootstrap ## Installing Common Dependencies -#### React-Native CLI +### React-Native CLI ```bash npm2yarn npm install react-native-cli --global ``` -#### Watchman +### Watchman ```bash brew install watchman ``` -#### xcpretty +### xcpretty ```bash gem install xcpretty From c540b757271646e288892793fd00574e342034a9 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Wed, 8 Nov 2023 15:49:04 +0200 Subject: [PATCH 032/185] docs: added suggestions --- docs/contributing.md | 4 +--- docs/contributing/code-of-conduct.md | 2 +- docs/contributing/code/example-projects.md | 2 +- docs/contributing/code/overview.md | 4 ++++ .../code/setting-up-the-dev-environment.md | 14 +++++++++++++- docs/contributing/code/submitting-pull-requests.md | 6 +++--- docs/contributing/feature-requests.md | 10 ++++------ docs/contributing/questions/asking-questions.md | 4 ++-- 8 files changed, 29 insertions(+), 17 deletions(-) diff --git a/docs/contributing.md b/docs/contributing.md index 229aa4e1ec..6641fa2d90 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1,10 +1,8 @@ # Contributing to Detox -## Call for Contributions - Detox, an open-source project, greatly values community involvement. Whether you're a mobile developer, QA specialist, or an open source enthusiast, your contribution could significantly enhance the reliability, user experience, and development process of mobile applications. -### Our Code of Conduct +## Our Code of Conduct To ensure an inclusive, safe environment for all contributors, we adhere to the [Contributor Covenant code of conduct]. This well-recognized code provides clear guidelines for respectful behavior. diff --git a/docs/contributing/code-of-conduct.md b/docs/contributing/code-of-conduct.md index c72d7bb1f9..060a64209a 100644 --- a/docs/contributing/code-of-conduct.md +++ b/docs/contributing/code-of-conduct.md @@ -16,7 +16,7 @@ For answers to common questions about this code of conduct, see the FAQ at ::: Our community is dedicated to providing a safe and inclusive environment for all contributors. -To ensure this, we have adopted the **[Contributor Covenant]*- code of conduct. +To ensure this, we have adopted the **[Contributor Covenant]** code of conduct. This widely recognized and respected code of conduct sets clear guidelines for behavior, and it's easy for everyone to understand and follow. By participating in the Detox community, you agree to abide by the terms of this code of conduct. diff --git a/docs/contributing/code/example-projects.md b/docs/contributing/code/example-projects.md index 42cd0afc5e..bc4cd18a92 100644 --- a/docs/contributing/code/example-projects.md +++ b/docs/contributing/code/example-projects.md @@ -4,7 +4,7 @@ Explore various example projects hosted in this monorepo to understand real-worl - `examples/demo-react-native-jest`: Demonstrates Detox usage in a React Native app project. - `examples/demo-native-ios`: Illustrates Detox usage in a pure-native iOS app. -- `examples/demo-native-android` (broken): Demonstrates Detox usage in a pure-native Android app. +- `examples/demo-native-android` (broken): Demonstrates Detox usage in a pure-native Android app. - `examples/demo-pure-native-android`: Highlights the _pure_ [Detox-Native](https://github.com/wix/Detox/tree/master/detox-native/README.md) project. To run end-to-end tests associated with these projects, refer to the [project-specific](https://github.com/wix/Detox/tree/master/examples) READMEs. diff --git a/docs/contributing/code/overview.md b/docs/contributing/code/overview.md index 875dcc10a1..f9b4943d1f 100644 --- a/docs/contributing/code/overview.md +++ b/docs/contributing/code/overview.md @@ -1,3 +1,7 @@ +--- +sidebar_label: Overview +--- + # Code Changes Overview Welcome to the code changes section! As a contributor, it's essential to understand the project's goals and adhere to its code of conduct. Before contributing, please review any existing issues related to your work, ensure your code is well-documented, and has adequate test coverage. It's also important that your code is compatible with the project's supported platforms and their versions. diff --git a/docs/contributing/code/setting-up-the-dev-environment.md b/docs/contributing/code/setting-up-the-dev-environment.md index a72dc305b4..33c1e2fda2 100644 --- a/docs/contributing/code/setting-up-the-dev-environment.md +++ b/docs/contributing/code/setting-up-the-dev-environment.md @@ -34,22 +34,34 @@ lerna bootstrap ### React-Native CLI +[react-native-cli] is a command line interface for React Native. + ```bash npm2yarn npm install react-native-cli --global ``` ### Watchman +[Watchman] is a tool by Facebook for watching changes in the filesystem. + ```bash brew install watchman ``` ### xcpretty +[xcpretty] is a fast and flexible formatter for xcodebuild. + ```bash gem install xcpretty ``` ## Building and Testing -Refer to the scripts `scripts/ci.ios.sh` and `scripts/ci.android.sh` to understand the build and test process. Ensure these scripts pass before submitting a pull request. +Refer to the scripts `scripts/ci.ios.sh` and `scripts/ci.android.sh` to understand the build and test process. + +Before submitting a pull request, please ensure at a minimum that your code adheres to our linting standards and that all unit tests run successfully without any errors. + +[react-native-cli]: https://www.npmjs.com/package/react-native-cli +[Watchman]: https://facebook.github.io/watchman/ +[xcpretty]: https://github.com/xcpretty/xcpretty diff --git a/docs/contributing/code/submitting-pull-requests.md b/docs/contributing/code/submitting-pull-requests.md index ae4ad076ff..e7f0d6535f 100644 --- a/docs/contributing/code/submitting-pull-requests.md +++ b/docs/contributing/code/submitting-pull-requests.md @@ -37,9 +37,9 @@ Refer to the [Documentation Changes] page for guidelines. Write descriptive, meaningful commit messages that follow the **Conventional Commits** format, specifying the type of change, the scope, and a concise description. For example: ```plaintext -fix(iOS): resolve crash on scrolling in iOS 17.0 -feat(Android): add new API for setting the device locale -chore(tests): update unit tests for new utility function +fix(ios): resolve crash on scrolling in iOS 17.0 +feat(android): add new API for setting the device locale +test: update unit tests for new utility function ``` More details on good commit messages can be found [here](https://www.conventionalcommits.org/en/v1.0.0/). diff --git a/docs/contributing/feature-requests.md b/docs/contributing/feature-requests.md index 9dd8ac09e1..5f18010b4f 100644 --- a/docs/contributing/feature-requests.md +++ b/docs/contributing/feature-requests.md @@ -2,11 +2,11 @@ Open dialogue and feedback, particularly feature requests, play an invaluable role in open-source communities like Detox. They provide insights into user preferences and requirements, aiding in the refinement and enhancement of the project. -Detox project provides various platforms for feedback: [GitHub Issues], [GitHub Discussions], and [Discord]. +Detox project provides various platforms for feedback: [GitHub Issues], [Twitter], and [Discord]. Keep in mind that feedback is varied, including feature suggestions, improvements, identification of missing functionality, and support requests. Each type of feedback has a preferred method of submission. -## Crafting Effective Feedback +## Giving Feedback Follow these tips to deliver clear, actionable feedback: @@ -15,12 +15,10 @@ Follow these tips to deliver clear, actionable feedback: - **Clarity and Respect:** Make your feedback specific, clear, and respectful, ensuring it is actionable for the maintainers. - **Guidelines:** Adhere to the below suggestions for each type of feedback. -## Feature Suggestions +## Suggesting Features When suggesting a new feature, remember to provide thorough information, such as examples, screenshots, and code snippets, to communicate your idea effectively. -## Crafting Effective Feature Suggestions - Just as in general feedback, make sure your feature request is: - **Specific:** Clearly describe the feature you wish to see implemented. @@ -30,5 +28,5 @@ Just as in general feedback, make sure your feature request is: By making your feedback as clear and detailed as possible, you'll greatly assist the maintainers in understanding and potentially implementing your suggestion. [GitHub Issues]: https://github.com/wix/Detox/issues -[GitHub Discussions]: https://github.com/wix/Detox/discussions +[Twitter]: https://twitter.com/detoxe2e [Discord]: https://discord.gg/CkD5QKheF5 diff --git a/docs/contributing/questions/asking-questions.md b/docs/contributing/questions/asking-questions.md index 4115f795ad..24dd749f58 100644 --- a/docs/contributing/questions/asking-questions.md +++ b/docs/contributing/questions/asking-questions.md @@ -10,7 +10,7 @@ In case you're facing an issue, consider the following steps before posting a qu - **Suspect a bug?** Refer to our [Reporting Bugs] guide to determine if you've encountered a Detox bug and how to report it. - **Search** for existing answers that may have addressed your question already. [Below](#where-to-ask), we've listed the right platforms to ask (and search) questions. -- Review our **documentation** thoroughly for any related information. Use the search bar on the top bar to streamline your search. +- Review our **documentation** thoroughly for any related information. Use document.querySelector('.DocSearch-Button').click()}>the search bar in the top right corner to streamline your search. If an existing answer doesn't fully address your question, feel free to ask a follow-up in the same thread if comment-enabled or create a new question, linking back to the original one if possible. @@ -18,7 +18,7 @@ If an existing answer doesn't fully address your question, feel free to ask a fo If you can't find an answer to your question, you can ask it in the following ways: -- **Discord**: Join our dedicated Discord server [here][Discord] to seek community assistance. +- **Discord**: Join our dedicated [Discord server][Discord] to seek community assistance. - **Stack Overflow**: Tag your question with ["Detox" on Stack Overflow] to reach the right audience. - **GitHub**: Raise an issue on our [Detox repository]. From 200c81bfa7b452ef5638f7f54bd919963efa69b5 Mon Sep 17 00:00:00 2001 From: Asaf Korem Date: Thu, 9 Nov 2023 14:57:56 +0200 Subject: [PATCH 033/185] docs(README): library -> framework. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 650fa7b651..a3fb3d46ed 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Detox

-Gray box end-to-end testing and automation library for mobile apps. +Gray box end-to-end testing and automation framework for mobile apps.

Demo From 758ebaf1c20490e9d1da34a2f3e6f2eafdd929d2 Mon Sep 17 00:00:00 2001 From: Asaf Korem Date: Thu, 9 Nov 2023 14:58:12 +0200 Subject: [PATCH 034/185] docs: add monorepo structure section. --- docs/contributing/code/overview.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/contributing/code/overview.md b/docs/contributing/code/overview.md index f9b4943d1f..4578493c36 100644 --- a/docs/contributing/code/overview.md +++ b/docs/contributing/code/overview.md @@ -17,4 +17,31 @@ We use [GitHub] for managing pull requests, conducting code reviews, and trackin The code review process is central to our collaboration. Every contribution must go through a review before merging to maintain the quality of our codebase. As a contributor, being willing to discuss your work, respond to feedback, and work with the community is key to improving the project and creating a positive environment for all contributors. +## Repository Structure + +Our GitHub repository is a monorepo, which means it contains multiple Detox-related projects and packages. + +The main package is the Detox framework, which is the core of the project. +It contains the native code for iOS and Android, as well as the JavaScript code. +The other projects are the Detox CLI, the Detox test app, example apps, and the Detox documentation website. + +Here's a high-level overview of the repository structure: + +- 📁 **detox-cli** - The CLI for Detox (e.g., `detox init`, `detox test`, read more about our [CLI docs]) +- 📁 **detox** - The Detox framework + - 📁 **android** - The Android native code + - 📁 **ios** - The iOS native code, including its native submodules (e.g., DetoxSync) + - 📁 **test** - The Detox test app, a full-feature React Native app for testing end-to-end testing + - 📁 **local-cli** - Local CLI commands for Detox development (e.g., `detox rebuild-framework-cache`, which rebuilds the iOS framework) + - 📁 **runners** - The Detox runners, which are used to run the tests + - 📁 **scripts** - Scripts for building the framework for publishing + - 📁 **src** - The JavaScript source code of Detox +- 📁 **website** - The documentation website of Detox (read more about our [documentation site docs]) +- 📁 **docs** - The documentation of Detox, written in Markdown and published on the website +- 📁 **examples** - Example apps for Detox (for more information, check the [list of example projects]) +- 📁 **scripts** - Scripts for building and testing Detox + [GitHub]: https://github.com/wix/Detox +[documentation site docs]: ../documentation.md +[list of example projects]: ./example-projects.md +[CLI docs]: ../../cli/overview.md From 7d8c7a4242f7425657f199008ecf9d5a651c34a0 Mon Sep 17 00:00:00 2001 From: Asaf Korem Date: Thu, 9 Nov 2023 14:59:14 +0200 Subject: [PATCH 035/185] docs(contribution): add building and testing section. --- .../code/setting-up-the-dev-environment.md | 114 +++++++++++++++++- 1 file changed, 113 insertions(+), 1 deletion(-) diff --git a/docs/contributing/code/setting-up-the-dev-environment.md b/docs/contributing/code/setting-up-the-dev-environment.md index 33c1e2fda2..4c2a379273 100644 --- a/docs/contributing/code/setting-up-the-dev-environment.md +++ b/docs/contributing/code/setting-up-the-dev-environment.md @@ -10,6 +10,10 @@ Please complete our [Introduction](introduction/getting-started.mdx) guides befo ## Setting Up The Monorepo Management +Our repository is a monorepo, which means it contains multiple Detox-related projects and packages. [Read more about our repository structure](../code/overview.md#repository-structure). + +To set up the monorepo locally, follow these steps: + Install the monorepo management tool, `lerna`: ```bash npm2yarn @@ -58,10 +62,118 @@ gem install xcpretty ## Building and Testing -Refer to the scripts `scripts/ci.ios.sh` and `scripts/ci.android.sh` to understand the build and test process. +Our JavaScript code is thoroughly verified with comprehensive unit tests, complemented by integration tests. +Additionally, our native code undergoes rigorous testing through both unit and integration tests. +To ensure complete functionality, we conduct end-to-end tests on a fully-featured React Native application, designed to encompass all our public APIs (refer to our [test app] for details). + +:::note Important Before submitting a pull request, please ensure at a minimum that your code adheres to our linting standards and that all unit tests run successfully without any errors. +::: + +The following sections describe how to build and test our code, with a general instruction. +However, we recommend you to refer to our `package.json` files for a complete list of available scripts: + +- Detox Framework: [`detox/package.json`] +- Detox Test App: [`detox/test/package.json`] + +### Unit Tests + +We are using [Jest] for running our unit tests. + +Under the `detox` directory, run the following command to run the unit tests with coverage: + +```bash +npm run test +``` + +We also have unit tests for our native code (Android only), you can run them using the following commands: + +```bash +npm run test:android-debug +npm run test:android-release +``` + +### iOS: Rebuilding the Framework + +After changing the native code of Detox iOS, you need to rebuild the Detox framework. This is done when running: + +```bash +detox build-framework-cache +``` + +Note that it is only required when you change the native code of Detox, or one of its dependencies (e.g. [DetoxSync]). +If you are only changing the JavaScript code, you don't need to rebuild the framework. + +### Building the Detox Test App + +The Detox test app is a full-feature React Native app for testing end-to-end testing. + +To build the Detox test app (`detox/test`), use the `build` command of the Detox CLI. +List of available configurations can be found under [`detox/test/e2e/detox.config.js`]. + +For example, to build the Detox test app for iOS in release mode, run: + +```bash +detox build --configuration ios.sim.release +``` + +### End-to-End Tests + +To run the end-to-end tests, use the `test` command of the Detox CLI. + +For example, to run the end-to-end tests for iOS in release mode, run: + +```bash +detox test --configuration ios.sim.release +``` + +### Integration Tests + +We are using [Jest] for running our integration tests. + +Under the `detox/test` directory, run the following command to run the integration tests: + +```bash +npm run integration +``` + +### Linting + +We are using [ESLint] for linting our JavaScript code. + +You can run the linter locally using the following command (under `detox/test`): + +```bash +npm run lint +``` + +We also test our types using the following command: + +```bash +npm run test:types +``` + +### Running the CI Scripts Locally + +On our CI, we are testing our changes on both iOS and Android, and we are linting our code. +You can run the same scripts we run on our CI locally, to ensure your changes are passing the tests (under the project's root directory): + +```bash +npm run ci.ios +npm run ci.android +``` + +Refer to the scripts `scripts/ci.ios.sh` and `scripts/ci.android.sh` to see how we run the tests on our CI. + [react-native-cli]: https://www.npmjs.com/package/react-native-cli [Watchman]: https://facebook.github.io/watchman/ [xcpretty]: https://github.com/xcpretty/xcpretty +[test app]: https://github.com/wix/Detox/tree/master/detox/test +[DetoxSync]: https://github.com/wix/DetoxSync +[`detox/test/e2e/detox.config.js`]: https://github.com/wix/Detox/blob/6e87dc13826341dba21ed0a732e5b57efa08e7b5/detox/test/e2e/detox.config.js#L137 +[ESLint]: https://eslint.org/ +[Jest]: https://jestjs.io/ +[`detox/package.json`]: https://github.com/wix/Detox/blob/master/detox/package.json +[`detox/test/package.json`]: https://github.com/wix/Detox/blob/master/detox/test/package.json From 094a2d387c262c12cbe69de79e662cfddddf41fa Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Sun, 12 Nov 2023 08:44:13 +0200 Subject: [PATCH 036/185] chore: bump jest-allure2-reporter (#4278) --- detox/package.json | 2 +- detox/test/package.json | 2 +- generation/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/detox/package.json b/detox/package.json index a76c616bcc..5969c59971 100644 --- a/detox/package.json +++ b/detox/package.json @@ -51,7 +51,7 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-unicorn": "^47.0.0", "jest": "^28.1.3", - "jest-allure2-reporter": "2.0.0-alpha.6", + "jest-allure2-reporter": "2.0.0-alpha.8", "mockdate": "^2.0.1", "prettier": "^2.4.1", "react-native": "0.71.10", diff --git a/detox/test/package.json b/detox/test/package.json index 4325ea6570..f54ded91ba 100644 --- a/detox/test/package.json +++ b/detox/test/package.json @@ -63,7 +63,7 @@ "express": "^4.15.3", "glob": "^7.2.0", "jest": "^29.2.1", - "jest-allure2-reporter": "2.0.0-alpha.6", + "jest-allure2-reporter": "2.0.0-alpha.8", "jest-junit": "^10.0.0", "lodash": "^4.14.1", "metro-react-native-babel-preset": "0.73.9", diff --git a/generation/package.json b/generation/package.json index 4e33b736ae..839e78b623 100644 --- a/generation/package.json +++ b/generation/package.json @@ -30,7 +30,7 @@ }, "devDependencies": { "jest": "^27.0.0", - "jest-allure2-reporter": "2.0.0-alpha.6", + "jest-allure2-reporter": "2.0.0-alpha.8", "lint-staged": "^6.0.0", "prettier": "^1.8.2" }, From 24f68c3bd7675ed6f508e3f5fea97509eb7991b2 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Thu, 16 Nov 2023 11:47:17 +0200 Subject: [PATCH 037/185] chore: update social stats --- .../src/components/Homepage/SubHeader/HomepageSubHeader.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/components/Homepage/SubHeader/HomepageSubHeader.js b/website/src/components/Homepage/SubHeader/HomepageSubHeader.js index b8cee7cb8b..5d4780b3ed 100644 --- a/website/src/components/Homepage/SubHeader/HomepageSubHeader.js +++ b/website/src/components/Homepage/SubHeader/HomepageSubHeader.js @@ -20,12 +20,12 @@ const HomepageSubHeader = () => { @detoxe2e - 486 + 489  followers - 10,598 + 10,653

From ebf40fb2c57151ea83bf725aff53b1501b5290b6 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Sat, 18 Nov 2023 19:59:39 +0200 Subject: [PATCH 038/185] chore: bump jest-allure2-reporter (#4284) --- detox/package.json | 2 +- detox/test/e2e/01.sanity.test.js | 10 ++++++++++ detox/test/package.json | 2 +- generation/package.json | 2 +- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/detox/package.json b/detox/package.json index 5969c59971..d0e2f915b9 100644 --- a/detox/package.json +++ b/detox/package.json @@ -51,7 +51,7 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-unicorn": "^47.0.0", "jest": "^28.1.3", - "jest-allure2-reporter": "2.0.0-alpha.8", + "jest-allure2-reporter": "2.0.0-alpha.10", "mockdate": "^2.0.1", "prettier": "^2.4.1", "react-native": "0.71.10", diff --git a/detox/test/e2e/01.sanity.test.js b/detox/test/e2e/01.sanity.test.js index d99fb8296a..dbd65f6126 100644 --- a/detox/test/e2e/01.sanity.test.js +++ b/detox/test/e2e/01.sanity.test.js @@ -1,3 +1,13 @@ +/** + * Sanity suite + * ------------ + * + * Tests in this suite ensure that the most basic functionality of Detox is + * working as expected: app launches, element matching, assertions, etc. + * + * @severity critical + * @tag sanity + */ describe('Sanity', () => { beforeEach(async () => { await device.reloadReactNative(); diff --git a/detox/test/package.json b/detox/test/package.json index f54ded91ba..46f5c3a0cd 100644 --- a/detox/test/package.json +++ b/detox/test/package.json @@ -63,7 +63,7 @@ "express": "^4.15.3", "glob": "^7.2.0", "jest": "^29.2.1", - "jest-allure2-reporter": "2.0.0-alpha.8", + "jest-allure2-reporter": "2.0.0-alpha.10", "jest-junit": "^10.0.0", "lodash": "^4.14.1", "metro-react-native-babel-preset": "0.73.9", diff --git a/generation/package.json b/generation/package.json index 839e78b623..c4932adc17 100644 --- a/generation/package.json +++ b/generation/package.json @@ -30,7 +30,7 @@ }, "devDependencies": { "jest": "^27.0.0", - "jest-allure2-reporter": "2.0.0-alpha.8", + "jest-allure2-reporter": "2.0.0-alpha.10", "lint-staged": "^6.0.0", "prettier": "^1.8.2" }, From 3865d6f6b30b4e01e72bb51118cf20bf7fad5e5a Mon Sep 17 00:00:00 2001 From: Asaf Korem <55082339+asafkorem@users.noreply.github.com> Date: Mon, 20 Nov 2023 12:19:18 +0200 Subject: [PATCH 039/185] docs: apply suggestions from code review Co-authored-by: d4vidi --- docs/contributing.md | 2 +- docs/contributing/code/example-projects.md | 6 +-- docs/contributing/code/overview.md | 9 ++-- .../code/setting-up-the-dev-environment.md | 41 +++++++++++-------- .../code/submitting-pull-requests.md | 15 ++----- 5 files changed, 37 insertions(+), 36 deletions(-) diff --git a/docs/contributing.md b/docs/contributing.md index 6641fa2d90..e2a04faacf 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -40,7 +40,7 @@ We plan to launch a "Core Contributor" program for individuals who have signific [Answers]: contributing/questions/answering-questions.md [Bug Reports]: contributing/reporting-bugs.md [Feature Suggestions]: contributing/feature-requests.md -[Code Submissions]: contributing/code/submitting-pull-requests.md +[Code Contributions]: contributing/code/overview.md [Code Reviews]: contributing/code/reviewing-pull-requests.md [Documentation Enhancements]: contributing/documentation.md [Discord server]: https://discord.gg/CkD5QKheF5 diff --git a/docs/contributing/code/example-projects.md b/docs/contributing/code/example-projects.md index bc4cd18a92..a023c6ee0a 100644 --- a/docs/contributing/code/example-projects.md +++ b/docs/contributing/code/example-projects.md @@ -3,8 +3,8 @@ Explore various example projects hosted in this monorepo to understand real-world usage and testing with Detox. - `examples/demo-react-native-jest`: Demonstrates Detox usage in a React Native app project. -- `examples/demo-native-ios`: Illustrates Detox usage in a pure-native iOS app. -- `examples/demo-native-android` (broken): Demonstrates Detox usage in a pure-native Android app. -- `examples/demo-pure-native-android`: Highlights the _pure_ [Detox-Native](https://github.com/wix/Detox/tree/master/detox-native/README.md) project. +- `examples/demo-native-ios`: Illustrates Detox usage in a native (non-React Native) iOS app. +- `examples/demo-native-android` (broken): Illustrates Detox usage in a native (non-React Native) Android app. +- `examples/demo-pure-native-android`: Illustrates the subsidiary [Detox-Native](https://github.com/wix/Detox/blob/master/detox/detox-native/README.md) project. To run end-to-end tests associated with these projects, refer to the [project-specific](https://github.com/wix/Detox/tree/master/examples) READMEs. diff --git a/docs/contributing/code/overview.md b/docs/contributing/code/overview.md index 4578493c36..62e9510940 100644 --- a/docs/contributing/code/overview.md +++ b/docs/contributing/code/overview.md @@ -29,13 +29,16 @@ Here's a high-level overview of the repository structure: - 📁 **detox-cli** - The CLI for Detox (e.g., `detox init`, `detox test`, read more about our [CLI docs]) - 📁 **detox** - The Detox framework - - 📁 **android** - The Android native code + - 📁 **android** - The Android native code, alongside native unit tests - 📁 **ios** - The iOS native code, including its native submodules (e.g., DetoxSync) - - 📁 **test** - The Detox test app, a full-feature React Native app for testing end-to-end testing + - 📁 **test** - The Detox self-test app: A full-feature React Native app for end-to-end testing Detox itself + - 📁 **src** - The app's JavaScript code + - 📁 **e2e** - The Detox self-tests + - 📁 **integration** - Detox integration self-tests - 📁 **local-cli** - Local CLI commands for Detox development (e.g., `detox rebuild-framework-cache`, which rebuilds the iOS framework) - 📁 **runners** - The Detox runners, which are used to run the tests - 📁 **scripts** - Scripts for building the framework for publishing - - 📁 **src** - The JavaScript source code of Detox + - 📁 **src** - The JavaScript source code of Detox. The include bundled JavaScript unit tests - 📁 **website** - The documentation website of Detox (read more about our [documentation site docs]) - 📁 **docs** - The documentation of Detox, written in Markdown and published on the website - 📁 **examples** - Example apps for Detox (for more information, check the [list of example projects]) diff --git a/docs/contributing/code/setting-up-the-dev-environment.md b/docs/contributing/code/setting-up-the-dev-environment.md index 4c2a379273..432e14103e 100644 --- a/docs/contributing/code/setting-up-the-dev-environment.md +++ b/docs/contributing/code/setting-up-the-dev-environment.md @@ -4,7 +4,7 @@ This document guides you through setting up your development environment to star :::important Prerequisites -Please complete our [Introduction](introduction/getting-started.mdx) guides before proceeding. This ensures you have the necessary tools and dependencies installed. +Please complete our [Introductory environment setup](introduction/environment-setup.md) guide before proceeding. This ensures you have the necessary tools and dependencies installed in order to _run_ Detox tests, which is a fundamental step before being able to contribute to the project itself. ::: @@ -78,22 +78,27 @@ However, we recommend you to refer to our `package.json` files for a complete li - Detox Framework: [`detox/package.json`] - Detox Test App: [`detox/test/package.json`] -### Unit Tests +### Unit Tests (Javascript) -We are using [Jest] for running our unit tests. +We use [Jest] for running our unit tests. -Under the `detox` directory, run the following command to run the unit tests with coverage: +Under the `detox/` directory, run the following command to run the unit tests with coverage: ```bash -npm run test +cd detox +npm run unit ``` +The unit tests reside alongside the JavaScript code. Typically, they can also be easily run directly from within an IDE such as [WebStorm](https://www.jetbrains.com/webstorm/) or [vscode](https://code.visualstudio.com/), even in debug (i.e step-by-step execution) mode. + +### Unit Tests - Android Native -We also have unit tests for our native code (Android only), you can run them using the following commands: +We also have unit tests for our native code (Android only). They reside alongside Detox's native Android code, under a dedicated subdirectory called `testFull`. You can run them using the following commands: ```bash -npm run test:android-debug -npm run test:android-release +cd detox +npm run unit:android-release ``` +The native unit tests can also be run in [Android Studio](https://developer.android.com/studio) (i.e. the IDE for Android apps development). Most tests can be run seamlessly using Android Studio's build-in support for unit-tests, but some require a plugin called [Spek](https://plugins.jetbrains.com/plugin/10915-spek-framework), which can be installed from within Android Studio itself - under the Plugins marketplace. ### iOS: Rebuilding the Framework @@ -108,20 +113,20 @@ If you are only changing the JavaScript code, you don't need to rebuild the fram ### Building the Detox Test App -The Detox test app is a full-feature React Native app for testing end-to-end testing. +The Detox self-test app is a full-feature React Native app for testing Detox end-to-end. -To build the Detox test app (`detox/test`), use the `build` command of the Detox CLI. +Before running the tests, you must first build the Detox test app. To do so, under `detox/test`, use the `build` command of the Detox CLI. List of available configurations can be found under [`detox/test/e2e/detox.config.js`]. For example, to build the Detox test app for iOS in release mode, run: ```bash +cd detox/test detox build --configuration ios.sim.release -``` ### End-to-End Tests -To run the end-to-end tests, use the `test` command of the Detox CLI. +To run the end-to-end tests (after building the app), use the `test` command of the Detox CLI. For example, to run the end-to-end tests for iOS in release mode, run: @@ -131,17 +136,19 @@ detox test --configuration ios.sim.release ### Integration Tests -We are using [Jest] for running our integration tests. +Besides unit tests and end-to-end tests, we have some middle ground integration tests, which typically run Detox in a custom headless-like mode (i.e. stubs replace devices). Those tests typically execute Detox from a command line in that mode, and then do some post-processing of the results. We use [Jest] for running our integration tests. + +The integration reside under a dedicated directory alongside Detox's the self-test app's code. -Under the `detox/test` directory, run the following command to run the integration tests: +Run the following command to run the integration tests: ```bash +cd detox/test npm run integration -``` ### Linting -We are using [ESLint] for linting our JavaScript code. +We use [ESLint] for linting our JavaScript code. You can run the linter locally using the following command (under `detox/test`): @@ -157,7 +164,7 @@ npm run test:types ### Running the CI Scripts Locally -On our CI, we are testing our changes on both iOS and Android, and we are linting our code. +On our CI, we test our changes on both iOS and Android, and we lint-check our code. You can run the same scripts we run on our CI locally, to ensure your changes are passing the tests (under the project's root directory): ```bash diff --git a/docs/contributing/code/submitting-pull-requests.md b/docs/contributing/code/submitting-pull-requests.md index e7f0d6535f..a6170a0324 100644 --- a/docs/contributing/code/submitting-pull-requests.md +++ b/docs/contributing/code/submitting-pull-requests.md @@ -4,7 +4,7 @@ Before creating a PR, it's recommended to consult with the Detox collaborators. Request a design review or assistance with planning the tests to ensure alignment with project goals. -Contact us on our [Discord Server] or open an issue on GitHub. +Contact us on our [Discord Server] or open an [issue on GitHub]. ::: @@ -15,17 +15,7 @@ Contact us on our [Discord Server] or open an issue on GitHub. ## Test Your Changes -- **Unit Testing:** Write unit tests for your code, usually located within the same directory as the unit under test. -- **End-to-End (E2E) Testing:** - - Our E2E tests are located under the `detox/test/e2e` directory, with each test file named after the feature it tests. These tests are testing a real React Native app (`detox/test`). - - Running the test can be done using `detox build` and `detox test` commands, as described in the `package.json` of the project. -- After changes to the native implementation of Detox (or `DetoxSync`), rebuild the framework before running the tests locally: - -```bash -detox rebuild-framework-cache -``` - -- **Integration Testing:** Usually, integration tests are not required. If there are special cases, perform integration tests under `detox/test/integration`. +Being a testing framework, Detox is a highly self-tested project. Be sure to add/fix test coverage over your work, by running the various test flavors associated with your changes. You can review them, [here](./setting-up-the-dev-environment.md#building-and-testing). ## Modify Documentation @@ -51,3 +41,4 @@ More details on good commit messages can be found [here](https://www.conventiona [Documentation Changes]: ../documentation.md [Discord Server]: https://discord.gg/CkD5QKheF5 +[issue on GitHub]: https://github.com/wix/Detox/issues/new/choose From 29e486a10ab8f8ad8d6189f200488343988b2159 Mon Sep 17 00:00:00 2001 From: Asaf Korem Date: Mon, 20 Nov 2023 12:30:10 +0200 Subject: [PATCH 040/185] docs: split setting-up-dev-env doc (add building-and-testing). --- .../contributing/code/building-and-testing.md | 136 ++++++++++++++++++ .../code/setting-up-the-dev-environment.md | 121 ---------------- .../code/submitting-pull-requests.md | 2 +- website/sidebars.js | 1 + 4 files changed, 138 insertions(+), 122 deletions(-) create mode 100644 docs/contributing/code/building-and-testing.md diff --git a/docs/contributing/code/building-and-testing.md b/docs/contributing/code/building-and-testing.md new file mode 100644 index 0000000000..54b1c03628 --- /dev/null +++ b/docs/contributing/code/building-and-testing.md @@ -0,0 +1,136 @@ +# Building and Testing + +Our JavaScript code is thoroughly verified with comprehensive unit tests, complemented by integration tests. +Additionally, our native code undergoes rigorous testing through both unit and integration tests. +To ensure complete functionality, we conduct end-to-end tests on a fully-featured React Native application, designed to encompass all our public APIs (refer to our [test app] for details). + +:::note Important + +Before submitting a pull request, please ensure at a minimum that your code adheres to our linting standards and that all unit tests run successfully without any errors. + +::: + +The following sections describe how to build and test our code, with a general instruction. +However, we recommend you to refer to our `package.json` files for a complete list of available scripts: + +- Detox Framework: [`detox/package.json`] +- Detox Test App: [`detox/test/package.json`] + +## Unit Tests (JavaScript) + +We use [Jest] for running our unit tests. + +Under the `detox/` directory, run the following command to run the unit tests with coverage: + +```bash +cd detox +npm run unit +``` +The unit tests reside alongside the JavaScript code. Typically, they can also be easily run directly from within an IDE such as [WebStorm](https://www.jetbrains.com/webstorm/) or [vscode](https://code.visualstudio.com/), even in debug (i.e step-by-step execution) mode. + +## Unit Tests - Android Native + +We also have unit tests for our native code (Android only). They reside alongside Detox's native Android code, under a dedicated subdirectory called `testFull`. You can run them using the following commands: + +```bash +cd detox +npm run unit:android-release +``` +The native unit tests can also be run in [Android Studio](https://developer.android.com/studio) (i.e. the IDE for Android apps development). Most tests can be run seamlessly using Android Studio's build-in support for unit-tests, but some require a plugin called [Spek](https://plugins.jetbrains.com/plugin/10915-spek-framework), which can be installed from within Android Studio itself - under the Plugins marketplace. + +## iOS: Rebuilding the Framework + +After changing the native code of Detox iOS, you need to rebuild the Detox framework. This is done when running: + +```bash +detox build-framework-cache +``` + +Note that it is only required when you change the native code of Detox, or one of its dependencies (e.g. [DetoxSync]). +If you are only changing the JavaScript code, you don't need to rebuild the framework. + +## Building the Detox Test App + +The Detox self-test app is a full-feature React Native app for testing Detox end-to-end. + +Before running the tests, you must first build the Detox test app. To do so, under `detox/test`, use the `build` command of the Detox CLI. +List of available configurations can be found under [`detox/test/e2e/detox.config.js`]. + +For example, to build the Detox test app for iOS in release mode, run: + +```bash +cd detox/test +detox build --configuration ios.sim.release + +### End-to-End Tests + +To run the end-to-end tests (after building the app), use the `test` command of the Detox CLI. + +For example, to run the end-to-end tests for iOS in release mode, run: + +```bash +cd detox/test +detox test --configuration ios.sim.release +``` + +To run in Android release mode, run: + +```bash +cd detox/test +detox test --configuration android.emu.release +``` + +:::note + +Mind that on Android, Detox expects a specific AVD with a [specific name](https://github.com/wix/Detox/blob/24f68c3bd7675ed6f508e3f5fea97509eb7991b2/detox/test/e2e/detox.config.js#L111) to be available on your computer. +Be sure to [install such an AVD](https://developer.android.com/studio/run/managing-avds#createavd), beforehand. + +::: + +## Integration Tests + +Besides unit tests and end-to-end tests, we have some middle ground integration tests, which typically run Detox in a custom headless-like mode (i.e. stubs replace devices). Those tests typically execute Detox from a command line in that mode, and then do some post-processing of the results. We use [Jest] for running our integration tests. + +The integration reside under a dedicated directory alongside Detox's the self-test app's code. + +Run the following command to run the integration tests: + +```bash +cd detox/test +npm run integration + +### Linting + +We use [ESLint] for linting our JavaScript code. + +You can run the linter locally using the following command (under `detox/test`): + +```bash +npm run lint +``` + +We also test our types using the following command: + +```bash +npm run test:types +``` + +## Running the CI Scripts Locally + +On our CI, we test our changes on both iOS and Android, and we lint-check our code. +You can run the same scripts we run on our CI locally, to ensure your changes are passing the tests (under the project's root directory): + +```bash +npm run ci.ios +npm run ci.android +``` + +Refer to the scripts `scripts/ci.ios.sh` and `scripts/ci.android.sh` to see how we run the tests on our CI. + +[test app]: https://github.com/wix/Detox/tree/master/detox/test +[DetoxSync]: https://github.com/wix/DetoxSync +[`detox/test/e2e/detox.config.js`]: https://github.com/wix/Detox/blob/6e87dc13826341dba21ed0a732e5b57efa08e7b5/detox/test/e2e/detox.config.js#L137 +[ESLint]: https://eslint.org/ +[Jest]: https://jestjs.io/ +[`detox/package.json`]: https://github.com/wix/Detox/blob/master/detox/package.json +[`detox/test/package.json`]: https://github.com/wix/Detox/blob/master/detox/test/package.json diff --git a/docs/contributing/code/setting-up-the-dev-environment.md b/docs/contributing/code/setting-up-the-dev-environment.md index 432e14103e..6db7592904 100644 --- a/docs/contributing/code/setting-up-the-dev-environment.md +++ b/docs/contributing/code/setting-up-the-dev-environment.md @@ -60,127 +60,6 @@ brew install watchman gem install xcpretty ``` -## Building and Testing - -Our JavaScript code is thoroughly verified with comprehensive unit tests, complemented by integration tests. -Additionally, our native code undergoes rigorous testing through both unit and integration tests. -To ensure complete functionality, we conduct end-to-end tests on a fully-featured React Native application, designed to encompass all our public APIs (refer to our [test app] for details). - -:::note Important - -Before submitting a pull request, please ensure at a minimum that your code adheres to our linting standards and that all unit tests run successfully without any errors. - -::: - -The following sections describe how to build and test our code, with a general instruction. -However, we recommend you to refer to our `package.json` files for a complete list of available scripts: - -- Detox Framework: [`detox/package.json`] -- Detox Test App: [`detox/test/package.json`] - -### Unit Tests (Javascript) - -We use [Jest] for running our unit tests. - -Under the `detox/` directory, run the following command to run the unit tests with coverage: - -```bash -cd detox -npm run unit -``` -The unit tests reside alongside the JavaScript code. Typically, they can also be easily run directly from within an IDE such as [WebStorm](https://www.jetbrains.com/webstorm/) or [vscode](https://code.visualstudio.com/), even in debug (i.e step-by-step execution) mode. - -### Unit Tests - Android Native - -We also have unit tests for our native code (Android only). They reside alongside Detox's native Android code, under a dedicated subdirectory called `testFull`. You can run them using the following commands: - -```bash -cd detox -npm run unit:android-release -``` -The native unit tests can also be run in [Android Studio](https://developer.android.com/studio) (i.e. the IDE for Android apps development). Most tests can be run seamlessly using Android Studio's build-in support for unit-tests, but some require a plugin called [Spek](https://plugins.jetbrains.com/plugin/10915-spek-framework), which can be installed from within Android Studio itself - under the Plugins marketplace. - -### iOS: Rebuilding the Framework - -After changing the native code of Detox iOS, you need to rebuild the Detox framework. This is done when running: - -```bash -detox build-framework-cache -``` - -Note that it is only required when you change the native code of Detox, or one of its dependencies (e.g. [DetoxSync]). -If you are only changing the JavaScript code, you don't need to rebuild the framework. - -### Building the Detox Test App - -The Detox self-test app is a full-feature React Native app for testing Detox end-to-end. - -Before running the tests, you must first build the Detox test app. To do so, under `detox/test`, use the `build` command of the Detox CLI. -List of available configurations can be found under [`detox/test/e2e/detox.config.js`]. - -For example, to build the Detox test app for iOS in release mode, run: - -```bash -cd detox/test -detox build --configuration ios.sim.release - -### End-to-End Tests - -To run the end-to-end tests (after building the app), use the `test` command of the Detox CLI. - -For example, to run the end-to-end tests for iOS in release mode, run: - -```bash -detox test --configuration ios.sim.release -``` - -### Integration Tests - -Besides unit tests and end-to-end tests, we have some middle ground integration tests, which typically run Detox in a custom headless-like mode (i.e. stubs replace devices). Those tests typically execute Detox from a command line in that mode, and then do some post-processing of the results. We use [Jest] for running our integration tests. - -The integration reside under a dedicated directory alongside Detox's the self-test app's code. - -Run the following command to run the integration tests: - -```bash -cd detox/test -npm run integration - -### Linting - -We use [ESLint] for linting our JavaScript code. - -You can run the linter locally using the following command (under `detox/test`): - -```bash -npm run lint -``` - -We also test our types using the following command: - -```bash -npm run test:types -``` - -### Running the CI Scripts Locally - -On our CI, we test our changes on both iOS and Android, and we lint-check our code. -You can run the same scripts we run on our CI locally, to ensure your changes are passing the tests (under the project's root directory): - -```bash -npm run ci.ios -npm run ci.android -``` - -Refer to the scripts `scripts/ci.ios.sh` and `scripts/ci.android.sh` to see how we run the tests on our CI. - [react-native-cli]: https://www.npmjs.com/package/react-native-cli [Watchman]: https://facebook.github.io/watchman/ [xcpretty]: https://github.com/xcpretty/xcpretty -[test app]: https://github.com/wix/Detox/tree/master/detox/test -[DetoxSync]: https://github.com/wix/DetoxSync -[`detox/test/e2e/detox.config.js`]: https://github.com/wix/Detox/blob/6e87dc13826341dba21ed0a732e5b57efa08e7b5/detox/test/e2e/detox.config.js#L137 -[ESLint]: https://eslint.org/ -[Jest]: https://jestjs.io/ -[`detox/package.json`]: https://github.com/wix/Detox/blob/master/detox/package.json -[`detox/test/package.json`]: https://github.com/wix/Detox/blob/master/detox/test/package.json diff --git a/docs/contributing/code/submitting-pull-requests.md b/docs/contributing/code/submitting-pull-requests.md index a6170a0324..0a525d6ba2 100644 --- a/docs/contributing/code/submitting-pull-requests.md +++ b/docs/contributing/code/submitting-pull-requests.md @@ -15,7 +15,7 @@ Contact us on our [Discord Server] or open an [issue on GitHub]. ## Test Your Changes -Being a testing framework, Detox is a highly self-tested project. Be sure to add/fix test coverage over your work, by running the various test flavors associated with your changes. You can review them, [here](./setting-up-the-dev-environment.md#building-and-testing). +Being a testing framework, Detox is a highly self-tested project. Be sure to add/fix test coverage over your work, by running the various test flavors associated with your changes. You can review them, [here](./building-and-testing). ## Modify Documentation diff --git a/website/sidebars.js b/website/sidebars.js index 8b8fe9edcf..f63dec8f23 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -136,6 +136,7 @@ const sidebars = { items: [ 'contributing/code/overview', 'contributing/code/setting-up-the-dev-environment', + 'contributing/code/building-and-testing', 'contributing/code/example-projects', 'contributing/code/submitting-pull-requests', 'contributing/code/reviewing-pull-requests', From 9766cdf8d6a88049bf3e907d2b28711ac94c1f49 Mon Sep 17 00:00:00 2001 From: Asaf Korem Date: Mon, 20 Nov 2023 12:33:09 +0200 Subject: [PATCH 041/185] docs: make doc-lint happy. --- docs/contributing/code/building-and-testing.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/contributing/code/building-and-testing.md b/docs/contributing/code/building-and-testing.md index 54b1c03628..e7a5ff26b5 100644 --- a/docs/contributing/code/building-and-testing.md +++ b/docs/contributing/code/building-and-testing.md @@ -26,6 +26,7 @@ Under the `detox/` directory, run the following command to run the unit tests wi cd detox npm run unit ``` + The unit tests reside alongside the JavaScript code. Typically, they can also be easily run directly from within an IDE such as [WebStorm](https://www.jetbrains.com/webstorm/) or [vscode](https://code.visualstudio.com/), even in debug (i.e step-by-step execution) mode. ## Unit Tests - Android Native @@ -36,6 +37,7 @@ We also have unit tests for our native code (Android only). They reside alongsid cd detox npm run unit:android-release ``` + The native unit tests can also be run in [Android Studio](https://developer.android.com/studio) (i.e. the IDE for Android apps development). Most tests can be run seamlessly using Android Studio's build-in support for unit-tests, but some require a plugin called [Spek](https://plugins.jetbrains.com/plugin/10915-spek-framework), which can be installed from within Android Studio itself - under the Plugins marketplace. ## iOS: Rebuilding the Framework From 789311f6fdf56e9a0f50f29fe393e7aee71afdb9 Mon Sep 17 00:00:00 2001 From: Asaf Korem Date: Mon, 20 Nov 2023 13:52:47 +0200 Subject: [PATCH 042/185] docs: fix md link. --- docs/contributing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contributing.md b/docs/contributing.md index e2a04faacf..869f6ec2ed 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -14,7 +14,7 @@ All contributions, regardless of size, can help shape Detox. We welcome: - **[Answers]**: Assisting other Detox users by providing guidance or responses to their questions. - **[Bug Reports]**: Help us identify and resolve Detox issues. - **[Feature Suggestions]**: Your ideas for new features can help us improve Detox according to user needs. -- **[Code Submissions]**: Directly contribute by fixing bugs, adding features, or improving our codebase. +- **[Code Contributions]**: Directly contribute by fixing bugs, adding features, or improving our codebase. - **[Code Reviews]**: Help maintain the quality of our codebase by reviewing others' contributions. - **[Documentation Enhancements]**: Improve Detox documentation from user guides to API references. - **Content Creation**: Share your knowledge of Detox through blog posts, tutorials, videos, and more. We're always excited to share your content on our [Twitter account] and [Discord server]. From 976f5c87e6606b20b3218892c7ffe8568bf11e02 Mon Sep 17 00:00:00 2001 From: Asaf Korem <55082339+asafkorem@users.noreply.github.com> Date: Wed, 22 Nov 2023 11:40:12 +0200 Subject: [PATCH 043/185] docs: fix web-views doc table of content. (#4289) --- docs/api/webviews.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/api/webviews.md b/docs/api/webviews.md index 24c5681d57..13f8a95f07 100644 --- a/docs/api/webviews.md +++ b/docs/api/webviews.md @@ -140,7 +140,6 @@ Web view actions are used to interact with elements within a web view: - [`focus()`] - [`moveCursorToEnd()`] - [`runScript()`] -- [`runScriptWithArgs()`] - [`getCurrentUrl()`] - [`getTitle()`] @@ -361,9 +360,7 @@ await expect(web.element(by.web.id('identifier'))).not.toHaveText('Hello World!' [name]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input#name -[`runScript()`]: webviews.md#runscriptscript - -[`runScriptWithArgs()`]: webviews.md#runscriptwithargsscript-args +[`runScript()`]: webviews.md#runscriptscript-args [`getCurrentUrl()`]: webviews.md#getcurrenturl From b2ce733bfafa0c1682a923cf2266782f59c77eef Mon Sep 17 00:00:00 2001 From: Tj Date: Wed, 22 Nov 2023 22:29:19 -0800 Subject: [PATCH 044/185] docs: update Launch Args guide (#4283) --- docs/guide/launch-args.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/launch-args.md b/docs/guide/launch-args.md index 8c2416e8b5..a8e192c907 100644 --- a/docs/guide/launch-args.md +++ b/docs/guide/launch-args.md @@ -41,5 +41,5 @@ Examples: Our official recommendation for getting the arguments inside the app is by integrating the [react-native-launch-arguments](https://github.com/iamolegga/react-native-launch-arguments) project, which provides that seamlessly. For those who are interested, here are the underlying details: -- On iOS, the specified launch arguments are passed as the process launch arguments and available through normal means. +- On iOS, the specified launch arguments are passed as the process launch arguments and available through normal means, such as accesing `[[NSProcessInfo processInfo] arguments]`. - On Android, the launch arguments are set as bundle-extra’s into the activity’s intent. They are therefore accessible on the native side via the current activity as: `currentActivity.getIntent().getBundleExtra("launchArgs")`. From 95308a48c0f8f07135ee4e27c2bfb489a12152bc Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Fri, 24 Nov 2023 19:03:16 +0200 Subject: [PATCH 045/185] chore: bump jest-allure-reporter@2.0.0-alpha.11 (#4291) --- .github/workflows/rapid-test.yml | 16 ++++++++++++++++ detox/package.json | 2 +- detox/test/package.json | 2 +- generation/package.json | 2 +- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rapid-test.yml b/.github/workflows/rapid-test.yml index 500b4b69af..166734f33d 100644 --- a/.github/workflows/rapid-test.yml +++ b/.github/workflows/rapid-test.yml @@ -47,6 +47,13 @@ jobs: - name: Generation tests run: npm test working-directory: generation + - name: Pack Allure results + run: zip -r allure-results.zip detox/allure-results detox/test/allure-results generation/allure-results + - name: Upload Allure results + uses: actions/upload-artifact@v3 + with: + name: allure-results-linux + path: allure-results.zip windows: name: Windows @@ -67,3 +74,12 @@ jobs: - name: Integration tests run: npm run integration working-directory: detox/test + - name: Pack Allure results + run: | + Compress-Archive -Path detox/allure-results -DestinationPath allure-results.zip + shell: pwsh + - name: Upload Allure results + uses: actions/upload-artifact@v3 + with: + name: allure-results-windows + path: allure-results.zip diff --git a/detox/package.json b/detox/package.json index d0e2f915b9..38d3382f9b 100644 --- a/detox/package.json +++ b/detox/package.json @@ -51,7 +51,7 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-unicorn": "^47.0.0", "jest": "^28.1.3", - "jest-allure2-reporter": "2.0.0-alpha.10", + "jest-allure2-reporter": "2.0.0-alpha.11", "mockdate": "^2.0.1", "prettier": "^2.4.1", "react-native": "0.71.10", diff --git a/detox/test/package.json b/detox/test/package.json index 46f5c3a0cd..71a1435b57 100644 --- a/detox/test/package.json +++ b/detox/test/package.json @@ -63,7 +63,7 @@ "express": "^4.15.3", "glob": "^7.2.0", "jest": "^29.2.1", - "jest-allure2-reporter": "2.0.0-alpha.10", + "jest-allure2-reporter": "2.0.0-alpha.11", "jest-junit": "^10.0.0", "lodash": "^4.14.1", "metro-react-native-babel-preset": "0.73.9", diff --git a/generation/package.json b/generation/package.json index c4932adc17..a06f1d9fe4 100644 --- a/generation/package.json +++ b/generation/package.json @@ -30,7 +30,7 @@ }, "devDependencies": { "jest": "^27.0.0", - "jest-allure2-reporter": "2.0.0-alpha.10", + "jest-allure2-reporter": "2.0.0-alpha.11", "lint-staged": "^6.0.0", "prettier": "^1.8.2" }, From a630b87fd750fa68367f001976b863a4db08c801 Mon Sep 17 00:00:00 2001 From: d4vidi Date: Sun, 10 Dec 2023 16:45:05 +0200 Subject: [PATCH 046/185] Re: Supervised android launch (#4293) --- .../com/wix/detox/ActivityLaunchHelper.kt | 76 ++++++++++++ .../src/full/java/com/wix/detox/Detox.java | 78 ++---------- .../src/full/java/com/wix/detox/DetoxMain.kt | 113 +++++++++++------- .../com/wix/detox/LaunchIntentsFactory.kt | 2 +- .../adapters/server/DetoxActionHandlers.kt | 4 - .../adapters/server/DetoxActionsDispatcher.kt | 22 +++- .../adapters/server/DetoxServerAdapter.kt | 2 - .../detox/adapters/server/DetoxServerInfo.kt | 6 +- .../detox/reactnative/ReactMarkersLogger.kt | 44 +++++++ .../detox/reactnative/ReactNativeExtension.kt | 57 ++++----- .../com/wix/detox/espresso/UiControllerSpy.kt | 3 - detox/src/android/espressoapi/Detox.js | 11 -- detox/test/e2e/19.crash-handling.test.js | 8 +- 13 files changed, 248 insertions(+), 178 deletions(-) create mode 100644 detox/android/detox/src/full/java/com/wix/detox/ActivityLaunchHelper.kt create mode 100644 detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactMarkersLogger.kt diff --git a/detox/android/detox/src/full/java/com/wix/detox/ActivityLaunchHelper.kt b/detox/android/detox/src/full/java/com/wix/detox/ActivityLaunchHelper.kt new file mode 100644 index 0000000000..fd03bad184 --- /dev/null +++ b/detox/android/detox/src/full/java/com/wix/detox/ActivityLaunchHelper.kt @@ -0,0 +1,76 @@ +package com.wix.detox + +import android.app.Instrumentation.ActivityMonitor +import android.content.Context +import android.content.Intent +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.ActivityTestRule + +class ActivityLaunchHelper(private val activityTestRule: ActivityTestRule<*>) { + + private val launchArgs = LaunchArgs() + private val intentsFactory = LaunchIntentsFactory() + + fun launchActivityUnderTest() { + val intent = extractInitialIntent() + activityTestRule.launchActivity(intent) + } + + fun launchMainActivity() { + val activity = activityTestRule.activity + launchActivitySync(intentsFactory.activityLaunchIntent(activity)) + } + + fun startActivityFromUrl(url: String) { + launchActivitySync(intentsFactory.intentWithUrl(url, false)) + } + + fun startActivityFromNotification(dataFilePath: String) { + val notificationData = NotificationDataParser(dataFilePath!!).toBundle() + val intent = intentsFactory.intentWithNotificationData(appContext, notificationData, false) + launchActivitySync(intent) + } + + private fun extractInitialIntent(): Intent = + if (launchArgs.hasUrlOverride()) { + intentsFactory.intentWithUrl(launchArgs.urlOverride, true) + } else if (launchArgs.hasNotificationPath()) { + val notificationData = NotificationDataParser(launchArgs.notificationPath).toBundle() + intentsFactory.intentWithNotificationData(appContext, notificationData, true) + } else { + intentsFactory.cleanIntent() + }.also { + it.putExtra(INTENT_LAUNCH_ARGS_KEY, launchArgs.asIntentBundle()) + } + + private fun launchActivitySync(intent: Intent) { + // Ideally, we would just call sActivityTestRule.launchActivity(intent) and get it over with. + // BUT!!! as it turns out, Espresso has an issue where doing this for an activity running in the background + // would have Espresso set up an ActivityMonitor which will spend its time waiting for the activity to load, *without + // ever being released*. It will finally fail after a 45 seconds timeout. + // Without going into full details, it seems that activity test rules were not meant to be used this way. However, + // the all-new ActivityScenario implementation introduced in androidx could probably support this (e.g. by using + // dedicated methods such as moveToState(), which give better control over the lifecycle). + // In any case, this is the core reason for this issue: https://github.com/wix/Detox/issues/1125 + // What it forces us to do, then, is this - + // 1. Launch the activity by "ourselves" from the OS (i.e. using context.startActivity()). + // 2. Set up an activity monitor by ourselves -- such that it would block until the activity is ready. + // ^ Hence the code below. + val activity = activityTestRule.activity + val activityMonitor = ActivityMonitor(activity.javaClass.name, null, true) + activity.startActivity(intent) + + InstrumentationRegistry.getInstrumentation().run { + addMonitor(activityMonitor) + waitForMonitorWithTimeout(activityMonitor, ACTIVITY_LAUNCH_TIMEOUT) + } + } + + private val appContext: Context + get() = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext + + companion object { + private const val INTENT_LAUNCH_ARGS_KEY = "launchArgs" + private const val ACTIVITY_LAUNCH_TIMEOUT = 10000L + } +} diff --git a/detox/android/detox/src/full/java/com/wix/detox/Detox.java b/detox/android/detox/src/full/java/com/wix/detox/Detox.java index 87d211c797..93769a11c4 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/Detox.java +++ b/detox/android/detox/src/full/java/com/wix/detox/Detox.java @@ -1,18 +1,13 @@ package com.wix.detox; -import android.app.Activity; -import android.app.Instrumentation; import android.content.Context; -import android.content.Intent; -import android.os.Bundle; - -import com.wix.detox.config.DetoxConfig; -import com.wix.detox.espresso.UiControllerSpy; import androidx.annotation.NonNull; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.rule.ActivityTestRule; +import com.wix.detox.config.DetoxConfig; + /** *

Static class.

* @@ -67,12 +62,7 @@ *

If not set, then Detox tests are no ops. So it's safe to mix it with other tests.

*/ public final class Detox { - private static final String INTENT_LAUNCH_ARGS_KEY = "launchArgs"; - private static final long ACTIVITY_LAUNCH_TIMEOUT = 10000L; - - private static final LaunchArgs sLaunchArgs = new LaunchArgs(); - private static final LaunchIntentsFactory sIntentsFactory = new LaunchIntentsFactory(); - private static ActivityTestRule sActivityTestRule; + private static ActivityLaunchHelper sActivityLaunchHelper; private Detox() { } @@ -132,72 +122,20 @@ public static void runTests(ActivityTestRule activityTestRule, @NonNull final Co DetoxConfig.CONFIG = detoxConfig; DetoxConfig.CONFIG.apply(); - sActivityTestRule = activityTestRule; - - UiControllerSpy.attachThroughProxy(); - - Intent intent = extractInitialIntent(); - sActivityTestRule.launchActivity(intent); - - try { - DetoxMain.run(context); - } catch (Exception e) { - Thread.currentThread().interrupt(); - throw new RuntimeException("Detox got interrupted prematurely", e); - } + sActivityLaunchHelper = new ActivityLaunchHelper(activityTestRule); + DetoxMain.run(context, sActivityLaunchHelper); } public static void launchMainActivity() { - final Activity activity = sActivityTestRule.getActivity(); - launchActivitySync(sIntentsFactory.activityLaunchIntent(activity)); + sActivityLaunchHelper.launchMainActivity(); } public static void startActivityFromUrl(String url) { - launchActivitySync(sIntentsFactory.intentWithUrl(url, false)); + sActivityLaunchHelper.startActivityFromUrl(url); } public static void startActivityFromNotification(String dataFilePath) { - Bundle notificationData = new NotificationDataParser(dataFilePath).toBundle(); - Intent intent = sIntentsFactory.intentWithNotificationData(getAppContext(), notificationData, false); - launchActivitySync(intent); - } - - private static Intent extractInitialIntent() { - Intent intent; - - if (sLaunchArgs.hasUrlOverride()) { - intent = sIntentsFactory.intentWithUrl(sLaunchArgs.getUrlOverride(), true); - } else if (sLaunchArgs.hasNotificationPath()) { - Bundle notificationData = new NotificationDataParser(sLaunchArgs.getNotificationPath()).toBundle(); - intent = sIntentsFactory.intentWithNotificationData(getAppContext(), notificationData, true); - } else { - intent = sIntentsFactory.cleanIntent(); - } - intent.putExtra(INTENT_LAUNCH_ARGS_KEY, sLaunchArgs.asIntentBundle()); - return intent; - } - - private static void launchActivitySync(Intent intent) { - // Ideally, we would just call sActivityTestRule.launchActivity(intent) and get it over with. - // BUT!!! as it turns out, Espresso has an issue where doing this for an activity running in the background - // would have Espresso set up an ActivityMonitor which will spend its time waiting for the activity to load, *without - // ever being released*. It will finally fail after a 45 seconds timeout. - // Without going into full details, it seems that activity test rules were not meant to be used this way. However, - // the all-new ActivityScenario implementation introduced in androidx could probably support this (e.g. by using - // dedicated methods such as moveToState(), which give better control over the lifecycle). - // In any case, this is the core reason for this issue: https://github.com/wix/Detox/issues/1125 - // What it forces us to do, then, is this - - // 1. Launch the activity by "ourselves" from the OS (i.e. using context.startActivity()). - // 2. Set up an activity monitor by ourselves -- such that it would block until the activity is ready. - // ^ Hence the code below. - - final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); - final Activity activity = sActivityTestRule.getActivity(); - final Instrumentation.ActivityMonitor activityMonitor = new Instrumentation.ActivityMonitor(activity.getClass().getName(), null, true); - - activity.startActivity(intent); - instrumentation.addMonitor(activityMonitor); - instrumentation.waitForMonitorWithTimeout(activityMonitor, ACTIVITY_LAUNCH_TIMEOUT); + sActivityLaunchHelper.startActivityFromNotification(dataFilePath); } private static Context getAppContext() { diff --git a/detox/android/detox/src/full/java/com/wix/detox/DetoxMain.kt b/detox/android/detox/src/full/java/com/wix/detox/DetoxMain.kt index 5e4f0bd88b..fd5200b18c 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/DetoxMain.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/DetoxMain.kt @@ -3,35 +3,62 @@ package com.wix.detox import android.content.Context import android.util.Log import com.wix.detox.adapters.server.* -import com.wix.detox.common.DetoxLog.Companion.LOG_TAG +import com.wix.detox.common.DetoxLog +import com.wix.detox.espresso.UiControllerSpy import com.wix.detox.instruments.DetoxInstrumentsManager import com.wix.detox.reactnative.ReactNativeExtension import com.wix.invoke.MethodInvocation +import java.util.concurrent.CountDownLatch -private const val INIT_ACTION = "_init" -private const val IS_READY_ACTION = "isReady" private const val TERMINATION_ACTION = "_terminate" object DetoxMain { + private val handshakeLock = CountDownLatch(1) + @JvmStatic - fun run(rnHostHolder: Context) { + fun run(rnHostHolder: Context, activityLaunchHelper: ActivityLaunchHelper) { val detoxServerInfo = DetoxServerInfo() - Log.i(LOG_TAG, "Detox server connection details: $detoxServerInfo") - val testEngineFacade = TestEngineFacade() val actionsDispatcher = DetoxActionsDispatcher() - val externalAdapter = DetoxServerAdapter(actionsDispatcher, detoxServerInfo, IS_READY_ACTION, TERMINATION_ACTION) - initActionHandlers(actionsDispatcher, externalAdapter, testEngineFacade, rnHostHolder) - actionsDispatcher.dispatchAction(INIT_ACTION, "", 0) + val serverAdapter = DetoxServerAdapter(actionsDispatcher, detoxServerInfo, TERMINATION_ACTION) + + initCrashHandler(serverAdapter) + initANRListener(serverAdapter) + initEspresso() + initReactNative() + + setupActionHandlers(actionsDispatcher, serverAdapter, testEngineFacade, rnHostHolder) + serverAdapter.connect() + + launchActivityOnCue(rnHostHolder, activityLaunchHelper) actionsDispatcher.join() } - private fun doInit(externalAdapter: DetoxServerAdapter, rnHostHolder: Context) { - externalAdapter.connect() + /** + * Launch the tested activity "on cue", namely, right after a connection is established and the handshake + * completes successfully. + * + * This has to be synchronized so that an `isReady` isn't handled *before* the activity is launched (albeit not fully + * initialized - all native modules and everything) and a react context is available. + * + * As a better alternative, it would make sense to execute this as a simple action from within the actions + * dispatcher (i.e. handler of `loginSuccess`), in which case, no inter-thread locking would be required + * thanks to the usage of Handlers. However, in this type of a solution, errors / crashes would be reported + * not by instrumentation itself, but based on the `AppWillTerminateWithError` message; In it's own, it is a good + * thing, but for a reason we're not sure of yet, it is ignored by the test runner at this point in the flow. + */ + @Synchronized + private fun launchActivityOnCue(rnHostHolder: Context, activityLaunchHelper: ActivityLaunchHelper) { + awaitHandshake() + launchActivity(rnHostHolder, activityLaunchHelper) + } + + private fun awaitHandshake() { + handshakeLock.await() + } - initCrashHandler(externalAdapter) - initANRListener(externalAdapter) - initReactNativeIfNeeded(rnHostHolder) + private fun onLoginSuccess() { + handshakeLock.countDown() } private fun doTeardown(serverAdapter: DetoxServerAdapter, actionsDispatcher: DetoxActionsDispatcher, testEngineFacade: TestEngineFacade) { @@ -41,35 +68,28 @@ object DetoxMain { actionsDispatcher.teardown() } - private fun initActionHandlers(actionsDispatcher: DetoxActionsDispatcher, serverAdapter: DetoxServerAdapter, testEngineFacade: TestEngineFacade, rnHostHolder: Context) { + private fun setupActionHandlers(actionsDispatcher: DetoxActionsDispatcher, serverAdapter: DetoxServerAdapter, testEngineFacade: TestEngineFacade, rnHostHolder: Context) { + class SynchronizedActionHandler(private val actionHandler: DetoxActionHandler): DetoxActionHandler { + override fun handle(params: String, messageId: Long) { + synchronized(this@DetoxMain) { + actionHandler.handle(params, messageId) + } + } + } + // Primary actions with(actionsDispatcher) { - val rnReloadHandler = ReactNativeReloadActionHandler(rnHostHolder, serverAdapter, testEngineFacade) + val readyHandler = SynchronizedActionHandler( ReadyActionHandler(serverAdapter, testEngineFacade) ) + val rnReloadHandler = SynchronizedActionHandler( ReactNativeReloadActionHandler(rnHostHolder, serverAdapter, testEngineFacade) ) - associateActionHandler(INIT_ACTION, object : DetoxActionHandler { - override fun handle(params: String, messageId: Long) = - synchronized(this@DetoxMain) { - this@DetoxMain.doInit(serverAdapter, rnHostHolder) - } - }) - associateActionHandler(IS_READY_ACTION, ReadyActionHandler(serverAdapter, testEngineFacade)) - - associateActionHandler("loginSuccess", ScarceActionHandler()) - associateActionHandler("reactNativeReload", object: DetoxActionHandler { - override fun handle(params: String, messageId: Long) = - synchronized(this@DetoxMain) { - rnReloadHandler.handle(params, messageId) - } - }) + associateActionHandler("loginSuccess", ::onLoginSuccess) + associateActionHandler("isReady", readyHandler) + associateActionHandler("reactNativeReload", rnReloadHandler) associateActionHandler("invoke", InvokeActionHandler(MethodInvocation(), serverAdapter)) associateActionHandler("cleanup", CleanupActionHandler(serverAdapter, testEngineFacade) { dispatchAction(TERMINATION_ACTION, "", 0) }) - associateActionHandler(TERMINATION_ACTION, object: DetoxActionHandler { - override fun handle(params: String, messageId: Long) { - this@DetoxMain.doTeardown(serverAdapter, actionsDispatcher, testEngineFacade) - } - }) + associateActionHandler(TERMINATION_ACTION) { -> doTeardown(serverAdapter, actionsDispatcher, testEngineFacade) } if (DetoxInstrumentsManager.supports()) { val instrumentsManager = DetoxInstrumentsManager(rnHostHolder) @@ -80,13 +100,8 @@ object DetoxMain { // Secondary actions with(actionsDispatcher) { - val queryStatusHandler = QueryStatusActionHandler(serverAdapter, testEngineFacade) - associateActionHandler("currentStatus", object: DetoxActionHandler { - override fun handle(params: String, messageId: Long) = - synchronized(this@DetoxMain) { - queryStatusHandler.handle(params, messageId) - } - }, false) + val queryStatusHandler = SynchronizedActionHandler( QueryStatusActionHandler(serverAdapter, testEngineFacade) ) + associateSecondaryActionHandler("currentStatus", queryStatusHandler) } } @@ -98,7 +113,17 @@ object DetoxMain { DetoxANRHandler(outboundServerAdapter).attach() } - private fun initReactNativeIfNeeded(rnHostHolder: Context) { + private fun initEspresso() { + UiControllerSpy.attachThroughProxy() + } + + private fun initReactNative() { + ReactNativeExtension.initIfNeeded() + } + + private fun launchActivity(rnHostHolder: Context, activityLaunchHelper: ActivityLaunchHelper) { + Log.i(DetoxLog.LOG_TAG, "Launching the tested activity!") + activityLaunchHelper.launchActivityUnderTest() ReactNativeExtension.waitForRNBootstrap(rnHostHolder) } } diff --git a/detox/android/detox/src/full/java/com/wix/detox/LaunchIntentsFactory.kt b/detox/android/detox/src/full/java/com/wix/detox/LaunchIntentsFactory.kt index 43bed23a44..80f5801c23 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/LaunchIntentsFactory.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/LaunchIntentsFactory.kt @@ -6,7 +6,7 @@ import android.content.Intent import android.net.Uri import android.os.Bundle -internal class LaunchIntentsFactory { +class LaunchIntentsFactory { /** * Constructs an intent tightly associated with a specific activity. diff --git a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionHandlers.kt b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionHandlers.kt index ef0416465e..1404f63abf 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionHandlers.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionHandlers.kt @@ -152,7 +152,3 @@ class InstrumentsEventsActionsHandler( outboundServerAdapter.sendMessage("eventDone", emptyMap(), messageId) } } - -class ScarceActionHandler: DetoxActionHandler { - override fun handle(params: String, messageId: Long) {} -} diff --git a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionsDispatcher.kt b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionsDispatcher.kt index ac82d2c3e7..c3d83d50ff 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionsDispatcher.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionsDispatcher.kt @@ -11,11 +11,18 @@ class DetoxActionsDispatcher { private val primaryExec = ActionsExecutor("detox.primary") private val secondaryExec = ActionsExecutor("detox.secondary") - fun associateActionHandler(type: String, actionHandler: DetoxActionHandler, isPrimary: Boolean = true) { - val actionsExecutor = (if (isPrimary) primaryExec else secondaryExec) - actionsExecutor.associateHandler(type, actionHandler) + fun associateActionHandler(type: String, actionHandler: DetoxActionHandler) = + associateActionHandler(type, actionHandler, true) + + fun associateActionHandler(type: String, handlerFunc: () -> Unit) { + associateActionHandler(type, object: DetoxActionHandler { + override fun handle(params: String, messageId: Long) = handlerFunc() + }) } + fun associateSecondaryActionHandler(type: String, actionHandler: DetoxActionHandler) = + associateActionHandler(type, actionHandler, false) + fun dispatchAction(type: String, params: String, messageId: Long) { (primaryExec.executeAction(type, params, messageId) || secondaryExec.executeAction(type, params, messageId)) @@ -33,6 +40,11 @@ class DetoxActionsDispatcher { primaryExec.join() secondaryExec.join() } + + private fun associateActionHandler(type: String, actionHandler: DetoxActionHandler, isPrimary: Boolean = true) { + val actionsExecutor = (if (isPrimary) primaryExec else secondaryExec) + actionsExecutor.associateHandler(type, actionHandler) + } } private class ActionsExecutor(name: String) { @@ -74,7 +86,5 @@ private class ActionsExecutor(name: String) { handler.looper.quit() } - fun join() { - thread.join() - } + fun join() = thread.join() } diff --git a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerAdapter.kt b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerAdapter.kt index 2d8b11fdcf..9873a72bc3 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerAdapter.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerAdapter.kt @@ -10,7 +10,6 @@ interface OutboundServerAdapter { class DetoxServerAdapter( private val actionsDispatcher: DetoxActionsDispatcher, private val detoxServerInfo: DetoxServerInfo, - private val readyActionType: String, private val terminationActionType: String) : WebSocketClient.WSEventsHandler, OutboundServerAdapter { @@ -27,7 +26,6 @@ class DetoxServerAdapter( override fun onConnect() { Log.i(DetoxLog.LOG_TAG, "Connected to server!") - actionsDispatcher.dispatchAction(readyActionType, "", -1000L) } override fun onClosed() { diff --git a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerInfo.kt b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerInfo.kt index f2828de4ea..947c69ea27 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerInfo.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerInfo.kt @@ -1,7 +1,9 @@ package com.wix.detox.adapters.server +import android.util.Log import androidx.test.platform.app.InstrumentationRegistry import com.wix.detox.LaunchArgs +import com.wix.detox.common.DetoxLog private const val DEFAULT_URL = "ws://localhost:8099" @@ -9,7 +11,7 @@ class DetoxServerInfo internal constructor(launchArgs: LaunchArgs = LaunchArgs() val serverUrl: String = launchArgs.detoxServerUrl ?: DEFAULT_URL val sessionId: String = launchArgs.detoxSessionId ?: InstrumentationRegistry.getInstrumentation().targetContext.applicationInfo.packageName - override fun toString(): String { - return "url=$serverUrl, sessionId=$sessionId" + init { + Log.i(DetoxLog.LOG_TAG, "Detox server connection details: url=$serverUrl, sessionId=$sessionId") } } diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactMarkersLogger.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactMarkersLogger.kt new file mode 100644 index 0000000000..bbabce01d3 --- /dev/null +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactMarkersLogger.kt @@ -0,0 +1,44 @@ +package com.wix.detox.reactnative + +import android.util.Log +import com.facebook.react.bridge.ReactMarker +import com.facebook.react.bridge.ReactMarkerConstants +import com.facebook.react.bridge.ReactMarkerConstants.* + +object ReactMarkersLogger : ReactMarker.MarkerListener { + + fun attach() { + ReactMarker.addListener(this) + } + + override fun logMarker(marker: ReactMarkerConstants, p1: String?, p2: Int) { + when { + marker == DOWNLOAD_START || + marker == DOWNLOAD_END || + marker == BUILD_REACT_INSTANCE_MANAGER_START || + marker == BUILD_REACT_INSTANCE_MANAGER_END || + marker == REACT_BRIDGE_LOADING_START || + marker == REACT_BRIDGE_LOADING_END || + marker == REACT_BRIDGELESS_LOADING_START || + marker == REACT_BRIDGELESS_LOADING_END || + marker == CREATE_MODULE_START || + marker == CREATE_MODULE_END || + marker == NATIVE_MODULE_SETUP_START || + marker == NATIVE_MODULE_SETUP_END || + marker == PRE_RUN_JS_BUNDLE_START || + marker == RUN_JS_BUNDLE_START || + marker == RUN_JS_BUNDLE_END || + marker == CONTENT_APPEARED || + marker == CREATE_CATALYST_INSTANCE_START || + marker == CREATE_CATALYST_INSTANCE_END || + marker == DESTROY_CATALYST_INSTANCE_START || + marker == DESTROY_CATALYST_INSTANCE_END || + marker == CREATE_REACT_CONTEXT_START || + marker == CREATE_REACT_CONTEXT_END || + marker == PROCESS_PACKAGES_START || + marker == PROCESS_PACKAGES_END || + false -> + Log.d("Detox.RNMarker", "$marker ($p1)") + } + } +} diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeExtension.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeExtension.kt index 19e4ae2473..1711eea37c 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeExtension.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeExtension.kt @@ -14,6 +14,31 @@ private const val LOG_TAG = "DetoxRNExt" object ReactNativeExtension { private var rnIdlingResources: ReactNativeIdlingResources? = null + fun initIfNeeded() { + if (!ReactNativeInfo.isReactNativeApp()) { + return + } + + ReactMarkersLogger.attach() + } + + /** + * Wait for React-Native to finish loading (i.e. make RN context available). + * + * @param applicationContext The app context, implicitly assumed to be a [ReactApplication] instance. + */ + fun waitForRNBootstrap(applicationContext: Context) { + if (!ReactNativeInfo.isReactNativeApp()) { + return + } + + (applicationContext as ReactApplication).let { + val reactContext = awaitNewReactNativeContext(it, null) + + enableOrDisableSynchronization(reactContext) + } + } + /** * Reloads the React Native context and thus all javascript code. * @@ -40,26 +65,6 @@ object ReactNativeExtension { val reactContext = awaitNewReactNativeContext(it, previousReactContext) enableOrDisableSynchronization(reactContext, networkSyncEnabled) - hackRN50OrHigherWaitForReady() - } - } - - /** - * Wait for React-Native to finish loading (i.e. make RN context available). - * - * @param applicationContext The app context, implicitly assumed to be a [ReactApplication] instance. - */ - @JvmStatic - fun waitForRNBootstrap(applicationContext: Context) { - if (!ReactNativeInfo.isReactNativeApp()) { - return - } - - (applicationContext as ReactApplication).let { - val reactContext = awaitNewReactNativeContext(it, null) - - enableOrDisableSynchronization(reactContext) - hackRN50OrHigherWaitForReady() } } @@ -145,18 +150,6 @@ object ReactNativeExtension { } } - private fun hackRN50OrHigherWaitForReady() { - if (ReactNativeInfo.rnVersion().minor in 50..62) { - try { - //TODO- Temp hack to make Detox usable for RN>=50 till we find a better sync solution. - Thread.sleep(1000) - } catch (e: InterruptedException) { - e.printStackTrace() - } - - } - } - private fun clearIdlingResources() { rnIdlingResources?.unregisterAll() rnIdlingResources = null diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/UiControllerSpy.kt b/detox/android/detox/src/main/java/com/wix/detox/espresso/UiControllerSpy.kt index 742af777f0..051be7931e 100644 --- a/detox/android/detox/src/main/java/com/wix/detox/espresso/UiControllerSpy.kt +++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/UiControllerSpy.kt @@ -11,11 +11,8 @@ class UiControllerSpy: MethodsSpy("uiController") { fun eventInjectionsIterator(): Iterator = historyOf("injectMotionEvent").iterator() companion object { - @JvmStatic val instance = UiControllerSpy() - @JvmStatic - @JvmOverloads fun attachThroughProxy(spy: UiControllerSpy = instance) { val eventsInjectorReflected = EventsInjectorReflected(getUiController()) diff --git a/detox/src/android/espressoapi/Detox.js b/detox/src/android/espressoapi/Detox.js index bbba04e3a2..dfa11192fa 100644 --- a/detox/src/android/espressoapi/Detox.js +++ b/detox/src/android/espressoapi/Detox.js @@ -58,17 +58,6 @@ class Detox { }; } - static extractInitialIntent() { - return { - target: { - type: "Class", - value: "com.wix.detox.Detox" - }, - method: "extractInitialIntent", - args: [] - }; - } - static getAppContext() { return { target: { diff --git a/detox/test/e2e/19.crash-handling.test.js b/detox/test/e2e/19.crash-handling.test.js index 1912538afa..0c1f120215 100644 --- a/detox/test/e2e/19.crash-handling.test.js +++ b/detox/test/e2e/19.crash-handling.test.js @@ -51,12 +51,14 @@ describe('Crash Handling', () => { it(':android: Should throw a detailed error upon app bootstrap crash', async () => { const error = await expectToThrow( () => relaunchAppWithArgs({ detoxAndroidCrashingActivity: true }), - 'Failed to run application on the device'); + 'The app has crashed, see the details below:'); // It's important that the native-error message (containing the native stack-trace) would also // be included in the error's stack property, in order for Jest (specifically) to properly output all // of that into the shell, as we expect it to. - jestExpect(error.stack).toContain('Native stacktrace dump:\njava.lang.IllegalStateException: This is an intentional crash!'); - jestExpect(error.stack).toContain('\tat com.example.CrashingActivity.onResume'); + jestExpect(error.stack).toContain('java.lang.RuntimeException: Unable to resume activity'); + + // In particular, we want the original cause to be bundled in. + jestExpect(error.stack).toContain('Caused by: java.lang.IllegalStateException: This is an intentional crash!'); }, 60000); }); From 4400519a8fdf3adb546766aa869b35f4c407f8e8 Mon Sep 17 00:00:00 2001 From: wixmobile Date: Sun, 10 Dec 2023 08:35:48 -0800 Subject: [PATCH 047/185] Publish 20.14.0 [ci skip] --- detox/package.json | 2 +- detox/test/package.json | 4 ++-- examples/demo-native-android/package.json | 4 ++-- examples/demo-native-ios/package.json | 4 ++-- examples/demo-plugin/package.json | 4 ++-- examples/demo-react-native-detox-instruments/package.json | 4 ++-- examples/demo-react-native/package.json | 4 ++-- generation/package.json | 2 +- lerna.json | 2 +- package.json | 2 +- 10 files changed, 16 insertions(+), 16 deletions(-) diff --git a/detox/package.json b/detox/package.json index 38d3382f9b..f84cf16284 100644 --- a/detox/package.json +++ b/detox/package.json @@ -1,7 +1,7 @@ { "name": "detox", "description": "E2E tests and automation for mobile", - "version": "20.13.5", + "version": "20.14.0", "bin": { "detox": "local-cli/cli.js" }, diff --git a/detox/test/package.json b/detox/test/package.json index 71a1435b57..d0756734c4 100644 --- a/detox/test/package.json +++ b/detox/test/package.json @@ -1,6 +1,6 @@ { "name": "detox-test", - "version": "20.13.5", + "version": "20.14.0", "private": true, "engines": { "node": ">=14.5.0" @@ -56,7 +56,7 @@ "@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/parser": "^5.4.0", "cross-env": "^7.0.3", - "detox": "^20.13.5", + "detox": "^20.14.0", "eslint": "^8.41.0", "eslint-plugin-unicorn": "^47.0.0", "execa": "^5.1.1", diff --git a/examples/demo-native-android/package.json b/examples/demo-native-android/package.json index 581f293e37..f1dc5c3b0c 100644 --- a/examples/demo-native-android/package.json +++ b/examples/demo-native-android/package.json @@ -1,13 +1,13 @@ { "name": "detox-demo-native-android", - "version": "20.13.5", + "version": "20.14.0", "private": true, "scripts": { "packager": "react-native start", "detox-server": "detox run-server" }, "devDependencies": { - "detox": "^20.13.5" + "detox": "^20.14.0" }, "detox": {} } diff --git a/examples/demo-native-ios/package.json b/examples/demo-native-ios/package.json index c2aa0383d4..fc0feb22dc 100644 --- a/examples/demo-native-ios/package.json +++ b/examples/demo-native-ios/package.json @@ -1,9 +1,9 @@ { "name": "detox-demo-native-ios", - "version": "20.13.5", + "version": "20.14.0", "private": true, "devDependencies": { - "detox": "^20.13.5" + "detox": "^20.14.0" }, "detox": { "specs": "", diff --git a/examples/demo-plugin/package.json b/examples/demo-plugin/package.json index 13f004b4eb..2d5c3c7db1 100644 --- a/examples/demo-plugin/package.json +++ b/examples/demo-plugin/package.json @@ -1,12 +1,12 @@ { "name": "demo-plugin", - "version": "20.13.5", + "version": "20.14.0", "private": true, "scripts": { "test:plugin": "detox test --configuration plugin -l verbose" }, "devDependencies": { - "detox": "^20.13.5", + "detox": "^20.14.0", "jest": "^28.0.0" }, "detox": { diff --git a/examples/demo-react-native-detox-instruments/package.json b/examples/demo-react-native-detox-instruments/package.json index a47b77d555..7092dcf79f 100644 --- a/examples/demo-react-native-detox-instruments/package.json +++ b/examples/demo-react-native-detox-instruments/package.json @@ -1,10 +1,10 @@ { "name": "demo-react-native-detox-instruments", - "version": "20.13.5", + "version": "20.14.0", "private": true, "scripts": {}, "devDependencies": { - "detox": "^20.13.5" + "detox": "^20.14.0" }, "detox": { "configurations": { diff --git a/examples/demo-react-native/package.json b/examples/demo-react-native/package.json index 409d1f255d..07a0d7a769 100644 --- a/examples/demo-react-native/package.json +++ b/examples/demo-react-native/package.json @@ -1,6 +1,6 @@ { "name": "example", - "version": "20.13.5", + "version": "20.14.0", "private": true, "scripts": { "start": "react-native start", @@ -32,7 +32,7 @@ "@types/fs-extra": "^9.0.13", "@types/jest": "^29.2.1", "@types/react": "^18.0.24", - "detox": "^20.13.5", + "detox": "^20.14.0", "fs-extra": "^9.1.0", "jest": "^29.2.1", "ts-jest": "^29.0.3", diff --git a/generation/package.json b/generation/package.json index a06f1d9fe4..a082c848ab 100644 --- a/generation/package.json +++ b/generation/package.json @@ -1,6 +1,6 @@ { "name": "generation", - "version": "20.13.2", + "version": "20.14.0", "description": "Generate wrapper code for native dependencies", "main": "index.js", "private": true, diff --git a/lerna.json b/lerna.json index e5ebf3fbb3..1788ea5781 100644 --- a/lerna.json +++ b/lerna.json @@ -13,7 +13,7 @@ "generation", "." ], - "version": "20.13.5", + "version": "20.14.0", "npmClient": "npm", "command": { "publish": { diff --git a/package.json b/package.json index 0c99489c75..4becf915fd 100644 --- a/package.json +++ b/package.json @@ -54,5 +54,5 @@ "shell-utils": "1.x.x", "unified": "^10.1.0" }, - "version": "20.13.5" + "version": "20.14.0" } From 95afa6129a6f776cf9cfa0abfeddf1a675a1ecc3 Mon Sep 17 00:00:00 2001 From: wixmobile Date: Sun, 10 Dec 2023 08:37:03 -0800 Subject: [PATCH 048/185] Publish docs version 20.x --- .../version-20.x/api/webviews.md | 5 +- .../version-20.x/config/testRunner.mdx | 10 + .../version-20.x/contributing.md | 219 +++--------------- .../contributing/code-of-conduct.md | 143 ++++++++++++ .../contributing/code/building-and-testing.md | 138 +++++++++++ .../contributing/code/example-projects.md | 10 + .../contributing/code/overview.md | 50 ++++ .../code/reviewing-pull-requests.md | 42 ++++ .../code/setting-up-the-dev-environment.md | 65 ++++++ .../code/submitting-pull-requests.md | 44 ++++ .../contributing/documentation.md | 37 +++ .../contributing/feature-requests.md | 32 +++ .../questions/answering-questions.md | 26 +++ .../questions/asking-questions.md | 34 +++ .../contributing/reporting-bugs.md | 29 +++ .../version-20.x/guide/launch-args.md | 2 +- .../introduction/environment-setup.md | 83 +++++++ .../introduction/getting-started.mdx | 108 ++++----- .../version-20.x-sidebars.json | 33 ++- 19 files changed, 850 insertions(+), 260 deletions(-) create mode 100644 website/versioned_docs/version-20.x/contributing/code-of-conduct.md create mode 100644 website/versioned_docs/version-20.x/contributing/code/building-and-testing.md create mode 100644 website/versioned_docs/version-20.x/contributing/code/example-projects.md create mode 100644 website/versioned_docs/version-20.x/contributing/code/overview.md create mode 100644 website/versioned_docs/version-20.x/contributing/code/reviewing-pull-requests.md create mode 100644 website/versioned_docs/version-20.x/contributing/code/setting-up-the-dev-environment.md create mode 100644 website/versioned_docs/version-20.x/contributing/code/submitting-pull-requests.md create mode 100644 website/versioned_docs/version-20.x/contributing/documentation.md create mode 100644 website/versioned_docs/version-20.x/contributing/feature-requests.md create mode 100644 website/versioned_docs/version-20.x/contributing/questions/answering-questions.md create mode 100644 website/versioned_docs/version-20.x/contributing/questions/asking-questions.md create mode 100644 website/versioned_docs/version-20.x/contributing/reporting-bugs.md create mode 100644 website/versioned_docs/version-20.x/introduction/environment-setup.md diff --git a/website/versioned_docs/version-20.x/api/webviews.md b/website/versioned_docs/version-20.x/api/webviews.md index 24c5681d57..13f8a95f07 100644 --- a/website/versioned_docs/version-20.x/api/webviews.md +++ b/website/versioned_docs/version-20.x/api/webviews.md @@ -140,7 +140,6 @@ Web view actions are used to interact with elements within a web view: - [`focus()`] - [`moveCursorToEnd()`] - [`runScript()`] -- [`runScriptWithArgs()`] - [`getCurrentUrl()`] - [`getTitle()`] @@ -361,9 +360,7 @@ await expect(web.element(by.web.id('identifier'))).not.toHaveText('Hello World!' [name]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input#name -[`runScript()`]: webviews.md#runscriptscript - -[`runScriptWithArgs()`]: webviews.md#runscriptwithargsscript-args +[`runScript()`]: webviews.md#runscriptscript-args [`getCurrentUrl()`]: webviews.md#getcurrenturl diff --git a/website/versioned_docs/version-20.x/config/testRunner.mdx b/website/versioned_docs/version-20.x/config/testRunner.mdx index ca7638d5f4..524f5d2c1d 100644 --- a/website/versioned_docs/version-20.x/config/testRunner.mdx +++ b/website/versioned_docs/version-20.x/config/testRunner.mdx @@ -142,6 +142,16 @@ Default: `false`. When true, tells `detox test` to cancel next retrying if it gets at least one report about a [permanent test suite failure](../api/internals.mdx#reporting-test-results). Has no effect, if [`testRunner.retries`] is undefined or set to zero. +### `testRunner.detached` \[boolean] + +Default: `false`. + +When true, tells `detox test` to spawn the test runner in a detached mode. + +This is useful in CI environments, where you want to intercept SIGINT and SIGTERM signals to gracefully shut down the test runner and the device. + +Instead of passing the kill signal to the child process (the test runner), Detox will send an emergency shutdown request to all the workers, and then it will wait for them to finish. + ### `testRunner.forwardEnv` \[boolean] Default: `false`. diff --git a/website/versioned_docs/version-20.x/contributing.md b/website/versioned_docs/version-20.x/contributing.md index 9014072c5e..869f6ec2ed 100644 --- a/website/versioned_docs/version-20.x/contributing.md +++ b/website/versioned_docs/version-20.x/contributing.md @@ -1,201 +1,48 @@ -# Contributing +# Contributing to Detox - +Detox, an open-source project, greatly values community involvement. Whether you're a mobile developer, QA specialist, or an open source enthusiast, your contribution could significantly enhance the reliability, user experience, and development process of mobile applications. -This guide is about contributing to our codebase. +## Our Code of Conduct -We don’t have any special guidelines - just some setup walk-through! +To ensure an inclusive, safe environment for all contributors, we adhere to the [Contributor Covenant code of conduct]. This well-recognized code provides clear guidelines for respectful behavior. -### Environment +## Ways to Contribute -First, complete our [Getting Started](introduction/getting-started.mdx) guide. +All contributions, regardless of size, can help shape Detox. We welcome: -#### Install the monorepo management tool, `lerna` +- **[Questions]**: Your inquiries can help identify gaps in our documentation and benefit all users. +- **[Answers]**: Assisting other Detox users by providing guidance or responses to their questions. +- **[Bug Reports]**: Help us identify and resolve Detox issues. +- **[Feature Suggestions]**: Your ideas for new features can help us improve Detox according to user needs. +- **[Code Contributions]**: Directly contribute by fixing bugs, adding features, or improving our codebase. +- **[Code Reviews]**: Help maintain the quality of our codebase by reviewing others' contributions. +- **[Documentation Enhancements]**: Improve Detox documentation from user guides to API references. +- **Content Creation**: Share your knowledge of Detox through blog posts, tutorials, videos, and more. We're always excited to share your content on our [Twitter account] and [Discord server]. -```bash npm2yarn -npm install lerna@3.x.x --global -``` +Your active participation helps build a vibrant community dedicated to improving mobile development. -For all the internal projects (detox, detox-cli, demos, test) `lerna` will create symbolic links in `node_modules` instead of `npm` copying the content of the projects. This way, any change you do on any code is there immediately. There is no need to update node modules or copy files between projects. +## Engaging in the Community -#### Install common React Native dependencies +Join our [Discord server] to discuss Detox, ask questions, and provide help. Follow our [Twitter account] for updates on the project. -React-Native CLI: +Becoming an active community member is a great way to start contributing to Detox. It provides familiarity with the project, helps identify opportunities to contribute, and allows guidance from other contributors. The more you engage, the more recognition your contributions receive and the greater impact you can make on the project's direction. -```bash npm2yarn -npm install react-native-cli --global -``` +## Appreciation and Recognition -Watchman: +All our contributors deserve recognition. We express our gratitude by featuring them in our [Release Notes]. -```bash -brew install watchman -``` +### Core Contributors Program -#### xcpretty +We plan to launch a "Core Contributor" program for individuals who have significantly contributed to Detox and shown deep understanding of the project while being active in the community. Core contributors will be invited to a private Discord channel for exclusive discussions and will receive a distinguished role on our Discord server. -You must also have `xcpretty` installed: - -```bash -gem install xcpretty -``` - -### Detox - -#### Clone Detox and Submodules - -```bash -git clone git@github.com:wix/Detox.git -cd detox -git submodule update --init --recursive -``` - -(this makes sure all git submodule dependencies have been properly checked out) - -#### Installing and Linking Internal Projects - -```bash -lerna bootstrap -``` - -#### Building and Testing - -##### Automatically - -`scripts/ci.ios.sh` and `scripts/ci.android.sh` are the scripts Detox runs in CI, they will run `lerna bootstrap`, unit tests, and E2E tests. Make sure these scripts pass before submitting a PR, this is exactly what Detox is going to run in CI. - -##### Manually - -The following steps can be run manually in order to build / test the various components of Detox. - -###### 1. Unit Tests and Lint - -```bash -lerna run test -``` - -Detox JS code is 100% test covered and is set to break the build if coverage gets below, so make sure you run unit tests (`lerna run test`) locally before pushing. - -Alternatively, to run only the JS tests, run the following from the `detox/detox` directory: - -```bash npm2yarn -npm run unit -``` - -or - -```bash npm2yarn -npm run unit:watch -``` - -After running the tests, _Jest_ will create a coverage report you can examine: - -```bash -cd detox -open coverage/lcov-report/index.html -``` - -###### 2. Running Detox E2E Coverage Tests - -Detox has a suite of end-to-end tests to test its own API while developing (and for regression); We maintain a special application that is "tested" against Detox’s API, but essentially, it’s the API that is tested, not the app. - -To run the tests, you must first build the native code and then run based on your target of choice (Android / iOS): - -- **iOS:** - - ```bash npm2yarn - cd detox/test - npm run build:ios - npm run e2e:ios - ``` - -- **Android:** - - ```bash npm2yarn - cd detox/test - npm run build:android - npm run e2e:android - ``` - -FYI Android test project includes two flavors: - -- `fromBin` - (**standard use case**) utilizes the precompiled `.aar` from `node_modules` just like a standard RN project. -- `fromSource` - compiles the project with RN sources from `node_modules`, this is useful when developing and debugging Espresso idle resource. - [Here](https://github.com/facebook/react-native/wiki/Building-from-source#android) are the prerequisites to compiling React Native from source. - -Each build can be triggered separately by running its Gradle assembling task (under `detox/test/android/`): - -```bash -./gradlew assembleFromSourceDebug -``` - -or: - -```bash -./gradlew assembleFromBinDebug -``` - -To run from Android Studio, React Native’s `react.gradle` script may require `node` to be in path. -On MacOS, environment variables can be exported to desktop applications by adding the following to your `.bashrc`/`.zshrc`: - -```bash -launchctl setenv PATH $PATH -``` - -###### 3. Android Native Unit-Tests - -Under `detox/`: - -```bash -npm run unit:android-release -``` - -:::caution Note -Due to limitations with Robolectric, this currently does not work with JDK 17. Best to use Java 11, as explained in the [Android environment setup guide](./guide/android-dev-env.md). -::: - -### Detox - Example Projects - -This is in fact a monorepo that also sports some example projects (for usage reference), alongside the main test project: - -- `examples/demo-react-native-jest`: Demonstrate usage of Detox in a React Native app project. -- `examples/demo-native-ios`: Demonstrates usage of Detox in a pure-native iOS app. -- `examples/demo-native-android` (broken): Demonstrates usage of Detox in a pure-native Android app. -- `examples/demo-pure-native-android`: Demonstrates usage of the _pure_ [Detox-Native](https://github.com/wix/Detox/tree/master/detox-native/README.md) project -- more... - -**In order to run E2E tests associated with any of these projects, refer to the [project-specific](https://github.com/wix/Detox/tree/master/examples) READMEs.** - -### Detox Documentation Website - -The [documentation website](https://wix.github.io/Detox) is built using [Docusaurus](https://docusaurus.io/). - -To run the website locally, run the following commands: - -```bash npm2yarn -cd website -npm install -npm start -``` - -#### Updating the Website - -To update a specific page, edit the corresponding markdown file in `docs/`. To add a new page, create a new markdown file in `docs/` and add a link to it in `website/sidebars.json`. - -##### Website Deployment - -While changes to the website are published automatically on every commit to `master` under the `Next` version, tagging and locking docs to a specific version is done automatically on every Detox release. - -In case you want to update the docs for a specific version, you can change the related files and code under `website/versioned_docs/version-/` and `website/versioned_sidebars/version--sidebars.json`. - -##### Update Old Versions - -To update a specific version with the latest changes: - -1. Remove the version from `versions.json`. -1. Run `npm run docusaurus docs:version `. - -##### Inspect Documentation Style Changes - -This [demo page](demo.mdx) serves as a visual representation of the styling and formatting of our documentation. -If you are changing the stylesheets, check with it to prevent undesirable visual regressions. +[Contributor Covenant code of conduct]: contributing/code-of-conduct.md +[Questions]: contributing/questions/asking-questions.md +[Answers]: contributing/questions/answering-questions.md +[Bug Reports]: contributing/reporting-bugs.md +[Feature Suggestions]: contributing/feature-requests.md +[Code Contributions]: contributing/code/overview.md +[Code Reviews]: contributing/code/reviewing-pull-requests.md +[Documentation Enhancements]: contributing/documentation.md +[Discord server]: https://discord.gg/CkD5QKheF5 +[Twitter account]: https://twitter.com/detoxe2e +[Release Notes]: https://github.com/wix/Detox/releases diff --git a/website/versioned_docs/version-20.x/contributing/code-of-conduct.md b/website/versioned_docs/version-20.x/contributing/code-of-conduct.md new file mode 100644 index 0000000000..060a64209a --- /dev/null +++ b/website/versioned_docs/version-20.x/contributing/code-of-conduct.md @@ -0,0 +1,143 @@ +# Code of Conduct + +:::note Attribution + +This Code of Conduct is adapted from the [Contributor Covenant], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +::: + +Our community is dedicated to providing a safe and inclusive environment for all contributors. +To ensure this, we have adopted the **[Contributor Covenant]** code of conduct. +This widely recognized and respected code of conduct sets clear guidelines for behavior, and it's easy for everyone to understand and follow. + +By participating in the Detox community, you agree to abide by the terms of this code of conduct. +We take violations of this code of conduct seriously, and we will not tolerate any form of harassment or discrimination. + +If you have any questions or concerns, please reach out to the project maintainers. + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of + any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, + without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at [our Discord server]. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +[Contributor Covenant]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations +[our Discord server]: https://discord.gg/CkD5QKheF5 diff --git a/website/versioned_docs/version-20.x/contributing/code/building-and-testing.md b/website/versioned_docs/version-20.x/contributing/code/building-and-testing.md new file mode 100644 index 0000000000..e7a5ff26b5 --- /dev/null +++ b/website/versioned_docs/version-20.x/contributing/code/building-and-testing.md @@ -0,0 +1,138 @@ +# Building and Testing + +Our JavaScript code is thoroughly verified with comprehensive unit tests, complemented by integration tests. +Additionally, our native code undergoes rigorous testing through both unit and integration tests. +To ensure complete functionality, we conduct end-to-end tests on a fully-featured React Native application, designed to encompass all our public APIs (refer to our [test app] for details). + +:::note Important + +Before submitting a pull request, please ensure at a minimum that your code adheres to our linting standards and that all unit tests run successfully without any errors. + +::: + +The following sections describe how to build and test our code, with a general instruction. +However, we recommend you to refer to our `package.json` files for a complete list of available scripts: + +- Detox Framework: [`detox/package.json`] +- Detox Test App: [`detox/test/package.json`] + +## Unit Tests (JavaScript) + +We use [Jest] for running our unit tests. + +Under the `detox/` directory, run the following command to run the unit tests with coverage: + +```bash +cd detox +npm run unit +``` + +The unit tests reside alongside the JavaScript code. Typically, they can also be easily run directly from within an IDE such as [WebStorm](https://www.jetbrains.com/webstorm/) or [vscode](https://code.visualstudio.com/), even in debug (i.e step-by-step execution) mode. + +## Unit Tests - Android Native + +We also have unit tests for our native code (Android only). They reside alongside Detox's native Android code, under a dedicated subdirectory called `testFull`. You can run them using the following commands: + +```bash +cd detox +npm run unit:android-release +``` + +The native unit tests can also be run in [Android Studio](https://developer.android.com/studio) (i.e. the IDE for Android apps development). Most tests can be run seamlessly using Android Studio's build-in support for unit-tests, but some require a plugin called [Spek](https://plugins.jetbrains.com/plugin/10915-spek-framework), which can be installed from within Android Studio itself - under the Plugins marketplace. + +## iOS: Rebuilding the Framework + +After changing the native code of Detox iOS, you need to rebuild the Detox framework. This is done when running: + +```bash +detox build-framework-cache +``` + +Note that it is only required when you change the native code of Detox, or one of its dependencies (e.g. [DetoxSync]). +If you are only changing the JavaScript code, you don't need to rebuild the framework. + +## Building the Detox Test App + +The Detox self-test app is a full-feature React Native app for testing Detox end-to-end. + +Before running the tests, you must first build the Detox test app. To do so, under `detox/test`, use the `build` command of the Detox CLI. +List of available configurations can be found under [`detox/test/e2e/detox.config.js`]. + +For example, to build the Detox test app for iOS in release mode, run: + +```bash +cd detox/test +detox build --configuration ios.sim.release + +### End-to-End Tests + +To run the end-to-end tests (after building the app), use the `test` command of the Detox CLI. + +For example, to run the end-to-end tests for iOS in release mode, run: + +```bash +cd detox/test +detox test --configuration ios.sim.release +``` + +To run in Android release mode, run: + +```bash +cd detox/test +detox test --configuration android.emu.release +``` + +:::note + +Mind that on Android, Detox expects a specific AVD with a [specific name](https://github.com/wix/Detox/blob/24f68c3bd7675ed6f508e3f5fea97509eb7991b2/detox/test/e2e/detox.config.js#L111) to be available on your computer. +Be sure to [install such an AVD](https://developer.android.com/studio/run/managing-avds#createavd), beforehand. + +::: + +## Integration Tests + +Besides unit tests and end-to-end tests, we have some middle ground integration tests, which typically run Detox in a custom headless-like mode (i.e. stubs replace devices). Those tests typically execute Detox from a command line in that mode, and then do some post-processing of the results. We use [Jest] for running our integration tests. + +The integration reside under a dedicated directory alongside Detox's the self-test app's code. + +Run the following command to run the integration tests: + +```bash +cd detox/test +npm run integration + +### Linting + +We use [ESLint] for linting our JavaScript code. + +You can run the linter locally using the following command (under `detox/test`): + +```bash +npm run lint +``` + +We also test our types using the following command: + +```bash +npm run test:types +``` + +## Running the CI Scripts Locally + +On our CI, we test our changes on both iOS and Android, and we lint-check our code. +You can run the same scripts we run on our CI locally, to ensure your changes are passing the tests (under the project's root directory): + +```bash +npm run ci.ios +npm run ci.android +``` + +Refer to the scripts `scripts/ci.ios.sh` and `scripts/ci.android.sh` to see how we run the tests on our CI. + +[test app]: https://github.com/wix/Detox/tree/master/detox/test +[DetoxSync]: https://github.com/wix/DetoxSync +[`detox/test/e2e/detox.config.js`]: https://github.com/wix/Detox/blob/6e87dc13826341dba21ed0a732e5b57efa08e7b5/detox/test/e2e/detox.config.js#L137 +[ESLint]: https://eslint.org/ +[Jest]: https://jestjs.io/ +[`detox/package.json`]: https://github.com/wix/Detox/blob/master/detox/package.json +[`detox/test/package.json`]: https://github.com/wix/Detox/blob/master/detox/test/package.json diff --git a/website/versioned_docs/version-20.x/contributing/code/example-projects.md b/website/versioned_docs/version-20.x/contributing/code/example-projects.md new file mode 100644 index 0000000000..a023c6ee0a --- /dev/null +++ b/website/versioned_docs/version-20.x/contributing/code/example-projects.md @@ -0,0 +1,10 @@ +# Example Projects + +Explore various example projects hosted in this monorepo to understand real-world usage and testing with Detox. + +- `examples/demo-react-native-jest`: Demonstrates Detox usage in a React Native app project. +- `examples/demo-native-ios`: Illustrates Detox usage in a native (non-React Native) iOS app. +- `examples/demo-native-android` (broken): Illustrates Detox usage in a native (non-React Native) Android app. +- `examples/demo-pure-native-android`: Illustrates the subsidiary [Detox-Native](https://github.com/wix/Detox/blob/master/detox/detox-native/README.md) project. + +To run end-to-end tests associated with these projects, refer to the [project-specific](https://github.com/wix/Detox/tree/master/examples) READMEs. diff --git a/website/versioned_docs/version-20.x/contributing/code/overview.md b/website/versioned_docs/version-20.x/contributing/code/overview.md new file mode 100644 index 0000000000..62e9510940 --- /dev/null +++ b/website/versioned_docs/version-20.x/contributing/code/overview.md @@ -0,0 +1,50 @@ +--- +sidebar_label: Overview +--- + +# Code Changes Overview + +Welcome to the code changes section! As a contributor, it's essential to understand the project's goals and adhere to its code of conduct. Before contributing, please review any existing issues related to your work, ensure your code is well-documented, and has adequate test coverage. It's also important that your code is compatible with the project's supported platforms and their versions. + +Our collaborative workflow is simple: + +1. **Identify an Issue:** If not exists already, create an issue for new features or bug reports, outlining your proposal or the identified problem. +2. **Propose a Solution:** Open a pull request with a proposed solution to the issue. On complex issues, it's recommended to discuss your approach with the community and maintainers before submitting a PR. +3. **Engage in Review:** A collaborator will review your pull request. Reviews from other contributors are also encouraged. +4. **Merge and Release:** After the review, a collaborator will merge your contribution, typically releasing it in the next version of the project. + +We use [GitHub] for managing pull requests, conducting code reviews, and tracking issues. + +The code review process is central to our collaboration. Every contribution must go through a review before merging to maintain the quality of our codebase. As a contributor, being willing to discuss your work, respond to feedback, and work with the community is key to improving the project and creating a positive environment for all contributors. + +## Repository Structure + +Our GitHub repository is a monorepo, which means it contains multiple Detox-related projects and packages. + +The main package is the Detox framework, which is the core of the project. +It contains the native code for iOS and Android, as well as the JavaScript code. +The other projects are the Detox CLI, the Detox test app, example apps, and the Detox documentation website. + +Here's a high-level overview of the repository structure: + +- 📁 **detox-cli** - The CLI for Detox (e.g., `detox init`, `detox test`, read more about our [CLI docs]) +- 📁 **detox** - The Detox framework + - 📁 **android** - The Android native code, alongside native unit tests + - 📁 **ios** - The iOS native code, including its native submodules (e.g., DetoxSync) + - 📁 **test** - The Detox self-test app: A full-feature React Native app for end-to-end testing Detox itself + - 📁 **src** - The app's JavaScript code + - 📁 **e2e** - The Detox self-tests + - 📁 **integration** - Detox integration self-tests + - 📁 **local-cli** - Local CLI commands for Detox development (e.g., `detox rebuild-framework-cache`, which rebuilds the iOS framework) + - 📁 **runners** - The Detox runners, which are used to run the tests + - 📁 **scripts** - Scripts for building the framework for publishing + - 📁 **src** - The JavaScript source code of Detox. The include bundled JavaScript unit tests +- 📁 **website** - The documentation website of Detox (read more about our [documentation site docs]) +- 📁 **docs** - The documentation of Detox, written in Markdown and published on the website +- 📁 **examples** - Example apps for Detox (for more information, check the [list of example projects]) +- 📁 **scripts** - Scripts for building and testing Detox + +[GitHub]: https://github.com/wix/Detox +[documentation site docs]: ../documentation.md +[list of example projects]: ./example-projects.md +[CLI docs]: ../../cli/overview.md diff --git a/website/versioned_docs/version-20.x/contributing/code/reviewing-pull-requests.md b/website/versioned_docs/version-20.x/contributing/code/reviewing-pull-requests.md new file mode 100644 index 0000000000..bebaf0cad4 --- /dev/null +++ b/website/versioned_docs/version-20.x/contributing/code/reviewing-pull-requests.md @@ -0,0 +1,42 @@ +# Review a Pull Request + +Reviewing a PR is an important part of the contribution process. It ensures the quality of the codebase and provides an opportunity for the community to collaborate and learn from each other. + +:::note + +Reviewers should refer to the [Submitting Pull Requests] page to understand the guidelines the contributor followed when creating the PR. This will provide context and ensure a thorough review process. + +::: + +## Understand the Changes + +- Ensure alignment with the project’s coding standards and overall architecture. +- Read the provided description and any linked issues for context. + +## Provide Constructive Feedback + +Community members can provide valuable feedback on PRs, especially if they have experience with the feature or area of the codebase. + +- Be respectful and courteous. +- Offer code snippet suggestions if something could be done better. +- Request changes if necessary and provide clear, constructive feedback. + +## Review the Code + +Experienced developers are encouraged to review PRs and provide feedback. This is a great way to learn more about the project and collaborate with the community. + +You can review the code by following these requirements: + +- Alignment with coding standards and architectural guidelines. +- Adequate test coverage (unit, integration, and e2e). +- Ensure the documentation reflects the changes in the PR, if necessary. Refer to the [Documentation Changes] page for guidelines. +- Meaningful and semantically correct commit messages. + +## Merging Pull Requests + +Every PR requires approval from a maintainer before it can be merged. + +However, we encourage the community to review and provide feedback on PRs. Helping with the review process not only improves the quality of the code changes, but also helps the maintainers by reducing the workload. + +[Documentation Changes]: ../documentation.md +[Submitting Pull Requests]: ./submitting-pull-requests.md diff --git a/website/versioned_docs/version-20.x/contributing/code/setting-up-the-dev-environment.md b/website/versioned_docs/version-20.x/contributing/code/setting-up-the-dev-environment.md new file mode 100644 index 0000000000..6db7592904 --- /dev/null +++ b/website/versioned_docs/version-20.x/contributing/code/setting-up-the-dev-environment.md @@ -0,0 +1,65 @@ +# Setting up the Development Environment + +This document guides you through setting up your development environment to start contributing to our codebase. + +:::important Prerequisites + +Please complete our [Introductory environment setup](introduction/environment-setup.md) guide before proceeding. This ensures you have the necessary tools and dependencies installed in order to _run_ Detox tests, which is a fundamental step before being able to contribute to the project itself. + +::: + +## Setting Up The Monorepo Management + +Our repository is a monorepo, which means it contains multiple Detox-related projects and packages. [Read more about our repository structure](../code/overview.md#repository-structure). + +To set up the monorepo locally, follow these steps: + +Install the monorepo management tool, `lerna`: + +```bash npm2yarn +npm install lerna@3.x.x --global +``` + +Clone the repository and navigate to the project directory: + +```bash +git clone git@github.com:wix/Detox.git +cd detox +git submodule update --init --recursive +``` + +From the project's root directory, install and link the internal projects: + +```bash +lerna bootstrap +``` + +## Installing Common Dependencies + +### React-Native CLI + +[react-native-cli] is a command line interface for React Native. + +```bash npm2yarn +npm install react-native-cli --global +``` + +### Watchman + +[Watchman] is a tool by Facebook for watching changes in the filesystem. + +```bash +brew install watchman +``` + +### xcpretty + +[xcpretty] is a fast and flexible formatter for xcodebuild. + +```bash +gem install xcpretty +``` + +[react-native-cli]: https://www.npmjs.com/package/react-native-cli +[Watchman]: https://facebook.github.io/watchman/ +[xcpretty]: https://github.com/xcpretty/xcpretty diff --git a/website/versioned_docs/version-20.x/contributing/code/submitting-pull-requests.md b/website/versioned_docs/version-20.x/contributing/code/submitting-pull-requests.md new file mode 100644 index 0000000000..0a525d6ba2 --- /dev/null +++ b/website/versioned_docs/version-20.x/contributing/code/submitting-pull-requests.md @@ -0,0 +1,44 @@ +# Submitting Pull Requests + +:::note + +Before creating a PR, it's recommended to consult with the Detox collaborators. Request a design review or assistance with planning the tests to ensure alignment with project goals. + +Contact us on our [Discord Server] or open an [issue on GitHub]. + +::: + +## Fork the Repository + +- Fork the repository to your own GitHub account. +- Create a new branch from the `master` branch for your work. + +## Test Your Changes + +Being a testing framework, Detox is a highly self-tested project. Be sure to add/fix test coverage over your work, by running the various test flavors associated with your changes. You can review them, [here](./building-and-testing). + +## Modify Documentation + +If your changes affect the public API, update the documentation accordingly to reflect your changes. +Refer to the [Documentation Changes] page for guidelines. + +## Commit Message Guidelines + +Write descriptive, meaningful commit messages that follow the **Conventional Commits** format, specifying the type of change, the scope, and a concise description. For example: + +```plaintext +fix(ios): resolve crash on scrolling in iOS 17.0 +feat(android): add new API for setting the device locale +test: update unit tests for new utility function +``` + +More details on good commit messages can be found [here](https://www.conventionalcommits.org/en/v1.0.0/). + +## Open the Pull Request + +- Fill in the provided PR template fields on GitHub. +- Provide a clear description of your changes and any necessary context. + +[Documentation Changes]: ../documentation.md +[Discord Server]: https://discord.gg/CkD5QKheF5 +[issue on GitHub]: https://github.com/wix/Detox/issues/new/choose diff --git a/website/versioned_docs/version-20.x/contributing/documentation.md b/website/versioned_docs/version-20.x/contributing/documentation.md new file mode 100644 index 0000000000..2298660b6a --- /dev/null +++ b/website/versioned_docs/version-20.x/contributing/documentation.md @@ -0,0 +1,37 @@ +# Documentation Site + +Contributions towards enhancing our documentation are highly valued in the Detox community. +Clear, concise, and comprehensive documentation enables users to understand and utilize the project more effectively. + +We appreciate all contributions, from correcting typos to creating new documentation pages. + +## Getting Started + +Our [documentation website](https://wix.github.io/Detox) is developed using [Docusaurus](https://docusaurus.io/). To set up a local version of the website for editing, follow these steps: + +```bash npm2yarn +cd website +npm install +npm start +``` + +## Making Updates + +To modify an existing page, find the associated markdown file in the `docs/` directory and make the required changes. To introduce a new page, create a markdown file in `docs/` and link to it in `website/sidebars.json`. + +## Deploying Changes + +Automatic updates to the website occur with each commit to the `master` branch, with these changes reflected under the `Next` version. The process of tagging and locking documentation to a specific version is automated and coincides with each Detox release. + +If there's a need to update the documentation of a particular version, modify the associated files and code located under `website/versioned_docs/version-/` and `website/versioned_sidebars/version--sidebars.json`. + +## Modifying Old Versions + +To update an older version with the latest changes: + +1. Remove the desired version from `versions.json`. +1. Execute `npm run docusaurus docs:version `. + +## Reviewing Style Modifications + +If you're making changes to the stylesheets, use this [demo page](../demo.mdx) to visually inspect the impact of your changes and avoid unintentional visual regressions. diff --git a/website/versioned_docs/version-20.x/contributing/feature-requests.md b/website/versioned_docs/version-20.x/contributing/feature-requests.md new file mode 100644 index 0000000000..5f18010b4f --- /dev/null +++ b/website/versioned_docs/version-20.x/contributing/feature-requests.md @@ -0,0 +1,32 @@ +# Feedback and Suggestions + +Open dialogue and feedback, particularly feature requests, play an invaluable role in open-source communities like Detox. They provide insights into user preferences and requirements, aiding in the refinement and enhancement of the project. + +Detox project provides various platforms for feedback: [GitHub Issues], [Twitter], and [Discord]. + +Keep in mind that feedback is varied, including feature suggestions, improvements, identification of missing functionality, and support requests. Each type of feedback has a preferred method of submission. + +## Giving Feedback + +Follow these tips to deliver clear, actionable feedback: + +- **Constructive Criticism:** Frame your feedback in a positive light, understanding the perspective of the project maintainers. +- **Comprehensive Details:** Include as much relevant information as possible to help maintainers understand the context and use case. +- **Clarity and Respect:** Make your feedback specific, clear, and respectful, ensuring it is actionable for the maintainers. +- **Guidelines:** Adhere to the below suggestions for each type of feedback. + +## Suggesting Features + +When suggesting a new feature, remember to provide thorough information, such as examples, screenshots, and code snippets, to communicate your idea effectively. + +Just as in general feedback, make sure your feature request is: + +- **Specific:** Clearly describe the feature you wish to see implemented. +- **Detailed:** Offer as much context as possible, including use cases, examples, and why this addition would benefit the community. +- **Respectful:** Remember, the project maintainers or contributors are not obligated to implement your request. Be polite and respectful in your request. + +By making your feedback as clear and detailed as possible, you'll greatly assist the maintainers in understanding and potentially implementing your suggestion. + +[GitHub Issues]: https://github.com/wix/Detox/issues +[Twitter]: https://twitter.com/detoxe2e +[Discord]: https://discord.gg/CkD5QKheF5 diff --git a/website/versioned_docs/version-20.x/contributing/questions/answering-questions.md b/website/versioned_docs/version-20.x/contributing/questions/answering-questions.md new file mode 100644 index 0000000000..be4c7108ba --- /dev/null +++ b/website/versioned_docs/version-20.x/contributing/questions/answering-questions.md @@ -0,0 +1,26 @@ +# Answering Questions + +Contributing answers is an invaluable part of supporting the Detox community. By sharing your knowledge and experience, you help others learn and grow, and you also contribute to enhancing Detox for the entire community. + +## Different Ways to Answer + +Answering a question is not limited to providing a solution in the comments. +Sometimes, you may not have a solution, but you can still contribute by: + +- **Guide towards a solution**: If unsure about an answer, provide guidance on where to look, including relevant documentation or resources. +- **Request more information**: Ask for more detail if required to better address the question. For example, you may ask for code snippets, screenshots, or reproducible examples. +- **Refer to existing answers**: Link an already answered question if applicable. + +## Guidelines for Writing Clear Answers + +When answering questions, follow these guidelines to ensure your answers are clear and actionable: + +- **Be specific**: Try to pinpoint what exact information you need to effectively answer the question. General requests such as "Please share more details" are not very helpful. +- **Maintain respect**: Ensure your communication is always polite and contextual. When asking for more information, it's better to explain why it's needed so that the person asking understands the relevance. +- **Completeness**: Make sure to provide a detailed answer that includes all relevant information, such as code snippets, screenshots, and examples. This practice helps others understand your answer and apply it to their use case. + +:::tip In a Nutshell + +When you're answering a question, remember to be specific, clear, and respectful. By providing detailed and well-thought out response, you will play an important role in contributing to and enhancing the knowledge of our community. + +::: diff --git a/website/versioned_docs/version-20.x/contributing/questions/asking-questions.md b/website/versioned_docs/version-20.x/contributing/questions/asking-questions.md new file mode 100644 index 0000000000..24dd749f58 --- /dev/null +++ b/website/versioned_docs/version-20.x/contributing/questions/asking-questions.md @@ -0,0 +1,34 @@ +# Asking Questions + +Posting queries on public forums increases visibility and chances of getting an answer, and it also helps others with similar issues. However, it's essential to ask questions the right way to get the right answers. + +In case you were able to resolve your issue, consider sharing your solution with the community. By doing so, you contribute to enhancing Detox for the entire community. + +## Before Asking + +In case you're facing an issue, consider the following steps before posting a question: + +- **Suspect a bug?** Refer to our [Reporting Bugs] guide to determine if you've encountered a Detox bug and how to report it. +- **Search** for existing answers that may have addressed your question already. [Below](#where-to-ask), we've listed the right platforms to ask (and search) questions. +- Review our **documentation** thoroughly for any related information. Use document.querySelector('.DocSearch-Button').click()}>the search bar in the top right corner to streamline your search. + +If an existing answer doesn't fully address your question, feel free to ask a follow-up in the same thread if comment-enabled or create a new question, linking back to the original one if possible. + +## Where to Ask + +If you can't find an answer to your question, you can ask it in the following ways: + +- **Discord**: Join our dedicated [Discord server][Discord] to seek community assistance. +- **Stack Overflow**: Tag your question with ["Detox" on Stack Overflow] to reach the right audience. +- **GitHub**: Raise an issue on our [Detox repository]. + +:::tip Pro Tip + +When posting a question, ensure to include all relevant information, such as code snippets, screenshots, and examples. This practice helps others understand your query and provide comprehensive answers. + +::: + +[Reporting Bugs]: ../reporting-bugs.md +[Discord]: https://discord.gg/CkD5QKheF5 +[Detox repository]: https://github.com/wix/Detox/issues +["Detox" on Stack Overflow]: https://stackoverflow.com/questions/tagged/detox diff --git a/website/versioned_docs/version-20.x/contributing/reporting-bugs.md b/website/versioned_docs/version-20.x/contributing/reporting-bugs.md new file mode 100644 index 0000000000..b8e3ae7f7d --- /dev/null +++ b/website/versioned_docs/version-20.x/contributing/reporting-bugs.md @@ -0,0 +1,29 @@ +# Reporting Bugs + +Encountering a bug? Your detailed report is key for us to identify and rectify the issue. + +Before reporting a bug, please review the following guidelines. + +## Search for Existing Issues + +Before creating a new issue, search for [existing issues] to see if the bug has already been reported. +If you find an existing issue, add a comment to it with any additional information you have. + +## Create a Bug Report + +If you can’t find an existing issue, create a new one. When creating a bug report, follow these guidelines: + +1. **Follow the Template:** While [creating a bug report on GitHub], adhere to the provided template for creating a clear and concise report. +2. **Reproduction Steps:** Providing a reproduction example is essential. +3. **Artifacts:** Include screenshots, logs, visibility artifacts, and any other relevant information. Be ready to provide more information upon request. +4. **Detailed Description:** Avoid creating issues with brief descriptions or general error messages. Providing a thorough description and context is vital. + +:::tip Tip + +Creating a minimal code reproduction is crucial as it helps other contributors and maintainers understand and investigate the issue better. +**Issues without a reproduction are less likely to be addressed**. + +::: + +[existing issues]: https://github.com/wix/Detox/issues +[creating a bug report on GitHub]: https://github.com/wix/Detox/issues/new/choose diff --git a/website/versioned_docs/version-20.x/guide/launch-args.md b/website/versioned_docs/version-20.x/guide/launch-args.md index 8c2416e8b5..a8e192c907 100644 --- a/website/versioned_docs/version-20.x/guide/launch-args.md +++ b/website/versioned_docs/version-20.x/guide/launch-args.md @@ -41,5 +41,5 @@ Examples: Our official recommendation for getting the arguments inside the app is by integrating the [react-native-launch-arguments](https://github.com/iamolegga/react-native-launch-arguments) project, which provides that seamlessly. For those who are interested, here are the underlying details: -- On iOS, the specified launch arguments are passed as the process launch arguments and available through normal means. +- On iOS, the specified launch arguments are passed as the process launch arguments and available through normal means, such as accesing `[[NSProcessInfo processInfo] arguments]`. - On Android, the launch arguments are set as bundle-extra’s into the activity’s intent. They are therefore accessible on the native side via the current activity as: `currentActivity.getIntent().getBundleExtra("launchArgs")`. diff --git a/website/versioned_docs/version-20.x/introduction/environment-setup.md b/website/versioned_docs/version-20.x/introduction/environment-setup.md new file mode 100644 index 0000000000..88039bb58d --- /dev/null +++ b/website/versioned_docs/version-20.x/introduction/environment-setup.md @@ -0,0 +1,83 @@ +# Environment Setup + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +import IntroRN from './partials/_getting-started-rn.md'; +import IntroExpo from './partials/_getting-started-expo.md'; +import IntroOther from './partials/_getting-started-other.mdx'; + +The _Introduction section_ walks you through setting up Detox in your project, one step at a time. + +You will find that some steps are longer than the others: some are a couple of paragraphs, while the others look like a dedicated multistep guide. +Bear with us - it is all necessary, and once set up, it is easy to move forward with writing tests at a high pace. + +Please select type of your mobile application before you start the tutorial: + + + + + + + + + + + + + +## React Native CLI Quickstart + +Your first step would be to complete the +[📚 React Native CLI Quickstart Guide](https://reactnative.dev/docs/next/environment-setup). + +![RN Env](../img/rn-env.png) + +

+ Open the link above and switch there to React Native CLI Quickstart tab to see + the interactive tutorial for Development OS and Target OS of your choice. +

+ +

+ Follow all + the steps (yes, even if you have a native app!) and + make sure you can create and run React Native apps on virtual testing devices. +

+ +![RN Env Done](../img/rn-env-done.png) + +## Detox Prerequisites + +To complete the environment setup, you still have 2 steps ahead: installing `detox-cli` and `applesimutils`. + +### 1. Command Line Tools (`detox-cli`) + +`detox-cli` is merely a script that passes commands through to +a local Detox executable located at `node_modules/.bin/detox`, +making it easier to operate Detox from the command line, e.g. `detox test -c ios.sim.debug`. + +It should be installed globally, enabling usage of the command line tools outside your npm scripts: + +```bash npm2yarn +npm install detox-cli --global +``` + +On the other hand, if you are fine with running Detox commands with `npx detox ...`, you may skip installing `detox-cli`. + +### 2. [MacOS Only] `applesimutils` + +This tool is **required** by Detox to work with iOS simulators. The recommended way to install +[`applesimutils`](https://github.com/wix/AppleSimulatorUtils) is via [Homebrew](https://brew.sh): + +```bash +brew tap wix/brew +brew install applesimutils +``` + +:::info Note + +Make sure to periodically update your version of `applesimutils` to the latest version. + +::: + +Now, when your environment is ready, let's move to the next section, [Project Setup](project-setup.mdx). diff --git a/website/versioned_docs/version-20.x/introduction/getting-started.mdx b/website/versioned_docs/version-20.x/introduction/getting-started.mdx index 7f8d906004..263d3bc163 100644 --- a/website/versioned_docs/version-20.x/introduction/getting-started.mdx +++ b/website/versioned_docs/version-20.x/introduction/getting-started.mdx @@ -1,85 +1,65 @@ -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; +# Getting Started -import IntroRN from './partials/_getting-started-rn.md'; -import IntroExpo from './partials/_getting-started-expo.md'; -import IntroOther from './partials/_getting-started-other.mdx'; +## Before You Start -# Environment Setup +### Overview of Detox -**Welcome to Detox!** +Detox is an open-source end-to-end (E2E) testing framework for React Native mobile applications. +Our main goal is to enable the testing of any end-to-end flow in the app, with maximum velocity and zero flakiness. +Detox tests your mobile app while it's running on a real device or simulator, simulating a real user's interactions. -The _Introduction section_ walks you through setting up Detox in your project, one step at a time. +![Detox overview](../img/Detox.gif) -You will find that some steps are longer than the others: some are a couple of paragraphs, while the others look like a dedicated multistep guide. -Bear with us - it is all necessary, and once set up, it is easy to move forward with writing tests at a high pace. +### What Does a Detox Test Look Like? -Please select type of your mobile application before you start the tutorial: +This is a test for a login screen, it runs on a device or simulator like an actual user: - - - - - - - - - - - +```js +describe('Login flow', () => { + beforeEach(async () => { + await device.reloadReactNative(); + }); -## React Native CLI Quickstart + it('should login successfully', async () => { + await element(by.id('email')).typeText('john@example.com'); + await element(by.id('password')).typeText('123456'); -Your first step would be to complete the -[📚 React Native CLI Quickstart Guide](https://reactnative.dev/docs/next/environment-setup). + const loginButton = element(by.text('Login')); + await loginButton.tap(); -![](../img/rn-env.png) - -

- Open the link above and switch there to React Native CLI Quickstart tab to see - the interactive tutorial for Development OS and Target OS of your choice. -

- -

- Follow all - the steps (yes, even if you have a native app!) and - make sure you can create and run React Native apps on virtual testing devices. -

- -![](../img/rn-env-done.png) - -## Detox Prerequisites - -To complete the environment setup, you still have 2 steps ahead: installing `detox-cli` and `applesimutils`. - -### 1. Command Line Tools (`detox-cli`) + await expect(loginButton).not.toExist(); + await expect(element(by.label('Welcome'))).toBeVisible(); + }); +}); +``` -`detox-cli` is merely a script that passes commands through to -a local Detox executable located at `node_modules/.bin/detox`, -making it easier to operate Detox from the command line, e.g. `detox test -c ios.sim.debug`. +### Gray box testing -It should be installed globally, enabling usage of the command line tools outside your npm scripts: +One of the most challenging aspects of automated testing on mobile is end-to-end testing. +UI testing frameworks are often not fully-deterministic, since they are built with a "black box" approach, where the test runner has no access to the internals of the app under test, and is unaware of asynchronous operations that take place in the app (such as network requests, animations, etc.). +This makes it difficult to write reliable E2E tests, and often leads to flaky tests that are hard to debug and maintain. -```bash npm2yarn -npm install detox-cli --global -``` +To address this issue, Detox adopts a "gray box" testing approach, by having access to the internals of the app under test. +This allows for more control and predictability in the testing process. For more information, you can read [how Detox works]. -On the other hand, if you are fine with running Detox commands with `npx detox ...`, you may skip installing `detox-cli`. +## Start with Detox -### 2. [MacOS Only] `applesimutils` +### Follow the Setup Guides -This tool is **required** by Detox to work with iOS simulators. The recommended way to install -[`applesimutils`](https://github.com/wix/AppleSimulatorUtils) is via [Homebrew](https://brew.sh): +The following guides in this Introduction section will help you get started with Detox, one step at a time, from setting up your environment to writing and running your first test. -```bash -brew tap wix/brew -brew install applesimutils -``` +### Join the Community 🤝 -:::info Note +As an open-source project, Detox encourages and welcomes contributions from the community. +Whether you're a developer, tester, or just someone who is passionate about making mobile development better, there's a place for you in the Detox community. +By contributing to Detox, you'll be helping to improve the reliability and user experience of mobile apps, and you'll be part of a team that is dedicated to making mobile development faster and more efficient. +We welcome all types of contributions, from code and documentation, to bug reports, questions and feature requests. -Make sure to periodically update your version of `applesimutils` to the latest version. +So, if you're interested to be part of the community, we encourage you to get involved by reading our [contributing guidelines], and join our [Discord server]. +We also have a [Twitter account] where we share updates about the project. -::: -Now, when your environment is ready, let's move to the next section, [Project Setup](project-setup.mdx). +[how Detox works]: ../articles/how-detox-works.md +[contributing guidelines]: contributing.md +[Discord server]: https://discord.gg/CkD5QKheF5 +[Twitter account]: https://twitter.com/detoxe2e diff --git a/website/versioned_sidebars/version-20.x-sidebars.json b/website/versioned_sidebars/version-20.x-sidebars.json index dccd8da976..37024477cb 100644 --- a/website/versioned_sidebars/version-20.x-sidebars.json +++ b/website/versioned_sidebars/version-20.x-sidebars.json @@ -5,6 +5,7 @@ "label": "Introduction", "items": [ "introduction/getting-started", + "introduction/environment-setup", "introduction/project-setup", "introduction/your-first-test", "introduction/debugging", @@ -46,11 +47,6 @@ { "type": "doc", "id": "guide/migration" - }, - { - "type": "doc", - "label": "Contributing", - "id": "contributing" } ], "apiSidebar": [ @@ -107,5 +103,32 @@ "articles/third-party-drivers" ] } + ], + "contributeSidebar": [ + "contributing", + { + "type": "category", + "label": "Questions & Answers", + "items": [ + "contributing/questions/asking-questions", + "contributing/questions/answering-questions" + ] + }, + "contributing/reporting-bugs", + "contributing/feature-requests", + { + "type": "category", + "label": "Code Changes", + "items": [ + "contributing/code/overview", + "contributing/code/setting-up-the-dev-environment", + "contributing/code/building-and-testing", + "contributing/code/example-projects", + "contributing/code/submitting-pull-requests", + "contributing/code/reviewing-pull-requests" + ] + }, + "contributing/documentation", + "contributing/code-of-conduct" ] } From 8d198068a309efcfcac6a74bd21da8b45c8a5d2c Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Tue, 12 Dec 2023 13:47:00 +0200 Subject: [PATCH 049/185] internal: stop pid override in logs (#4294) --- detox/src/utils/childProcess/spawn.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/detox/src/utils/childProcess/spawn.js b/detox/src/utils/childProcess/spawn.js index b0f0ed30da..bd803d853f 100644 --- a/detox/src/utils/childProcess/spawn.js +++ b/detox/src/utils/childProcess/spawn.js @@ -45,7 +45,7 @@ async function interruptProcess(childProcessPromise, schedule) { const childProcess = childProcessPromise.childProcess; const cpid = childProcess.pid; const spawnargs = childProcess.spawnargs.join(' '); - const log = rootLogger.child({ event: 'SPAWN_KILL', pid: cpid }); + const log = rootLogger.child({ event: 'SPAWN_KILL', cpid }); const handles = _.mapValues({ ...DEFAULT_KILL_SCHEDULE, ...schedule }, (ms, signal) => { return setTimeout(() => { From 1512e9aa473d2c8fccdbaa11cc6b642c5eb17cfe Mon Sep 17 00:00:00 2001 From: wixmobile Date: Tue, 12 Dec 2023 07:55:48 -0800 Subject: [PATCH 050/185] Publish 20.14.1 [ci skip] --- detox/package.json | 2 +- detox/test/package.json | 4 ++-- examples/demo-native-android/package.json | 4 ++-- examples/demo-native-ios/package.json | 4 ++-- examples/demo-plugin/package.json | 4 ++-- examples/demo-react-native-detox-instruments/package.json | 4 ++-- examples/demo-react-native/package.json | 4 ++-- lerna.json | 2 +- package.json | 2 +- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/detox/package.json b/detox/package.json index f84cf16284..9a2b6537e4 100644 --- a/detox/package.json +++ b/detox/package.json @@ -1,7 +1,7 @@ { "name": "detox", "description": "E2E tests and automation for mobile", - "version": "20.14.0", + "version": "20.14.1", "bin": { "detox": "local-cli/cli.js" }, diff --git a/detox/test/package.json b/detox/test/package.json index d0756734c4..914a79a257 100644 --- a/detox/test/package.json +++ b/detox/test/package.json @@ -1,6 +1,6 @@ { "name": "detox-test", - "version": "20.14.0", + "version": "20.14.1", "private": true, "engines": { "node": ">=14.5.0" @@ -56,7 +56,7 @@ "@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/parser": "^5.4.0", "cross-env": "^7.0.3", - "detox": "^20.14.0", + "detox": "^20.14.1", "eslint": "^8.41.0", "eslint-plugin-unicorn": "^47.0.0", "execa": "^5.1.1", diff --git a/examples/demo-native-android/package.json b/examples/demo-native-android/package.json index f1dc5c3b0c..4ef0179171 100644 --- a/examples/demo-native-android/package.json +++ b/examples/demo-native-android/package.json @@ -1,13 +1,13 @@ { "name": "detox-demo-native-android", - "version": "20.14.0", + "version": "20.14.1", "private": true, "scripts": { "packager": "react-native start", "detox-server": "detox run-server" }, "devDependencies": { - "detox": "^20.14.0" + "detox": "^20.14.1" }, "detox": {} } diff --git a/examples/demo-native-ios/package.json b/examples/demo-native-ios/package.json index fc0feb22dc..06d744e422 100644 --- a/examples/demo-native-ios/package.json +++ b/examples/demo-native-ios/package.json @@ -1,9 +1,9 @@ { "name": "detox-demo-native-ios", - "version": "20.14.0", + "version": "20.14.1", "private": true, "devDependencies": { - "detox": "^20.14.0" + "detox": "^20.14.1" }, "detox": { "specs": "", diff --git a/examples/demo-plugin/package.json b/examples/demo-plugin/package.json index 2d5c3c7db1..d2241b33f4 100644 --- a/examples/demo-plugin/package.json +++ b/examples/demo-plugin/package.json @@ -1,12 +1,12 @@ { "name": "demo-plugin", - "version": "20.14.0", + "version": "20.14.1", "private": true, "scripts": { "test:plugin": "detox test --configuration plugin -l verbose" }, "devDependencies": { - "detox": "^20.14.0", + "detox": "^20.14.1", "jest": "^28.0.0" }, "detox": { diff --git a/examples/demo-react-native-detox-instruments/package.json b/examples/demo-react-native-detox-instruments/package.json index 7092dcf79f..7343057898 100644 --- a/examples/demo-react-native-detox-instruments/package.json +++ b/examples/demo-react-native-detox-instruments/package.json @@ -1,10 +1,10 @@ { "name": "demo-react-native-detox-instruments", - "version": "20.14.0", + "version": "20.14.1", "private": true, "scripts": {}, "devDependencies": { - "detox": "^20.14.0" + "detox": "^20.14.1" }, "detox": { "configurations": { diff --git a/examples/demo-react-native/package.json b/examples/demo-react-native/package.json index 07a0d7a769..447b846f84 100644 --- a/examples/demo-react-native/package.json +++ b/examples/demo-react-native/package.json @@ -1,6 +1,6 @@ { "name": "example", - "version": "20.14.0", + "version": "20.14.1", "private": true, "scripts": { "start": "react-native start", @@ -32,7 +32,7 @@ "@types/fs-extra": "^9.0.13", "@types/jest": "^29.2.1", "@types/react": "^18.0.24", - "detox": "^20.14.0", + "detox": "^20.14.1", "fs-extra": "^9.1.0", "jest": "^29.2.1", "ts-jest": "^29.0.3", diff --git a/lerna.json b/lerna.json index 1788ea5781..716ad586fa 100644 --- a/lerna.json +++ b/lerna.json @@ -13,7 +13,7 @@ "generation", "." ], - "version": "20.14.0", + "version": "20.14.1", "npmClient": "npm", "command": { "publish": { diff --git a/package.json b/package.json index 4becf915fd..2061de6ad2 100644 --- a/package.json +++ b/package.json @@ -54,5 +54,5 @@ "shell-utils": "1.x.x", "unified": "^10.1.0" }, - "version": "20.14.0" + "version": "20.14.1" } From 87c661e29f2ed263227655b40f46640268ef29ba Mon Sep 17 00:00:00 2001 From: d4vidi Date: Mon, 18 Dec 2023 17:21:43 +0200 Subject: [PATCH 051/185] Revert "Re: Supervised android launch (#4293)" (#4296) This reverts commit a630b87fd750fa68367f001976b863a4db08c801. --- .../com/wix/detox/ActivityLaunchHelper.kt | 76 ------------ .../src/full/java/com/wix/detox/Detox.java | 78 ++++++++++-- .../src/full/java/com/wix/detox/DetoxMain.kt | 113 +++++++----------- .../com/wix/detox/LaunchIntentsFactory.kt | 2 +- .../adapters/server/DetoxActionHandlers.kt | 4 + .../adapters/server/DetoxActionsDispatcher.kt | 22 +--- .../adapters/server/DetoxServerAdapter.kt | 2 + .../detox/adapters/server/DetoxServerInfo.kt | 6 +- .../detox/reactnative/ReactMarkersLogger.kt | 44 ------- .../detox/reactnative/ReactNativeExtension.kt | 57 +++++---- .../com/wix/detox/espresso/UiControllerSpy.kt | 3 + detox/src/android/espressoapi/Detox.js | 11 ++ detox/test/e2e/19.crash-handling.test.js | 8 +- 13 files changed, 178 insertions(+), 248 deletions(-) delete mode 100644 detox/android/detox/src/full/java/com/wix/detox/ActivityLaunchHelper.kt delete mode 100644 detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactMarkersLogger.kt diff --git a/detox/android/detox/src/full/java/com/wix/detox/ActivityLaunchHelper.kt b/detox/android/detox/src/full/java/com/wix/detox/ActivityLaunchHelper.kt deleted file mode 100644 index fd03bad184..0000000000 --- a/detox/android/detox/src/full/java/com/wix/detox/ActivityLaunchHelper.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.wix.detox - -import android.app.Instrumentation.ActivityMonitor -import android.content.Context -import android.content.Intent -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.rule.ActivityTestRule - -class ActivityLaunchHelper(private val activityTestRule: ActivityTestRule<*>) { - - private val launchArgs = LaunchArgs() - private val intentsFactory = LaunchIntentsFactory() - - fun launchActivityUnderTest() { - val intent = extractInitialIntent() - activityTestRule.launchActivity(intent) - } - - fun launchMainActivity() { - val activity = activityTestRule.activity - launchActivitySync(intentsFactory.activityLaunchIntent(activity)) - } - - fun startActivityFromUrl(url: String) { - launchActivitySync(intentsFactory.intentWithUrl(url, false)) - } - - fun startActivityFromNotification(dataFilePath: String) { - val notificationData = NotificationDataParser(dataFilePath!!).toBundle() - val intent = intentsFactory.intentWithNotificationData(appContext, notificationData, false) - launchActivitySync(intent) - } - - private fun extractInitialIntent(): Intent = - if (launchArgs.hasUrlOverride()) { - intentsFactory.intentWithUrl(launchArgs.urlOverride, true) - } else if (launchArgs.hasNotificationPath()) { - val notificationData = NotificationDataParser(launchArgs.notificationPath).toBundle() - intentsFactory.intentWithNotificationData(appContext, notificationData, true) - } else { - intentsFactory.cleanIntent() - }.also { - it.putExtra(INTENT_LAUNCH_ARGS_KEY, launchArgs.asIntentBundle()) - } - - private fun launchActivitySync(intent: Intent) { - // Ideally, we would just call sActivityTestRule.launchActivity(intent) and get it over with. - // BUT!!! as it turns out, Espresso has an issue where doing this for an activity running in the background - // would have Espresso set up an ActivityMonitor which will spend its time waiting for the activity to load, *without - // ever being released*. It will finally fail after a 45 seconds timeout. - // Without going into full details, it seems that activity test rules were not meant to be used this way. However, - // the all-new ActivityScenario implementation introduced in androidx could probably support this (e.g. by using - // dedicated methods such as moveToState(), which give better control over the lifecycle). - // In any case, this is the core reason for this issue: https://github.com/wix/Detox/issues/1125 - // What it forces us to do, then, is this - - // 1. Launch the activity by "ourselves" from the OS (i.e. using context.startActivity()). - // 2. Set up an activity monitor by ourselves -- such that it would block until the activity is ready. - // ^ Hence the code below. - val activity = activityTestRule.activity - val activityMonitor = ActivityMonitor(activity.javaClass.name, null, true) - activity.startActivity(intent) - - InstrumentationRegistry.getInstrumentation().run { - addMonitor(activityMonitor) - waitForMonitorWithTimeout(activityMonitor, ACTIVITY_LAUNCH_TIMEOUT) - } - } - - private val appContext: Context - get() = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext - - companion object { - private const val INTENT_LAUNCH_ARGS_KEY = "launchArgs" - private const val ACTIVITY_LAUNCH_TIMEOUT = 10000L - } -} diff --git a/detox/android/detox/src/full/java/com/wix/detox/Detox.java b/detox/android/detox/src/full/java/com/wix/detox/Detox.java index 93769a11c4..87d211c797 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/Detox.java +++ b/detox/android/detox/src/full/java/com/wix/detox/Detox.java @@ -1,13 +1,18 @@ package com.wix.detox; +import android.app.Activity; +import android.app.Instrumentation; import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import com.wix.detox.config.DetoxConfig; +import com.wix.detox.espresso.UiControllerSpy; import androidx.annotation.NonNull; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.rule.ActivityTestRule; -import com.wix.detox.config.DetoxConfig; - /** *

Static class.

* @@ -62,7 +67,12 @@ *

If not set, then Detox tests are no ops. So it's safe to mix it with other tests.

*/ public final class Detox { - private static ActivityLaunchHelper sActivityLaunchHelper; + private static final String INTENT_LAUNCH_ARGS_KEY = "launchArgs"; + private static final long ACTIVITY_LAUNCH_TIMEOUT = 10000L; + + private static final LaunchArgs sLaunchArgs = new LaunchArgs(); + private static final LaunchIntentsFactory sIntentsFactory = new LaunchIntentsFactory(); + private static ActivityTestRule sActivityTestRule; private Detox() { } @@ -122,20 +132,72 @@ public static void runTests(ActivityTestRule activityTestRule, @NonNull final Co DetoxConfig.CONFIG = detoxConfig; DetoxConfig.CONFIG.apply(); - sActivityLaunchHelper = new ActivityLaunchHelper(activityTestRule); - DetoxMain.run(context, sActivityLaunchHelper); + sActivityTestRule = activityTestRule; + + UiControllerSpy.attachThroughProxy(); + + Intent intent = extractInitialIntent(); + sActivityTestRule.launchActivity(intent); + + try { + DetoxMain.run(context); + } catch (Exception e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Detox got interrupted prematurely", e); + } } public static void launchMainActivity() { - sActivityLaunchHelper.launchMainActivity(); + final Activity activity = sActivityTestRule.getActivity(); + launchActivitySync(sIntentsFactory.activityLaunchIntent(activity)); } public static void startActivityFromUrl(String url) { - sActivityLaunchHelper.startActivityFromUrl(url); + launchActivitySync(sIntentsFactory.intentWithUrl(url, false)); } public static void startActivityFromNotification(String dataFilePath) { - sActivityLaunchHelper.startActivityFromNotification(dataFilePath); + Bundle notificationData = new NotificationDataParser(dataFilePath).toBundle(); + Intent intent = sIntentsFactory.intentWithNotificationData(getAppContext(), notificationData, false); + launchActivitySync(intent); + } + + private static Intent extractInitialIntent() { + Intent intent; + + if (sLaunchArgs.hasUrlOverride()) { + intent = sIntentsFactory.intentWithUrl(sLaunchArgs.getUrlOverride(), true); + } else if (sLaunchArgs.hasNotificationPath()) { + Bundle notificationData = new NotificationDataParser(sLaunchArgs.getNotificationPath()).toBundle(); + intent = sIntentsFactory.intentWithNotificationData(getAppContext(), notificationData, true); + } else { + intent = sIntentsFactory.cleanIntent(); + } + intent.putExtra(INTENT_LAUNCH_ARGS_KEY, sLaunchArgs.asIntentBundle()); + return intent; + } + + private static void launchActivitySync(Intent intent) { + // Ideally, we would just call sActivityTestRule.launchActivity(intent) and get it over with. + // BUT!!! as it turns out, Espresso has an issue where doing this for an activity running in the background + // would have Espresso set up an ActivityMonitor which will spend its time waiting for the activity to load, *without + // ever being released*. It will finally fail after a 45 seconds timeout. + // Without going into full details, it seems that activity test rules were not meant to be used this way. However, + // the all-new ActivityScenario implementation introduced in androidx could probably support this (e.g. by using + // dedicated methods such as moveToState(), which give better control over the lifecycle). + // In any case, this is the core reason for this issue: https://github.com/wix/Detox/issues/1125 + // What it forces us to do, then, is this - + // 1. Launch the activity by "ourselves" from the OS (i.e. using context.startActivity()). + // 2. Set up an activity monitor by ourselves -- such that it would block until the activity is ready. + // ^ Hence the code below. + + final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); + final Activity activity = sActivityTestRule.getActivity(); + final Instrumentation.ActivityMonitor activityMonitor = new Instrumentation.ActivityMonitor(activity.getClass().getName(), null, true); + + activity.startActivity(intent); + instrumentation.addMonitor(activityMonitor); + instrumentation.waitForMonitorWithTimeout(activityMonitor, ACTIVITY_LAUNCH_TIMEOUT); } private static Context getAppContext() { diff --git a/detox/android/detox/src/full/java/com/wix/detox/DetoxMain.kt b/detox/android/detox/src/full/java/com/wix/detox/DetoxMain.kt index fd5200b18c..5e4f0bd88b 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/DetoxMain.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/DetoxMain.kt @@ -3,62 +3,35 @@ package com.wix.detox import android.content.Context import android.util.Log import com.wix.detox.adapters.server.* -import com.wix.detox.common.DetoxLog -import com.wix.detox.espresso.UiControllerSpy +import com.wix.detox.common.DetoxLog.Companion.LOG_TAG import com.wix.detox.instruments.DetoxInstrumentsManager import com.wix.detox.reactnative.ReactNativeExtension import com.wix.invoke.MethodInvocation -import java.util.concurrent.CountDownLatch +private const val INIT_ACTION = "_init" +private const val IS_READY_ACTION = "isReady" private const val TERMINATION_ACTION = "_terminate" object DetoxMain { - private val handshakeLock = CountDownLatch(1) - @JvmStatic - fun run(rnHostHolder: Context, activityLaunchHelper: ActivityLaunchHelper) { + fun run(rnHostHolder: Context) { val detoxServerInfo = DetoxServerInfo() + Log.i(LOG_TAG, "Detox server connection details: $detoxServerInfo") + val testEngineFacade = TestEngineFacade() val actionsDispatcher = DetoxActionsDispatcher() - val serverAdapter = DetoxServerAdapter(actionsDispatcher, detoxServerInfo, TERMINATION_ACTION) - - initCrashHandler(serverAdapter) - initANRListener(serverAdapter) - initEspresso() - initReactNative() - - setupActionHandlers(actionsDispatcher, serverAdapter, testEngineFacade, rnHostHolder) - serverAdapter.connect() - - launchActivityOnCue(rnHostHolder, activityLaunchHelper) + val externalAdapter = DetoxServerAdapter(actionsDispatcher, detoxServerInfo, IS_READY_ACTION, TERMINATION_ACTION) + initActionHandlers(actionsDispatcher, externalAdapter, testEngineFacade, rnHostHolder) + actionsDispatcher.dispatchAction(INIT_ACTION, "", 0) actionsDispatcher.join() } - /** - * Launch the tested activity "on cue", namely, right after a connection is established and the handshake - * completes successfully. - * - * This has to be synchronized so that an `isReady` isn't handled *before* the activity is launched (albeit not fully - * initialized - all native modules and everything) and a react context is available. - * - * As a better alternative, it would make sense to execute this as a simple action from within the actions - * dispatcher (i.e. handler of `loginSuccess`), in which case, no inter-thread locking would be required - * thanks to the usage of Handlers. However, in this type of a solution, errors / crashes would be reported - * not by instrumentation itself, but based on the `AppWillTerminateWithError` message; In it's own, it is a good - * thing, but for a reason we're not sure of yet, it is ignored by the test runner at this point in the flow. - */ - @Synchronized - private fun launchActivityOnCue(rnHostHolder: Context, activityLaunchHelper: ActivityLaunchHelper) { - awaitHandshake() - launchActivity(rnHostHolder, activityLaunchHelper) - } - - private fun awaitHandshake() { - handshakeLock.await() - } + private fun doInit(externalAdapter: DetoxServerAdapter, rnHostHolder: Context) { + externalAdapter.connect() - private fun onLoginSuccess() { - handshakeLock.countDown() + initCrashHandler(externalAdapter) + initANRListener(externalAdapter) + initReactNativeIfNeeded(rnHostHolder) } private fun doTeardown(serverAdapter: DetoxServerAdapter, actionsDispatcher: DetoxActionsDispatcher, testEngineFacade: TestEngineFacade) { @@ -68,28 +41,35 @@ object DetoxMain { actionsDispatcher.teardown() } - private fun setupActionHandlers(actionsDispatcher: DetoxActionsDispatcher, serverAdapter: DetoxServerAdapter, testEngineFacade: TestEngineFacade, rnHostHolder: Context) { - class SynchronizedActionHandler(private val actionHandler: DetoxActionHandler): DetoxActionHandler { - override fun handle(params: String, messageId: Long) { - synchronized(this@DetoxMain) { - actionHandler.handle(params, messageId) - } - } - } - + private fun initActionHandlers(actionsDispatcher: DetoxActionsDispatcher, serverAdapter: DetoxServerAdapter, testEngineFacade: TestEngineFacade, rnHostHolder: Context) { // Primary actions with(actionsDispatcher) { - val readyHandler = SynchronizedActionHandler( ReadyActionHandler(serverAdapter, testEngineFacade) ) - val rnReloadHandler = SynchronizedActionHandler( ReactNativeReloadActionHandler(rnHostHolder, serverAdapter, testEngineFacade) ) + val rnReloadHandler = ReactNativeReloadActionHandler(rnHostHolder, serverAdapter, testEngineFacade) - associateActionHandler("loginSuccess", ::onLoginSuccess) - associateActionHandler("isReady", readyHandler) - associateActionHandler("reactNativeReload", rnReloadHandler) + associateActionHandler(INIT_ACTION, object : DetoxActionHandler { + override fun handle(params: String, messageId: Long) = + synchronized(this@DetoxMain) { + this@DetoxMain.doInit(serverAdapter, rnHostHolder) + } + }) + associateActionHandler(IS_READY_ACTION, ReadyActionHandler(serverAdapter, testEngineFacade)) + + associateActionHandler("loginSuccess", ScarceActionHandler()) + associateActionHandler("reactNativeReload", object: DetoxActionHandler { + override fun handle(params: String, messageId: Long) = + synchronized(this@DetoxMain) { + rnReloadHandler.handle(params, messageId) + } + }) associateActionHandler("invoke", InvokeActionHandler(MethodInvocation(), serverAdapter)) associateActionHandler("cleanup", CleanupActionHandler(serverAdapter, testEngineFacade) { dispatchAction(TERMINATION_ACTION, "", 0) }) - associateActionHandler(TERMINATION_ACTION) { -> doTeardown(serverAdapter, actionsDispatcher, testEngineFacade) } + associateActionHandler(TERMINATION_ACTION, object: DetoxActionHandler { + override fun handle(params: String, messageId: Long) { + this@DetoxMain.doTeardown(serverAdapter, actionsDispatcher, testEngineFacade) + } + }) if (DetoxInstrumentsManager.supports()) { val instrumentsManager = DetoxInstrumentsManager(rnHostHolder) @@ -100,8 +80,13 @@ object DetoxMain { // Secondary actions with(actionsDispatcher) { - val queryStatusHandler = SynchronizedActionHandler( QueryStatusActionHandler(serverAdapter, testEngineFacade) ) - associateSecondaryActionHandler("currentStatus", queryStatusHandler) + val queryStatusHandler = QueryStatusActionHandler(serverAdapter, testEngineFacade) + associateActionHandler("currentStatus", object: DetoxActionHandler { + override fun handle(params: String, messageId: Long) = + synchronized(this@DetoxMain) { + queryStatusHandler.handle(params, messageId) + } + }, false) } } @@ -113,17 +98,7 @@ object DetoxMain { DetoxANRHandler(outboundServerAdapter).attach() } - private fun initEspresso() { - UiControllerSpy.attachThroughProxy() - } - - private fun initReactNative() { - ReactNativeExtension.initIfNeeded() - } - - private fun launchActivity(rnHostHolder: Context, activityLaunchHelper: ActivityLaunchHelper) { - Log.i(DetoxLog.LOG_TAG, "Launching the tested activity!") - activityLaunchHelper.launchActivityUnderTest() + private fun initReactNativeIfNeeded(rnHostHolder: Context) { ReactNativeExtension.waitForRNBootstrap(rnHostHolder) } } diff --git a/detox/android/detox/src/full/java/com/wix/detox/LaunchIntentsFactory.kt b/detox/android/detox/src/full/java/com/wix/detox/LaunchIntentsFactory.kt index 80f5801c23..43bed23a44 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/LaunchIntentsFactory.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/LaunchIntentsFactory.kt @@ -6,7 +6,7 @@ import android.content.Intent import android.net.Uri import android.os.Bundle -class LaunchIntentsFactory { +internal class LaunchIntentsFactory { /** * Constructs an intent tightly associated with a specific activity. diff --git a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionHandlers.kt b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionHandlers.kt index 1404f63abf..ef0416465e 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionHandlers.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionHandlers.kt @@ -152,3 +152,7 @@ class InstrumentsEventsActionsHandler( outboundServerAdapter.sendMessage("eventDone", emptyMap(), messageId) } } + +class ScarceActionHandler: DetoxActionHandler { + override fun handle(params: String, messageId: Long) {} +} diff --git a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionsDispatcher.kt b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionsDispatcher.kt index c3d83d50ff..ac82d2c3e7 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionsDispatcher.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionsDispatcher.kt @@ -11,18 +11,11 @@ class DetoxActionsDispatcher { private val primaryExec = ActionsExecutor("detox.primary") private val secondaryExec = ActionsExecutor("detox.secondary") - fun associateActionHandler(type: String, actionHandler: DetoxActionHandler) = - associateActionHandler(type, actionHandler, true) - - fun associateActionHandler(type: String, handlerFunc: () -> Unit) { - associateActionHandler(type, object: DetoxActionHandler { - override fun handle(params: String, messageId: Long) = handlerFunc() - }) + fun associateActionHandler(type: String, actionHandler: DetoxActionHandler, isPrimary: Boolean = true) { + val actionsExecutor = (if (isPrimary) primaryExec else secondaryExec) + actionsExecutor.associateHandler(type, actionHandler) } - fun associateSecondaryActionHandler(type: String, actionHandler: DetoxActionHandler) = - associateActionHandler(type, actionHandler, false) - fun dispatchAction(type: String, params: String, messageId: Long) { (primaryExec.executeAction(type, params, messageId) || secondaryExec.executeAction(type, params, messageId)) @@ -40,11 +33,6 @@ class DetoxActionsDispatcher { primaryExec.join() secondaryExec.join() } - - private fun associateActionHandler(type: String, actionHandler: DetoxActionHandler, isPrimary: Boolean = true) { - val actionsExecutor = (if (isPrimary) primaryExec else secondaryExec) - actionsExecutor.associateHandler(type, actionHandler) - } } private class ActionsExecutor(name: String) { @@ -86,5 +74,7 @@ private class ActionsExecutor(name: String) { handler.looper.quit() } - fun join() = thread.join() + fun join() { + thread.join() + } } diff --git a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerAdapter.kt b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerAdapter.kt index 9873a72bc3..2d8b11fdcf 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerAdapter.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerAdapter.kt @@ -10,6 +10,7 @@ interface OutboundServerAdapter { class DetoxServerAdapter( private val actionsDispatcher: DetoxActionsDispatcher, private val detoxServerInfo: DetoxServerInfo, + private val readyActionType: String, private val terminationActionType: String) : WebSocketClient.WSEventsHandler, OutboundServerAdapter { @@ -26,6 +27,7 @@ class DetoxServerAdapter( override fun onConnect() { Log.i(DetoxLog.LOG_TAG, "Connected to server!") + actionsDispatcher.dispatchAction(readyActionType, "", -1000L) } override fun onClosed() { diff --git a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerInfo.kt b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerInfo.kt index 947c69ea27..f2828de4ea 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerInfo.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerInfo.kt @@ -1,9 +1,7 @@ package com.wix.detox.adapters.server -import android.util.Log import androidx.test.platform.app.InstrumentationRegistry import com.wix.detox.LaunchArgs -import com.wix.detox.common.DetoxLog private const val DEFAULT_URL = "ws://localhost:8099" @@ -11,7 +9,7 @@ class DetoxServerInfo internal constructor(launchArgs: LaunchArgs = LaunchArgs() val serverUrl: String = launchArgs.detoxServerUrl ?: DEFAULT_URL val sessionId: String = launchArgs.detoxSessionId ?: InstrumentationRegistry.getInstrumentation().targetContext.applicationInfo.packageName - init { - Log.i(DetoxLog.LOG_TAG, "Detox server connection details: url=$serverUrl, sessionId=$sessionId") + override fun toString(): String { + return "url=$serverUrl, sessionId=$sessionId" } } diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactMarkersLogger.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactMarkersLogger.kt deleted file mode 100644 index bbabce01d3..0000000000 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactMarkersLogger.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.wix.detox.reactnative - -import android.util.Log -import com.facebook.react.bridge.ReactMarker -import com.facebook.react.bridge.ReactMarkerConstants -import com.facebook.react.bridge.ReactMarkerConstants.* - -object ReactMarkersLogger : ReactMarker.MarkerListener { - - fun attach() { - ReactMarker.addListener(this) - } - - override fun logMarker(marker: ReactMarkerConstants, p1: String?, p2: Int) { - when { - marker == DOWNLOAD_START || - marker == DOWNLOAD_END || - marker == BUILD_REACT_INSTANCE_MANAGER_START || - marker == BUILD_REACT_INSTANCE_MANAGER_END || - marker == REACT_BRIDGE_LOADING_START || - marker == REACT_BRIDGE_LOADING_END || - marker == REACT_BRIDGELESS_LOADING_START || - marker == REACT_BRIDGELESS_LOADING_END || - marker == CREATE_MODULE_START || - marker == CREATE_MODULE_END || - marker == NATIVE_MODULE_SETUP_START || - marker == NATIVE_MODULE_SETUP_END || - marker == PRE_RUN_JS_BUNDLE_START || - marker == RUN_JS_BUNDLE_START || - marker == RUN_JS_BUNDLE_END || - marker == CONTENT_APPEARED || - marker == CREATE_CATALYST_INSTANCE_START || - marker == CREATE_CATALYST_INSTANCE_END || - marker == DESTROY_CATALYST_INSTANCE_START || - marker == DESTROY_CATALYST_INSTANCE_END || - marker == CREATE_REACT_CONTEXT_START || - marker == CREATE_REACT_CONTEXT_END || - marker == PROCESS_PACKAGES_START || - marker == PROCESS_PACKAGES_END || - false -> - Log.d("Detox.RNMarker", "$marker ($p1)") - } - } -} diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeExtension.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeExtension.kt index 1711eea37c..19e4ae2473 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeExtension.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeExtension.kt @@ -14,31 +14,6 @@ private const val LOG_TAG = "DetoxRNExt" object ReactNativeExtension { private var rnIdlingResources: ReactNativeIdlingResources? = null - fun initIfNeeded() { - if (!ReactNativeInfo.isReactNativeApp()) { - return - } - - ReactMarkersLogger.attach() - } - - /** - * Wait for React-Native to finish loading (i.e. make RN context available). - * - * @param applicationContext The app context, implicitly assumed to be a [ReactApplication] instance. - */ - fun waitForRNBootstrap(applicationContext: Context) { - if (!ReactNativeInfo.isReactNativeApp()) { - return - } - - (applicationContext as ReactApplication).let { - val reactContext = awaitNewReactNativeContext(it, null) - - enableOrDisableSynchronization(reactContext) - } - } - /** * Reloads the React Native context and thus all javascript code. * @@ -65,6 +40,26 @@ object ReactNativeExtension { val reactContext = awaitNewReactNativeContext(it, previousReactContext) enableOrDisableSynchronization(reactContext, networkSyncEnabled) + hackRN50OrHigherWaitForReady() + } + } + + /** + * Wait for React-Native to finish loading (i.e. make RN context available). + * + * @param applicationContext The app context, implicitly assumed to be a [ReactApplication] instance. + */ + @JvmStatic + fun waitForRNBootstrap(applicationContext: Context) { + if (!ReactNativeInfo.isReactNativeApp()) { + return + } + + (applicationContext as ReactApplication).let { + val reactContext = awaitNewReactNativeContext(it, null) + + enableOrDisableSynchronization(reactContext) + hackRN50OrHigherWaitForReady() } } @@ -150,6 +145,18 @@ object ReactNativeExtension { } } + private fun hackRN50OrHigherWaitForReady() { + if (ReactNativeInfo.rnVersion().minor in 50..62) { + try { + //TODO- Temp hack to make Detox usable for RN>=50 till we find a better sync solution. + Thread.sleep(1000) + } catch (e: InterruptedException) { + e.printStackTrace() + } + + } + } + private fun clearIdlingResources() { rnIdlingResources?.unregisterAll() rnIdlingResources = null diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/UiControllerSpy.kt b/detox/android/detox/src/main/java/com/wix/detox/espresso/UiControllerSpy.kt index 051be7931e..742af777f0 100644 --- a/detox/android/detox/src/main/java/com/wix/detox/espresso/UiControllerSpy.kt +++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/UiControllerSpy.kt @@ -11,8 +11,11 @@ class UiControllerSpy: MethodsSpy("uiController") { fun eventInjectionsIterator(): Iterator = historyOf("injectMotionEvent").iterator() companion object { + @JvmStatic val instance = UiControllerSpy() + @JvmStatic + @JvmOverloads fun attachThroughProxy(spy: UiControllerSpy = instance) { val eventsInjectorReflected = EventsInjectorReflected(getUiController()) diff --git a/detox/src/android/espressoapi/Detox.js b/detox/src/android/espressoapi/Detox.js index dfa11192fa..bbba04e3a2 100644 --- a/detox/src/android/espressoapi/Detox.js +++ b/detox/src/android/espressoapi/Detox.js @@ -58,6 +58,17 @@ class Detox { }; } + static extractInitialIntent() { + return { + target: { + type: "Class", + value: "com.wix.detox.Detox" + }, + method: "extractInitialIntent", + args: [] + }; + } + static getAppContext() { return { target: { diff --git a/detox/test/e2e/19.crash-handling.test.js b/detox/test/e2e/19.crash-handling.test.js index 0c1f120215..1912538afa 100644 --- a/detox/test/e2e/19.crash-handling.test.js +++ b/detox/test/e2e/19.crash-handling.test.js @@ -51,14 +51,12 @@ describe('Crash Handling', () => { it(':android: Should throw a detailed error upon app bootstrap crash', async () => { const error = await expectToThrow( () => relaunchAppWithArgs({ detoxAndroidCrashingActivity: true }), - 'The app has crashed, see the details below:'); + 'Failed to run application on the device'); // It's important that the native-error message (containing the native stack-trace) would also // be included in the error's stack property, in order for Jest (specifically) to properly output all // of that into the shell, as we expect it to. - jestExpect(error.stack).toContain('java.lang.RuntimeException: Unable to resume activity'); - - // In particular, we want the original cause to be bundled in. - jestExpect(error.stack).toContain('Caused by: java.lang.IllegalStateException: This is an intentional crash!'); + jestExpect(error.stack).toContain('Native stacktrace dump:\njava.lang.IllegalStateException: This is an intentional crash!'); + jestExpect(error.stack).toContain('\tat com.example.CrashingActivity.onResume'); }, 60000); }); From 281536e93191a2d88bf7bd591b6ee95c316f817b Mon Sep 17 00:00:00 2001 From: wixmobile Date: Mon, 18 Dec 2023 08:10:09 -0800 Subject: [PATCH 052/185] Publish 20.14.2 [ci skip] --- detox/package.json | 2 +- detox/test/package.json | 4 ++-- examples/demo-native-android/package.json | 4 ++-- examples/demo-native-ios/package.json | 4 ++-- examples/demo-plugin/package.json | 4 ++-- examples/demo-react-native-detox-instruments/package.json | 4 ++-- examples/demo-react-native/package.json | 4 ++-- lerna.json | 2 +- package.json | 2 +- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/detox/package.json b/detox/package.json index 9a2b6537e4..cd374a4c34 100644 --- a/detox/package.json +++ b/detox/package.json @@ -1,7 +1,7 @@ { "name": "detox", "description": "E2E tests and automation for mobile", - "version": "20.14.1", + "version": "20.14.2", "bin": { "detox": "local-cli/cli.js" }, diff --git a/detox/test/package.json b/detox/test/package.json index 914a79a257..6afec23925 100644 --- a/detox/test/package.json +++ b/detox/test/package.json @@ -1,6 +1,6 @@ { "name": "detox-test", - "version": "20.14.1", + "version": "20.14.2", "private": true, "engines": { "node": ">=14.5.0" @@ -56,7 +56,7 @@ "@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/parser": "^5.4.0", "cross-env": "^7.0.3", - "detox": "^20.14.1", + "detox": "^20.14.2", "eslint": "^8.41.0", "eslint-plugin-unicorn": "^47.0.0", "execa": "^5.1.1", diff --git a/examples/demo-native-android/package.json b/examples/demo-native-android/package.json index 4ef0179171..9dd3fb923f 100644 --- a/examples/demo-native-android/package.json +++ b/examples/demo-native-android/package.json @@ -1,13 +1,13 @@ { "name": "detox-demo-native-android", - "version": "20.14.1", + "version": "20.14.2", "private": true, "scripts": { "packager": "react-native start", "detox-server": "detox run-server" }, "devDependencies": { - "detox": "^20.14.1" + "detox": "^20.14.2" }, "detox": {} } diff --git a/examples/demo-native-ios/package.json b/examples/demo-native-ios/package.json index 06d744e422..aba1693e80 100644 --- a/examples/demo-native-ios/package.json +++ b/examples/demo-native-ios/package.json @@ -1,9 +1,9 @@ { "name": "detox-demo-native-ios", - "version": "20.14.1", + "version": "20.14.2", "private": true, "devDependencies": { - "detox": "^20.14.1" + "detox": "^20.14.2" }, "detox": { "specs": "", diff --git a/examples/demo-plugin/package.json b/examples/demo-plugin/package.json index d2241b33f4..880e2b4663 100644 --- a/examples/demo-plugin/package.json +++ b/examples/demo-plugin/package.json @@ -1,12 +1,12 @@ { "name": "demo-plugin", - "version": "20.14.1", + "version": "20.14.2", "private": true, "scripts": { "test:plugin": "detox test --configuration plugin -l verbose" }, "devDependencies": { - "detox": "^20.14.1", + "detox": "^20.14.2", "jest": "^28.0.0" }, "detox": { diff --git a/examples/demo-react-native-detox-instruments/package.json b/examples/demo-react-native-detox-instruments/package.json index 7343057898..8fea4b06ad 100644 --- a/examples/demo-react-native-detox-instruments/package.json +++ b/examples/demo-react-native-detox-instruments/package.json @@ -1,10 +1,10 @@ { "name": "demo-react-native-detox-instruments", - "version": "20.14.1", + "version": "20.14.2", "private": true, "scripts": {}, "devDependencies": { - "detox": "^20.14.1" + "detox": "^20.14.2" }, "detox": { "configurations": { diff --git a/examples/demo-react-native/package.json b/examples/demo-react-native/package.json index 447b846f84..c0b20fba0d 100644 --- a/examples/demo-react-native/package.json +++ b/examples/demo-react-native/package.json @@ -1,6 +1,6 @@ { "name": "example", - "version": "20.14.1", + "version": "20.14.2", "private": true, "scripts": { "start": "react-native start", @@ -32,7 +32,7 @@ "@types/fs-extra": "^9.0.13", "@types/jest": "^29.2.1", "@types/react": "^18.0.24", - "detox": "^20.14.1", + "detox": "^20.14.2", "fs-extra": "^9.1.0", "jest": "^29.2.1", "ts-jest": "^29.0.3", diff --git a/lerna.json b/lerna.json index 716ad586fa..f1a35093f7 100644 --- a/lerna.json +++ b/lerna.json @@ -13,7 +13,7 @@ "generation", "." ], - "version": "20.14.1", + "version": "20.14.2", "npmClient": "npm", "command": { "publish": { diff --git a/package.json b/package.json index 2061de6ad2..4e49da7577 100644 --- a/package.json +++ b/package.json @@ -54,5 +54,5 @@ "shell-utils": "1.x.x", "unified": "^10.1.0" }, - "version": "20.14.1" + "version": "20.14.2" } From 375d47054a45acb32da0c7655178d62ba6604a81 Mon Sep 17 00:00:00 2001 From: Georgy Date: Tue, 19 Dec 2023 12:34:39 +0200 Subject: [PATCH 053/185] fix(android): swipe is not working correctly on API 33-34 (#4299) * Upgraded espresso version to 3.5.1 in order to fix the issue with swipe on android 33/34 --- detox/android/detox/build.gradle | 8 ++++---- detox/android/detox/proguard-rules-app.pro | 3 +++ .../com/wix/detox/espresso/assertion/ViewAssertions.java | 3 ++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/detox/android/detox/build.gradle b/detox/android/detox/build.gradle index 7ebad1f876..e2496e292c 100644 --- a/detox/android/detox/build.gradle +++ b/detox/android/detox/build.gradle @@ -101,13 +101,13 @@ dependencies { // Versions are in-sync with the 'androidx-test-1.4.0' release/tag of the android-test github repo, // used by the Detox generator. See https://github.com/android/android-test/releases/tag/androidx-test-1.4.0 // Important: Should remain so when generator tag is replaced! - api('androidx.test.espresso:espresso-core:3.4.0') { + api('androidx.test.espresso:espresso-core:3.5.1') { because 'Needed all across Detox but also makes Espresso seamlessly provided to Detox users with hybrid apps/E2E-tests.' } - api('androidx.test.espresso:espresso-web:3.4.0') { + api('androidx.test.espresso:espresso-web:3.5.1') { because 'Web-View testing' } - api('androidx.test.espresso:espresso-contrib:3.4.0') { + api('androidx.test.espresso:espresso-contrib:3.5.1') { because 'Android datepicker support' exclude group: "org.checkerframework", module: "checker" } @@ -150,7 +150,7 @@ dependencies { testImplementation "org.jetbrains.kotlin:kotlin-test:$_kotlinVersion" testImplementation 'org.apache.commons:commons-io:1.3.2' testImplementation 'org.mockito.kotlin:mockito-kotlin:4.0.0' - testImplementation 'org.robolectric:robolectric:4.4' + testImplementation 'org.robolectric:robolectric:4.11.1' testImplementation("com.google.android.material:material:$_materialMinVersion") { because 'Material components are mentioned explicitly (e.g. Slider in get-attributes handler)' diff --git a/detox/android/detox/proguard-rules-app.pro b/detox/android/detox/proguard-rules-app.pro index f230633216..1932cf4105 100644 --- a/detox/android/detox/proguard-rules-app.pro +++ b/detox/android/detox/proguard-rules-app.pro @@ -19,3 +19,6 @@ -keep class kotlin.text.** { *; } -keep class kotlin.io.** { *; } -keep class okhttp3.** { *; } + +-keep class androidx.concurrent.futures.CallbackToFutureAdapter$* { *; } +-keep class androidx.concurrent.futures.CallbackToFutureAdapter { *; } diff --git a/detox/android/detox/src/full/java/com/wix/detox/espresso/assertion/ViewAssertions.java b/detox/android/detox/src/full/java/com/wix/detox/espresso/assertion/ViewAssertions.java index 7784de010f..7559232a12 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/espresso/assertion/ViewAssertions.java +++ b/detox/android/detox/src/full/java/com/wix/detox/espresso/assertion/ViewAssertions.java @@ -24,8 +24,9 @@ public class ViewAssertions { * which is more suitable for Detox' separated interaction-matcher architecture. * See {@link MatchesViewAssertion} for more details. */ + @SuppressWarnings("unchecked") public static ViewAssertion matches(final Matcher viewMatcher) { - return new MatchesViewAssertion(checkNotNull(viewMatcher)); + return new MatchesViewAssertion((Matcher) checkNotNull(viewMatcher)); } /** From a7bbc444bab6fbce3d561cd110c40ba1b2b84a6b Mon Sep 17 00:00:00 2001 From: wixmobile Date: Tue, 19 Dec 2023 07:39:08 -0800 Subject: [PATCH 054/185] Publish 20.14.3 [ci skip] --- detox/package.json | 2 +- detox/test/package.json | 4 ++-- examples/demo-native-android/package.json | 4 ++-- examples/demo-native-ios/package.json | 4 ++-- examples/demo-plugin/package.json | 4 ++-- examples/demo-react-native-detox-instruments/package.json | 4 ++-- examples/demo-react-native/package.json | 4 ++-- lerna.json | 2 +- package.json | 2 +- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/detox/package.json b/detox/package.json index cd374a4c34..a0f284345e 100644 --- a/detox/package.json +++ b/detox/package.json @@ -1,7 +1,7 @@ { "name": "detox", "description": "E2E tests and automation for mobile", - "version": "20.14.2", + "version": "20.14.3", "bin": { "detox": "local-cli/cli.js" }, diff --git a/detox/test/package.json b/detox/test/package.json index 6afec23925..79bdbe0c2a 100644 --- a/detox/test/package.json +++ b/detox/test/package.json @@ -1,6 +1,6 @@ { "name": "detox-test", - "version": "20.14.2", + "version": "20.14.3", "private": true, "engines": { "node": ">=14.5.0" @@ -56,7 +56,7 @@ "@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/parser": "^5.4.0", "cross-env": "^7.0.3", - "detox": "^20.14.2", + "detox": "^20.14.3", "eslint": "^8.41.0", "eslint-plugin-unicorn": "^47.0.0", "execa": "^5.1.1", diff --git a/examples/demo-native-android/package.json b/examples/demo-native-android/package.json index 9dd3fb923f..1bda01c4bd 100644 --- a/examples/demo-native-android/package.json +++ b/examples/demo-native-android/package.json @@ -1,13 +1,13 @@ { "name": "detox-demo-native-android", - "version": "20.14.2", + "version": "20.14.3", "private": true, "scripts": { "packager": "react-native start", "detox-server": "detox run-server" }, "devDependencies": { - "detox": "^20.14.2" + "detox": "^20.14.3" }, "detox": {} } diff --git a/examples/demo-native-ios/package.json b/examples/demo-native-ios/package.json index aba1693e80..ffd0a59d93 100644 --- a/examples/demo-native-ios/package.json +++ b/examples/demo-native-ios/package.json @@ -1,9 +1,9 @@ { "name": "detox-demo-native-ios", - "version": "20.14.2", + "version": "20.14.3", "private": true, "devDependencies": { - "detox": "^20.14.2" + "detox": "^20.14.3" }, "detox": { "specs": "", diff --git a/examples/demo-plugin/package.json b/examples/demo-plugin/package.json index 880e2b4663..c256f96df1 100644 --- a/examples/demo-plugin/package.json +++ b/examples/demo-plugin/package.json @@ -1,12 +1,12 @@ { "name": "demo-plugin", - "version": "20.14.2", + "version": "20.14.3", "private": true, "scripts": { "test:plugin": "detox test --configuration plugin -l verbose" }, "devDependencies": { - "detox": "^20.14.2", + "detox": "^20.14.3", "jest": "^28.0.0" }, "detox": { diff --git a/examples/demo-react-native-detox-instruments/package.json b/examples/demo-react-native-detox-instruments/package.json index 8fea4b06ad..88d3dd8483 100644 --- a/examples/demo-react-native-detox-instruments/package.json +++ b/examples/demo-react-native-detox-instruments/package.json @@ -1,10 +1,10 @@ { "name": "demo-react-native-detox-instruments", - "version": "20.14.2", + "version": "20.14.3", "private": true, "scripts": {}, "devDependencies": { - "detox": "^20.14.2" + "detox": "^20.14.3" }, "detox": { "configurations": { diff --git a/examples/demo-react-native/package.json b/examples/demo-react-native/package.json index c0b20fba0d..23fe69e586 100644 --- a/examples/demo-react-native/package.json +++ b/examples/demo-react-native/package.json @@ -1,6 +1,6 @@ { "name": "example", - "version": "20.14.2", + "version": "20.14.3", "private": true, "scripts": { "start": "react-native start", @@ -32,7 +32,7 @@ "@types/fs-extra": "^9.0.13", "@types/jest": "^29.2.1", "@types/react": "^18.0.24", - "detox": "^20.14.2", + "detox": "^20.14.3", "fs-extra": "^9.1.0", "jest": "^29.2.1", "ts-jest": "^29.0.3", diff --git a/lerna.json b/lerna.json index f1a35093f7..cf4d360901 100644 --- a/lerna.json +++ b/lerna.json @@ -13,7 +13,7 @@ "generation", "." ], - "version": "20.14.2", + "version": "20.14.3", "npmClient": "npm", "command": { "publish": { diff --git a/package.json b/package.json index 4e49da7577..b79e068be9 100644 --- a/package.json +++ b/package.json @@ -54,5 +54,5 @@ "shell-utils": "1.x.x", "unified": "^10.1.0" }, - "version": "20.14.2" + "version": "20.14.3" } From 87d3db89f0d0c05a931b73acf0ae8614fa6c1a8d Mon Sep 17 00:00:00 2001 From: d4vidi Date: Sun, 24 Dec 2023 13:46:52 +0200 Subject: [PATCH 055/185] Fix supervised android launch (#4303) --- .../com/wix/detox/ActivityLaunchHelper.kt | 78 ++++++++++++ .../src/full/java/com/wix/detox/Detox.java | 78 ++---------- .../src/full/java/com/wix/detox/DetoxMain.kt | 113 +++++++++++------- .../com/wix/detox/LaunchIntentsFactory.kt | 2 +- .../com/wix/detox/NotificationDataParser.kt | 2 +- .../adapters/server/DetoxActionHandlers.kt | 4 - .../adapters/server/DetoxActionsDispatcher.kt | 22 +++- .../adapters/server/DetoxServerAdapter.kt | 2 - .../detox/adapters/server/DetoxServerInfo.kt | 6 +- .../detox/reactnative/ReactMarkersLogger.kt | 44 +++++++ .../detox/reactnative/ReactNativeExtension.kt | 57 ++++----- .../com/wix/detox/espresso/UiControllerSpy.kt | 3 - .../com/wix/detox/ActivityLaunchHelperTest.kt | 111 +++++++++++++++++ .../espresso/matcher/RegexMatcherTest.kt | 3 - detox/src/android/espressoapi/Detox.js | 11 -- detox/test/e2e/15.urls-and-launchArgs.test.js | 20 ++++ detox/test/e2e/15.urls.test.js | 29 ++--- detox/test/e2e/18.user-activities.test.js | 12 +- detox/test/e2e/19.crash-handling.test.js | 8 +- detox/test/e2e/22.launch-args.test.js | 60 ++++------ detox/test/e2e/drivers/launch-args-driver.js | 33 +++++ detox/test/e2e/drivers/url-driver.js | 26 ++++ detox/test/src/Screens/LaunchUrlScreen.js | 45 +++++++ detox/test/src/Screens/index.js | 2 + detox/test/src/app.js | 58 +++------ 25 files changed, 547 insertions(+), 282 deletions(-) create mode 100644 detox/android/detox/src/full/java/com/wix/detox/ActivityLaunchHelper.kt create mode 100644 detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactMarkersLogger.kt create mode 100644 detox/android/detox/src/testFull/java/com/wix/detox/ActivityLaunchHelperTest.kt create mode 100644 detox/test/e2e/15.urls-and-launchArgs.test.js create mode 100644 detox/test/e2e/drivers/launch-args-driver.js create mode 100644 detox/test/e2e/drivers/url-driver.js create mode 100644 detox/test/src/Screens/LaunchUrlScreen.js diff --git a/detox/android/detox/src/full/java/com/wix/detox/ActivityLaunchHelper.kt b/detox/android/detox/src/full/java/com/wix/detox/ActivityLaunchHelper.kt new file mode 100644 index 0000000000..7ccc113a6d --- /dev/null +++ b/detox/android/detox/src/full/java/com/wix/detox/ActivityLaunchHelper.kt @@ -0,0 +1,78 @@ +package com.wix.detox + +import android.app.Instrumentation.ActivityMonitor +import android.content.Context +import android.content.Intent +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.ActivityTestRule + +class ActivityLaunchHelper + @JvmOverloads constructor( + private val activityTestRule: ActivityTestRule<*>, + private val launchArgs: LaunchArgs = LaunchArgs(), + private val intentsFactory: LaunchIntentsFactory = LaunchIntentsFactory(), + private val notificationDataParserGen: (String) -> NotificationDataParser = { path -> NotificationDataParser(path) } +) { + fun launchActivityUnderTest() { + val intent = extractInitialIntent() + activityTestRule.launchActivity(intent) + } + + fun launchMainActivity() { + val activity = activityTestRule.activity + launchActivitySync(intentsFactory.activityLaunchIntent(activity)) + } + + fun startActivityFromUrl(url: String) { + launchActivitySync(intentsFactory.intentWithUrl(url, false)) + } + + fun startActivityFromNotification(dataFilePath: String) { + val notificationData = notificationDataParserGen(dataFilePath).toBundle() + val intent = intentsFactory.intentWithNotificationData(appContext, notificationData, false) + launchActivitySync(intent) + } + + private fun extractInitialIntent(): Intent = + (if (launchArgs.hasUrlOverride()) { + intentsFactory.intentWithUrl(launchArgs.urlOverride, true) + } else if (launchArgs.hasNotificationPath()) { + val notificationData = notificationDataParserGen(launchArgs.notificationPath).toBundle() + intentsFactory.intentWithNotificationData(appContext, notificationData, true) + } else { + intentsFactory.cleanIntent() + }).also { + it.putExtra(INTENT_LAUNCH_ARGS_KEY, launchArgs.asIntentBundle()) + } + + private fun launchActivitySync(intent: Intent) { + // Ideally, we would just call sActivityTestRule.launchActivity(intent) and get it over with. + // BUT!!! as it turns out, Espresso has an issue where doing this for an activity running in the background + // would have Espresso set up an ActivityMonitor which will spend its time waiting for the activity to load, *without + // ever being released*. It will finally fail after a 45 seconds timeout. + // Without going into full details, it seems that activity test rules were not meant to be used this way. However, + // the all-new ActivityScenario implementation introduced in androidx could probably support this (e.g. by using + // dedicated methods such as moveToState(), which give better control over the lifecycle). + // In any case, this is the core reason for this issue: https://github.com/wix/Detox/issues/1125 + // What it forces us to do, then, is this - + // 1. Launch the activity by "ourselves" from the OS (i.e. using context.startActivity()). + // 2. Set up an activity monitor by ourselves -- such that it would block until the activity is ready. + // ^ Hence the code below. + val activity = activityTestRule.activity + val activityMonitor = ActivityMonitor(activity.javaClass.name, null, true) + activity.startActivity(intent) + + InstrumentationRegistry.getInstrumentation().run { + addMonitor(activityMonitor) + waitForMonitorWithTimeout(activityMonitor, ACTIVITY_LAUNCH_TIMEOUT) + } + } + + private val appContext: Context + get() = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext + + companion object { + private const val INTENT_LAUNCH_ARGS_KEY = "launchArgs" + private const val ACTIVITY_LAUNCH_TIMEOUT = 10000L + } +} diff --git a/detox/android/detox/src/full/java/com/wix/detox/Detox.java b/detox/android/detox/src/full/java/com/wix/detox/Detox.java index 87d211c797..93769a11c4 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/Detox.java +++ b/detox/android/detox/src/full/java/com/wix/detox/Detox.java @@ -1,18 +1,13 @@ package com.wix.detox; -import android.app.Activity; -import android.app.Instrumentation; import android.content.Context; -import android.content.Intent; -import android.os.Bundle; - -import com.wix.detox.config.DetoxConfig; -import com.wix.detox.espresso.UiControllerSpy; import androidx.annotation.NonNull; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.rule.ActivityTestRule; +import com.wix.detox.config.DetoxConfig; + /** *

Static class.

* @@ -67,12 +62,7 @@ *

If not set, then Detox tests are no ops. So it's safe to mix it with other tests.

*/ public final class Detox { - private static final String INTENT_LAUNCH_ARGS_KEY = "launchArgs"; - private static final long ACTIVITY_LAUNCH_TIMEOUT = 10000L; - - private static final LaunchArgs sLaunchArgs = new LaunchArgs(); - private static final LaunchIntentsFactory sIntentsFactory = new LaunchIntentsFactory(); - private static ActivityTestRule sActivityTestRule; + private static ActivityLaunchHelper sActivityLaunchHelper; private Detox() { } @@ -132,72 +122,20 @@ public static void runTests(ActivityTestRule activityTestRule, @NonNull final Co DetoxConfig.CONFIG = detoxConfig; DetoxConfig.CONFIG.apply(); - sActivityTestRule = activityTestRule; - - UiControllerSpy.attachThroughProxy(); - - Intent intent = extractInitialIntent(); - sActivityTestRule.launchActivity(intent); - - try { - DetoxMain.run(context); - } catch (Exception e) { - Thread.currentThread().interrupt(); - throw new RuntimeException("Detox got interrupted prematurely", e); - } + sActivityLaunchHelper = new ActivityLaunchHelper(activityTestRule); + DetoxMain.run(context, sActivityLaunchHelper); } public static void launchMainActivity() { - final Activity activity = sActivityTestRule.getActivity(); - launchActivitySync(sIntentsFactory.activityLaunchIntent(activity)); + sActivityLaunchHelper.launchMainActivity(); } public static void startActivityFromUrl(String url) { - launchActivitySync(sIntentsFactory.intentWithUrl(url, false)); + sActivityLaunchHelper.startActivityFromUrl(url); } public static void startActivityFromNotification(String dataFilePath) { - Bundle notificationData = new NotificationDataParser(dataFilePath).toBundle(); - Intent intent = sIntentsFactory.intentWithNotificationData(getAppContext(), notificationData, false); - launchActivitySync(intent); - } - - private static Intent extractInitialIntent() { - Intent intent; - - if (sLaunchArgs.hasUrlOverride()) { - intent = sIntentsFactory.intentWithUrl(sLaunchArgs.getUrlOverride(), true); - } else if (sLaunchArgs.hasNotificationPath()) { - Bundle notificationData = new NotificationDataParser(sLaunchArgs.getNotificationPath()).toBundle(); - intent = sIntentsFactory.intentWithNotificationData(getAppContext(), notificationData, true); - } else { - intent = sIntentsFactory.cleanIntent(); - } - intent.putExtra(INTENT_LAUNCH_ARGS_KEY, sLaunchArgs.asIntentBundle()); - return intent; - } - - private static void launchActivitySync(Intent intent) { - // Ideally, we would just call sActivityTestRule.launchActivity(intent) and get it over with. - // BUT!!! as it turns out, Espresso has an issue where doing this for an activity running in the background - // would have Espresso set up an ActivityMonitor which will spend its time waiting for the activity to load, *without - // ever being released*. It will finally fail after a 45 seconds timeout. - // Without going into full details, it seems that activity test rules were not meant to be used this way. However, - // the all-new ActivityScenario implementation introduced in androidx could probably support this (e.g. by using - // dedicated methods such as moveToState(), which give better control over the lifecycle). - // In any case, this is the core reason for this issue: https://github.com/wix/Detox/issues/1125 - // What it forces us to do, then, is this - - // 1. Launch the activity by "ourselves" from the OS (i.e. using context.startActivity()). - // 2. Set up an activity monitor by ourselves -- such that it would block until the activity is ready. - // ^ Hence the code below. - - final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); - final Activity activity = sActivityTestRule.getActivity(); - final Instrumentation.ActivityMonitor activityMonitor = new Instrumentation.ActivityMonitor(activity.getClass().getName(), null, true); - - activity.startActivity(intent); - instrumentation.addMonitor(activityMonitor); - instrumentation.waitForMonitorWithTimeout(activityMonitor, ACTIVITY_LAUNCH_TIMEOUT); + sActivityLaunchHelper.startActivityFromNotification(dataFilePath); } private static Context getAppContext() { diff --git a/detox/android/detox/src/full/java/com/wix/detox/DetoxMain.kt b/detox/android/detox/src/full/java/com/wix/detox/DetoxMain.kt index 5e4f0bd88b..fd5200b18c 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/DetoxMain.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/DetoxMain.kt @@ -3,35 +3,62 @@ package com.wix.detox import android.content.Context import android.util.Log import com.wix.detox.adapters.server.* -import com.wix.detox.common.DetoxLog.Companion.LOG_TAG +import com.wix.detox.common.DetoxLog +import com.wix.detox.espresso.UiControllerSpy import com.wix.detox.instruments.DetoxInstrumentsManager import com.wix.detox.reactnative.ReactNativeExtension import com.wix.invoke.MethodInvocation +import java.util.concurrent.CountDownLatch -private const val INIT_ACTION = "_init" -private const val IS_READY_ACTION = "isReady" private const val TERMINATION_ACTION = "_terminate" object DetoxMain { + private val handshakeLock = CountDownLatch(1) + @JvmStatic - fun run(rnHostHolder: Context) { + fun run(rnHostHolder: Context, activityLaunchHelper: ActivityLaunchHelper) { val detoxServerInfo = DetoxServerInfo() - Log.i(LOG_TAG, "Detox server connection details: $detoxServerInfo") - val testEngineFacade = TestEngineFacade() val actionsDispatcher = DetoxActionsDispatcher() - val externalAdapter = DetoxServerAdapter(actionsDispatcher, detoxServerInfo, IS_READY_ACTION, TERMINATION_ACTION) - initActionHandlers(actionsDispatcher, externalAdapter, testEngineFacade, rnHostHolder) - actionsDispatcher.dispatchAction(INIT_ACTION, "", 0) + val serverAdapter = DetoxServerAdapter(actionsDispatcher, detoxServerInfo, TERMINATION_ACTION) + + initCrashHandler(serverAdapter) + initANRListener(serverAdapter) + initEspresso() + initReactNative() + + setupActionHandlers(actionsDispatcher, serverAdapter, testEngineFacade, rnHostHolder) + serverAdapter.connect() + + launchActivityOnCue(rnHostHolder, activityLaunchHelper) actionsDispatcher.join() } - private fun doInit(externalAdapter: DetoxServerAdapter, rnHostHolder: Context) { - externalAdapter.connect() + /** + * Launch the tested activity "on cue", namely, right after a connection is established and the handshake + * completes successfully. + * + * This has to be synchronized so that an `isReady` isn't handled *before* the activity is launched (albeit not fully + * initialized - all native modules and everything) and a react context is available. + * + * As a better alternative, it would make sense to execute this as a simple action from within the actions + * dispatcher (i.e. handler of `loginSuccess`), in which case, no inter-thread locking would be required + * thanks to the usage of Handlers. However, in this type of a solution, errors / crashes would be reported + * not by instrumentation itself, but based on the `AppWillTerminateWithError` message; In it's own, it is a good + * thing, but for a reason we're not sure of yet, it is ignored by the test runner at this point in the flow. + */ + @Synchronized + private fun launchActivityOnCue(rnHostHolder: Context, activityLaunchHelper: ActivityLaunchHelper) { + awaitHandshake() + launchActivity(rnHostHolder, activityLaunchHelper) + } + + private fun awaitHandshake() { + handshakeLock.await() + } - initCrashHandler(externalAdapter) - initANRListener(externalAdapter) - initReactNativeIfNeeded(rnHostHolder) + private fun onLoginSuccess() { + handshakeLock.countDown() } private fun doTeardown(serverAdapter: DetoxServerAdapter, actionsDispatcher: DetoxActionsDispatcher, testEngineFacade: TestEngineFacade) { @@ -41,35 +68,28 @@ object DetoxMain { actionsDispatcher.teardown() } - private fun initActionHandlers(actionsDispatcher: DetoxActionsDispatcher, serverAdapter: DetoxServerAdapter, testEngineFacade: TestEngineFacade, rnHostHolder: Context) { + private fun setupActionHandlers(actionsDispatcher: DetoxActionsDispatcher, serverAdapter: DetoxServerAdapter, testEngineFacade: TestEngineFacade, rnHostHolder: Context) { + class SynchronizedActionHandler(private val actionHandler: DetoxActionHandler): DetoxActionHandler { + override fun handle(params: String, messageId: Long) { + synchronized(this@DetoxMain) { + actionHandler.handle(params, messageId) + } + } + } + // Primary actions with(actionsDispatcher) { - val rnReloadHandler = ReactNativeReloadActionHandler(rnHostHolder, serverAdapter, testEngineFacade) + val readyHandler = SynchronizedActionHandler( ReadyActionHandler(serverAdapter, testEngineFacade) ) + val rnReloadHandler = SynchronizedActionHandler( ReactNativeReloadActionHandler(rnHostHolder, serverAdapter, testEngineFacade) ) - associateActionHandler(INIT_ACTION, object : DetoxActionHandler { - override fun handle(params: String, messageId: Long) = - synchronized(this@DetoxMain) { - this@DetoxMain.doInit(serverAdapter, rnHostHolder) - } - }) - associateActionHandler(IS_READY_ACTION, ReadyActionHandler(serverAdapter, testEngineFacade)) - - associateActionHandler("loginSuccess", ScarceActionHandler()) - associateActionHandler("reactNativeReload", object: DetoxActionHandler { - override fun handle(params: String, messageId: Long) = - synchronized(this@DetoxMain) { - rnReloadHandler.handle(params, messageId) - } - }) + associateActionHandler("loginSuccess", ::onLoginSuccess) + associateActionHandler("isReady", readyHandler) + associateActionHandler("reactNativeReload", rnReloadHandler) associateActionHandler("invoke", InvokeActionHandler(MethodInvocation(), serverAdapter)) associateActionHandler("cleanup", CleanupActionHandler(serverAdapter, testEngineFacade) { dispatchAction(TERMINATION_ACTION, "", 0) }) - associateActionHandler(TERMINATION_ACTION, object: DetoxActionHandler { - override fun handle(params: String, messageId: Long) { - this@DetoxMain.doTeardown(serverAdapter, actionsDispatcher, testEngineFacade) - } - }) + associateActionHandler(TERMINATION_ACTION) { -> doTeardown(serverAdapter, actionsDispatcher, testEngineFacade) } if (DetoxInstrumentsManager.supports()) { val instrumentsManager = DetoxInstrumentsManager(rnHostHolder) @@ -80,13 +100,8 @@ object DetoxMain { // Secondary actions with(actionsDispatcher) { - val queryStatusHandler = QueryStatusActionHandler(serverAdapter, testEngineFacade) - associateActionHandler("currentStatus", object: DetoxActionHandler { - override fun handle(params: String, messageId: Long) = - synchronized(this@DetoxMain) { - queryStatusHandler.handle(params, messageId) - } - }, false) + val queryStatusHandler = SynchronizedActionHandler( QueryStatusActionHandler(serverAdapter, testEngineFacade) ) + associateSecondaryActionHandler("currentStatus", queryStatusHandler) } } @@ -98,7 +113,17 @@ object DetoxMain { DetoxANRHandler(outboundServerAdapter).attach() } - private fun initReactNativeIfNeeded(rnHostHolder: Context) { + private fun initEspresso() { + UiControllerSpy.attachThroughProxy() + } + + private fun initReactNative() { + ReactNativeExtension.initIfNeeded() + } + + private fun launchActivity(rnHostHolder: Context, activityLaunchHelper: ActivityLaunchHelper) { + Log.i(DetoxLog.LOG_TAG, "Launching the tested activity!") + activityLaunchHelper.launchActivityUnderTest() ReactNativeExtension.waitForRNBootstrap(rnHostHolder) } } diff --git a/detox/android/detox/src/full/java/com/wix/detox/LaunchIntentsFactory.kt b/detox/android/detox/src/full/java/com/wix/detox/LaunchIntentsFactory.kt index 43bed23a44..80f5801c23 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/LaunchIntentsFactory.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/LaunchIntentsFactory.kt @@ -6,7 +6,7 @@ import android.content.Intent import android.net.Uri import android.os.Bundle -internal class LaunchIntentsFactory { +class LaunchIntentsFactory { /** * Constructs an intent tightly associated with a specific activity. diff --git a/detox/android/detox/src/full/java/com/wix/detox/NotificationDataParser.kt b/detox/android/detox/src/full/java/com/wix/detox/NotificationDataParser.kt index 455599b825..f2e2d8a97f 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/NotificationDataParser.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/NotificationDataParser.kt @@ -5,7 +5,7 @@ import com.wix.detox.common.JsonConverter import com.wix.detox.common.TextFileReader import org.json.JSONObject -internal class NotificationDataParser(private val notificationPath: String) { +class NotificationDataParser(private val notificationPath: String) { fun toBundle(): Bundle { val rawData = readNotificationFromFile() val json = JSONObject(rawData) diff --git a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionHandlers.kt b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionHandlers.kt index ef0416465e..1404f63abf 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionHandlers.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionHandlers.kt @@ -152,7 +152,3 @@ class InstrumentsEventsActionsHandler( outboundServerAdapter.sendMessage("eventDone", emptyMap(), messageId) } } - -class ScarceActionHandler: DetoxActionHandler { - override fun handle(params: String, messageId: Long) {} -} diff --git a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionsDispatcher.kt b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionsDispatcher.kt index ac82d2c3e7..c3d83d50ff 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionsDispatcher.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxActionsDispatcher.kt @@ -11,11 +11,18 @@ class DetoxActionsDispatcher { private val primaryExec = ActionsExecutor("detox.primary") private val secondaryExec = ActionsExecutor("detox.secondary") - fun associateActionHandler(type: String, actionHandler: DetoxActionHandler, isPrimary: Boolean = true) { - val actionsExecutor = (if (isPrimary) primaryExec else secondaryExec) - actionsExecutor.associateHandler(type, actionHandler) + fun associateActionHandler(type: String, actionHandler: DetoxActionHandler) = + associateActionHandler(type, actionHandler, true) + + fun associateActionHandler(type: String, handlerFunc: () -> Unit) { + associateActionHandler(type, object: DetoxActionHandler { + override fun handle(params: String, messageId: Long) = handlerFunc() + }) } + fun associateSecondaryActionHandler(type: String, actionHandler: DetoxActionHandler) = + associateActionHandler(type, actionHandler, false) + fun dispatchAction(type: String, params: String, messageId: Long) { (primaryExec.executeAction(type, params, messageId) || secondaryExec.executeAction(type, params, messageId)) @@ -33,6 +40,11 @@ class DetoxActionsDispatcher { primaryExec.join() secondaryExec.join() } + + private fun associateActionHandler(type: String, actionHandler: DetoxActionHandler, isPrimary: Boolean = true) { + val actionsExecutor = (if (isPrimary) primaryExec else secondaryExec) + actionsExecutor.associateHandler(type, actionHandler) + } } private class ActionsExecutor(name: String) { @@ -74,7 +86,5 @@ private class ActionsExecutor(name: String) { handler.looper.quit() } - fun join() { - thread.join() - } + fun join() = thread.join() } diff --git a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerAdapter.kt b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerAdapter.kt index 2d8b11fdcf..9873a72bc3 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerAdapter.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerAdapter.kt @@ -10,7 +10,6 @@ interface OutboundServerAdapter { class DetoxServerAdapter( private val actionsDispatcher: DetoxActionsDispatcher, private val detoxServerInfo: DetoxServerInfo, - private val readyActionType: String, private val terminationActionType: String) : WebSocketClient.WSEventsHandler, OutboundServerAdapter { @@ -27,7 +26,6 @@ class DetoxServerAdapter( override fun onConnect() { Log.i(DetoxLog.LOG_TAG, "Connected to server!") - actionsDispatcher.dispatchAction(readyActionType, "", -1000L) } override fun onClosed() { diff --git a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerInfo.kt b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerInfo.kt index f2828de4ea..947c69ea27 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerInfo.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/adapters/server/DetoxServerInfo.kt @@ -1,7 +1,9 @@ package com.wix.detox.adapters.server +import android.util.Log import androidx.test.platform.app.InstrumentationRegistry import com.wix.detox.LaunchArgs +import com.wix.detox.common.DetoxLog private const val DEFAULT_URL = "ws://localhost:8099" @@ -9,7 +11,7 @@ class DetoxServerInfo internal constructor(launchArgs: LaunchArgs = LaunchArgs() val serverUrl: String = launchArgs.detoxServerUrl ?: DEFAULT_URL val sessionId: String = launchArgs.detoxSessionId ?: InstrumentationRegistry.getInstrumentation().targetContext.applicationInfo.packageName - override fun toString(): String { - return "url=$serverUrl, sessionId=$sessionId" + init { + Log.i(DetoxLog.LOG_TAG, "Detox server connection details: url=$serverUrl, sessionId=$sessionId") } } diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactMarkersLogger.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactMarkersLogger.kt new file mode 100644 index 0000000000..bbabce01d3 --- /dev/null +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactMarkersLogger.kt @@ -0,0 +1,44 @@ +package com.wix.detox.reactnative + +import android.util.Log +import com.facebook.react.bridge.ReactMarker +import com.facebook.react.bridge.ReactMarkerConstants +import com.facebook.react.bridge.ReactMarkerConstants.* + +object ReactMarkersLogger : ReactMarker.MarkerListener { + + fun attach() { + ReactMarker.addListener(this) + } + + override fun logMarker(marker: ReactMarkerConstants, p1: String?, p2: Int) { + when { + marker == DOWNLOAD_START || + marker == DOWNLOAD_END || + marker == BUILD_REACT_INSTANCE_MANAGER_START || + marker == BUILD_REACT_INSTANCE_MANAGER_END || + marker == REACT_BRIDGE_LOADING_START || + marker == REACT_BRIDGE_LOADING_END || + marker == REACT_BRIDGELESS_LOADING_START || + marker == REACT_BRIDGELESS_LOADING_END || + marker == CREATE_MODULE_START || + marker == CREATE_MODULE_END || + marker == NATIVE_MODULE_SETUP_START || + marker == NATIVE_MODULE_SETUP_END || + marker == PRE_RUN_JS_BUNDLE_START || + marker == RUN_JS_BUNDLE_START || + marker == RUN_JS_BUNDLE_END || + marker == CONTENT_APPEARED || + marker == CREATE_CATALYST_INSTANCE_START || + marker == CREATE_CATALYST_INSTANCE_END || + marker == DESTROY_CATALYST_INSTANCE_START || + marker == DESTROY_CATALYST_INSTANCE_END || + marker == CREATE_REACT_CONTEXT_START || + marker == CREATE_REACT_CONTEXT_END || + marker == PROCESS_PACKAGES_START || + marker == PROCESS_PACKAGES_END || + false -> + Log.d("Detox.RNMarker", "$marker ($p1)") + } + } +} diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeExtension.kt b/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeExtension.kt index 19e4ae2473..1711eea37c 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeExtension.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/ReactNativeExtension.kt @@ -14,6 +14,31 @@ private const val LOG_TAG = "DetoxRNExt" object ReactNativeExtension { private var rnIdlingResources: ReactNativeIdlingResources? = null + fun initIfNeeded() { + if (!ReactNativeInfo.isReactNativeApp()) { + return + } + + ReactMarkersLogger.attach() + } + + /** + * Wait for React-Native to finish loading (i.e. make RN context available). + * + * @param applicationContext The app context, implicitly assumed to be a [ReactApplication] instance. + */ + fun waitForRNBootstrap(applicationContext: Context) { + if (!ReactNativeInfo.isReactNativeApp()) { + return + } + + (applicationContext as ReactApplication).let { + val reactContext = awaitNewReactNativeContext(it, null) + + enableOrDisableSynchronization(reactContext) + } + } + /** * Reloads the React Native context and thus all javascript code. * @@ -40,26 +65,6 @@ object ReactNativeExtension { val reactContext = awaitNewReactNativeContext(it, previousReactContext) enableOrDisableSynchronization(reactContext, networkSyncEnabled) - hackRN50OrHigherWaitForReady() - } - } - - /** - * Wait for React-Native to finish loading (i.e. make RN context available). - * - * @param applicationContext The app context, implicitly assumed to be a [ReactApplication] instance. - */ - @JvmStatic - fun waitForRNBootstrap(applicationContext: Context) { - if (!ReactNativeInfo.isReactNativeApp()) { - return - } - - (applicationContext as ReactApplication).let { - val reactContext = awaitNewReactNativeContext(it, null) - - enableOrDisableSynchronization(reactContext) - hackRN50OrHigherWaitForReady() } } @@ -145,18 +150,6 @@ object ReactNativeExtension { } } - private fun hackRN50OrHigherWaitForReady() { - if (ReactNativeInfo.rnVersion().minor in 50..62) { - try { - //TODO- Temp hack to make Detox usable for RN>=50 till we find a better sync solution. - Thread.sleep(1000) - } catch (e: InterruptedException) { - e.printStackTrace() - } - - } - } - private fun clearIdlingResources() { rnIdlingResources?.unregisterAll() rnIdlingResources = null diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/UiControllerSpy.kt b/detox/android/detox/src/main/java/com/wix/detox/espresso/UiControllerSpy.kt index 742af777f0..051be7931e 100644 --- a/detox/android/detox/src/main/java/com/wix/detox/espresso/UiControllerSpy.kt +++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/UiControllerSpy.kt @@ -11,11 +11,8 @@ class UiControllerSpy: MethodsSpy("uiController") { fun eventInjectionsIterator(): Iterator = historyOf("injectMotionEvent").iterator() companion object { - @JvmStatic val instance = UiControllerSpy() - @JvmStatic - @JvmOverloads fun attachThroughProxy(spy: UiControllerSpy = instance) { val eventsInjectorReflected = EventsInjectorReflected(getUiController()) diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/ActivityLaunchHelperTest.kt b/detox/android/detox/src/testFull/java/com/wix/detox/ActivityLaunchHelperTest.kt new file mode 100644 index 0000000000..5817635353 --- /dev/null +++ b/detox/android/detox/src/testFull/java/com/wix/detox/ActivityLaunchHelperTest.kt @@ -0,0 +1,111 @@ +package com.wix.detox + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import org.mockito.kotlin.* +import androidx.test.rule.ActivityTestRule +import org.junit.runner.RunWith +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyString +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class ActivityLaunchHelperTest { + + private val initialURL = "detox://unit-test" + private val bundleExtraLaunchArgs = "launchArgs" + private val notificationPath = "path/to/notification.data" + + private lateinit var intent: Intent + private lateinit var launchArgsAsBundle: Bundle + private lateinit var notificationDataAsBundle: Bundle + private lateinit var testRule: ActivityTestRule + private lateinit var launchArgs: LaunchArgs + private lateinit var intentsFactory: LaunchIntentsFactory + private lateinit var notificationDataParser: NotificationDataParser + + private fun uut() = ActivityLaunchHelper(testRule, launchArgs, intentsFactory, { notificationDataParser }) + + @Before + fun setup() { + intent = Intent() + launchArgsAsBundle = mock() + notificationDataAsBundle = mock() + + testRule = mock() + launchArgs = mock() { + on { asIntentBundle() }.thenReturn(launchArgsAsBundle) + } + intentsFactory = mock() + notificationDataParser = mock() { + on { toBundle() }.thenReturn(notificationDataAsBundle) + } + } + + @Test + fun `default-activity -- should launch using test rule, with a clean intent`() { + givenCleanLaunch() + uut().launchActivityUnderTest() + verify(testRule).launchActivity(eq(intent)) + } + + @Test + fun `default-activity -- should apply launch args to intent`() { + givenCleanLaunch() + uut().launchActivityUnderTest() + assertIntentHasLaunchArgs() + } + + @Test + fun `default activity, with a url -- should launch based on the url`() { + givenLaunchWithInitialURL() + uut().launchActivityUnderTest() + verify(testRule).launchActivity(eq(intent)) + verify(intentsFactory).intentWithUrl(initialURL, true) + } + + @Test + fun `default activity, with a url -- should apply launch args to intent`() { + givenLaunchWithInitialURL() + uut().launchActivityUnderTest() + assertIntentHasLaunchArgs() + } + + @Test + fun `default activity, with notification data -- should launch with the data as bundle`() { + givenLaunchWithNotificationData() + uut().launchActivityUnderTest() + verify(testRule).launchActivity(eq(intent)) + verify(intentsFactory).intentWithNotificationData(any(), eq(notificationDataAsBundle), eq(true)) + } + + @Test + fun `default activity, with notification data -- should apply launch args to intent`() { + givenLaunchWithNotificationData() + uut().launchActivityUnderTest() + assertIntentHasLaunchArgs() + } + + private fun givenCleanLaunch() { + whenever(intentsFactory.cleanIntent()).thenReturn(intent) + } + private fun givenLaunchWithInitialURL() { + whenever(launchArgs.hasUrlOverride()).thenReturn(true) + whenever(launchArgs.urlOverride).thenReturn(initialURL) + whenever(intentsFactory.intentWithUrl(anyString(), anyBoolean())).thenReturn(intent) + } + private fun givenLaunchWithNotificationData() { + whenever(launchArgs.hasNotificationPath()).thenReturn(true) + whenever(launchArgs.notificationPath).thenReturn(notificationPath) + whenever(intentsFactory.intentWithNotificationData(any(), any(), anyBoolean())) + .thenReturn(intent) + } + private fun assertIntentHasLaunchArgs() { + assertThat(intent.hasExtra(bundleExtraLaunchArgs)).isTrue + assertThat(intent.getBundleExtra(bundleExtraLaunchArgs)).isEqualTo(launchArgsAsBundle) + } +} diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/matcher/RegexMatcherTest.kt b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/matcher/RegexMatcherTest.kt index 4f07654686..78f52cb84e 100644 --- a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/matcher/RegexMatcherTest.kt +++ b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/matcher/RegexMatcherTest.kt @@ -3,10 +3,7 @@ package com.wix.detox.espresso.matcher import org.junit.Test import kotlin.test.assertFalse import kotlin.test.assertTrue -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -@RunWith(RobolectricTestRunner::class) class RegexMatcherTest { @Test fun `should work with string matching regex`() { diff --git a/detox/src/android/espressoapi/Detox.js b/detox/src/android/espressoapi/Detox.js index bbba04e3a2..dfa11192fa 100644 --- a/detox/src/android/espressoapi/Detox.js +++ b/detox/src/android/espressoapi/Detox.js @@ -58,17 +58,6 @@ class Detox { }; } - static extractInitialIntent() { - return { - target: { - type: "Class", - value: "com.wix.detox.Detox" - }, - method: "extractInitialIntent", - args: [] - }; - } - static getAppContext() { return { target: { diff --git a/detox/test/e2e/15.urls-and-launchArgs.test.js b/detox/test/e2e/15.urls-and-launchArgs.test.js new file mode 100644 index 0000000000..cd795dbcbe --- /dev/null +++ b/detox/test/e2e/15.urls-and-launchArgs.test.js @@ -0,0 +1,20 @@ +const { urlDriver } = require('./drivers/url-driver'); +const { launchArgsDriver } = require('./drivers/launch-args-driver'); + +describe(':android: Launch arguments while handing launch URLs', () => { + it('should pass user args in normally', async () => { + const userArgs = { + how: 'about some', + pie: '3.14', + }; + const detoxLaunchArgs = urlDriver.withDetoxArgs.andUserArgs(userArgs); + + await device.launchApp({ newInstance: true, ...detoxLaunchArgs }); + await urlDriver.navToUrlScreen(); + await urlDriver.assertUrl(detoxLaunchArgs.url); + + await device.reloadReactNative(); + await launchArgsDriver.navToLaunchArgsScreen(); + await launchArgsDriver.assertLaunchArgs(userArgs); + }); +}); diff --git a/detox/test/e2e/15.urls.test.js b/detox/test/e2e/15.urls.test.js index 70de0cfdfd..e36b5937e5 100644 --- a/detox/test/e2e/15.urls.test.js +++ b/detox/test/e2e/15.urls.test.js @@ -1,3 +1,5 @@ +const { urlDriver } = require('./drivers/url-driver'); + describe('Open URLs', () => { afterAll(async () => { await device.launchApp({ @@ -7,38 +9,31 @@ describe('Open URLs', () => { }); }); - const withDefaultArgs = () => ({ - url: 'detoxtesturlscheme://such-string?arg1=first&arg2=second', - launchArgs: undefined, - }); - - const withSingleInstanceActivityArgs = () => ({ - url: 'detoxtesturlscheme.singleinstance://such-string', - launchArgs: { detoxAndroidSingleInstanceActivity: true }, - }); - describe.each([ - ['(default)', withDefaultArgs()], - [':android: (single activity)', withSingleInstanceActivityArgs()], + ['(default)', urlDriver.withDetoxArgs.default()], + [':android: (single activity)', urlDriver.withDetoxArgs.forSingleInstanceActivityLaunch()], ])('%s', (_platform, {url, launchArgs}) => { it(`device.launchApp() with a URL and a fresh app should launch app and trigger handling open url handling in app`, async () => { await device.launchApp({newInstance: true, url, launchArgs}); - await expect(element(by.text(url))).toBeVisible(); + await urlDriver.navToUrlScreen(); + await urlDriver.assertUrl(url); }); it(`device.openURL() should trigger open url handling in app when app is in foreground`, async () => { await device.launchApp({newInstance: true, launchArgs}); - await expect(element(by.text(url))).not.toBeVisible(); + await urlDriver.navToUrlScreen(); + await urlDriver.assertNoUrl(url); await device.openURL({url}); - await expect(element(by.text(url))).toBeVisible(); + await urlDriver.assertUrl(url); }); it(`device.launchApp() with a URL should trigger url handling when app is in background`, async () => { await device.launchApp({newInstance: true, launchArgs}); - await expect(element(by.text(url))).not.toBeVisible(); + await urlDriver.navToUrlScreen(); + await urlDriver.assertNoUrl(url); await device.sendToHome(); await device.launchApp({newInstance: false, url}); - await expect(element(by.text(url))).toBeVisible(); + await urlDriver.assertUrl(url); }); }); }); diff --git a/detox/test/e2e/18.user-activities.test.js b/detox/test/e2e/18.user-activities.test.js index 722a8809cc..c108ba2236 100644 --- a/detox/test/e2e/18.user-activities.test.js +++ b/detox/test/e2e/18.user-activities.test.js @@ -1,23 +1,27 @@ +const { urlDriver } = require('./drivers/url-driver'); const DetoxConstants = require('detox').DetoxConstants; describe(':ios: User Activity', () => { it('Init from browsing web', async () => { // await device.__debug_sleep(10000); await device.launchApp({newInstance: true, userActivity: userActivityBrowsingWeb}); - await expect(element(by.text('https://my.deeplink.dtx'))).toBeVisible(); + await urlDriver.navToUrlScreen(); + await urlDriver.assertUrl('https://my.deeplink.dtx'); }); it('Background searchable item', async () => { await device.launchApp({newInstance: true}); + await urlDriver.navToUrlScreen(); await device.sendToHome(); await device.launchApp({newInstance: false, userActivity: userActivitySearchableItem}); - await expect(element(by.text('com.test.itemId'))).toBeVisible(); + await urlDriver.assertUrl('com.test.itemId'); }); it('Foreground browsing web', async () => { await device.launchApp({newInstance: true}); + await urlDriver.navToUrlScreen(); await device.sendUserActivity(userActivityBrowsingWeb); - await expect(element(by.text('https://my.deeplink.dtx'))).toBeVisible(); + await urlDriver.assertUrl('https://my.deeplink.dtx'); }); }); @@ -31,4 +35,4 @@ const userActivitySearchableItem = { "activityType": DetoxConstants.userActivityTypes.searchableItem, "userInfo": {} }; -userActivitySearchableItem.userInfo[DetoxConstants.searchableItemActivityIdentifier] = "com.test.itemId" \ No newline at end of file +userActivitySearchableItem.userInfo[DetoxConstants.searchableItemActivityIdentifier] = "com.test.itemId" diff --git a/detox/test/e2e/19.crash-handling.test.js b/detox/test/e2e/19.crash-handling.test.js index 1912538afa..0c1f120215 100644 --- a/detox/test/e2e/19.crash-handling.test.js +++ b/detox/test/e2e/19.crash-handling.test.js @@ -51,12 +51,14 @@ describe('Crash Handling', () => { it(':android: Should throw a detailed error upon app bootstrap crash', async () => { const error = await expectToThrow( () => relaunchAppWithArgs({ detoxAndroidCrashingActivity: true }), - 'Failed to run application on the device'); + 'The app has crashed, see the details below:'); // It's important that the native-error message (containing the native stack-trace) would also // be included in the error's stack property, in order for Jest (specifically) to properly output all // of that into the shell, as we expect it to. - jestExpect(error.stack).toContain('Native stacktrace dump:\njava.lang.IllegalStateException: This is an intentional crash!'); - jestExpect(error.stack).toContain('\tat com.example.CrashingActivity.onResume'); + jestExpect(error.stack).toContain('java.lang.RuntimeException: Unable to resume activity'); + + // In particular, we want the original cause to be bundled in. + jestExpect(error.stack).toContain('Caused by: java.lang.IllegalStateException: This is an intentional crash!'); }, 60000); }); diff --git a/detox/test/e2e/22.launch-args.test.js b/detox/test/e2e/22.launch-args.test.js index a87bc5ab03..201a02397d 100644 --- a/detox/test/e2e/22.launch-args.test.js +++ b/detox/test/e2e/22.launch-args.test.js @@ -1,5 +1,6 @@ /* global by, device, element */ const _ = require('lodash'); +const { launchArgsDriver: driver } = require('./drivers/launch-args-driver'); // Note: Android-only as, according to Leo, on iOS there's no added value here compared to // existing tests that check deep-link URLs. Combined with the fact that we do not yet @@ -13,26 +14,27 @@ describe(':android: Launch arguments', () => { beforeEach(async () => { await device.selectApp('exampleWithArgs'); - assertPreconfiguredValues(device.appLaunchArgs.get(), defaultArgs); + driver.assertPreconfiguredValues(device.appLaunchArgs.get(), defaultArgs); }); it('should preserve a shared arg in spite of app reselection', async () => { const override = { ama: 'zed' }; try { - assertPreconfiguredValues(device.appLaunchArgs.get(), defaultArgs); - assertPreconfiguredValues(device.appLaunchArgs.shared.get(), {}); + driver.assertPreconfiguredValues(device.appLaunchArgs.get(), defaultArgs); + driver.assertPreconfiguredValues(device.appLaunchArgs.shared.get(), {}); device.appLaunchArgs.shared.modify(override); - assertPreconfiguredValues(device.appLaunchArgs.get(), { ...defaultArgs, ...override }); - assertPreconfiguredValues(device.appLaunchArgs.shared.get(), override); + driver.assertPreconfiguredValues(device.appLaunchArgs.get(), { ...defaultArgs, ...override }); + driver.assertPreconfiguredValues(device.appLaunchArgs.shared.get(), override); await device.selectApp('example'); - assertPreconfiguredValues(device.appLaunchArgs.get(), override); - assertPreconfiguredValues(device.appLaunchArgs.shared.get(), override); + driver.assertPreconfiguredValues(device.appLaunchArgs.get(), override); + driver.assertPreconfiguredValues(device.appLaunchArgs.shared.get(), override); await device.launchApp({ newInstance: true }); - await assertLaunchArgs(override); + await driver.navToLaunchArgsScreen(); + await driver.assertLaunchArgs(override); } finally { device.appLaunchArgs.shared.reset(); } @@ -46,7 +48,8 @@ describe(':android: Launch arguments', () => { }; await device.launchApp({ newInstance: true, launchArgs }); - await assertLaunchArgs(launchArgs); + await driver.navToLaunchArgsScreen(); + await driver.assertLaunchArgs(launchArgs); }); it('should handle complex args when used on-site', async () => { @@ -61,7 +64,8 @@ describe(':android: Launch arguments', () => { }; await device.launchApp({ newInstance: true, launchArgs }); - await assertLaunchArgs({ + await driver.navToLaunchArgsScreen(); + await driver.assertLaunchArgs({ complex: JSON.stringify(launchArgs.complex), complexlist: JSON.stringify(launchArgs.complexlist), }); @@ -75,7 +79,8 @@ describe(':android: Launch arguments', () => { }); await device.launchApp({ newInstance: true }); - await assertLaunchArgs({ + await driver.navToLaunchArgsScreen(); + await driver.assertLaunchArgs({ 'goo': 'gle!', 'ama': 'zon', 'micro': 'soft', @@ -93,7 +98,8 @@ describe(':android: Launch arguments', () => { }); await device.launchApp({ newInstance: true, launchArgs }); - await assertLaunchArgs({ anArg: 'aValue!' }); + await driver.navToLaunchArgsScreen(); + await driver.assertLaunchArgs({ anArg: 'aValue!' }); }); // Ref: https://developer.android.com/studio/test/command-line#AMOptionsSyntax @@ -106,33 +112,7 @@ describe(':android: Launch arguments', () => { }; await device.launchApp({ newInstance: true, launchArgs }); - await assertLaunchArgs({ hello: 'world' }, ['debug', 'log', 'size']); + await driver.navToLaunchArgsScreen(); + await driver.assertLaunchArgs({ hello: 'world' }, ['debug', 'log', 'size']); }); - - async function assertLaunchArgs(expected, notExpected) { - await element(by.text('Launch Args')).tap(); - - if (expected) { - for (const [key, value] of Object.entries(expected)) { - await expect(element(by.id(`launchArg-${key}.name`))).toBeVisible(); - await expect(element(by.id(`launchArg-${key}.value`))).toHaveText(`${value}`); - } - } - - if (notExpected) { - for (const key of notExpected) { - await expect(element(by.id(`launchArg-${key}.name`))).not.toBeVisible(); - } - } - } - - function assertPreconfiguredValues(initArgs, expectedInitArgs) { - if (!_.isEqual(initArgs, expectedInitArgs)) { - throw new Error( - `Precondition failure: Preconfigured launch arguments (in detox.config.js) do not match the expected value.\n` + - `Expected: ${JSON.stringify(expectedInitArgs)}\n` + - `Received: ${JSON.stringify(initArgs)}` - ); - } - } }); diff --git a/detox/test/e2e/drivers/launch-args-driver.js b/detox/test/e2e/drivers/launch-args-driver.js new file mode 100644 index 0000000000..fcf3d168e7 --- /dev/null +++ b/detox/test/e2e/drivers/launch-args-driver.js @@ -0,0 +1,33 @@ +const _ = require("lodash"); +const driver = { + navToLaunchArgsScreen: () => element(by.text('Launch Args')).tap(), + + assertPreconfiguredValues: (initArgs, expectedInitArgs) => { + if (!_.isEqual(initArgs, expectedInitArgs)) { + throw new Error( + `Precondition failure: Preconfigured launch arguments (in detox.config.js) do not match the expected value.\n` + + `Expected: ${JSON.stringify(expectedInitArgs)}\n` + + `Received: ${JSON.stringify(initArgs)}` + ); + } + }, + + assertLaunchArgs: async (expected, notExpected) => { + if (expected) { + for (const [key, value] of Object.entries(expected)) { + await expect(element(by.id(`launchArg-${key}.name`))).toBeVisible(); + await expect(element(by.id(`launchArg-${key}.value`))).toHaveText(`${value}`); + } + } + + if (notExpected) { + for (const key of notExpected) { + await expect(element(by.id(`launchArg-${key}.name`))).not.toBeVisible(); + } + } + } +} + +module.exports = { + launchArgsDriver: driver, +}; diff --git a/detox/test/e2e/drivers/url-driver.js b/detox/test/e2e/drivers/url-driver.js new file mode 100644 index 0000000000..c41f084380 --- /dev/null +++ b/detox/test/e2e/drivers/url-driver.js @@ -0,0 +1,26 @@ +const driver = { + withDetoxArgs: { + default: () => ({ + url: 'detoxtesturlscheme://such-string?arg1=first&arg2=second', + launchArgs: undefined, + }), + + andUserArgs: (launchArgs) => ({ + url: 'detoxtesturlscheme', + launchArgs, + }), + + forSingleInstanceActivityLaunch: () => ({ + url: 'detoxtesturlscheme.singleinstance://such-string', + launchArgs: { detoxAndroidSingleInstanceActivity: true }, + }), + }, + + navToUrlScreen: () => element(by.text('Init URL')).tap(), + assertUrl: (url) => expect(element(by.text(url))).toBeVisible(), + assertNoUrl: (url) => expect(element(by.text(url))).not.toBeVisible(), +}; + +module.exports = { + urlDriver: driver, +}; diff --git a/detox/test/src/Screens/LaunchUrlScreen.js b/detox/test/src/Screens/LaunchUrlScreen.js new file mode 100644 index 0000000000..a4f35dc967 --- /dev/null +++ b/detox/test/src/Screens/LaunchUrlScreen.js @@ -0,0 +1,45 @@ +import React, { Component } from 'react'; +import { + Linking, + Text, + View, +} from 'react-native'; + +export default class LaunchUrlScreen extends Component { + + constructor(props) { + super(props); + + Linking.addEventListener('url', (params) => this._handleOpenURL(params)); + + this.state = { + url: undefined, + } + } + + async componentDidMount() { + const url = await Linking.getInitialURL(); + this.setState({ + url, + }); + } + + renderText(text) { + return ( + + + {text} + + + ); + } + + render() { + return this.renderText(this.state.url); + } + + _handleOpenURL(params) { + console.log('App@handleOpenURL:', params); + this.setState({url: params.url}); + } +} diff --git a/detox/test/src/Screens/index.js b/detox/test/src/Screens/index.js index dbc95dfa54..68852e6e17 100644 --- a/detox/test/src/Screens/index.js +++ b/detox/test/src/Screens/index.js @@ -17,6 +17,7 @@ import LocationScreen from './LocationScreen'; import ShakeScreen from './ShakeScreen'; import DatePickerScreen from './DatePickerScreen'; import LanguageScreen from './LanguageScreen'; +import LaunchUrlScreen from './LaunchUrlScreen'; import LaunchArgsScreen from './LaunchArgsScreen'; import LaunchNotificationScreen from './LaunchNotificationScreen'; import PickerViewScreen from './PickerViewScreen'; @@ -50,6 +51,7 @@ export { DatePickerScreen, PickerViewScreen, LanguageScreen, + LaunchUrlScreen, LaunchArgsScreen, LaunchNotificationScreen, DeviceScreen, diff --git a/detox/test/src/app.js b/detox/test/src/app.js index 7753962799..2e55a95ec4 100644 --- a/detox/test/src/app.js +++ b/detox/test/src/app.js @@ -3,7 +3,6 @@ import { Text, View, TouchableOpacity, - Linking, Platform, NativeModules, } from 'react-native'; @@ -21,22 +20,13 @@ export default class example extends Component { this.state = { screen: undefined, screenProps: {}, - url: undefined, notification: undefined, }; - Linking.addEventListener('url', (params) => this._handleOpenURL(params)); - this.setScreen = this.setScreen.bind(this); } - async componentDidMount() { - const url = await Linking.getInitialURL(); - if (url) { - console.log('App@didMount: Found pending URL', url); - this.setState({url: url}); - } - } + async componentDidMount() {} renderButton(title, onPressCallback) { return ( @@ -68,33 +58,11 @@ export default class example extends Component { }); } - renderText(text) { - return ( - - - {text} - - - ); - } - renderInlineSeparator() { return | ; } - render() { - if (this.state.url) { - console.log('App@render: rendering a URL:', this.state.url); - return this.renderText(this.state.url); - } - - if (this.state.screen) { - console.log('App@render: JS rendering screen'); - const Screen = this.state.screen; - return ; - } - - console.log('App@render: JS rendering main screen'); + renderMainMenu() { return ( @@ -150,12 +118,29 @@ export default class example extends Component { {this.renderScreenButton('Element-Screenshots', Screens.ElementScreenshotScreen)} + {this.renderScreenButton('Init URL', Screens.LaunchUrlScreen)} + {isAndroid && this.renderInlineSeparator()} {isAndroid && this.renderScreenButton('Launch Args', Screens.LaunchArgsScreen)} {isAndroid && this.renderInlineSeparator()} {isAndroid && this.renderScreenButton('Launch-Notification', Screens.LaunchNotificationScreen)} ); + } + + renderInnerScreen() { + const Screen = this.state.screen; + return ; + } + + render() { + if (this.state.screen) { + console.log('App@render: JS rendering screen'); + return this.renderInnerScreen(); + } + + console.log('App@render: JS rendering main screen'); + return this.renderMainMenu(); } setScreen(name) { @@ -178,9 +163,4 @@ export default class example extends Component { ); } - - _handleOpenURL(params) { - console.log('App@handleOpenURL:', params); - this.setState({url: params.url}); - } } From 7e6ab808abdf8754a120bca27b4a1d998883cdc2 Mon Sep 17 00:00:00 2001 From: wixmobile Date: Sun, 24 Dec 2023 04:32:57 -0800 Subject: [PATCH 056/185] Publish 20.14.4 [ci skip] --- detox/package.json | 2 +- detox/test/package.json | 4 ++-- examples/demo-native-android/package.json | 4 ++-- examples/demo-native-ios/package.json | 4 ++-- examples/demo-plugin/package.json | 4 ++-- examples/demo-react-native-detox-instruments/package.json | 4 ++-- examples/demo-react-native/package.json | 4 ++-- lerna.json | 2 +- package.json | 2 +- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/detox/package.json b/detox/package.json index a0f284345e..08f5c4a646 100644 --- a/detox/package.json +++ b/detox/package.json @@ -1,7 +1,7 @@ { "name": "detox", "description": "E2E tests and automation for mobile", - "version": "20.14.3", + "version": "20.14.4", "bin": { "detox": "local-cli/cli.js" }, diff --git a/detox/test/package.json b/detox/test/package.json index 79bdbe0c2a..a47c96c45c 100644 --- a/detox/test/package.json +++ b/detox/test/package.json @@ -1,6 +1,6 @@ { "name": "detox-test", - "version": "20.14.3", + "version": "20.14.4", "private": true, "engines": { "node": ">=14.5.0" @@ -56,7 +56,7 @@ "@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/parser": "^5.4.0", "cross-env": "^7.0.3", - "detox": "^20.14.3", + "detox": "^20.14.4", "eslint": "^8.41.0", "eslint-plugin-unicorn": "^47.0.0", "execa": "^5.1.1", diff --git a/examples/demo-native-android/package.json b/examples/demo-native-android/package.json index 1bda01c4bd..bd882dada4 100644 --- a/examples/demo-native-android/package.json +++ b/examples/demo-native-android/package.json @@ -1,13 +1,13 @@ { "name": "detox-demo-native-android", - "version": "20.14.3", + "version": "20.14.4", "private": true, "scripts": { "packager": "react-native start", "detox-server": "detox run-server" }, "devDependencies": { - "detox": "^20.14.3" + "detox": "^20.14.4" }, "detox": {} } diff --git a/examples/demo-native-ios/package.json b/examples/demo-native-ios/package.json index ffd0a59d93..f70e627f68 100644 --- a/examples/demo-native-ios/package.json +++ b/examples/demo-native-ios/package.json @@ -1,9 +1,9 @@ { "name": "detox-demo-native-ios", - "version": "20.14.3", + "version": "20.14.4", "private": true, "devDependencies": { - "detox": "^20.14.3" + "detox": "^20.14.4" }, "detox": { "specs": "", diff --git a/examples/demo-plugin/package.json b/examples/demo-plugin/package.json index c256f96df1..85d5597952 100644 --- a/examples/demo-plugin/package.json +++ b/examples/demo-plugin/package.json @@ -1,12 +1,12 @@ { "name": "demo-plugin", - "version": "20.14.3", + "version": "20.14.4", "private": true, "scripts": { "test:plugin": "detox test --configuration plugin -l verbose" }, "devDependencies": { - "detox": "^20.14.3", + "detox": "^20.14.4", "jest": "^28.0.0" }, "detox": { diff --git a/examples/demo-react-native-detox-instruments/package.json b/examples/demo-react-native-detox-instruments/package.json index 88d3dd8483..c907162bff 100644 --- a/examples/demo-react-native-detox-instruments/package.json +++ b/examples/demo-react-native-detox-instruments/package.json @@ -1,10 +1,10 @@ { "name": "demo-react-native-detox-instruments", - "version": "20.14.3", + "version": "20.14.4", "private": true, "scripts": {}, "devDependencies": { - "detox": "^20.14.3" + "detox": "^20.14.4" }, "detox": { "configurations": { diff --git a/examples/demo-react-native/package.json b/examples/demo-react-native/package.json index 23fe69e586..be984bbf56 100644 --- a/examples/demo-react-native/package.json +++ b/examples/demo-react-native/package.json @@ -1,6 +1,6 @@ { "name": "example", - "version": "20.14.3", + "version": "20.14.4", "private": true, "scripts": { "start": "react-native start", @@ -32,7 +32,7 @@ "@types/fs-extra": "^9.0.13", "@types/jest": "^29.2.1", "@types/react": "^18.0.24", - "detox": "^20.14.3", + "detox": "^20.14.4", "fs-extra": "^9.1.0", "jest": "^29.2.1", "ts-jest": "^29.0.3", diff --git a/lerna.json b/lerna.json index cf4d360901..1a5ec04427 100644 --- a/lerna.json +++ b/lerna.json @@ -13,7 +13,7 @@ "generation", "." ], - "version": "20.14.3", + "version": "20.14.4", "npmClient": "npm", "command": { "publish": { diff --git a/package.json b/package.json index b79e068be9..fa059acb82 100644 --- a/package.json +++ b/package.json @@ -54,5 +54,5 @@ "shell-utils": "1.x.x", "unified": "^10.1.0" }, - "version": "20.14.3" + "version": "20.14.4" } From 1b449469be1ed70b335a758a6523f2615d767a9e Mon Sep 17 00:00:00 2001 From: Georgy Date: Sun, 24 Dec 2023 18:02:18 +0200 Subject: [PATCH 057/185] Fixed crash cause by new version of espresso (#4305) Co-authored-by: d4vidi --- detox/android/detox/proguard-rules-app.pro | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/detox/android/detox/proguard-rules-app.pro b/detox/android/detox/proguard-rules-app.pro index 1932cf4105..6fa919bbba 100644 --- a/detox/android/detox/proguard-rules-app.pro +++ b/detox/android/detox/proguard-rules-app.pro @@ -20,5 +20,4 @@ -keep class kotlin.io.** { *; } -keep class okhttp3.** { *; } --keep class androidx.concurrent.futures.CallbackToFutureAdapter$* { *; } --keep class androidx.concurrent.futures.CallbackToFutureAdapter { *; } +-keep class androidx.concurrent.futures.** { *; } From 826ba256c4a629c12497396c612cdff98794efc8 Mon Sep 17 00:00:00 2001 From: wixmobile Date: Sun, 24 Dec 2023 08:46:12 -0800 Subject: [PATCH 058/185] Publish 20.14.5 [ci skip] --- detox/package.json | 2 +- detox/test/package.json | 4 ++-- examples/demo-native-android/package.json | 4 ++-- examples/demo-native-ios/package.json | 4 ++-- examples/demo-plugin/package.json | 4 ++-- examples/demo-react-native-detox-instruments/package.json | 4 ++-- examples/demo-react-native/package.json | 4 ++-- lerna.json | 2 +- package.json | 2 +- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/detox/package.json b/detox/package.json index 08f5c4a646..7f3c4f2167 100644 --- a/detox/package.json +++ b/detox/package.json @@ -1,7 +1,7 @@ { "name": "detox", "description": "E2E tests and automation for mobile", - "version": "20.14.4", + "version": "20.14.5", "bin": { "detox": "local-cli/cli.js" }, diff --git a/detox/test/package.json b/detox/test/package.json index a47c96c45c..82c26ae4e7 100644 --- a/detox/test/package.json +++ b/detox/test/package.json @@ -1,6 +1,6 @@ { "name": "detox-test", - "version": "20.14.4", + "version": "20.14.5", "private": true, "engines": { "node": ">=14.5.0" @@ -56,7 +56,7 @@ "@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/parser": "^5.4.0", "cross-env": "^7.0.3", - "detox": "^20.14.4", + "detox": "^20.14.5", "eslint": "^8.41.0", "eslint-plugin-unicorn": "^47.0.0", "execa": "^5.1.1", diff --git a/examples/demo-native-android/package.json b/examples/demo-native-android/package.json index bd882dada4..1b1102bec3 100644 --- a/examples/demo-native-android/package.json +++ b/examples/demo-native-android/package.json @@ -1,13 +1,13 @@ { "name": "detox-demo-native-android", - "version": "20.14.4", + "version": "20.14.5", "private": true, "scripts": { "packager": "react-native start", "detox-server": "detox run-server" }, "devDependencies": { - "detox": "^20.14.4" + "detox": "^20.14.5" }, "detox": {} } diff --git a/examples/demo-native-ios/package.json b/examples/demo-native-ios/package.json index f70e627f68..83e7c39980 100644 --- a/examples/demo-native-ios/package.json +++ b/examples/demo-native-ios/package.json @@ -1,9 +1,9 @@ { "name": "detox-demo-native-ios", - "version": "20.14.4", + "version": "20.14.5", "private": true, "devDependencies": { - "detox": "^20.14.4" + "detox": "^20.14.5" }, "detox": { "specs": "", diff --git a/examples/demo-plugin/package.json b/examples/demo-plugin/package.json index 85d5597952..da3be16b23 100644 --- a/examples/demo-plugin/package.json +++ b/examples/demo-plugin/package.json @@ -1,12 +1,12 @@ { "name": "demo-plugin", - "version": "20.14.4", + "version": "20.14.5", "private": true, "scripts": { "test:plugin": "detox test --configuration plugin -l verbose" }, "devDependencies": { - "detox": "^20.14.4", + "detox": "^20.14.5", "jest": "^28.0.0" }, "detox": { diff --git a/examples/demo-react-native-detox-instruments/package.json b/examples/demo-react-native-detox-instruments/package.json index c907162bff..27ff3bc84a 100644 --- a/examples/demo-react-native-detox-instruments/package.json +++ b/examples/demo-react-native-detox-instruments/package.json @@ -1,10 +1,10 @@ { "name": "demo-react-native-detox-instruments", - "version": "20.14.4", + "version": "20.14.5", "private": true, "scripts": {}, "devDependencies": { - "detox": "^20.14.4" + "detox": "^20.14.5" }, "detox": { "configurations": { diff --git a/examples/demo-react-native/package.json b/examples/demo-react-native/package.json index be984bbf56..b00354ced1 100644 --- a/examples/demo-react-native/package.json +++ b/examples/demo-react-native/package.json @@ -1,6 +1,6 @@ { "name": "example", - "version": "20.14.4", + "version": "20.14.5", "private": true, "scripts": { "start": "react-native start", @@ -32,7 +32,7 @@ "@types/fs-extra": "^9.0.13", "@types/jest": "^29.2.1", "@types/react": "^18.0.24", - "detox": "^20.14.4", + "detox": "^20.14.5", "fs-extra": "^9.1.0", "jest": "^29.2.1", "ts-jest": "^29.0.3", diff --git a/lerna.json b/lerna.json index 1a5ec04427..5bdf06bace 100644 --- a/lerna.json +++ b/lerna.json @@ -13,7 +13,7 @@ "generation", "." ], - "version": "20.14.4", + "version": "20.14.5", "npmClient": "npm", "command": { "publish": { diff --git a/package.json b/package.json index fa059acb82..1d538a54c4 100644 --- a/package.json +++ b/package.json @@ -54,5 +54,5 @@ "shell-utils": "1.x.x", "unified": "^10.1.0" }, - "version": "20.14.4" + "version": "20.14.5" } From a603040b577ec16cc859d1a74c5f817db863775d Mon Sep 17 00:00:00 2001 From: sonnguyenduc <127935387+sonnguyenduc@users.noreply.github.com> Date: Wed, 27 Dec 2023 16:31:49 +0700 Subject: [PATCH 059/185] fix bug --- .../com/wix/detox/espresso/DetoxAction.java | 8 ++++- .../com/wix/detox/espresso/DetoxMatcher.java | 2 +- .../detox/espresso/action/RNClickAction.java | 2 +- .../AnimatedModuleIdlingResource.java | 34 ++++++++++++++++++- detox/scripts/build_framework.ios.sh | 4 ++- package.json | 3 +- 6 files changed, 47 insertions(+), 6 deletions(-) diff --git a/detox/android/detox/src/full/java/com/wix/detox/espresso/DetoxAction.java b/detox/android/detox/src/full/java/com/wix/detox/espresso/DetoxAction.java index 8474e3526d..d96ebdb1df 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/espresso/DetoxAction.java +++ b/detox/android/detox/src/full/java/com/wix/detox/espresso/DetoxAction.java @@ -170,7 +170,13 @@ public static ViewAction setDatePickerDate(String dateString, String formatStrin Calendar cal = Calendar.getInstance(); cal.setTime(date); - return PickerActions.setDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH)); + + // if formatString contain hh:mm, we need to set time + if (formatString.toLowerCase().contains("hh:mm")) { + return PickerActions.setTime(cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE)); + } else { + return PickerActions.setDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH)); + } } public static ViewAction adjustSliderToPosition(final double newPosition) { diff --git a/detox/android/detox/src/full/java/com/wix/detox/espresso/DetoxMatcher.java b/detox/android/detox/src/full/java/com/wix/detox/espresso/DetoxMatcher.java index f9a23fcaaa..329309125c 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/espresso/DetoxMatcher.java +++ b/detox/android/detox/src/full/java/com/wix/detox/espresso/DetoxMatcher.java @@ -90,7 +90,7 @@ public static Matcher matcherForClass(String className) { } public static Matcher matcherForSufficientlyVisible(int pct) { - return isDisplayingAtLeast(pct); + return isDisplayingAtLeast(pct > 50 ? 50 : pct); } public static Matcher matcherForNotVisible() { diff --git a/detox/android/detox/src/full/java/com/wix/detox/espresso/action/RNClickAction.java b/detox/android/detox/src/full/java/com/wix/detox/espresso/action/RNClickAction.java index 68bf039064..6f395a6a6d 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/espresso/action/RNClickAction.java +++ b/detox/android/detox/src/full/java/com/wix/detox/espresso/action/RNClickAction.java @@ -40,7 +40,7 @@ public RNClickAction(CoordinatesProvider coordinatesProvider) { @Override public Matcher getConstraints() { - return isDisplayingAtLeast(75); + return isDisplayingAtLeast(50); } @Override diff --git a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/AnimatedModuleIdlingResource.java b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/AnimatedModuleIdlingResource.java index 7381312973..2991418b49 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/AnimatedModuleIdlingResource.java +++ b/detox/android/detox/src/full/java/com/wix/detox/reactnative/idlingresources/AnimatedModuleIdlingResource.java @@ -55,8 +55,20 @@ public class AnimatedModuleIdlingResource implements DescriptiveIdlingResource, private final static String METHOD_HAS_ACTIVE_ANIMATIONS = "hasActiveAnimations"; + // max timeout is 5 seconds + private final static long MAX_TIMEOUT = 5000L; + // loading max timeout is 15 seconds + private final static long MAX_LOADING_TIMEOUT = 15000L; + // assume it idle for 1 seconds + private final static long IDLE_TIME = 1000L; + + private static boolean isInit = true; + + private static long last_idle_time = System.currentTimeMillis(); + private final static Map busyHint = new HashMap() {{ - put("reason", "Animations running on screen"); + float busy_time = System.currentTimeMillis() - last_idle_time; + put("reason", "Animations running on screen: " + busy_time + " ms"); }}; private ResourceCallback callback = null; @@ -85,6 +97,21 @@ public Map getBusyHint() { @Override public boolean isIdleNow() { + // if busy for more than max_timeout seconds, we assume it's stuck + // the longest animation we have is loading screen, which is only showed at initial + final long max_timeout = isInit ? MAX_LOADING_TIMEOUT : MAX_TIMEOUT; + if (System.currentTimeMillis() - last_idle_time > max_timeout) { + Log.e(LOG_TAG, "AnimatedModule is stuck."); + if (callback != null) { + callback.onTransitionToIdle(); + } + if (System.currentTimeMillis() - last_idle_time > max_timeout + IDLE_TIME) { + isInit = false; + last_idle_time = System.currentTimeMillis(); + } + return true; + } + Class animModuleClass = null; try { animModuleClass = Class.forName(CLASS_ANIMATED_MODULE); @@ -93,6 +120,7 @@ public boolean isIdleNow() { if (callback != null) { callback.onTransitionToIdle(); } + last_idle_time = System.currentTimeMillis(); return true; } @@ -109,15 +137,18 @@ public boolean isIdleNow() { if (callback != null) { callback.onTransitionToIdle(); } + last_idle_time = System.currentTimeMillis(); return true; } if (ReactNativeInfo.rnVersion().getMinor() >= 51) { if(isIdleRN51(animModuleClass)) { + last_idle_time = System.currentTimeMillis(); return true; } } else { if (isIdleRNOld(animModuleClass)) { + last_idle_time = System.currentTimeMillis(); return true; } } @@ -134,6 +165,7 @@ public boolean isIdleNow() { callback.onTransitionToIdle(); } // Log.i(LOG_TAG, "AnimatedModule is idle."); + last_idle_time = System.currentTimeMillis(); return true; } diff --git a/detox/scripts/build_framework.ios.sh b/detox/scripts/build_framework.ios.sh index 77cdcca5e4..b7f6e8c0bf 100755 --- a/detox/scripts/build_framework.ios.sh +++ b/detox/scripts/build_framework.ios.sh @@ -8,11 +8,13 @@ detoxVersion=`node -p "require('${detoxRootPath}/package.json').version"` sha1=`(echo "${detoxVersion}" && xcodebuild -version) | shasum | awk '{print $1}' #"${2}"` detoxFrameworkDirPath="$HOME/Library/Detox/ios/${sha1}" +mkdir -p "${detoxFrameworkDirPath}" +rm -r "${detoxFrameworkDirPath}" detoxFrameworkPath="${detoxFrameworkDirPath}/Detox.framework" function prepareAndBuildFramework () { - if [ -d "$detoxRootPath"/ios ]; then + if [ -d "$detoxRootPath"/iosz ]; then detoxSourcePath="${detoxRootPath}"/ios echo "Dev mode, building from ${detoxSourcePath}" buildFramework "${detoxSourcePath}" diff --git a/package.json b/package.json index e654038ed7..c28122e61a 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "release": "node scripts/ci.release.js", "package:android": "node scripts/ci.android-release.js", "package:ios": "scripts/ci.ios-release.sh", - "postinstall": "node scripts/postinstall.js" + "postinstall": "node scripts/postinstall.js", + "fwpostinstall": "node detox/scripts/postinstall.js" }, "repository": { "type": "git", From a5292610c7f98f19be8197d0623ca8541ad496c8 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Wed, 27 Dec 2023 13:17:21 +0200 Subject: [PATCH 060/185] refactor: dynamic device allocation (#4290) --- detox/jest.config.js | 2 +- detox/src/DetoxWorker.js | 2 +- detox/src/DetoxWorker.test.js | 4 +- detox/src/ipc/IPCClient.js | 4 +- detox/src/ipc/IPCServer.js | 6 +- detox/src/ipc/ipc.test.js | 8 +- detox/src/realms/DetoxContext.js | 4 +- detox/src/realms/DetoxPrimaryContext.js | 100 +++-- detox/src/realms/DetoxPrimaryContext.test.js | 398 +++++++++++------- detox/src/realms/DetoxSecondaryContext.js | 4 +- .../DetoxPrimaryContext.test.js.snap | 8 + 11 files changed, 351 insertions(+), 189 deletions(-) diff --git a/detox/jest.config.js b/detox/jest.config.js index 0901108b36..6ca2c4a514 100644 --- a/detox/jest.config.js +++ b/detox/jest.config.js @@ -74,7 +74,7 @@ module.exports = { 'runners/jest/testEnvironment', 'src/DetoxWorker.js', 'src/logger/utils/streamUtils.js', - 'src/realms' + 'src/realms', ], resetMocks: true, resetModules: true, diff --git a/detox/src/DetoxWorker.js b/detox/src/DetoxWorker.js index 237ef37dae..7d3f69a32f 100644 --- a/detox/src/DetoxWorker.js +++ b/detox/src/DetoxWorker.js @@ -132,7 +132,7 @@ class DetoxWorker { }; this._artifactsManager = artifactsManagerFactory.createArtifactsManager(this._artifactsConfig, commonDeps); - this._deviceCookie = yield this._context[symbols.allocateDevice](); + this._deviceCookie = yield this._context[symbols.allocateDevice](this._deviceConfig); this.device = runtimeDeviceFactory.createRuntimeDevice( this._deviceCookie, diff --git a/detox/src/DetoxWorker.test.js b/detox/src/DetoxWorker.test.js index 4fe220fac4..9946e7d1a9 100644 --- a/detox/src/DetoxWorker.test.js +++ b/detox/src/DetoxWorker.test.js @@ -133,7 +133,9 @@ describe('DetoxWorker', () => { expect(envValidator.validate).toHaveBeenCalled()); it('should allocate a device', () => { - expect(detoxContext[symbols.allocateDevice]).toHaveBeenCalledWith(); + expect(detoxContext[symbols.allocateDevice]).toHaveBeenCalledWith(expect.objectContaining({ + type: 'fake.device', + })); }); it('should create a runtime-device based on the allocation result (cookie)', () => diff --git a/detox/src/ipc/IPCClient.js b/detox/src/ipc/IPCClient.js index ca55543610..bcc5384326 100644 --- a/detox/src/ipc/IPCClient.js +++ b/detox/src/ipc/IPCClient.js @@ -60,8 +60,8 @@ class IPCClient { this._sessionState.patch(sessionState); } - async allocateDevice() { - const { deviceCookie, error } = deserializeObjectWithError(await this._emit('allocateDevice', {})); + async allocateDevice(deviceConfig) { + const { deviceCookie, error } = deserializeObjectWithError(await this._emit('allocateDevice', { deviceConfig })); if (error) { throw error; } diff --git a/detox/src/ipc/IPCServer.js b/detox/src/ipc/IPCServer.js index 51f4e2f10e..eea96f233c 100644 --- a/detox/src/ipc/IPCServer.js +++ b/detox/src/ipc/IPCServer.js @@ -9,7 +9,7 @@ class IPCServer { * @param {import('./SessionState')} options.sessionState * @param {Detox.Logger} options.logger * @param {object} options.callbacks - * @param {() => Promise} options.callbacks.onAllocateDevice + * @param {(deviceConfig: DetoxInternals.RuntimeConfig['device']) => Promise} options.callbacks.onAllocateDevice * @param {(cookie: any) => Promise} options.callbacks.onDeallocateDevice */ constructor({ sessionState, logger, callbacks }) { @@ -104,11 +104,11 @@ class IPCServer { this._ipc.server.broadcast('sessionStateUpdate', newState); } - async onAllocateDevice(_payload, socket) { + async onAllocateDevice({ deviceConfig }, socket) { let deviceCookie; try { - deviceCookie = await this._callbacks.onAllocateDevice(); + deviceCookie = await this._callbacks.onAllocateDevice(deviceConfig); this._ipc.server.emit(socket, 'allocateDeviceDone', { deviceCookie }); } catch (error) { this._ipc.server.emit(socket, 'allocateDeviceDone', serializeObjectWithError({ error })); diff --git a/detox/src/ipc/ipc.test.js b/detox/src/ipc/ipc.test.js index 29f70758b0..6e33cd97fc 100644 --- a/detox/src/ipc/ipc.test.js +++ b/detox/src/ipc/ipc.test.js @@ -362,18 +362,22 @@ describe('IPC', () => { }); describe('integration', () => { + const deviceConfig = { type: 'stub.device' }; + beforeEach(() => ipcServer.init()); beforeEach(() => ipcClient1.init()); describe('onAllocateDevice', () => { it('should allocate a device and return its cookie', async () => { callbacks.onAllocateDevice.mockResolvedValue({ id: 'device-1' }); - await expect(ipcClient1.allocateDevice()).resolves.toEqual({ id: 'device-1' }); + await expect(ipcClient1.allocateDevice(deviceConfig)).resolves.toEqual({ id: 'device-1' }); + expect(callbacks.onAllocateDevice).toHaveBeenCalledWith(deviceConfig); }); it('should return an error if allocation fails', async () => { callbacks.onAllocateDevice.mockRejectedValue(new Error('foo')); - await expect(ipcClient1.allocateDevice()).rejects.toThrow('foo'); + await expect(ipcClient1.allocateDevice(deviceConfig)).rejects.toThrow('foo'); + expect(callbacks.onAllocateDevice).toHaveBeenCalledWith(deviceConfig); }); }); diff --git a/detox/src/realms/DetoxContext.js b/detox/src/realms/DetoxContext.js index 67c6d6ea40..93d45f882f 100644 --- a/detox/src/realms/DetoxContext.js +++ b/detox/src/realms/DetoxContext.js @@ -152,10 +152,10 @@ class DetoxContext { } /** @abstract */ - async [symbols.allocateDevice]() {} + async [symbols.allocateDevice](_deviceConfig) {} /** @abstract */ - async [symbols.deallocateDevice]() {} + async [symbols.deallocateDevice](_deviceCookie) {} async [symbols.uninstallWorker]() { try { diff --git a/detox/src/realms/DetoxPrimaryContext.js b/detox/src/realms/DetoxPrimaryContext.js index feb061fdb9..5db099591c 100644 --- a/detox/src/realms/DetoxPrimaryContext.js +++ b/detox/src/realms/DetoxPrimaryContext.js @@ -23,7 +23,9 @@ const _emergencyTeardown = Symbol('emergencyTeardown'); const _lifecycleLogger = Symbol('lifecycleLogger'); const _sessionFile = Symbol('sessionFile'); const _logFinalError = Symbol('logFinalError'); -const _deviceAllocator = Symbol('deviceAllocator'); +const _cookieAllocators = Symbol('cookieAllocators'); +const _deviceAllocators = Symbol('deviceAllocators'); +const _createDeviceAllocator = Symbol('createDeviceAllocator'); //#endregion class DetoxPrimaryContext extends DetoxContext { @@ -32,7 +34,8 @@ class DetoxPrimaryContext extends DetoxContext { this[_dirty] = false; this[_wss] = null; - this[_deviceAllocator] = null; + this[_cookieAllocators] = {}; + this[_deviceAllocators] = {}; /** Path to file where the initial session object is serialized */ this[_sessionFile] = ''; @@ -85,7 +88,6 @@ class DetoxPrimaryContext extends DetoxContext { const detoxConfig = await this[symbols.resolveConfig](opts); const { - device: deviceConfig, logger: loggerConfig, session: sessionConfig } = detoxConfig; @@ -109,16 +111,6 @@ class DetoxPrimaryContext extends DetoxContext { await this[_ipcServer].init(); - const environmentFactory = require('../environmentFactory'); - - const { deviceAllocatorFactory } = environmentFactory.createFactories(deviceConfig); - this[_deviceAllocator] = deviceAllocatorFactory.createDeviceAllocator({ - detoxConfig, - detoxSession: this[$sessionState], - }); - - await this[_deviceAllocator].init(); - // TODO: Detox-server creation ought to be delegated to a generator/factory. const DetoxServer = require('../server/DetoxServer'); if (sessionConfig.autoStart) { @@ -162,17 +154,20 @@ class DetoxPrimaryContext extends DetoxContext { } /** @override */ - async [symbols.allocateDevice]() { - const { device } = this[$sessionState].detoxConfig; - const deviceCookie = await this[_deviceAllocator].allocate(device); + async [symbols.allocateDevice](deviceConfig) { + const deviceAllocator = await this[_createDeviceAllocator](deviceConfig); + const deviceCookie = await deviceAllocator.allocate(deviceConfig); + this[_cookieAllocators][deviceCookie.id] = deviceAllocator; try { - return await this[_deviceAllocator].postAllocate(deviceCookie); + return await deviceAllocator.postAllocate(deviceCookie); } catch (e) { try { - await this[_deviceAllocator].free(deviceCookie, { shutdown: true }); + await deviceAllocator.free(deviceCookie, { shutdown: true }); } catch (e2) { - this[symbols.logger].error({ cat: 'device', err: e2 }, `Failed to free ${deviceCookie.name || deviceCookie.id} after a failed allocation`); + this[symbols.logger].error({ cat: 'device', err: e2 }, `Failed to free ${deviceCookie.name || deviceCookie.id} after a failed allocation attempt`); + } finally { + delete this[_cookieAllocators][deviceCookie.id]; } throw e; @@ -181,7 +176,17 @@ class DetoxPrimaryContext extends DetoxContext { /** @override */ async [symbols.deallocateDevice](cookie) { - await this[_deviceAllocator].free(cookie); + const deviceAllocator = this[_cookieAllocators][cookie.id]; + if (!deviceAllocator) { + throw new DetoxRuntimeError({ + message: `Cannot deallocate device ${cookie.id} because it was not allocated by this context.`, + hint: `See the actually known allocated devices below:`, + debugInfo: Object.keys(this[_cookieAllocators]).map(id => `- ${id}`).join('\n'), + }); + } + + await deviceAllocator.free(cookie); + delete this[_cookieAllocators][cookie.id]; } /** @override */ @@ -191,11 +196,18 @@ class DetoxPrimaryContext extends DetoxContext { await this[symbols.uninstallWorker](); } } finally { - if (this[_deviceAllocator]) { - await this[_deviceAllocator].cleanup(); - this[_deviceAllocator] = null; + for (const key of Object.keys(this[_deviceAllocators])) { + const deviceAllocator = this[_deviceAllocators][key]; + delete this[_deviceAllocators][key]; + try { + await deviceAllocator.cleanup(); + } catch (err) { + this[symbols.logger].error({ cat: 'device', err }, `Failed to cleanup the device allocation driver for ${key}`); + } } + this[_cookieAllocators] = {}; + if (this[_wss]) { await this[_wss].close(); this[_wss] = null; @@ -227,11 +239,18 @@ class DetoxPrimaryContext extends DetoxContext { return; } - if (this[_deviceAllocator]) { - this[_deviceAllocator].emergencyCleanup(); - this[_deviceAllocator] = null; + for (const key of Object.keys(this[_deviceAllocators])) { + const deviceAllocator = this[_deviceAllocators][key]; + delete this[_deviceAllocators][key]; + try { + deviceAllocator.emergencyCleanup(); + } catch (err) { + this[symbols.logger].error({ cat: 'device', err }, `Failed to clean up the device allocation driver for ${key} in emergency mode`); + } } + this[_cookieAllocators] = {}; + if (this[_wss]) { this[_wss].close(); } @@ -253,6 +272,35 @@ class DetoxPrimaryContext extends DetoxContext { } }; + /** @param {Detox.DetoxDeviceConfig} deviceConfig */ + [_createDeviceAllocator] = async (deviceConfig) => { + const deviceType = deviceConfig.type; + if (!this[_deviceAllocators][deviceType]) { + const environmentFactory = require('../environmentFactory'); + const { deviceAllocatorFactory } = environmentFactory.createFactories(deviceConfig); + const { detoxConfig } = this[$sessionState]; + const deviceAllocator = deviceAllocatorFactory.createDeviceAllocator({ + detoxConfig, + detoxSession: this[$sessionState], + }); + + try { + await deviceAllocator.init(); + this[_deviceAllocators][deviceType] = deviceAllocator; + } catch (e) { + try { + await deviceAllocator.cleanup(); + } catch (e2) { + this[symbols.logger].error({ cat: 'device', err: e2 }, `Failed to cleanup the device allocation driver for ${deviceType} after a failed initialization`); + } + + throw e; + } + } + + return this[_deviceAllocators][deviceType]; + }; + [_logFinalError] = (err) => { this[_lifecycleLogger].error(err, 'Encountered an error while merging the process logs:'); }; diff --git a/detox/src/realms/DetoxPrimaryContext.test.js b/detox/src/realms/DetoxPrimaryContext.test.js index de374c853b..3f373f09b3 100644 --- a/detox/src/realms/DetoxPrimaryContext.test.js +++ b/detox/src/realms/DetoxPrimaryContext.test.js @@ -53,15 +53,22 @@ describe('DetoxPrimaryContext', () => { let DetoxWorker; //#endregion + /** @type {import('./DetoxPrimaryContext')} */ + let context; /** @type {import('./DetoxInternalsFacade')} */ let facade; + /** @type {import('./symbols')} */ + let symbols; const detoxServer = () => latestInstanceOf(DetoxServer); const ipcServer = () => latestInstanceOf(IPCServer); const detoxWorker = () => latestInstanceOf(DetoxWorker); + // @ts-ignore + const log = () => logger.DetoxLogger.instances[0]; const logFinalizer = () => latestInstanceOf(logger.DetoxLogFinalizer); const getSignalHandler = () => lastCallTo(signalExit)[FIRST_ARGUMENT]; const facadeInit = () => facade.init({ workerId: null }); + const facadeInitWithWorker = async () => facade.init({ workerId: WORKER_ID }); backupProcessEnv(); @@ -72,8 +79,9 @@ describe('DetoxPrimaryContext', () => { const DetoxPrimaryContext = require('./DetoxPrimaryContext'); const DetoxInternalsFacade = require('./DetoxInternalsFacade'); - const context = new DetoxPrimaryContext(); + context = new DetoxPrimaryContext(); facade = new DetoxInternalsFacade(context); + symbols = require('./symbols'); }); describe('when not initialized', () => { @@ -94,12 +102,12 @@ describe('DetoxPrimaryContext', () => { }); }); - describe('when initializing', () => { + describe('when initialized', () => { + beforeEach(facadeInit); + it('should create an IPC server with a valid session state', async () => { const expectedIPCServerName = `primary-${process.pid}`; - await facadeInit(); - expect(IPCServer).toHaveBeenCalledWith(expect.objectContaining({ sessionState: expect.objectContaining({ id: expect.stringMatching(UUID_REGEXP), @@ -109,56 +117,10 @@ describe('DetoxPrimaryContext', () => { }); it('should init the IPC server', async () => { - await facadeInit(); expect(ipcServer().init).toHaveBeenCalled(); }); - it('should init the device allocation driver', async () => { - await facadeInit(); - expect(deviceAllocator.init).toHaveBeenCalled(); - }); - - describe('given detox-server auto-start enabled via config', () => { - beforeEach(() => detoxConfigDriver.givenDetoxServerAutostart()); - - it('should create the Detox server', async () => { - const expectedServerArgs = { - port: 0, - standalone: false, - }; - - await facadeInit(); - expect(DetoxServer).toHaveBeenCalledWith(expectedServerArgs); - }); - - it('should create the Detox server based on a specified port', async () => { - const port = '666'; - detoxConfigDriver.givenDetoxServerPort(port); - - const expectedServerArgs = { - port, - standalone: false, - }; - await facadeInit(); - expect(DetoxServer).toHaveBeenCalledWith(expectedServerArgs); - }); - - it('should start the server', async () => { - await facadeInit(); - expect(detoxServer().open).toHaveBeenCalled(); - }); - }); - - describe('given detox-server auto-start disabled via config', () => { - it('should not create a server', async () => { - await facadeInit(); - expect(DetoxServer).not.toHaveBeenCalled(); - }); - }); - it('should save the session state onto the context-shared file', async () => { - await facadeInit(); - expect(fs.writeFile).toHaveBeenCalledWith( expect.stringMatching(TEMP_FILE_REGEXP), expect.any(String), @@ -171,150 +133,182 @@ describe('DetoxPrimaryContext', () => { }); it('should export context-shared file via DETOX_CONFIG_SNAPSHOT_PATH', async () => { - await facadeInit(); - expect(process.env.DETOX_CONFIG_SNAPSHOT_PATH).toBeDefined(); expect(process.env.DETOX_CONFIG_SNAPSHOT_PATH).toMatch(TEMP_FILE_REGEXP); }); - it('should install a worker if called without options', async () => { - await facade.init(); - expect(facade.session).toEqual(expect.objectContaining({ workerId: 'worker' })); - expect(detoxWorker().init).toHaveBeenCalled(); + it('should reject further initializations', async () => { + await expect(() => facadeInit()).rejects.toThrowErrorMatchingSnapshot(); }); - it('should install a worker if worker ID has been specified', async () => { - await facade.init({ workerId: WORKER_ID }); - expect(facade.session).toEqual(expect.objectContaining({ workerId: WORKER_ID })); - expect(detoxWorker().init).toHaveBeenCalled(); + it('should change status to "active"', async () => { + expect(facade.getStatus()).toBe('active'); }); - it('should register the worker at the IPC server\'s', async () => { - await facade.init({ workerId: WORKER_ID }); - expect(ipcServer().onRegisterWorker).toHaveBeenCalledWith({ workerId: WORKER_ID }); - }); + describe('when a device is being allocated', () => { + let cookie; - describe('given an initialization failure', () => { - it('should report status as "init"', async () => { - IPCServer.prototype.init = jest.fn().mockRejectedValue(new Error('init failed')); + beforeEach(async () => { + cookie = await allocateSomeDevice(); + }); - await expect(() => facadeInit()).rejects.toThrow(); - expect(facade.getStatus()).toBe('init'); + it('should return a cookie', async () => { + expect(cookie).toEqual({ id: 'a-device-id' }); }); - }); - }); - describe('when initialized', () => { - it('should reject further initializations', async () => { - await facadeInit(); - await expect(() => facadeInit()).rejects.toThrowErrorMatchingSnapshot(); - }); + it('should call the device allocator', async () => { + expect(deviceAllocator.init).toHaveBeenCalled(); + expect(deviceAllocator.allocate).toHaveBeenCalled(); + expect(deviceAllocator.postAllocate).toHaveBeenCalled(); + }); - it('should change status to "active"', async () => { - await facadeInit(); - expect(facade.getStatus()).toBe('active'); - }); + it('can be deallocated', async () => { + await expect(deallocateDevice(cookie)).resolves.toBeUndefined(); + }); - describe('then cleaned-up', () => { - it('should uninstall an assigned worker', async () => { - await facade.init({ workerId: WORKER_ID }); - await facade.cleanup(); + it('should throw on attempt to deallocate a cookie that does not belong to this context', async () => { + await expect(deallocateDevice({ id: 'some-other-device' })).rejects.toThrowErrorMatchingSnapshot(); + }); - expect(detoxWorker().cleanup).toHaveBeenCalled(); + it('cannot be deallocated twice', async () => { + await deallocateDevice(cookie); + await expect(deallocateDevice(cookie)).rejects.toThrowError(/Cannot deallocate device/); }); - it('should clean up the allocation driver', async () => { - await facadeInit(); - await facade.cleanup(); + describe('and then the context has been cleaned up', () => { + beforeEach(async () => { + await facade.cleanup(); + }); - expect(deviceAllocator.cleanup).toHaveBeenCalled(); + it('should clean up the allocation driver', async () => { + expect(deviceAllocator.cleanup).toHaveBeenCalled(); + }); + + it('should not be able to find that cookie anymore', async () => { + await expect(deallocateDevice(cookie)).rejects.toThrowError(/Cannot deallocate device/); + }); }); - it('should close the detox server', async () => { - detoxConfigDriver.givenDetoxServerAutostart(); + describe('and then the context has been cleaned up with an allocator cleanup error', () => { + let error = new Error('cleanup failed'); - await facadeInit(); - await facade.cleanup(); + beforeEach(async () => { + deviceAllocator.cleanup.mockRejectedValue(error); + }); - expect(detoxServer().close).toHaveBeenCalled(); + it('should log the error but not throw', async () => { + await expect(facade.cleanup()).resolves.toBeUndefined(); + expect(log().error).toHaveBeenCalledWith({ cat: 'device', err: error }, `Failed to cleanup the device allocation driver for some.device`); + }); }); - it('should close the ipc server', async () => { - await facadeInit(); - await facade.cleanup(); + describe('on emergency context cleanup', () => { + beforeEach(async () => { + const signalHandler = getSignalHandler(); + signalHandler(123, 'SIGSMT'); + }); - expect(ipcServer().dispose).toHaveBeenCalled(); + it('should call emergencyCleanup in allocation driver', async () => { + expect(deviceAllocator.emergencyCleanup).toHaveBeenCalled(); + }); }); - it('should delete the context-shared file', async () => { - await facadeInit(); - await facade.cleanup(); + describe('on emergency context cleanup with an allocator cleanup error', () => { + let error = new Error('cleanup failed'); - expect(fs.remove).toHaveBeenCalledWith(expect.stringMatching(TEMP_FILE_REGEXP)); + beforeEach(async () => { + deviceAllocator.emergencyCleanup.mockImplementation(() => { throw error; }); + }); + + it('should log the error but not throw', async () => { + const signalHandler = getSignalHandler(); + expect(() => signalHandler(123, 'SIGSMT')).not.toThrow(); + expect(log().error).toHaveBeenCalledWith({ cat: 'device', err: error }, `Failed to clean up the device allocation driver for some.device in emergency mode`); + }); }); + }); - it('should finalize the logger', async () => { - await facadeInit(); - await facade.cleanup(); - expect(logFinalizer().finalize).toHaveBeenCalled(); + describe('when a device is being allocated using a faulty driver', () => { + beforeEach(() => { + deviceAllocator.init.mockRejectedValue(new Error('init failed')); }); - it('should change intermediate status to "cleanup"', async () => { - expect.assertions(1); - await facadeInit(); + it('should destroy the allocation driver immediately', async () => { + await expect(allocateSomeDevice()).rejects.toThrow(/init failed/); + expect(deviceAllocator.cleanup).toHaveBeenCalled(); + }); - ipcServer().dispose.mockImplementation(async () => { - expect(facade.getStatus()).toBe('cleanup'); + describe('and the driver fails to clean up', () => { + beforeEach(() => { + deviceAllocator.cleanup.mockRejectedValue(new Error('cleanup failed')); }); - await facade.cleanup(); + it('should log the error', async () => { + await expect(allocateSomeDevice()).rejects.toThrow(/init failed/); + expect(log().error).toHaveBeenCalledWith({ cat: 'device', err: new Error('cleanup failed') }, `Failed to cleanup the device allocation driver for some.device after a failed initialization`); + }); }); + }); - it('should restore status to "inactive"', async () => { - await facadeInit(); - await facade.cleanup(); - expect(facade.getStatus()).toBe('inactive'); + describe('when a faulty device is being allocated', () => { + beforeEach(async () => { + deviceAllocator.postAllocate.mockRejectedValue(new Error('postAllocate failed')); }); - describe('given a worker clean-up error', () => { - const facadeInitWithWorker = async () => facade.init({ workerId: WORKER_ID }); - const facadeCleanup = async () => expect(() => facade.cleanup()).rejects.toThrow(); + it('should free the device after an error', async () => { + await expect(allocateSomeDevice()).rejects.toThrow(/postAllocate failed/); + expect(deviceAllocator.free).toHaveBeenCalled(); + }); - beforeEach(async () => { - detoxConfigDriver.givenDetoxServerAutostart(); - await facadeInitWithWorker(); + describe('and cannot be freed properly', () => { + let error = new Error('free failed'); - detoxWorker().cleanup.mockRejectedValue(new Error('')); + beforeEach(async () => { + deviceAllocator.free.mockRejectedValue(error); }); - it('should clean-up nonetheless', async () => { - await facadeCleanup(); - expect(detoxServer().close).toHaveBeenCalled(); - expect(ipcServer().dispose).toHaveBeenCalled(); + it('should throw the original allocation error', async () => { + await expect(allocateSomeDevice()).rejects.toThrow(/postAllocate failed/); + expect(log().error).toHaveBeenCalledWith({ cat: 'device', err: error }, `Failed to free a-device-id after a failed allocation attempt`); }); + }); + }); - it('should restore status to "inactive"', async () => { - await facadeCleanup(); - expect(facade.getStatus()).toBe('inactive'); + describe('and cleaning up', () => { + it('should change intermediate status to "cleanup"', async () => { + expect.assertions(1); + ipcServer().dispose.mockImplementation(async () => { + expect(facade.getStatus()).toBe('cleanup'); }); + await facade.cleanup(); }); }); - describe('given an exit signal', () => { - beforeEach(async () => { - detoxConfigDriver.givenDetoxServerAutostart(); + describe('and cleaned up', () => { + beforeEach(async () => facade.cleanup()); - await facadeInit(); + it('should close the ipc server', async () => { + expect(ipcServer().dispose).toHaveBeenCalled(); + }); - const signalHandler = getSignalHandler(); - signalHandler(123, 'SIGSMT'); + it('should delete the context-shared file', async () => { + expect(fs.remove).toHaveBeenCalledWith(expect.stringMatching(TEMP_FILE_REGEXP)); + }); + + it('should finalize the logger', async () => { + expect(logFinalizer().finalize).toHaveBeenCalled(); }); - it('should *emergency* cleanup the global lifecycle handler', () => - expect(deviceAllocator.emergencyCleanup).toHaveBeenCalled()); + it('should restore status to "inactive"', async () => { + expect(facade.getStatus()).toBe('inactive'); + }); + }); - it('should close the detox server', async () => - expect(detoxServer().close).toHaveBeenCalled()); + describe('given an exit signal', () => { + beforeEach(async () => { + const signalHandler = getSignalHandler(); + signalHandler(123, 'SIGSMT'); + }); it('should close the ipc server', async () => expect(ipcServer().dispose).toHaveBeenCalled()); @@ -325,19 +319,47 @@ describe('DetoxPrimaryContext', () => { it('should finalize the logger', async () => expect(logFinalizer().finalizeSync).toHaveBeenCalled()); }); + }); - describe('given a broken exit signal', () => { - let signalHandler; - beforeEach(async () => { - detoxConfigDriver.givenDetoxServerAutostart(); - await facadeInit(); + describe('when initialized with no options', () => { + beforeEach(async () => facade.init()); + + it('should also install a worker', async () => { + expect(detoxWorker().init).toHaveBeenCalled(); + expect(facade.session).toEqual(expect.objectContaining({ workerId: 'worker' })); + }); + }); + + describe('when initialized with auto-start of Detox server', () => { + beforeEach(() => detoxConfigDriver.givenDetoxServerAutostart()); + beforeEach(facadeInit); + + it('should create the Detox server', async () => { + expect(DetoxServer).toHaveBeenCalledWith({ + port: 0, + standalone: false, + }); + }); + + it('should start the server', async () => { + expect(detoxServer().open).toHaveBeenCalled(); + }); + + describe('and cleaned up', () => { + beforeEach(async () => facade.cleanup()); - signalHandler = getSignalHandler(); + it('should close the detox server', async () => { + expect(detoxServer().close).toHaveBeenCalled(); }); + }); - it('should do nothing', () => { + describe('given a non-conforming exit signal', () => { + beforeEach(async () => { + const signalHandler = getSignalHandler(); signalHandler(123, undefined); + }); + it('should do nothing', () => { expect(deviceAllocator.emergencyCleanup).not.toHaveBeenCalled(); expect(detoxServer().close).not.toHaveBeenCalled(); expect(ipcServer().dispose).not.toHaveBeenCalled(); @@ -345,6 +367,76 @@ describe('DetoxPrimaryContext', () => { }); }); + describe('when initialized with Detox server on a certain port', () => { + const port = '666'; + + beforeEach(() => detoxConfigDriver.givenDetoxServerAutostart(port)); + beforeEach(() => detoxConfigDriver.givenDetoxServerPort(port)); + beforeEach(facadeInit); + + it('should create it', async () => { + expect(DetoxServer).toHaveBeenCalledWith({ + port, + standalone: false, + }); + }); + }); + + describe('when initialized without auto-start of Detox server', () => { + beforeEach(facadeInit); + + it('should not create a server', async () => { + expect(DetoxServer).not.toHaveBeenCalled(); + }); + }); + + describe('when initialized not successfully', () => { + it('should report status as "init"', async () => { + IPCServer.prototype.init = jest.fn().mockRejectedValue(new Error('init failed')); + + await expect(() => facadeInit()).rejects.toThrow(); + expect(facade.getStatus()).toBe('init'); + }); + }); + + describe('when initialized with a worker', () => { + beforeEach(() => detoxConfigDriver.givenDetoxServerAutostart()); + beforeEach(facadeInitWithWorker); + + it('should install a worker if worker ID has been specified', async () => { + expect(facade.session).toEqual(expect.objectContaining({ workerId: WORKER_ID })); + expect(detoxWorker().init).toHaveBeenCalled(); + }); + + it('should register the worker at the IPC server\'s', async () => { + expect(ipcServer().onRegisterWorker).toHaveBeenCalledWith({ workerId: WORKER_ID }); + }); + + describe('and cleaned up', () => { + beforeEach(async () => facade.cleanup()); + + it('should uninstall an assigned worker', async () => { + expect(detoxWorker().cleanup).toHaveBeenCalled(); + }); + }); + + describe('and cleaned up with an error', () => { + beforeEach(async () => { + detoxWorker().cleanup.mockRejectedValue(new Error('')); + await expect(() => facade.cleanup()).rejects.toThrow(); + }); + + it('should clean-up nonetheless', async () => { + expect(detoxServer().close).toHaveBeenCalled(); + expect(ipcServer().dispose).toHaveBeenCalled(); + }); + + it('should restore status to "inactive"', async () => { + expect(facade.getStatus()).toBe('inactive'); + }); + }); + }); + function _initDetoxConfig() { detoxConfigDriver = new DetoxConfigDriver(_.cloneDeep(DETOX_CONFIG_BASE)); @@ -369,8 +461,8 @@ describe('DetoxPrimaryContext', () => { deviceAllocator = { init: jest.fn(), - allocate: jest.fn(), - postAllocate: jest.fn(), + allocate: jest.fn().mockResolvedValue({ id: 'a-device-id' }), + postAllocate: jest.fn().mockResolvedValue({ id: 'a-device-id' }), free: jest.fn(), cleanup: jest.fn(), emergencyCleanup: jest.fn(), @@ -391,6 +483,14 @@ describe('DetoxPrimaryContext', () => { DetoxWorker = jest.requireMock('../DetoxWorker'); } + async function allocateSomeDevice() { + return context[symbols.allocateDevice]({ type: 'some.device' }); + } + + async function deallocateDevice(cookie) { + return context[symbols.deallocateDevice](cookie); + } + class DetoxConfigDriver { constructor(detoxConfig) { this.detoxConfig = detoxConfig; diff --git a/detox/src/realms/DetoxSecondaryContext.js b/detox/src/realms/DetoxSecondaryContext.js index 108daf29ce..e5795f1ef8 100644 --- a/detox/src/realms/DetoxSecondaryContext.js +++ b/detox/src/realms/DetoxSecondaryContext.js @@ -63,9 +63,9 @@ class DetoxSecondaryContext extends DetoxContext { } /** @override */ - async [symbols.allocateDevice]() { + async [symbols.allocateDevice](deviceConfig) { if (this[_ipcClient]) { - const deviceCookie = await this[_ipcClient].allocateDevice(); + const deviceCookie = await this[_ipcClient].allocateDevice(deviceConfig); return deviceCookie; } else { throw new DetoxInternalError('Detected an attempt to allocate a device using a non-initialized context.'); diff --git a/detox/src/realms/__snapshots__/DetoxPrimaryContext.test.js.snap b/detox/src/realms/__snapshots__/DetoxPrimaryContext.test.js.snap index d15431ff0f..768841cddd 100644 --- a/detox/src/realms/__snapshots__/DetoxPrimaryContext.test.js.snap +++ b/detox/src/realms/__snapshots__/DetoxPrimaryContext.test.js.snap @@ -7,6 +7,14 @@ HINT: If you are using Detox with Jest according to the latest guide, please rep https://github.com/wix/Detox/issues" `; +exports[`DetoxPrimaryContext when initialized when a device is being allocated should throw on attempt to deallocate a cookie that does not belong to this context 1`] = ` +"Cannot deallocate device some-other-device because it was not allocated by this context. + +HINT: See the actually known allocated devices below: + +- a-device-id" +`; + exports[`DetoxPrimaryContext when not initialized should throw on attempt to get a worker 1`] = ` "Detox worker instance has not been installed in this context (DetoxPrimaryContext). From a9113e6dd3e4774f03d1f920301932b6379b3ef7 Mon Sep 17 00:00:00 2001 From: sonnguyenduc <127935387+sonnguyenduc@users.noreply.github.com> Date: Wed, 27 Dec 2023 18:54:47 +0700 Subject: [PATCH 061/185] fix check visible --- .../src/full/java/com/wix/detox/espresso/DetoxMatcher.java | 2 +- .../java/com/wix/detox/espresso/action/RNClickAction.java | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/detox/android/detox/src/full/java/com/wix/detox/espresso/DetoxMatcher.java b/detox/android/detox/src/full/java/com/wix/detox/espresso/DetoxMatcher.java index 329309125c..f9a23fcaaa 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/espresso/DetoxMatcher.java +++ b/detox/android/detox/src/full/java/com/wix/detox/espresso/DetoxMatcher.java @@ -90,7 +90,7 @@ public static Matcher matcherForClass(String className) { } public static Matcher matcherForSufficientlyVisible(int pct) { - return isDisplayingAtLeast(pct > 50 ? 50 : pct); + return isDisplayingAtLeast(pct); } public static Matcher matcherForNotVisible() { diff --git a/detox/android/detox/src/full/java/com/wix/detox/espresso/action/RNClickAction.java b/detox/android/detox/src/full/java/com/wix/detox/espresso/action/RNClickAction.java index 6f395a6a6d..b8b7f9753b 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/espresso/action/RNClickAction.java +++ b/detox/android/detox/src/full/java/com/wix/detox/espresso/action/RNClickAction.java @@ -19,6 +19,8 @@ public class RNClickAction implements ViewAction { private final GeneralClickAction clickAction; + private final int initPct = 100; + private static long first_check_time = System.currentTimeMillis(); public RNClickAction() { clickAction = new GeneralClickAction( @@ -40,7 +42,9 @@ public RNClickAction(CoordinatesProvider coordinatesProvider) { @Override public Matcher getConstraints() { - return isDisplayingAtLeast(50); + float checking_time = System.currentTimeMillis() - first_check_time; + int pct = initPct - (int)(checking_time * 5); + return isDisplayingAtLeast(pct < 50 ? 50 : pct); } @Override From 6d4601d7da1b8f42255e3c08609d796b15ba7af8 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Wed, 27 Dec 2023 14:45:05 +0200 Subject: [PATCH 062/185] internal: use jest-environment-emit (#4212) https://github.com/wix-incubator/jest-environment-emit --- detox/package.json | 3 +- detox/runners/jest/testEnvironment/index.js | 32 +++++++++++++++------ detox/test/e2e/jest.config.js | 10 +++++++ detox/test/package.json | 4 ++- generation/package.json | 4 +-- 5 files changed, 40 insertions(+), 13 deletions(-) diff --git a/detox/package.json b/detox/package.json index 7f3c4f2167..7d0612d525 100644 --- a/detox/package.json +++ b/detox/package.json @@ -51,7 +51,7 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-unicorn": "^47.0.0", "jest": "^28.1.3", - "jest-allure2-reporter": "2.0.0-alpha.11", + "jest-allure2-reporter": "^2.0.0-beta.2", "mockdate": "^2.0.1", "prettier": "^2.4.1", "react-native": "0.71.10", @@ -72,6 +72,7 @@ "funpermaproxy": "^1.1.0", "glob": "^8.0.3", "ini": "^1.3.4", + "jest-environment-emit": "^1.0.3", "json-cycle": "^1.3.0", "lodash": "^4.17.11", "multi-sort-stream": "^1.0.3", diff --git a/detox/runners/jest/testEnvironment/index.js b/detox/runners/jest/testEnvironment/index.js index 9418a65ed7..159b1b1248 100644 --- a/detox/runners/jest/testEnvironment/index.js +++ b/detox/runners/jest/testEnvironment/index.js @@ -1,5 +1,6 @@ const path = require('path'); +const WithEmitter = require('jest-environment-emit'); const resolveFrom = require('resolve-from'); const maybeNodeEnvironment = require(resolveFrom(process.cwd(), 'jest-environment-node')); /** @type {typeof import('@jest/environment').JestEnvironment} */ @@ -31,7 +32,7 @@ const log = detox.log.child({ cat: 'lifecycle,jest-environment' }); /** * @see https://www.npmjs.com/package/jest-circus#overview */ -class DetoxCircusEnvironment extends NodeEnvironment { +class DetoxCircusEnvironment extends WithEmitter(NodeEnvironment) { constructor(config, context) { super(assertJestCircus27(config), assertExistingContext(context)); @@ -62,6 +63,8 @@ class DetoxCircusEnvironment extends NodeEnvironment { SpecReporter, WorkerAssignReporter, }); + + this.testEvents.on('*', this._onTestEvent.bind(this)); } /** @override */ @@ -72,19 +75,13 @@ class DetoxCircusEnvironment extends NodeEnvironment { // @ts-expect-error TS2425 async handleTestEvent(event, state) { + await super.handleTestEvent(event, state); + if (detox.session.unsafe_earlyTeardown) { if (event.name === 'test_fn_start' || event.name === 'hook_start') { throw new Error('Detox halted test execution due to an early teardown request'); } } - - this._timer.schedule(state.testTimeout != null ? state.testTimeout : this.setupTimeout); - - if (SYNC_CIRCUS_EVENTS.has(event.name)) { - this._handleTestEventSync(event, state); - } else { - await this._handleTestEventAsync(event, state); - } } /** @override */ @@ -147,6 +144,23 @@ class DetoxCircusEnvironment extends NodeEnvironment { } } + /** @private */ + _onTestEvent({ type, event, state }) { + const timeout = state && state.testTimeout != null ? state.testTimeout : this.setupTimeout; + + this._timer.schedule(timeout); + + if (event) { + if (SYNC_CIRCUS_EVENTS.has(event.name)) { + this._handleTestEventSync(event, state); + } else { + return this._handleTestEventAsync(event, state); + } + } else { + return this._handleTestEventAsync({ name: type }, null); + } + } + /** @private */ async _handleTestEventAsync(event, state = null) { const description = `handling ${state ? 'jest-circus' : 'jest-environment'} "${event.name}" event`; diff --git a/detox/test/e2e/jest.config.js b/detox/test/e2e/jest.config.js index 4830228ff8..da69b592b5 100644 --- a/detox/test/e2e/jest.config.js +++ b/detox/test/e2e/jest.config.js @@ -15,6 +15,9 @@ module.exports = async () => { /** @type {import('jest-allure2-reporter').ReporterOptions} */ const jestAllure2ReporterOptions = { overwrite: !process.env.CI, + attachments: { + fileHandler: 'copy', + }, testCase: { labels: { package: ({ filePath }) => filePath.slice(1).join('/'), @@ -44,6 +47,13 @@ module.exports = async () => { return { 'rootDir': path.join(__dirname, '../..'), 'testEnvironment': './test/e2e/testEnvironment.js', + 'testEnvironmentOptions': { + 'eventListeners': [ + 'jest-metadata/environment-listener', + 'jest-allure2-reporter/environment-listener', + require.resolve('detox-allure2-adapter'), + ] + }, 'testRunner': './test/node_modules/jest-circus/runner', 'testMatch': [ '/test/e2e/**/*.test.{js,ts}', diff --git a/detox/test/package.json b/detox/test/package.json index 82c26ae4e7..e261504fb6 100644 --- a/detox/test/package.json +++ b/detox/test/package.json @@ -57,14 +57,16 @@ "@typescript-eslint/parser": "^5.4.0", "cross-env": "^7.0.3", "detox": "^20.14.5", + "detox-allure2-adapter": "^1.0.0-alpha.2", "eslint": "^8.41.0", "eslint-plugin-unicorn": "^47.0.0", "execa": "^5.1.1", "express": "^4.15.3", "glob": "^7.2.0", "jest": "^29.2.1", - "jest-allure2-reporter": "2.0.0-alpha.11", + "jest-allure2-reporter": "^2.0.0-beta.2", "jest-junit": "^10.0.0", + "jest-metadata": "^1.3.1", "lodash": "^4.14.1", "metro-react-native-babel-preset": "0.73.9", "nyc": "^15.1.0", diff --git a/generation/package.json b/generation/package.json index a082c848ab..e788779bdf 100644 --- a/generation/package.json +++ b/generation/package.json @@ -1,6 +1,6 @@ { "name": "generation", - "version": "20.14.0", + "version": "20.14.2-smoke.0", "description": "Generate wrapper code for native dependencies", "main": "index.js", "private": true, @@ -30,7 +30,7 @@ }, "devDependencies": { "jest": "^27.0.0", - "jest-allure2-reporter": "2.0.0-alpha.11", + "jest-allure2-reporter": "^2.0.0-beta.2", "lint-staged": "^6.0.0", "prettier": "^1.8.2" }, From 0a94e82a3d2119f26dde144cbe95533577095161 Mon Sep 17 00:00:00 2001 From: d4vidi Date: Wed, 27 Dec 2023 17:51:58 +0200 Subject: [PATCH 063/185] Fix refs and docs involving testBinaryPath (#4311) --- .../drivers/android/tools/ApkValidator.js | 2 +- .../android/tools/ApkValidator.test.js | 1 + .../__snapshots__/ApkValidator.test.js.snap | 8 +-- .../runtime/drivers/android/AndroidDriver.js | 2 +- .../__snapshots__/AndroidDriver.test.js.snap | 2 +- .../partials/_project-setup-apps-android.mdx | 67 +++++++++++++++---- 6 files changed, 63 insertions(+), 19 deletions(-) diff --git a/detox/src/devices/common/drivers/android/tools/ApkValidator.js b/detox/src/devices/common/drivers/android/tools/ApkValidator.js index b6fcb8a3ae..3fe5c659bc 100644 --- a/detox/src/devices/common/drivers/android/tools/ApkValidator.js +++ b/detox/src/devices/common/drivers/android/tools/ApkValidator.js @@ -1,6 +1,6 @@ const DetoxRuntimeError = require('../../../../../errors/DetoxRuntimeError'); -const setupGuideHint = 'For further assistance, visit the Android setup guide: https://github.com/wix/Detox/blob/master/docs/Introduction.Android.md'; +const setupGuideHint = 'For further assistance, visit the project setup guide (select the Android tabs): https://wix.github.io/Detox/docs/introduction/project-setup'; class ApkValidator { constructor(aapt) { diff --git a/detox/src/devices/common/drivers/android/tools/ApkValidator.test.js b/detox/src/devices/common/drivers/android/tools/ApkValidator.test.js index b93deece43..4f4c92d629 100644 --- a/detox/src/devices/common/drivers/android/tools/ApkValidator.test.js +++ b/detox/src/devices/common/drivers/android/tools/ApkValidator.test.js @@ -1,3 +1,4 @@ + describe('APK validation', () => { const binaryPath = 'mock-bin-path'; diff --git a/detox/src/devices/common/drivers/android/tools/__snapshots__/ApkValidator.test.js.snap b/detox/src/devices/common/drivers/android/tools/__snapshots__/ApkValidator.test.js.snap index 215dde3dfc..35b07b32d1 100644 --- a/detox/src/devices/common/drivers/android/tools/__snapshots__/ApkValidator.test.js.snap +++ b/detox/src/devices/common/drivers/android/tools/__snapshots__/ApkValidator.test.js.snap @@ -4,7 +4,7 @@ exports[`APK validation App APK validation should throw a descriptive error if a "App APK at path mock-bin-path was detected as the *test* APK! HINT: Your binary path was probably wrongly set in the active Detox configuration. -For further assistance, visit the Android setup guide: https://github.com/wix/Detox/blob/master/docs/Introduction.Android.md" +For further assistance, visit the project setup guide (select the Android tabs): https://wix.github.io/Detox/docs/introduction/project-setup" `; exports[`APK validation App APK validation should throw a specific error if AAPT throws 1`] = ` @@ -12,14 +12,14 @@ exports[`APK validation App APK validation should throw a specific error if AAPT Error: mock error HINT: Check that the binary path in the active Detox configuration has been set to a path of an APK file. -For further assistance, visit the Android setup guide: https://github.com/wix/Detox/blob/master/docs/Introduction.Android.md" +For further assistance, visit the project setup guide (select the Android tabs): https://wix.github.io/Detox/docs/introduction/project-setup" `; exports[`APK validation Test APK validation should throw a descriptive error if APK happens to be an app APK 1`] = ` "Test APK at path mock-test-bin-path was detected as the *app* APK! HINT: Your test test-binary path was probably wrongly set in the active Detox configuration. -For further assistance, visit the Android setup guide: https://github.com/wix/Detox/blob/master/docs/Introduction.Android.md" +For further assistance, visit the project setup guide (select the Android tabs): https://wix.github.io/Detox/docs/introduction/project-setup" `; exports[`APK validation Test APK validation should throw a specific error if AAPT throws 1`] = ` @@ -27,5 +27,5 @@ exports[`APK validation Test APK validation should throw a specific error if AAP Error: mock error HINT: Check that the test-binary path in the active Detox configuration has been set to a path of an APK file. -For further assistance, visit the Android setup guide: https://github.com/wix/Detox/blob/master/docs/Introduction.Android.md" +For further assistance, visit the project setup guide (select the Android tabs): https://wix.github.io/Detox/docs/introduction/project-setup" `; diff --git a/detox/src/devices/runtime/drivers/android/AndroidDriver.js b/detox/src/devices/runtime/drivers/android/AndroidDriver.js index 8ff4d523cf..7f0f2cbc30 100644 --- a/detox/src/devices/runtime/drivers/android/AndroidDriver.js +++ b/detox/src/devices/runtime/drivers/android/AndroidDriver.js @@ -280,7 +280,7 @@ class AndroidDriver extends DeviceDriverBase { throw new DetoxRuntimeError({ message: `The test APK could not be found at path: '${testApkPath}'`, hint: 'Try running the detox build command, and make sure it was configured to execute a build command (e.g. \'./gradlew assembleAndroidTest\')' + - '\nFor further assistance, visit the Android setup guide: https://github.com/wix/Detox/blob/master/docs/Introduction.Android.md', + '\nFor further assistance, visit the project setup guide (select the Android tabs): https://wix.github.io/Detox/docs/introduction/project-setup', }); } return testApkPath; diff --git a/detox/src/devices/runtime/drivers/android/__snapshots__/AndroidDriver.test.js.snap b/detox/src/devices/runtime/drivers/android/__snapshots__/AndroidDriver.test.js.snap index 566c9cd809..57e0ef834c 100644 --- a/detox/src/devices/runtime/drivers/android/__snapshots__/AndroidDriver.test.js.snap +++ b/detox/src/devices/runtime/drivers/android/__snapshots__/AndroidDriver.test.js.snap @@ -4,5 +4,5 @@ exports[`Android driver App installation should throw if auto test-binary path r "The test APK could not be found at path: 'testApkPathOf(absolutePathOf(mock-bin-path))' HINT: Try running the detox build command, and make sure it was configured to execute a build command (e.g. './gradlew assembleAndroidTest') -For further assistance, visit the Android setup guide: https://github.com/wix/Detox/blob/master/docs/Introduction.Android.md" +For further assistance, visit the project setup guide (select the Android tabs): https://wix.github.io/Detox/docs/introduction/project-setup" `; diff --git a/docs/introduction/partials/_project-setup-apps-android.mdx b/docs/introduction/partials/_project-setup-apps-android.mdx index e53091f22f..547d041d5f 100644 --- a/docs/introduction/partials/_project-setup-apps-android.mdx +++ b/docs/introduction/partials/_project-setup-apps-android.mdx @@ -2,7 +2,7 @@ import FlavorizedCodeBlock from '@site/src/components/FlavorizedCodeBlock'; -Check **binaryPath** and **build** configs for `android.debug` and `android.release` mode in your Detox config: +Check the **build** and **binaryPath** attributes for the `android.debug` and `android.release` Detox configs: ```js title=".detoxrc.js" module.exports = { @@ -10,15 +10,55 @@ module.exports = { 'android.debug': { type: 'android.apk', // highlight-start - binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk', - build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug' + build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug', + binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk' // highlight-end }, 'android.release': { type: 'android.apk', // highlight-start + build: 'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release', + binaryPath: 'android/app/build/outputs/apk/release/app-release.apk' + // highlight-end + }, + // ... + }, + // ... +}; +``` + +If you have a typical React Native project, these values should already be sufficient and correct. + +#### testBinaryPath + +In Android automation testing, there are in fact 2 app binaries involved: +1. The _app_ APK, containing your app's code. +2. The _test_ APK, containing _test_ code. That includes Detox's native code, Espresso and more. + +In some projects, it might make sense for the test APK to be generated over a separate flow, through which is may end up +being put it some custom, +[non-default path](https://stackoverflow.com/questions/43670463/where-is-the-test-apk-located-in-android-project). +One such example is an optimization where the test APK is prebuilt once and used across multiple app variations. This is +a place where the `testBinaryPath` attribute can come to the rescue; It can be applied in order to set the custom path +to the test APK explicitly: + +```js title=".detoxrc.js" +module.exports = { + apps: { + 'android.debug': { + type: 'android.apk', + build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug', + binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk', + // highlight-start + testBinaryPath: 'custom/path/to/app-debug-androidTest.apk' + // highlight-end + }, + 'android.release': { + type: 'android.apk', binaryPath: 'android/app/build/outputs/apk/release/app-release.apk', - build: 'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release' + build: 'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release', + // highlight-start + testBinaryPath: 'custom/path/to/app-release-androidTest.apk' // highlight-end }, // ... @@ -27,23 +67,26 @@ module.exports = { }; ``` -If you have a typical React Native project, these values should already be correct. +> Mind that in the common case, the `testBinaryPath` attributes is not explicitly required, simply because Detox knows +how to locate it in one of the default paths automatically. + +#### Product flavors -On the other hand, if your app has extra [`productFlavors`](https://developer.android.com/studio/build/build-variants#product-flavors) -(let's imagine you have `driver` and `passenger` flavors of a taxi application), then you should rewrite your apps config -for both **debug** and **release** configurations, e.g.: +On even more advanced use cases, apps may have additional, custom [`productFlavors`](https://developer.android.com/studio/build/build-variants#product-flavors) +(consider the case of a `driver` and `passenger` flavors of a taxi application). In this case, you should rewrite your apps config, +for both **debug** and **release** configurations, according to those flavors, e.g.: - {(conf) => `\ - '${conf.toLowerCase()}.android.debug': { + {(flavor) => `\ + '${flavor.toLowerCase()}.android.debug': { type: 'android.apk', - binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk', -+ binaryPath: 'android/app/build/outputs/apk/${conf.toLowerCase()}/debug/app-${conf.toLowerCase()}-debug.apk', ++ binaryPath: 'android/app/build/outputs/apk/${flavor.toLowerCase()}/debug/app-${flavor.toLowerCase()}-debug.apk', - build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug', -+ build: 'cd android && ./gradlew assemble${conf}Debug assemble${conf}DebugAndroidTest -DtestBuildType=debug', ++ build: 'cd android && ./gradlew assemble${flavor}Debug assemble${flavor}DebugAndroidTest -DtestBuildType=debug', },`} From 5beb1135ce9cf6a3b0166a71835cae8b7211c5ba Mon Sep 17 00:00:00 2001 From: sonnguyenduc <127935387+sonnguyenduc@users.noreply.github.com> Date: Wed, 27 Dec 2023 23:30:22 +0700 Subject: [PATCH 064/185] Update RNClickAction.java --- .../wix/detox/espresso/action/RNClickAction.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/detox/android/detox/src/full/java/com/wix/detox/espresso/action/RNClickAction.java b/detox/android/detox/src/full/java/com/wix/detox/espresso/action/RNClickAction.java index b8b7f9753b..6814390ec0 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/espresso/action/RNClickAction.java +++ b/detox/android/detox/src/full/java/com/wix/detox/espresso/action/RNClickAction.java @@ -19,8 +19,6 @@ public class RNClickAction implements ViewAction { private final GeneralClickAction clickAction; - private final int initPct = 100; - private static long first_check_time = System.currentTimeMillis(); public RNClickAction() { clickAction = new GeneralClickAction( @@ -42,9 +40,17 @@ public RNClickAction(CoordinatesProvider coordinatesProvider) { @Override public Matcher getConstraints() { - float checking_time = System.currentTimeMillis() - first_check_time; - int pct = initPct - (int)(checking_time * 5); - return isDisplayingAtLeast(pct < 50 ? 50 : pct); + Matcher matcher = isDisplayingAtLeast(75); + // if no view matched, wait a second and try again with a lower threshold + if (!matcher.matches(null)) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + System.out.println(e); + } + return isDisplayingAtLeast(50); + } + return matcher; } @Override From b0c390f5c5a5c5cf73338d1ed8f27e7832b8def7 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Thu, 28 Dec 2023 11:07:53 +0200 Subject: [PATCH 065/185] docs: minor updates to project setup --- .../introduction/partials/_project-setup-apps-android.mdx | 8 ++++++-- docs/introduction/partials/_project-setup-devices-ios.mdx | 6 +++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/introduction/partials/_project-setup-apps-android.mdx b/docs/introduction/partials/_project-setup-apps-android.mdx index 547d041d5f..b78e45f51c 100644 --- a/docs/introduction/partials/_project-setup-apps-android.mdx +++ b/docs/introduction/partials/_project-setup-apps-android.mdx @@ -67,13 +67,17 @@ module.exports = { }; ``` -> Mind that in the common case, the `testBinaryPath` attributes is not explicitly required, simply because Detox knows +:::info Note + +In the common case, the `testBinaryPath` attributes is not explicitly required, simply because Detox knows how to locate it in one of the default paths automatically. +::: + #### Product flavors On even more advanced use cases, apps may have additional, custom [`productFlavors`](https://developer.android.com/studio/build/build-variants#product-flavors) -(consider the case of a `driver` and `passenger` flavors of a taxi application). In this case, you should rewrite your apps config, +(for example, `driver` and `passenger` flavors of a taxi application). In this case, you should rewrite your apps config, for both **debug** and **release** configurations, according to those flavors, e.g.: Date: Thu, 28 Dec 2023 11:09:12 +0200 Subject: [PATCH 066/185] chore: update iPhone version in detox init template --- detox/detox.d.ts | 2 +- detox/local-cli/init.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/detox/detox.d.ts b/detox/detox.d.ts index 785321e455..86314cccdc 100644 --- a/detox/detox.d.ts +++ b/detox/detox.d.ts @@ -214,7 +214,7 @@ declare global { * This flag tells Detox to print messages like these every time the device gets assigned to a specific suite: * * ```plain text - * 18:03:29.869 detox[40125] i starter.test.js is assigned to 4EC84833-C7EA-4CA3-A6E9-5C30A29EA596 (iPhone 12 Pro Max) + * 18:03:29.869 detox[40125] i starter.test.js is assigned to 4EC84833-C7EA-4CA3-A6E9-5C30A29EA596 (iPhone 15) * ``` * * @default true diff --git a/detox/local-cli/init.js b/detox/local-cli/init.js index fe86589eb9..daee19d8d0 100644 --- a/detox/local-cli/init.js +++ b/detox/local-cli/init.js @@ -106,7 +106,7 @@ function createDefaultConfigurations() { simulator: { type: 'ios.simulator', device: { - type: 'iPhone 12', + type: 'iPhone 15', }, }, attached: { From 6b5dc5cbab1d1f87f8bdf2b9cbb8ab67f0b7223c Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Thu, 28 Dec 2023 11:36:28 +0200 Subject: [PATCH 067/185] chore: upgrade to Jest 29 (#4313) --- detox/package.json | 4 +- .../__snapshots__/AsyncWebSocket.test.js.snap | 4 +- .../client/__snapshots__/Client.test.js.snap | 2 +- .../SyncStatusFormatter.test.js.snap | 110 +++++------ .../exec/__snapshots__/AAPT.test.js.snap | 4 +- .../__snapshots__/DetoxLogger.test.js.snap | 6 +- .../DetoxLogFinalizer.test.js.snap | 172 +++++++++--------- .../__snapshots__/DetoxServer.test.js.snap | 16 +- .../server-integration.test.js.snap | 98 +++++----- examples/demo-plugin/package.json | 2 +- 10 files changed, 209 insertions(+), 209 deletions(-) diff --git a/detox/package.json b/detox/package.json index 7d0612d525..c84092762a 100644 --- a/detox/package.json +++ b/detox/package.json @@ -37,7 +37,7 @@ "@types/bunyan": "^1.8.8", "@types/child-process-promise": "^2.2.1", "@types/fs-extra": "^9.0.13", - "@types/jest": "^28.1.8", + "@types/jest": "^29.0.0", "@types/node": "^14.18.33", "@types/node-ipc": "^9.2.0", "@types/ws": "^7.4.0", @@ -50,7 +50,7 @@ "eslint-plugin-no-only-tests": "^3.1.0", "eslint-plugin-node": "^11.1.0", "eslint-plugin-unicorn": "^47.0.0", - "jest": "^28.1.3", + "jest": "^29.0.0", "jest-allure2-reporter": "^2.0.0-beta.2", "mockdate": "^2.0.1", "prettier": "^2.4.1", diff --git a/detox/src/client/__snapshots__/AsyncWebSocket.test.js.snap b/detox/src/client/__snapshots__/AsyncWebSocket.test.js.snap index 55939c975b..6c068d41c4 100644 --- a/detox/src/client/__snapshots__/AsyncWebSocket.test.js.snap +++ b/detox/src/client/__snapshots__/AsyncWebSocket.test.js.snap @@ -62,7 +62,7 @@ https://github.com/wix/Detox/issues `; exports[`AsyncWebSocket .send() when opened should fail if the message timeout has expired 1`] = ` -"The pending request #0 (\\"invoke\\") has been rejected due to the following error: +"The pending request #0 ("invoke") has been rejected due to the following error: The tester has not received a response within 5000ms timeout to the message: @@ -88,7 +88,7 @@ The payload was: `; exports[`AsyncWebSocket .send() when opened should reject all messages in the flight if there's an error 1`] = ` -"The pending request #0 (\\"invoke\\") has been rejected due to the following error: +"The pending request #0 ("invoke") has been rejected due to the following error: Failed to deliver the message to the Detox server: diff --git a/detox/src/client/__snapshots__/Client.test.js.snap b/detox/src/client/__snapshots__/Client.test.js.snap index 2a22a9b83c..5fca80866d 100644 --- a/detox/src/client/__snapshots__/Client.test.js.snap +++ b/detox/src/client/__snapshots__/Client.test.js.snap @@ -39,7 +39,7 @@ HINT: To print view hierarchy on failed actions/matches, use log-level verbose o exports[`Client .execute() should throw even if a non-error object is thrown 1`] = `"non-error"`; exports[`Client .execute() should throw on an unsupported result 1`] = ` -"Tried to invoke an action on app, got an unsupported response: {\\"type\\":\\"unsupportedResult\\",\\"params\\":{\\"foo\\":\\"bar\\"}} +"Tried to invoke an action on app, got an unsupported response: {"type":"unsupportedResult","params":{"foo":"bar"}} Please report this issue on our GitHub tracker: https://github.com/wix/Detox/issues" `; diff --git a/detox/src/client/actions/formatters/__snapshots__/SyncStatusFormatter.test.js.snap b/detox/src/client/actions/formatters/__snapshots__/SyncStatusFormatter.test.js.snap index 241ec7b1e1..4eb40e243f 100644 --- a/detox/src/client/actions/formatters/__snapshots__/SyncStatusFormatter.test.js.snap +++ b/detox/src/client/actions/formatters/__snapshots__/SyncStatusFormatter.test.js.snap @@ -1,92 +1,92 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Sync Status Formatter assertions should throw error when \`app_status\` is \`busy\` but \`busy_resources\` is empty 1`] = ` -"Given sync status is not compatible with the status schema (\`SyncStatusSchema.js\`), given status: {\\"app_status\\":\\"busy\\",\\"busy_resources\\":[]}. +"Given sync status is not compatible with the status schema (\`SyncStatusSchema.js\`), given status: {"app_status":"busy","busy_resources":[]}. With reasons: -• must NOT have additional properties in path \\"#/oneOf/0/additionalProperties\\" with params: {\\"additionalProperty\\":\\"busy_resources\\"} -• must NOT have fewer than 1 items in path \\"#/oneOf/1/properties/busy_resources/minItems\\" with params: {\\"limit\\":1} -• must match exactly one schema in oneOf in path \\"#/oneOf\\" with params: {\\"passingSchemas\\":null} +• must NOT have additional properties in path "#/oneOf/0/additionalProperties" with params: {"additionalProperty":"busy_resources"} +• must NOT have fewer than 1 items in path "#/oneOf/1/properties/busy_resources/minItems" with params: {"limit":1} +• must match exactly one schema in oneOf in path "#/oneOf" with params: {"passingSchemas":null} Please report this issue on our GitHub tracker: https://github.com/wix/Detox/issues" `; exports[`Sync Status Formatter assertions should throw error when \`app_status\` is \`busy\` but \`busy_resources\` is missing 1`] = ` -"Given sync status is not compatible with the status schema (\`SyncStatusSchema.js\`), given status: {\\"app_status\\":\\"busy\\"}. +"Given sync status is not compatible with the status schema (\`SyncStatusSchema.js\`), given status: {"app_status":"busy"}. With reasons: -• must be equal to constant in path \\"#/oneOf/0/properties/app_status/const\\" with params: {\\"allowedValue\\":\\"idle\\"} -• must have required property 'busy_resources' in path \\"#/oneOf/1/required\\" with params: {\\"missingProperty\\":\\"busy_resources\\"} -• must match exactly one schema in oneOf in path \\"#/oneOf\\" with params: {\\"passingSchemas\\":null} +• must be equal to constant in path "#/oneOf/0/properties/app_status/const" with params: {"allowedValue":"idle"} +• must have required property 'busy_resources' in path "#/oneOf/1/required" with params: {"missingProperty":"busy_resources"} +• must match exactly one schema in oneOf in path "#/oneOf" with params: {"passingSchemas":null} Please report this issue on our GitHub tracker: https://github.com/wix/Detox/issues" `; exports[`Sync Status Formatter assertions should throw error when \`app_status\` is invalid 1`] = ` -"Given sync status is not compatible with the status schema (\`SyncStatusSchema.js\`), given status: {\\"app_status\\":\\"foo\\"}. +"Given sync status is not compatible with the status schema (\`SyncStatusSchema.js\`), given status: {"app_status":"foo"}. With reasons: -• must be equal to constant in path \\"#/oneOf/0/properties/app_status/const\\" with params: {\\"allowedValue\\":\\"idle\\"} -• must have required property 'busy_resources' in path \\"#/oneOf/1/required\\" with params: {\\"missingProperty\\":\\"busy_resources\\"} -• must match exactly one schema in oneOf in path \\"#/oneOf\\" with params: {\\"passingSchemas\\":null} +• must be equal to constant in path "#/oneOf/0/properties/app_status/const" with params: {"allowedValue":"idle"} +• must have required property 'busy_resources' in path "#/oneOf/1/required" with params: {"missingProperty":"busy_resources"} +• must match exactly one schema in oneOf in path "#/oneOf" with params: {"passingSchemas":null} Please report this issue on our GitHub tracker: https://github.com/wix/Detox/issues" `; exports[`Sync Status Formatter assertions should throw error when \`app_status\` is missing 1`] = ` -"Given sync status is not compatible with the status schema (\`SyncStatusSchema.js\`), given status: {\\"busy_resource\\":[]}. +"Given sync status is not compatible with the status schema (\`SyncStatusSchema.js\`), given status: {"busy_resource":[]}. With reasons: -• must have required property 'app_status' in path \\"#/oneOf/0/required\\" with params: {\\"missingProperty\\":\\"app_status\\"} -• must have required property 'app_status' in path \\"#/oneOf/1/required\\" with params: {\\"missingProperty\\":\\"app_status\\"} -• must match exactly one schema in oneOf in path \\"#/oneOf\\" with params: {\\"passingSchemas\\":null} +• must have required property 'app_status' in path "#/oneOf/0/required" with params: {"missingProperty":"app_status"} +• must have required property 'app_status' in path "#/oneOf/1/required" with params: {"missingProperty":"app_status"} +• must match exactly one schema in oneOf in path "#/oneOf" with params: {"passingSchemas":null} Please report this issue on our GitHub tracker: https://github.com/wix/Detox/issues" `; exports[`Sync Status Formatter assertions should throw error when a busy resource is invalid 1`] = ` -"Given sync status is not compatible with the status schema (\`SyncStatusSchema.js\`), given status: {\\"app_status\\":\\"busy\\",\\"busy_resources\\":[{\\"name\\":\\"foo\\"}]}. +"Given sync status is not compatible with the status schema (\`SyncStatusSchema.js\`), given status: {"app_status":"busy","busy_resources":[{"name":"foo"}]}. With reasons: -• must NOT have additional properties in path \\"#/oneOf/0/additionalProperties\\" with params: {\\"additionalProperty\\":\\"busy_resources\\"} -• must have required property 'description' in path \\"#/oneOf/1/properties/busy_resources/items/anyOf/0/required\\" with params: {\\"missingProperty\\":\\"description\\"} -• must have required property 'description' in path \\"#/oneOf/1/properties/busy_resources/items/anyOf/1/required\\" with params: {\\"missingProperty\\":\\"description\\"} -• must have required property 'description' in path \\"#/oneOf/1/properties/busy_resources/items/anyOf/2/required\\" with params: {\\"missingProperty\\":\\"description\\"} -• must have required property 'description' in path \\"#/oneOf/1/properties/busy_resources/items/anyOf/3/required\\" with params: {\\"missingProperty\\":\\"description\\"} -• must be equal to constant in path \\"#/oneOf/1/properties/busy_resources/items/anyOf/4/properties/name/const\\" with params: {\\"allowedValue\\":\\"timers\\"} -• must have required property 'description' in path \\"#/oneOf/1/properties/busy_resources/items/anyOf/5/required\\" with params: {\\"missingProperty\\":\\"description\\"} -• must have required property 'description' in path \\"#/oneOf/1/properties/busy_resources/items/anyOf/6/required\\" with params: {\\"missingProperty\\":\\"description\\"} -• must have required property 'description' in path \\"#/oneOf/1/properties/busy_resources/items/anyOf/7/required\\" with params: {\\"missingProperty\\":\\"description\\"} -• must have required property 'description' in path \\"#/oneOf/1/properties/busy_resources/items/anyOf/8/required\\" with params: {\\"missingProperty\\":\\"description\\"} -• must have required property 'description' in path \\"#/oneOf/1/properties/busy_resources/items/anyOf/9/required\\" with params: {\\"missingProperty\\":\\"description\\"} -• must be equal to constant in path \\"#/oneOf/1/properties/busy_resources/items/anyOf/10/properties/name/const\\" with params: {\\"allowedValue\\":\\"io\\"} -• must be equal to constant in path \\"#/oneOf/1/properties/busy_resources/items/anyOf/11/properties/name/const\\" with params: {\\"allowedValue\\":\\"bridge\\"} -• must have required property 'description' in path \\"#/oneOf/1/properties/busy_resources/items/anyOf/12/required\\" with params: {\\"missingProperty\\":\\"description\\"} -• must match a schema in anyOf in path \\"#/oneOf/1/properties/busy_resources/items/anyOf\\" with params: {} -• must match exactly one schema in oneOf in path \\"#/oneOf\\" with params: {\\"passingSchemas\\":null} +• must NOT have additional properties in path "#/oneOf/0/additionalProperties" with params: {"additionalProperty":"busy_resources"} +• must have required property 'description' in path "#/oneOf/1/properties/busy_resources/items/anyOf/0/required" with params: {"missingProperty":"description"} +• must have required property 'description' in path "#/oneOf/1/properties/busy_resources/items/anyOf/1/required" with params: {"missingProperty":"description"} +• must have required property 'description' in path "#/oneOf/1/properties/busy_resources/items/anyOf/2/required" with params: {"missingProperty":"description"} +• must have required property 'description' in path "#/oneOf/1/properties/busy_resources/items/anyOf/3/required" with params: {"missingProperty":"description"} +• must be equal to constant in path "#/oneOf/1/properties/busy_resources/items/anyOf/4/properties/name/const" with params: {"allowedValue":"timers"} +• must have required property 'description' in path "#/oneOf/1/properties/busy_resources/items/anyOf/5/required" with params: {"missingProperty":"description"} +• must have required property 'description' in path "#/oneOf/1/properties/busy_resources/items/anyOf/6/required" with params: {"missingProperty":"description"} +• must have required property 'description' in path "#/oneOf/1/properties/busy_resources/items/anyOf/7/required" with params: {"missingProperty":"description"} +• must have required property 'description' in path "#/oneOf/1/properties/busy_resources/items/anyOf/8/required" with params: {"missingProperty":"description"} +• must have required property 'description' in path "#/oneOf/1/properties/busy_resources/items/anyOf/9/required" with params: {"missingProperty":"description"} +• must be equal to constant in path "#/oneOf/1/properties/busy_resources/items/anyOf/10/properties/name/const" with params: {"allowedValue":"io"} +• must be equal to constant in path "#/oneOf/1/properties/busy_resources/items/anyOf/11/properties/name/const" with params: {"allowedValue":"bridge"} +• must have required property 'description' in path "#/oneOf/1/properties/busy_resources/items/anyOf/12/required" with params: {"missingProperty":"description"} +• must match a schema in anyOf in path "#/oneOf/1/properties/busy_resources/items/anyOf" with params: {} +• must match exactly one schema in oneOf in path "#/oneOf" with params: {"passingSchemas":null} Please report this issue on our GitHub tracker: https://github.com/wix/Detox/issues" `; exports[`Sync Status Formatter assertions should throw error when resource \`name\` is missing 1`] = ` -"Given sync status is not compatible with the status schema (\`SyncStatusSchema.js\`), given status: {\\"app_status\\":\\"busy\\",\\"busy_resources\\":[{\\"description\\":{\\"foo\\":\\"bar\\"}}]}. +"Given sync status is not compatible with the status schema (\`SyncStatusSchema.js\`), given status: {"app_status":"busy","busy_resources":[{"description":{"foo":"bar"}}]}. With reasons: -• must NOT have additional properties in path \\"#/oneOf/0/additionalProperties\\" with params: {\\"additionalProperty\\":\\"busy_resources\\"} -• must have required property 'name' in path \\"#/oneOf/1/properties/busy_resources/items/anyOf/0/required\\" with params: {\\"missingProperty\\":\\"name\\"} -• must have required property 'name' in path \\"#/oneOf/1/properties/busy_resources/items/anyOf/1/required\\" with params: {\\"missingProperty\\":\\"name\\"} -• must have required property 'name' in path \\"#/oneOf/1/properties/busy_resources/items/anyOf/2/required\\" with params: {\\"missingProperty\\":\\"name\\"} -• must have required property 'name' in path \\"#/oneOf/1/properties/busy_resources/items/anyOf/3/required\\" with params: {\\"missingProperty\\":\\"name\\"} -• must have required property 'name' in path \\"#/oneOf/1/properties/busy_resources/items/anyOf/4/required\\" with params: {\\"missingProperty\\":\\"name\\"} -• must have required property 'name' in path \\"#/oneOf/1/properties/busy_resources/items/anyOf/5/required\\" with params: {\\"missingProperty\\":\\"name\\"} -• must have required property 'name' in path \\"#/oneOf/1/properties/busy_resources/items/anyOf/6/required\\" with params: {\\"missingProperty\\":\\"name\\"} -• must have required property 'name' in path \\"#/oneOf/1/properties/busy_resources/items/anyOf/7/required\\" with params: {\\"missingProperty\\":\\"name\\"} -• must have required property 'name' in path \\"#/oneOf/1/properties/busy_resources/items/anyOf/8/required\\" with params: {\\"missingProperty\\":\\"name\\"} -• must have required property 'name' in path \\"#/oneOf/1/properties/busy_resources/items/anyOf/9/required\\" with params: {\\"missingProperty\\":\\"name\\"} -• must have required property 'name' in path \\"#/oneOf/1/properties/busy_resources/items/anyOf/10/required\\" with params: {\\"missingProperty\\":\\"name\\"} -• must have required property 'name' in path \\"#/oneOf/1/properties/busy_resources/items/anyOf/11/required\\" with params: {\\"missingProperty\\":\\"name\\"} -• must have required property 'name' in path \\"#/oneOf/1/properties/busy_resources/items/anyOf/12/required\\" with params: {\\"missingProperty\\":\\"name\\"} -• must match a schema in anyOf in path \\"#/oneOf/1/properties/busy_resources/items/anyOf\\" with params: {} -• must match exactly one schema in oneOf in path \\"#/oneOf\\" with params: {\\"passingSchemas\\":null} +• must NOT have additional properties in path "#/oneOf/0/additionalProperties" with params: {"additionalProperty":"busy_resources"} +• must have required property 'name' in path "#/oneOf/1/properties/busy_resources/items/anyOf/0/required" with params: {"missingProperty":"name"} +• must have required property 'name' in path "#/oneOf/1/properties/busy_resources/items/anyOf/1/required" with params: {"missingProperty":"name"} +• must have required property 'name' in path "#/oneOf/1/properties/busy_resources/items/anyOf/2/required" with params: {"missingProperty":"name"} +• must have required property 'name' in path "#/oneOf/1/properties/busy_resources/items/anyOf/3/required" with params: {"missingProperty":"name"} +• must have required property 'name' in path "#/oneOf/1/properties/busy_resources/items/anyOf/4/required" with params: {"missingProperty":"name"} +• must have required property 'name' in path "#/oneOf/1/properties/busy_resources/items/anyOf/5/required" with params: {"missingProperty":"name"} +• must have required property 'name' in path "#/oneOf/1/properties/busy_resources/items/anyOf/6/required" with params: {"missingProperty":"name"} +• must have required property 'name' in path "#/oneOf/1/properties/busy_resources/items/anyOf/7/required" with params: {"missingProperty":"name"} +• must have required property 'name' in path "#/oneOf/1/properties/busy_resources/items/anyOf/8/required" with params: {"missingProperty":"name"} +• must have required property 'name' in path "#/oneOf/1/properties/busy_resources/items/anyOf/9/required" with params: {"missingProperty":"name"} +• must have required property 'name' in path "#/oneOf/1/properties/busy_resources/items/anyOf/10/required" with params: {"missingProperty":"name"} +• must have required property 'name' in path "#/oneOf/1/properties/busy_resources/items/anyOf/11/required" with params: {"missingProperty":"name"} +• must have required property 'name' in path "#/oneOf/1/properties/busy_resources/items/anyOf/12/required" with params: {"missingProperty":"name"} +• must match a schema in anyOf in path "#/oneOf/1/properties/busy_resources/items/anyOf" with params: {} +• must match exactly one schema in oneOf in path "#/oneOf" with params: {"passingSchemas":null} Please report this issue on our GitHub tracker: https://github.com/wix/Detox/issues" @@ -109,7 +109,7 @@ exports[`Sync Status Formatter busy status should format "delayed_perform_select exports[`Sync Status Formatter busy status should format "dispatch_queue" correctly 1`] = ` "The app is busy with the following tasks: -• There are 123 work items pending on the dispatch queue: \\"foo\\"." +• There are 123 work items pending on the dispatch queue: "foo"." `; exports[`Sync Status Formatter busy status should format "io" correctly 1`] = ` @@ -149,17 +149,17 @@ exports[`Sync Status Formatter busy status should format "network" correctly 1`] exports[`Sync Status Formatter busy status should format "one_time_events" correctly 1`] = ` "The app is busy with the following tasks: -• The event \\"foo\\" is taking place with object: \\"bar\\"." +• The event "foo" is taking place with object: "bar"." `; exports[`Sync Status Formatter busy status should format "one_time_events" correctly when there is no object 1`] = ` "The app is busy with the following tasks: -• The event \\"foo\\" is taking place." +• The event "foo" is taking place." `; exports[`Sync Status Formatter busy status should format "run_loop" correctly 1`] = ` "The app is busy with the following tasks: -• Run loop \\"foo\\" is awake." +• Run loop "foo" is awake." `; exports[`Sync Status Formatter busy status should format "timers" correctly when there are timers in description 1`] = ` @@ -209,7 +209,7 @@ exports[`Sync Status Formatter busy status should format "ui" correctly #3 1`] = exports[`Sync Status Formatter busy status should format "unknown" correctly 1`] = ` "The app is busy with the following tasks: -• Resource \\"foo.bar#baz\\" is busy." +• Resource "foo.bar#baz" is busy." `; exports[`Sync Status Formatter should format idle status correctly 1`] = `"The app seems to be idle"`; diff --git a/detox/src/devices/common/drivers/android/exec/__snapshots__/AAPT.test.js.snap b/detox/src/devices/common/drivers/android/exec/__snapshots__/AAPT.test.js.snap index e3affe010d..c464e8818c 100644 --- a/detox/src/devices/common/drivers/android/exec/__snapshots__/AAPT.test.js.snap +++ b/detox/src/devices/common/drivers/android/exec/__snapshots__/AAPT.test.js.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`AAPT Checking whether APK holds instrumentation testing should execute the AAPT command with proper args 1`] = `"\\"escaped(mockSdk/build-tools/30.0.0/aapt)\\" dump xmlstrings \\"escaped(path/to/app.apk)\\" AndroidManifest.xml"`; +exports[`AAPT Checking whether APK holds instrumentation testing should execute the AAPT command with proper args 1`] = `""escaped(mockSdk/build-tools/30.0.0/aapt)" dump xmlstrings "escaped(path/to/app.apk)" AndroidManifest.xml"`; -exports[`AAPT Reading package name should execute the AAPT command with proper args 1`] = `"\\"escaped(mockSdk/build-tools/30.0.0/aapt)\\" dump badging \\"escaped(path/to/app.apk)\\""`; +exports[`AAPT Reading package name should execute the AAPT command with proper args 1`] = `""escaped(mockSdk/build-tools/30.0.0/aapt)" dump badging "escaped(path/to/app.apk)""`; diff --git a/detox/src/logger/__snapshots__/DetoxLogger.test.js.snap b/detox/src/logger/__snapshots__/DetoxLogger.test.js.snap index fd1ce5ff3d..47c3a06c1b 100644 --- a/detox/src/logger/__snapshots__/DetoxLogger.test.js.snap +++ b/detox/src/logger/__snapshots__/DetoxLogger.test.js.snap @@ -15,7 +15,7 @@ exports[`DetoxLogger - main functionality - should format messages according to origin: some-module/index.js 00:00:00.000 detox[PID] i custom-category:MESSAGE A message with a payload data: { - \\"foo\\": \\"bar\\" + "foo": "bar" } 00:00:00.000 detox[PID] i custom-category One more message with a payload data: raw string data" @@ -59,13 +59,13 @@ exports[`DetoxLogger - main functionality - should format messages according to origin: some-module/index.js 00:00:00.000 detox[PID] i custom-category:MESSAGE A message with a payload data: { - \\"foo\\": \\"bar\\" + "foo": "bar" } 00:00:00.000 detox[PID] i custom-category One more message with a payload data: raw string data 00:00:00.000 detox[PID] i custom-category:MESSAGE Trace message 00:00:00.000 detox[PID] i someMethodCall - args: (\\"stringArgument\\", {\\"prop\\":\\"value\\"})" + args: ("stringArgument", {"prop":"value"})" `; exports[`DetoxLogger - main functionality - should format messages according to the log level: warn 1`] = ` diff --git a/detox/src/logger/utils/__snapshots__/DetoxLogFinalizer.test.js.snap b/detox/src/logger/utils/__snapshots__/DetoxLogFinalizer.test.js.snap index 872ba9cc2f..c508a95ba4 100644 --- a/detox/src/logger/utils/__snapshots__/DetoxLogFinalizer.test.js.snap +++ b/detox/src/logger/utils/__snapshots__/DetoxLogFinalizer.test.js.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`DetoxLogFinalizer createEventStream should create a stream of Chrome Trace format events 1`] = ` -Array [ - Object { - "args": Object { +[ + { + "args": { "name": "primary", }, "name": "process_name", @@ -12,8 +12,8 @@ Array [ "tid": 0, "ts": 1672531200000000, }, - Object { - "args": Object { + { + "args": { "sort_index": 0, }, "name": "process_sort_index", @@ -22,8 +22,8 @@ Array [ "tid": 0, "ts": 1672531200000000, }, - Object { - "args": Object { + { + "args": { "name": "main", }, "name": "thread_name", @@ -32,8 +32,8 @@ Array [ "tid": 0, "ts": 1672531200000000, }, - Object { - "args": Object { + { + "args": { "sort_index": 0, }, "name": "thread_sort_index", @@ -42,8 +42,8 @@ Array [ "tid": 0, "ts": 1672531200000000, }, - Object { - "args": Object { + { + "args": { "level": 30, "v": 0, }, @@ -54,8 +54,8 @@ Array [ "tid": 0, "ts": 1672531200000000, }, - Object { - "args": Object { + { + "args": { "name": "secondary", }, "name": "process_name", @@ -64,8 +64,8 @@ Array [ "tid": 0, "ts": 1672531200500000, }, - Object { - "args": Object { + { + "args": { "sort_index": 1, }, "name": "process_sort_index", @@ -74,8 +74,8 @@ Array [ "tid": 0, "ts": 1672531200500000, }, - Object { - "args": Object { + { + "args": { "level": 30, "v": 0, }, @@ -86,8 +86,8 @@ Array [ "tid": 0, "ts": 1672531200500000, }, - Object { - "args": Object { + { + "args": { "id": 1, "level": 10, "v": 0, @@ -99,8 +99,8 @@ Array [ "tid": 0, "ts": 1672531201000000, }, - Object { - "args": Object { + { + "args": { "level": 20, "v": 0, }, @@ -111,8 +111,8 @@ Array [ "tid": 0, "ts": 1672531201000000, }, - Object { - "args": Object { + { + "args": { "id": 1, "level": 10, "v": 0, @@ -124,8 +124,8 @@ Array [ "tid": 0, "ts": 1672531201100000, }, - Object { - "args": Object { + { + "args": { "id": 1, "level": 10, "v": 0, @@ -136,8 +136,8 @@ Array [ "tid": 0, "ts": 1672531201400000, }, - Object { - "args": Object { + { + "args": { "name": "parallel", }, "name": "thread_name", @@ -146,8 +146,8 @@ Array [ "tid": 1, "ts": 1672531201500000, }, - Object { - "args": Object { + { + "args": { "sort_index": 1, }, "name": "thread_sort_index", @@ -156,8 +156,8 @@ Array [ "tid": 1, "ts": 1672531201500000, }, - Object { - "args": Object { + { + "args": { "id": 2, "level": 10, "v": 0, @@ -169,8 +169,8 @@ Array [ "tid": 1, "ts": 1672531201500000, }, - Object { - "args": Object { + { + "args": { "id": 1, "level": 10, "v": 0, @@ -181,8 +181,8 @@ Array [ "tid": 0, "ts": 1672531202000000, }, - Object { - "args": Object { + { + "args": { "level": 10, "v": 0, }, @@ -192,8 +192,8 @@ Array [ "tid": 0, "ts": 1672531202250000, }, - Object { - "args": Object { + { + "args": { "id": 2, "level": 10, "v": 0, @@ -208,9 +208,9 @@ Array [ `; exports[`DetoxLogFinalizer finalize should convert JSONL logs to Chrome Trace format: chrome-trace 1`] = ` -Array [ - Object { - "args": Object { +[ + { + "args": { "name": "primary", }, "name": "process_name", @@ -219,8 +219,8 @@ Array [ "tid": 0, "ts": 1672531200000000, }, - Object { - "args": Object { + { + "args": { "sort_index": 0, }, "name": "process_sort_index", @@ -229,8 +229,8 @@ Array [ "tid": 0, "ts": 1672531200000000, }, - Object { - "args": Object { + { + "args": { "name": "main", }, "name": "thread_name", @@ -239,8 +239,8 @@ Array [ "tid": 0, "ts": 1672531200000000, }, - Object { - "args": Object { + { + "args": { "sort_index": 0, }, "name": "thread_sort_index", @@ -249,8 +249,8 @@ Array [ "tid": 0, "ts": 1672531200000000, }, - Object { - "args": Object { + { + "args": { "level": 30, "v": 0, }, @@ -261,8 +261,8 @@ Array [ "tid": 0, "ts": 1672531200000000, }, - Object { - "args": Object { + { + "args": { "name": "secondary", }, "name": "process_name", @@ -271,8 +271,8 @@ Array [ "tid": 0, "ts": 1672531200500000, }, - Object { - "args": Object { + { + "args": { "sort_index": 1, }, "name": "process_sort_index", @@ -281,8 +281,8 @@ Array [ "tid": 0, "ts": 1672531200500000, }, - Object { - "args": Object { + { + "args": { "name": "parallel", }, "name": "thread_name", @@ -291,8 +291,8 @@ Array [ "tid": 3, "ts": 1672531200500000, }, - Object { - "args": Object { + { + "args": { "sort_index": 3, }, "name": "thread_sort_index", @@ -301,8 +301,8 @@ Array [ "tid": 3, "ts": 1672531200500000, }, - Object { - "args": Object { + { + "args": { "level": 30, "v": 0, }, @@ -313,8 +313,8 @@ Array [ "tid": 3, "ts": 1672531200500000, }, - Object { - "args": Object { + { + "args": { "name": "parallel", }, "name": "thread_name", @@ -323,8 +323,8 @@ Array [ "tid": 1, "ts": 1672531201000000, }, - Object { - "args": Object { + { + "args": { "sort_index": 1, }, "name": "thread_sort_index", @@ -333,8 +333,8 @@ Array [ "tid": 1, "ts": 1672531201000000, }, - Object { - "args": Object { + { + "args": { "id": 1, "level": 10, "v": 0, @@ -346,8 +346,8 @@ Array [ "tid": 1, "ts": 1672531201000000, }, - Object { - "args": Object { + { + "args": { "name": "undefined", }, "name": "thread_name", @@ -356,8 +356,8 @@ Array [ "tid": 4, "ts": 1672531201000000, }, - Object { - "args": Object { + { + "args": { "sort_index": 4, }, "name": "thread_sort_index", @@ -366,8 +366,8 @@ Array [ "tid": 4, "ts": 1672531201000000, }, - Object { - "args": Object { + { + "args": { "level": 20, "v": 0, }, @@ -378,8 +378,8 @@ Array [ "tid": 4, "ts": 1672531201000000, }, - Object { - "args": Object { + { + "args": { "id": 1, "level": 10, "v": 0, @@ -391,8 +391,8 @@ Array [ "tid": 1, "ts": 1672531201100000, }, - Object { - "args": Object { + { + "args": { "id": 1, "level": 10, "v": 0, @@ -403,8 +403,8 @@ Array [ "tid": 1, "ts": 1672531201400000, }, - Object { - "args": Object { + { + "args": { "name": "parallel", }, "name": "thread_name", @@ -413,8 +413,8 @@ Array [ "tid": 2, "ts": 1672531201500000, }, - Object { - "args": Object { + { + "args": { "sort_index": 2, }, "name": "thread_sort_index", @@ -423,8 +423,8 @@ Array [ "tid": 2, "ts": 1672531201500000, }, - Object { - "args": Object { + { + "args": { "id": 2, "level": 10, "v": 0, @@ -436,8 +436,8 @@ Array [ "tid": 2, "ts": 1672531201500000, }, - Object { - "args": Object { + { + "args": { "id": 1, "level": 10, "v": 0, @@ -448,8 +448,8 @@ Array [ "tid": 1, "ts": 1672531202000000, }, - Object { - "args": Object { + { + "args": { "level": 10, "v": 0, }, @@ -459,8 +459,8 @@ Array [ "tid": 3, "ts": 1672531202250000, }, - Object { - "args": Object { + { + "args": { "id": 2, "level": 10, "v": 0, diff --git a/detox/src/server/__snapshots__/DetoxServer.test.js.snap b/detox/src/server/__snapshots__/DetoxServer.test.js.snap index aa56a028bc..ef14ccbdb7 100644 --- a/detox/src/server/__snapshots__/DetoxServer.test.js.snap +++ b/detox/src/server/__snapshots__/DetoxServer.test.js.snap @@ -1,8 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`DetoxServer .open() / .close() should WARN log a message upon unsuccessful server closing (error emit case) 1`] = ` -Array [ - Object { +[ + { "err": [Error: TEST_ERROR], }, "Detox server has been closed abruptly! See the error details below:", @@ -10,8 +10,8 @@ Array [ `; exports[`DetoxServer .open() / .close() should WARN log a message upon unsuccessful server closing (rejection case) 1`] = ` -Array [ - Object { +[ + { "err": [Error: TEST_ERROR], }, "Detox server has been closed abruptly! See the error details below:", @@ -19,8 +19,8 @@ Array [ `; exports[`DetoxServer .open() / .close() should WARN log a message upon unsuccessful server closing (timeout case) 1`] = ` -Array [ - Object { +[ + { "err": [DetoxRuntimeError: Detox server close callback was not invoked within the 10000 ms timeout], }, "Detox server has been closed abruptly! See the error details below:", @@ -28,8 +28,8 @@ Array [ `; exports[`DetoxServer should ERROR log messages from wss.Server 1`] = ` -Array [ - Object { +[ + { "err": [Error: TEST_ERROR], }, "Detox server has got an unhandled error:", diff --git a/detox/src/server/__tests__/__snapshots__/server-integration.test.js.snap b/detox/src/server/__tests__/__snapshots__/server-integration.test.js.snap index e6c77eacc8..c58f8b1355 100644 --- a/detox/src/server/__tests__/__snapshots__/server-integration.test.js.snap +++ b/detox/src/server/__tests__/__snapshots__/server-integration.test.js.snap @@ -1,8 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Detox server integration "app" connects first, and then disconnects 1`] = ` -Array [ - Object { +[ + { "cat": "ws-server,ws-session", "id": 8081, }, @@ -12,8 +12,8 @@ Array [ `; exports[`Detox server integration "tester" connects first, and then disconnects 1`] = ` -Array [ - Object { +[ + { "cat": "ws-server,ws-session", "id": 8081, }, @@ -23,8 +23,8 @@ Array [ `; exports[`Detox server integration edge cases .registerSession - calling twice 1`] = ` -Array [ - Object { +[ + { "cat": "ws-server,ws-session", "id": 10, "role": "app", @@ -38,18 +38,18 @@ https://github.com/wix/Detox/issues", `; exports[`Detox server integration edge cases app dispatches "ready" action before login 1`] = ` -Array [ - Object { +[ + { "cat": "ws-server,ws-session", "id": 10, }, - "The app has dispatched \\"ready\\" action too early.", + "The app has dispatched "ready" action too early.", ] `; exports[`Detox server integration edge cases attempt to register the same connection twice 1`] = ` -Array [ - Object { +[ + { "cat": "ws-server,ws-session", "id": 10, }, @@ -60,8 +60,8 @@ https://github.com/wix/Detox/issues", `; exports[`Detox server integration edge cases attempt to unregister an unknown connection 1`] = ` -Array [ - Object { +[ + { "cat": "ws-server,ws-session", }, "DetoxInternalError: Cannot unregister an unknown WebSocket instance. @@ -71,12 +71,12 @@ https://github.com/wix/Detox/issues", `; exports[`Detox server integration edge cases login - empty .params 1`] = ` -Array [ - Object { +[ + { "cat": "ws-server,ws-session", "id": 10, }, - Object { + { "error": [DetoxRuntimeError: Invalid login action received, it has no .params HINT: Please report this issue on our GitHub tracker: @@ -91,12 +91,12 @@ https://github.com/wix/Detox/issues `; exports[`Detox server integration edge cases login - invalid .role 1`] = ` -Array [ - Object { +[ + { "cat": "ws-server,ws-session", "id": 10, }, - Object { + { "error": [DetoxRuntimeError: Invalid login action received, it has invalid .role HINT: Please report this issue on our GitHub tracker: @@ -115,12 +115,12 @@ https://github.com/wix/Detox/issues `; exports[`Detox server integration edge cases login - missing .sessionId 1`] = ` -Array [ - Object { +[ + { "cat": "ws-server,ws-session", "id": 10, }, - Object { + { "error": [DetoxRuntimeError: Invalid login action received, it has no .sessionId HINT: Please report this issue on our GitHub tracker: @@ -139,12 +139,12 @@ https://github.com/wix/Detox/issues `; exports[`Detox server integration edge cases login - non-string .sessionId 1`] = ` -Array [ - Object { +[ + { "cat": "ws-server,ws-session", "id": 10, }, - Object { + { "error": [DetoxRuntimeError: Invalid login action received, it has a non-string .sessionId HINT: Please report this issue on our GitHub tracker: @@ -165,15 +165,15 @@ https://github.com/wix/Detox/issues `; exports[`Detox server integration edge cases login twice (as app) 1`] = ` -Array [ - Object { +[ + { "cat": "ws-server,ws-session", "id": 10, "role": "app", "sessionId": "aSession", "trackingId": "app", }, - Object { + { "error": [DetoxInternalError: Cannot log in twice into the same session (aSession) being "app" already Please report this issue on our GitHub tracker: https://github.com/wix/Detox/issues], @@ -183,15 +183,15 @@ https://github.com/wix/Detox/issues], `; exports[`Detox server integration edge cases login twice (as tester) + socket send error 1`] = ` -Array [ - Object { +[ + { "cat": "ws-server,ws-session", "id": 10, "role": "tester", "sessionId": "aSession", "trackingId": "tester", }, - Object { + { "err": [Error: TestError], }, "Cannot forward the error details to the tester due to the error:", @@ -199,15 +199,15 @@ Array [ `; exports[`Detox server integration edge cases login twice (as tester) + socket send error 2`] = ` -Array [ - Object { +[ + { "cat": "ws-server,ws-session", "id": 10, "role": "tester", "sessionId": "aSession", "trackingId": "tester", }, - Object { + { "error": [DetoxInternalError: Cannot log in twice into the same session (aSession) being "tester" already Please report this issue on our GitHub tracker: https://github.com/wix/Detox/issues], @@ -217,12 +217,12 @@ https://github.com/wix/Detox/issues], `; exports[`Detox server integration edge cases on(message) - malformed data 1`] = ` -Array [ - Object { +[ + { "cat": "ws-server,ws-session", "id": 10, }, - Object { + { "error": [DetoxRuntimeError: The payload received is not a valid JSON. HINT: Please report this issue on our GitHub tracker: @@ -233,12 +233,12 @@ https://github.com/wix/Detox/issues], `; exports[`Detox server integration edge cases on(message) - no .type 1`] = ` -Array [ - Object { +[ + { "cat": "ws-server,ws-session", "id": 10, }, - Object { + { "error": [DetoxRuntimeError: Cannot process an action without a type. HINT: Please report this issue on our GitHub tracker: @@ -253,12 +253,12 @@ https://github.com/wix/Detox/issues `; exports[`Detox server integration edge cases receiving an action before we login 1`] = ` -Array [ - Object { +[ + { "cat": "ws-server,ws-session", "id": 10, }, - Object { + { "error": [DetoxRuntimeError: Action dispatched too early, there is no session to use: HINT: Please report this issue on our GitHub tracker: @@ -274,12 +274,12 @@ https://github.com/wix/Detox/issues `; exports[`Detox server integration edge cases socket error 1`] = ` -Array [ - Object { +[ + { "cat": "ws-server,ws-session", "id": 10, }, - Object { + { "error": [Error: Test error], }, "Caught socket error:", @@ -287,15 +287,15 @@ Array [ `; exports[`Detox server integration tester and app interconnect and then disconnect 1`] = ` -Array [ - Object { +[ + { "cat": "ws-server,ws-session", "id": 11, "role": "app", "sessionId": "aSession", "trackingId": "app", }, - Object { + { "error": [DetoxRuntimeError: Cannot forward the message to the Detox client. { diff --git a/examples/demo-plugin/package.json b/examples/demo-plugin/package.json index da3be16b23..ca7cb74686 100644 --- a/examples/demo-plugin/package.json +++ b/examples/demo-plugin/package.json @@ -7,7 +7,7 @@ }, "devDependencies": { "detox": "^20.14.5", - "jest": "^28.0.0" + "jest": "^29.0.0" }, "detox": { "testRunner": { From 80bff595538780c7b8019c7db796142414600cf9 Mon Sep 17 00:00:00 2001 From: wixmobile Date: Thu, 28 Dec 2023 03:34:36 -0800 Subject: [PATCH 068/185] Publish 20.14.6 [ci skip] --- detox/package.json | 2 +- detox/test/package.json | 4 ++-- examples/demo-native-android/package.json | 4 ++-- examples/demo-native-ios/package.json | 4 ++-- examples/demo-plugin/package.json | 4 ++-- examples/demo-react-native-detox-instruments/package.json | 4 ++-- examples/demo-react-native/package.json | 4 ++-- generation/package.json | 2 +- lerna.json | 2 +- package.json | 2 +- 10 files changed, 16 insertions(+), 16 deletions(-) diff --git a/detox/package.json b/detox/package.json index c84092762a..f3b91ae63f 100644 --- a/detox/package.json +++ b/detox/package.json @@ -1,7 +1,7 @@ { "name": "detox", "description": "E2E tests and automation for mobile", - "version": "20.14.5", + "version": "20.14.6", "bin": { "detox": "local-cli/cli.js" }, diff --git a/detox/test/package.json b/detox/test/package.json index e261504fb6..8a5646ccae 100644 --- a/detox/test/package.json +++ b/detox/test/package.json @@ -1,6 +1,6 @@ { "name": "detox-test", - "version": "20.14.5", + "version": "20.14.6", "private": true, "engines": { "node": ">=14.5.0" @@ -56,7 +56,7 @@ "@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/parser": "^5.4.0", "cross-env": "^7.0.3", - "detox": "^20.14.5", + "detox": "^20.14.6", "detox-allure2-adapter": "^1.0.0-alpha.2", "eslint": "^8.41.0", "eslint-plugin-unicorn": "^47.0.0", diff --git a/examples/demo-native-android/package.json b/examples/demo-native-android/package.json index 1b1102bec3..b98b93922f 100644 --- a/examples/demo-native-android/package.json +++ b/examples/demo-native-android/package.json @@ -1,13 +1,13 @@ { "name": "detox-demo-native-android", - "version": "20.14.5", + "version": "20.14.6", "private": true, "scripts": { "packager": "react-native start", "detox-server": "detox run-server" }, "devDependencies": { - "detox": "^20.14.5" + "detox": "^20.14.6" }, "detox": {} } diff --git a/examples/demo-native-ios/package.json b/examples/demo-native-ios/package.json index 83e7c39980..dc1f29bbb7 100644 --- a/examples/demo-native-ios/package.json +++ b/examples/demo-native-ios/package.json @@ -1,9 +1,9 @@ { "name": "detox-demo-native-ios", - "version": "20.14.5", + "version": "20.14.6", "private": true, "devDependencies": { - "detox": "^20.14.5" + "detox": "^20.14.6" }, "detox": { "specs": "", diff --git a/examples/demo-plugin/package.json b/examples/demo-plugin/package.json index ca7cb74686..74071f37fb 100644 --- a/examples/demo-plugin/package.json +++ b/examples/demo-plugin/package.json @@ -1,12 +1,12 @@ { "name": "demo-plugin", - "version": "20.14.5", + "version": "20.14.6", "private": true, "scripts": { "test:plugin": "detox test --configuration plugin -l verbose" }, "devDependencies": { - "detox": "^20.14.5", + "detox": "^20.14.6", "jest": "^29.0.0" }, "detox": { diff --git a/examples/demo-react-native-detox-instruments/package.json b/examples/demo-react-native-detox-instruments/package.json index 27ff3bc84a..050ae7a334 100644 --- a/examples/demo-react-native-detox-instruments/package.json +++ b/examples/demo-react-native-detox-instruments/package.json @@ -1,10 +1,10 @@ { "name": "demo-react-native-detox-instruments", - "version": "20.14.5", + "version": "20.14.6", "private": true, "scripts": {}, "devDependencies": { - "detox": "^20.14.5" + "detox": "^20.14.6" }, "detox": { "configurations": { diff --git a/examples/demo-react-native/package.json b/examples/demo-react-native/package.json index b00354ced1..4304b895be 100644 --- a/examples/demo-react-native/package.json +++ b/examples/demo-react-native/package.json @@ -1,6 +1,6 @@ { "name": "example", - "version": "20.14.5", + "version": "20.14.6", "private": true, "scripts": { "start": "react-native start", @@ -32,7 +32,7 @@ "@types/fs-extra": "^9.0.13", "@types/jest": "^29.2.1", "@types/react": "^18.0.24", - "detox": "^20.14.5", + "detox": "^20.14.6", "fs-extra": "^9.1.0", "jest": "^29.2.1", "ts-jest": "^29.0.3", diff --git a/generation/package.json b/generation/package.json index e788779bdf..028f16d5f7 100644 --- a/generation/package.json +++ b/generation/package.json @@ -1,6 +1,6 @@ { "name": "generation", - "version": "20.14.2-smoke.0", + "version": "20.14.6", "description": "Generate wrapper code for native dependencies", "main": "index.js", "private": true, diff --git a/lerna.json b/lerna.json index 5bdf06bace..5c02488c46 100644 --- a/lerna.json +++ b/lerna.json @@ -13,7 +13,7 @@ "generation", "." ], - "version": "20.14.5", + "version": "20.14.6", "npmClient": "npm", "command": { "publish": { diff --git a/package.json b/package.json index 1d538a54c4..6a2d3f4a71 100644 --- a/package.json +++ b/package.json @@ -54,5 +54,5 @@ "shell-utils": "1.x.x", "unified": "^10.1.0" }, - "version": "20.14.5" + "version": "20.14.6" } From 04863eb55d5291b512b8e0e28172c033b065130e Mon Sep 17 00:00:00 2001 From: wixmobile Date: Thu, 28 Dec 2023 03:35:37 -0800 Subject: [PATCH 069/185] Publish docs version 20.x --- .../partials/_project-setup-apps-android.mdx | 71 +++++++++++++++---- .../partials/_project-setup-devices-ios.mdx | 6 +- 2 files changed, 62 insertions(+), 15 deletions(-) diff --git a/website/versioned_docs/version-20.x/introduction/partials/_project-setup-apps-android.mdx b/website/versioned_docs/version-20.x/introduction/partials/_project-setup-apps-android.mdx index e53091f22f..b78e45f51c 100644 --- a/website/versioned_docs/version-20.x/introduction/partials/_project-setup-apps-android.mdx +++ b/website/versioned_docs/version-20.x/introduction/partials/_project-setup-apps-android.mdx @@ -2,7 +2,7 @@ import FlavorizedCodeBlock from '@site/src/components/FlavorizedCodeBlock'; -Check **binaryPath** and **build** configs for `android.debug` and `android.release` mode in your Detox config: +Check the **build** and **binaryPath** attributes for the `android.debug` and `android.release` Detox configs: ```js title=".detoxrc.js" module.exports = { @@ -10,15 +10,55 @@ module.exports = { 'android.debug': { type: 'android.apk', // highlight-start - binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk', - build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug' + build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug', + binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk' // highlight-end }, 'android.release': { type: 'android.apk', // highlight-start + build: 'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release', + binaryPath: 'android/app/build/outputs/apk/release/app-release.apk' + // highlight-end + }, + // ... + }, + // ... +}; +``` + +If you have a typical React Native project, these values should already be sufficient and correct. + +#### testBinaryPath + +In Android automation testing, there are in fact 2 app binaries involved: +1. The _app_ APK, containing your app's code. +2. The _test_ APK, containing _test_ code. That includes Detox's native code, Espresso and more. + +In some projects, it might make sense for the test APK to be generated over a separate flow, through which is may end up +being put it some custom, +[non-default path](https://stackoverflow.com/questions/43670463/where-is-the-test-apk-located-in-android-project). +One such example is an optimization where the test APK is prebuilt once and used across multiple app variations. This is +a place where the `testBinaryPath` attribute can come to the rescue; It can be applied in order to set the custom path +to the test APK explicitly: + +```js title=".detoxrc.js" +module.exports = { + apps: { + 'android.debug': { + type: 'android.apk', + build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug', + binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk', + // highlight-start + testBinaryPath: 'custom/path/to/app-debug-androidTest.apk' + // highlight-end + }, + 'android.release': { + type: 'android.apk', binaryPath: 'android/app/build/outputs/apk/release/app-release.apk', - build: 'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release' + build: 'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release', + // highlight-start + testBinaryPath: 'custom/path/to/app-release-androidTest.apk' // highlight-end }, // ... @@ -27,23 +67,30 @@ module.exports = { }; ``` -If you have a typical React Native project, these values should already be correct. +:::info Note + +In the common case, the `testBinaryPath` attributes is not explicitly required, simply because Detox knows +how to locate it in one of the default paths automatically. + +::: + +#### Product flavors -On the other hand, if your app has extra [`productFlavors`](https://developer.android.com/studio/build/build-variants#product-flavors) -(let's imagine you have `driver` and `passenger` flavors of a taxi application), then you should rewrite your apps config -for both **debug** and **release** configurations, e.g.: +On even more advanced use cases, apps may have additional, custom [`productFlavors`](https://developer.android.com/studio/build/build-variants#product-flavors) +(for example, `driver` and `passenger` flavors of a taxi application). In this case, you should rewrite your apps config, +for both **debug** and **release** configurations, according to those flavors, e.g.: - {(conf) => `\ - '${conf.toLowerCase()}.android.debug': { + {(flavor) => `\ + '${flavor.toLowerCase()}.android.debug': { type: 'android.apk', - binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk', -+ binaryPath: 'android/app/build/outputs/apk/${conf.toLowerCase()}/debug/app-${conf.toLowerCase()}-debug.apk', ++ binaryPath: 'android/app/build/outputs/apk/${flavor.toLowerCase()}/debug/app-${flavor.toLowerCase()}-debug.apk', - build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug', -+ build: 'cd android && ./gradlew assemble${conf}Debug assemble${conf}DebugAndroidTest -DtestBuildType=debug', ++ build: 'cd android && ./gradlew assemble${flavor}Debug assemble${flavor}DebugAndroidTest -DtestBuildType=debug', },`} diff --git a/website/versioned_docs/version-20.x/introduction/partials/_project-setup-devices-ios.mdx b/website/versioned_docs/version-20.x/introduction/partials/_project-setup-devices-ios.mdx index 133a26f3f2..f7fbc8a0cc 100644 --- a/website/versioned_docs/version-20.x/introduction/partials/_project-setup-devices-ios.mdx +++ b/website/versioned_docs/version-20.x/introduction/partials/_project-setup-devices-ios.mdx @@ -1,4 +1,4 @@ -To check if a specific device type (e.g. `iPhone 12`) is installed locally, run: +To check if a specific device type (e.g. `iPhone 15`) is installed locally, run: ```bash xcrun simctl list devicetypes @@ -14,8 +14,8 @@ iPhone 5 (com.apple.CoreSimulator.SimDeviceType.iPhone-5) iPhone SE (2nd generation) (com.apple.CoreSimulator.SimDeviceType.iPhone-SE--2nd-generation-) iPhone 12 mini (com.apple.CoreSimulator.SimDeviceType.iPhone-12-mini) // highlight-next-line -iPhone 12 (com.apple.CoreSimulator.SimDeviceType.iPhone-12) -iPhone 12 Pro (com.apple.CoreSimulator.SimDeviceType.iPhone-12-Pro) +iPhone 15 (com.apple.CoreSimulator.SimDeviceType.iPhone-15) +iPhone 15 Pro (com.apple.CoreSimulator.SimDeviceType.iPhone-15-Pro) … ``` From e7ef6daba424f357e86226004d99536f531b0347 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Thu, 28 Dec 2023 14:34:08 +0200 Subject: [PATCH 070/185] chore: update dev dependencies (#4314) --- detox/package.json | 25 ++++++++++----------- detox/runners/jest/testEnvironment/index.js | 3 ++- detox/src/realms/DetoxContext.js | 2 +- detox/test/package.json | 20 ++++++++--------- detox/tsconfig.json | 2 +- generation/package.json | 2 +- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/detox/package.json b/detox/package.json index f3b91ae63f..d89d141be4 100644 --- a/detox/package.json +++ b/detox/package.json @@ -36,27 +36,26 @@ "devDependencies": { "@types/bunyan": "^1.8.8", "@types/child-process-promise": "^2.2.1", - "@types/fs-extra": "^9.0.13", + "@types/fs-extra": "^11.0.4", "@types/jest": "^29.0.0", "@types/node": "^14.18.33", "@types/node-ipc": "^9.2.0", "@types/ws": "^7.4.0", - "@typescript-eslint/eslint-plugin": "^5.59.8", - "@typescript-eslint/parser": "^5.59.8", + "@typescript-eslint/eslint-plugin": "^6.16.0", + "@typescript-eslint/parser": "^6.16.0", "cross-env": "^7.0.3", - "eslint": "^8.41.0", - "eslint-plugin-ecmascript-compat": "^3.0.0", - "eslint-plugin-import": "^2.27.5", + "eslint": "^8.56.0", + "eslint-plugin-ecmascript-compat": "^3.1.0", + "eslint-plugin-import": "^2.29.1", "eslint-plugin-no-only-tests": "^3.1.0", "eslint-plugin-node": "^11.1.0", - "eslint-plugin-unicorn": "^47.0.0", + "eslint-plugin-unicorn": "^50.0.1", "jest": "^29.0.0", - "jest-allure2-reporter": "^2.0.0-beta.2", - "mockdate": "^2.0.1", - "prettier": "^2.4.1", + "jest-allure2-reporter": "^2.0.0-beta.4", + "prettier": "^3.1.1", "react-native": "0.71.10", "react-native-codegen": "^0.0.8", - "typescript": "^4.5.2", + "typescript": "^5.3.3", "wtfnode": "^0.9.1" }, "dependencies": { @@ -72,7 +71,7 @@ "funpermaproxy": "^1.1.0", "glob": "^8.0.3", "ini": "^1.3.4", - "jest-environment-emit": "^1.0.3", + "jest-environment-emit": "^1.0.5", "json-cycle": "^1.3.0", "lodash": "^4.17.11", "multi-sort-stream": "^1.0.3", @@ -105,7 +104,7 @@ } }, "engines": { - "node": ">=14.5.0" + "node": ">=14.14.0" }, "browserslist": [ "node 14" diff --git a/detox/runners/jest/testEnvironment/index.js b/detox/runners/jest/testEnvironment/index.js index 159b1b1248..06902467dd 100644 --- a/detox/runners/jest/testEnvironment/index.js +++ b/detox/runners/jest/testEnvironment/index.js @@ -1,6 +1,6 @@ const path = require('path'); -const WithEmitter = require('jest-environment-emit'); +const WithEmitter = require('jest-environment-emit').default; const resolveFrom = require('resolve-from'); const maybeNodeEnvironment = require(resolveFrom(process.cwd(), 'jest-environment-node')); /** @type {typeof import('@jest/environment').JestEnvironment} */ @@ -75,6 +75,7 @@ class DetoxCircusEnvironment extends WithEmitter(NodeEnvironment) { // @ts-expect-error TS2425 async handleTestEvent(event, state) { + // @ts-expect-error TS2855 await super.handleTestEvent(event, state); if (detox.session.unsafe_earlyTeardown) { diff --git a/detox/src/realms/DetoxContext.js b/detox/src/realms/DetoxContext.js index 93d45f882f..bf5111d343 100644 --- a/detox/src/realms/DetoxContext.js +++ b/detox/src/realms/DetoxContext.js @@ -47,7 +47,7 @@ class DetoxContext { this[$sessionState] = this[$restoreSessionState](); /** - * @type {DetoxLogger & Detox.Logger} + * @type {import('../logger/').DetoxLogger & Detox.Logger} */ this[symbols.logger] = new DetoxLogger({ file: temporary.for.jsonl(`${this[$sessionState].id}.${process.pid}`), diff --git a/detox/test/package.json b/detox/test/package.json index 8a5646ccae..2042688e14 100644 --- a/detox/test/package.json +++ b/detox/test/package.json @@ -48,23 +48,23 @@ "@babel/core": "^7.20.0", "@babel/preset-env": "^7.20.0", "@babel/runtime": "^7.20.0", - "@react-native-community/eslint-config": "^3.0.1", + "@react-native-community/eslint-config": "^3.2.0", "@tsconfig/react-native": "^2.0.2", - "@types/jest": "^29.2.1", - "@types/node": "^14.18.33", - "@types/react": "^18.0.24", - "@typescript-eslint/eslint-plugin": "^5.4.0", - "@typescript-eslint/parser": "^5.4.0", + "@types/jest": "^29.5.11", + "@types/node": "^16.18.68", + "@types/react": "^18.2.45", + "@typescript-eslint/eslint-plugin": "^6.16.0", + "@typescript-eslint/parser": "^6.16.0", "cross-env": "^7.0.3", "detox": "^20.14.6", "detox-allure2-adapter": "^1.0.0-alpha.2", - "eslint": "^8.41.0", - "eslint-plugin-unicorn": "^47.0.0", + "eslint": "^8.56.0", + "eslint-plugin-unicorn": "^50.0.1", "execa": "^5.1.1", "express": "^4.15.3", "glob": "^7.2.0", "jest": "^29.2.1", - "jest-allure2-reporter": "^2.0.0-beta.2", + "jest-allure2-reporter": "^2.0.0-beta.4", "jest-junit": "^10.0.0", "jest-metadata": "^1.3.1", "lodash": "^4.14.1", @@ -73,7 +73,7 @@ "p-iteration": "^1.1.8", "pngjs": "^3.4.0", "react-native-codegen": "^0.0.8", - "typescript": "^4.1.3" + "typescript": "^5.3.3" }, "jest-junit": { "suiteName": "Detox E2E tests", diff --git a/detox/tsconfig.json b/detox/tsconfig.json index d1e0ba1c3b..8b02df1d38 100644 --- a/detox/tsconfig.json +++ b/detox/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "module": "commonjs", + "module": "node16", "lib": ["es2022"], "target": "ES2022", "allowJs": true, diff --git a/generation/package.json b/generation/package.json index 028f16d5f7..4fdd0fb3dd 100644 --- a/generation/package.json +++ b/generation/package.json @@ -30,7 +30,7 @@ }, "devDependencies": { "jest": "^27.0.0", - "jest-allure2-reporter": "^2.0.0-beta.2", + "jest-allure2-reporter": "^2.0.0-beta.4", "lint-staged": "^6.0.0", "prettier": "^1.8.2" }, From 8a7344cc9082a215cdf2a0f107f8b71bf1c6927d Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Thu, 28 Dec 2023 17:29:52 +0200 Subject: [PATCH 071/185] fix(ios): increase maxBuffer for applesimutils --list (#4315) --- .../common/drivers/ios/tools/AppleSimUtils.js | 1 + detox/src/utils/childProcess/exec.js | 5 ++-- detox/src/utils/childProcess/exec.test.js | 27 ++++++++++++------- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/detox/src/devices/common/drivers/ios/tools/AppleSimUtils.js b/detox/src/devices/common/drivers/ios/tools/AppleSimUtils.js index 06ce77e488..7f4d9771c4 100644 --- a/detox/src/devices/common/drivers/ios/tools/AppleSimUtils.js +++ b/detox/src/devices/common/drivers/ios/tools/AppleSimUtils.js @@ -33,6 +33,7 @@ class AppleSimUtils { args: `--list ${joinArgs(query)}`, retries: 1, statusLogs: listOptions.trying ? { trying: listOptions.trying } : undefined, + maxBuffer: 4 * 1024 * 1024, }; const response = await this._execAppleSimUtils(options); const parsed = this._parseResponseFromAppleSimUtils(response); diff --git a/detox/src/utils/childProcess/exec.js b/detox/src/utils/childProcess/exec.js index 4cf25c7aa9..c24ca635e8 100644 --- a/detox/src/utils/childProcess/exec.js +++ b/detox/src/utils/childProcess/exec.js @@ -14,9 +14,10 @@ async function execWithRetriesAndLogs(bin, options = {}) { interval = 1000, prefix = null, args = null, - timeout = 0, + timeout, statusLogs = {}, verbosity = 'normal', + maxBuffer, } = options; const trackingId = execsCounter.inc(); @@ -35,7 +36,7 @@ async function execWithRetriesAndLogs(bin, options = {}) { } else if (statusLogs.retrying) { _logExecRetrying(logger, cmd, tryNumber, lastError); } - result = await exec(cmd, { timeout }); + result = await exec(cmd, _.omitBy({ timeout, maxBuffer }, _.isUndefined)); }); } catch (err) { const failReason = err.code == null && timeout > 0 diff --git a/detox/src/utils/childProcess/exec.test.js b/detox/src/utils/childProcess/exec.test.js index 33e5d42e89..919a9464f2 100644 --- a/detox/src/utils/childProcess/exec.test.js +++ b/detox/src/utils/childProcess/exec.test.js @@ -21,7 +21,7 @@ describe('Exec utils', () => { it(`exec command with no arguments ends successfully`, async () => { mockCppSuccessful(cpp); await exec.execWithRetriesAndLogs('bin'); - expect(cpp.exec).toHaveBeenCalledWith(`bin`, { timeout: 0 }); + expect(cpp.exec).toHaveBeenCalledWith(`bin`, {}); }); it(`exec command with arguments ends successfully`, async () => { @@ -30,7 +30,7 @@ describe('Exec utils', () => { const options = { args: `--argument 123` }; await exec.execWithRetriesAndLogs('bin', options); - expect(cpp.exec).toHaveBeenCalledWith(`bin --argument 123`, { timeout: 0 }); + expect(cpp.exec).toHaveBeenCalledWith(`bin --argument 123`, {}); }); it(`exec command with env-vars pass-through (i.e. no custom env-vars specification`, async () => { @@ -50,7 +50,7 @@ describe('Exec utils', () => { }; await exec.execWithRetriesAndLogs('bin', options); - expect(cpp.exec).toHaveBeenCalledWith(`export MY_PREFIX && bin --argument 123`, { timeout: 0 }); + expect(cpp.exec).toHaveBeenCalledWith(`export MY_PREFIX && bin --argument 123`, {}); }); it(`exec command with prefix (no args) ends successfully`, async () => { @@ -59,7 +59,7 @@ describe('Exec utils', () => { const options = { prefix: `export MY_PREFIX` }; await exec.execWithRetriesAndLogs('bin', options); - expect(cpp.exec).toHaveBeenCalledWith(`export MY_PREFIX && bin`, { timeout: 0 }); + expect(cpp.exec).toHaveBeenCalledWith(`export MY_PREFIX && bin`, {}); }); it(`exec command log using a custom logger`, async () => { @@ -86,7 +86,7 @@ describe('Exec utils', () => { }; await exec.execWithRetriesAndLogs('bin', options); - expect(cpp.exec).toHaveBeenCalledWith(`bin --argument 123`, { timeout: 0 }); + expect(cpp.exec).toHaveBeenCalledWith(`bin --argument 123`, {}); expect(logger.debug).toHaveBeenCalledWith({ event: 'EXEC_TRY', retryNumber: 1 }, options.statusLogs.trying); expect(logger.debug).toHaveBeenCalledWith({ event: 'EXEC_TRY', retryNumber: 2 }, options.statusLogs.trying); expect(logger.trace).toHaveBeenCalledWith({ event: 'EXEC_TRY_FAIL' }, 'error result'); @@ -108,7 +108,7 @@ describe('Exec utils', () => { await exec.execWithRetriesAndLogs('bin', options); - expect(cpp.exec).toHaveBeenCalledWith(`bin --argument 123`, { timeout: 0 }); + expect(cpp.exec).toHaveBeenCalledWith(`bin --argument 123`, {}); expect(logger.debug).toHaveBeenCalledWith({ event: 'EXEC_RETRY' }, '(Retry #1)', 'bin --argument 123'); expect(logger.debug).not.toHaveBeenCalledWith({ event: 'EXEC_RETRY' }, expect.stringContaining('Retry #0'), expect.any(String)); expect(logger.trace).toHaveBeenCalledWith({ event: 'EXEC_TRY_FAIL' }, 'error result'); @@ -162,7 +162,7 @@ describe('Exec utils', () => { await exec.execWithRetriesAndLogs('bin', { retries: 0, interval: 1 }); fail('expected execWithRetriesAndLogs() to throw'); } catch (object) { - expect(cpp.exec).toHaveBeenCalledWith(`bin`, { timeout: 0 }); + expect(cpp.exec).toHaveBeenCalledWith(`bin`, {}); expect(logger.error.mock.calls).toHaveLength(3); expect(logger.error).toHaveBeenCalledWith(expect.objectContaining({ event: 'EXEC_FAIL' }), expect.anything()); } @@ -175,7 +175,7 @@ describe('Exec utils', () => { await exec.execWithRetriesAndLogs('bin', { verbosity: 'low', retries: 0, interval: 1 }); fail('expected execWithRetriesAndLogs() to throw'); } catch (object) { - expect(cpp.exec).toHaveBeenCalledWith(`bin`, { timeout: 0 }); + expect(cpp.exec).toHaveBeenCalledWith(`bin`, {}); expect(logger.error).not.toHaveBeenCalled(); expect(logger.debug.mock.calls).toHaveLength(4); } @@ -193,6 +193,13 @@ describe('Exec utils', () => { } }); + it(`exec command with a given maxBuffer`, async () => { + mockCppSuccessful(cpp); + + await exec.execWithRetriesAndLogs('bin', { maxBuffer: 1000 }); + expect(cpp.exec).toHaveBeenCalledWith(`bin`, { maxBuffer: 1000 }); + }); + it(`exec command with multiple failures`, async () => { const errorResult = returnErrorWithValue('error result'); cpp.exec @@ -207,7 +214,7 @@ describe('Exec utils', () => { await exec.execWithRetriesAndLogs('bin', { retries: 5, interval: 1 }); fail('expected execWithRetriesAndLogs() to throw'); } catch (object) { - expect(cpp.exec).toHaveBeenCalledWith(`bin`, { timeout: 0 }); + expect(cpp.exec).toHaveBeenCalledWith(`bin`, {}); expect(cpp.exec).toHaveBeenCalledTimes(6); expect(object).toBeDefined(); } @@ -226,7 +233,7 @@ describe('Exec utils', () => { .mockResolvedValueOnce(successfulResult); await exec.execWithRetriesAndLogs('bin', { retries: 6, interval: 1 }); - expect(cpp.exec).toHaveBeenCalledWith(`bin`, { timeout: 0 }); + expect(cpp.exec).toHaveBeenCalledWith(`bin`, {}); expect(cpp.exec).toHaveBeenCalledTimes(6); }); From 60d185b984219ad0d9ae1b929ddd22d0a493435b Mon Sep 17 00:00:00 2001 From: wixmobile Date: Thu, 28 Dec 2023 08:35:01 -0800 Subject: [PATCH 072/185] Publish 20.14.7 [ci skip] --- detox/package.json | 2 +- detox/test/package.json | 4 ++-- examples/demo-native-android/package.json | 4 ++-- examples/demo-native-ios/package.json | 4 ++-- examples/demo-plugin/package.json | 4 ++-- examples/demo-react-native-detox-instruments/package.json | 4 ++-- examples/demo-react-native/package.json | 4 ++-- generation/package.json | 2 +- lerna.json | 2 +- package.json | 2 +- 10 files changed, 16 insertions(+), 16 deletions(-) diff --git a/detox/package.json b/detox/package.json index d89d141be4..784dadc2ad 100644 --- a/detox/package.json +++ b/detox/package.json @@ -1,7 +1,7 @@ { "name": "detox", "description": "E2E tests and automation for mobile", - "version": "20.14.6", + "version": "20.14.7", "bin": { "detox": "local-cli/cli.js" }, diff --git a/detox/test/package.json b/detox/test/package.json index 2042688e14..298d2a5052 100644 --- a/detox/test/package.json +++ b/detox/test/package.json @@ -1,6 +1,6 @@ { "name": "detox-test", - "version": "20.14.6", + "version": "20.14.7", "private": true, "engines": { "node": ">=14.5.0" @@ -56,7 +56,7 @@ "@typescript-eslint/eslint-plugin": "^6.16.0", "@typescript-eslint/parser": "^6.16.0", "cross-env": "^7.0.3", - "detox": "^20.14.6", + "detox": "^20.14.7", "detox-allure2-adapter": "^1.0.0-alpha.2", "eslint": "^8.56.0", "eslint-plugin-unicorn": "^50.0.1", diff --git a/examples/demo-native-android/package.json b/examples/demo-native-android/package.json index b98b93922f..a89f9c0804 100644 --- a/examples/demo-native-android/package.json +++ b/examples/demo-native-android/package.json @@ -1,13 +1,13 @@ { "name": "detox-demo-native-android", - "version": "20.14.6", + "version": "20.14.7", "private": true, "scripts": { "packager": "react-native start", "detox-server": "detox run-server" }, "devDependencies": { - "detox": "^20.14.6" + "detox": "^20.14.7" }, "detox": {} } diff --git a/examples/demo-native-ios/package.json b/examples/demo-native-ios/package.json index dc1f29bbb7..7cac431651 100644 --- a/examples/demo-native-ios/package.json +++ b/examples/demo-native-ios/package.json @@ -1,9 +1,9 @@ { "name": "detox-demo-native-ios", - "version": "20.14.6", + "version": "20.14.7", "private": true, "devDependencies": { - "detox": "^20.14.6" + "detox": "^20.14.7" }, "detox": { "specs": "", diff --git a/examples/demo-plugin/package.json b/examples/demo-plugin/package.json index 74071f37fb..6449a47e56 100644 --- a/examples/demo-plugin/package.json +++ b/examples/demo-plugin/package.json @@ -1,12 +1,12 @@ { "name": "demo-plugin", - "version": "20.14.6", + "version": "20.14.7", "private": true, "scripts": { "test:plugin": "detox test --configuration plugin -l verbose" }, "devDependencies": { - "detox": "^20.14.6", + "detox": "^20.14.7", "jest": "^29.0.0" }, "detox": { diff --git a/examples/demo-react-native-detox-instruments/package.json b/examples/demo-react-native-detox-instruments/package.json index 050ae7a334..b3a80416ca 100644 --- a/examples/demo-react-native-detox-instruments/package.json +++ b/examples/demo-react-native-detox-instruments/package.json @@ -1,10 +1,10 @@ { "name": "demo-react-native-detox-instruments", - "version": "20.14.6", + "version": "20.14.7", "private": true, "scripts": {}, "devDependencies": { - "detox": "^20.14.6" + "detox": "^20.14.7" }, "detox": { "configurations": { diff --git a/examples/demo-react-native/package.json b/examples/demo-react-native/package.json index 4304b895be..89b26a17ec 100644 --- a/examples/demo-react-native/package.json +++ b/examples/demo-react-native/package.json @@ -1,6 +1,6 @@ { "name": "example", - "version": "20.14.6", + "version": "20.14.7", "private": true, "scripts": { "start": "react-native start", @@ -32,7 +32,7 @@ "@types/fs-extra": "^9.0.13", "@types/jest": "^29.2.1", "@types/react": "^18.0.24", - "detox": "^20.14.6", + "detox": "^20.14.7", "fs-extra": "^9.1.0", "jest": "^29.2.1", "ts-jest": "^29.0.3", diff --git a/generation/package.json b/generation/package.json index 4fdd0fb3dd..ae271c92ff 100644 --- a/generation/package.json +++ b/generation/package.json @@ -1,6 +1,6 @@ { "name": "generation", - "version": "20.14.6", + "version": "20.14.7", "description": "Generate wrapper code for native dependencies", "main": "index.js", "private": true, diff --git a/lerna.json b/lerna.json index 5c02488c46..54c16b3f44 100644 --- a/lerna.json +++ b/lerna.json @@ -13,7 +13,7 @@ "generation", "." ], - "version": "20.14.6", + "version": "20.14.7", "npmClient": "npm", "command": { "publish": { diff --git a/package.json b/package.json index 6a2d3f4a71..119fac57a6 100644 --- a/package.json +++ b/package.json @@ -54,5 +54,5 @@ "shell-utils": "1.x.x", "unified": "^10.1.0" }, - "version": "20.14.6" + "version": "20.14.7" } From f7e875a555a3f26e4aa46a75101b5c3facb74863 Mon Sep 17 00:00:00 2001 From: Asaf Korem Date: Mon, 1 Jan 2024 16:38:52 +0200 Subject: [PATCH 073/185] fix(test, iOS): patch boost podspec due to pod-install issue. Patch to resolve this issue: https://github.com/boostorg/boost/issues/843 --- detox/test/scripts/postinstall.js | 56 ++++++++----------- .../demo-react-native/scripts/postinstall.js | 49 ++++++++-------- package.json | 3 +- scripts/postinstall.js | 11 ---- 4 files changed, 51 insertions(+), 68 deletions(-) delete mode 100755 scripts/postinstall.js diff --git a/detox/test/scripts/postinstall.js b/detox/test/scripts/postinstall.js index d2282cd40f..41322c42f1 100644 --- a/detox/test/scripts/postinstall.js +++ b/detox/test/scripts/postinstall.js @@ -1,44 +1,34 @@ const fs = require('fs-extra'); const path = require('path'); -const semver = require('semver'); +const cp = require('child_process'); -const rnVersion = function() { - const rnPackageJson = require('react-native/package.json'); - return rnPackageJson.version; -}(); +function patchBoostPodspec() { + // Patch boost.podspec.json to use a different source URL due to an issue with the original + // one https://github.com/boostorg/boost/issues/843 + console.log('[POST-INSTALL] Applying boost.podspec patch...'); -function overrideGradleWrapperVersion() { - const gradleWrapperOld = - '#!!! Patched by post-install script !!!\n' + - '#!!! Do not commit !!!\n' + - 'distributionBase=GRADLE_USER_HOME\n' + - 'distributionPath=wrapper/dists\n' + - 'zipStoreBase=GRADLE_USER_HOME\n' + - 'zipStorePath=wrapper/dists\n' + - 'distributionUrl=https\\://services.gradle.org/distributions/gradle-6.9-all.zip\n' + - '#!!! Do not commit !!!\n'; + const boostPodspecPath = path.join(process.cwd(), 'node_modules', 'react-native', 'third-party-podspecs', 'boost.podspec'); + const boostPodspec = fs.readFileSync(boostPodspecPath, 'utf8'); + const boostPodspecPatched = boostPodspec.replace( + 'https://boostorg.jfrog.io/artifactory/main/release/1.76.0/source/boost_1_76_0.tar.bz2', + 'https://sourceforge.net/projects/boost/files/boost/1.76.0/boost_1_76_0.tar.bz2' + ); - const GRADLE_WRAPPER_PROPS_PATH = path.join('android', 'gradle', 'wrapper', 'gradle-wrapper.properties'); - - console.log('[POST-INSTALL] Patching gradle-wrapper.properties file back to gradle v6.9..'); - try { - fs.writeFileSync(GRADLE_WRAPPER_PROPS_PATH, gradleWrapperOld); - } catch (e) { - console.warn('[POST-INSTALL] Couldn\'t path the gradle-wrapper.properties file', e); - } + fs.writeFileSync(boostPodspecPath, boostPodspecPatched, 'utf8'); } -function run() { - console.log('[POST-INSTALL] Running Detox\'s test-app post-install script...'); +function podInstallIfRequired() { + if (process.platform === 'darwin' && !process.env.DETOX_DISABLE_POD_INSTALL) { + console.log('[POST-INSTALL] Running pod install...'); + patchBoostPodspec(); - const version = semver.minor(rnVersion); - - if (version < 68) { - console.log(`[POST-INSTALL] RN Version ${version} is lower than 68 - Applying dedicated patches...`); - overrideGradleWrapperVersion(); + cp.execSync('pod install', { + cwd: path.join(process.cwd(), 'ios'), + stdio: 'inherit' + }); } - - console.log('[POST-INSTALL] Completed!'); } -run(); +console.log('[POST-INSTALL] Running Detox\'s test-app post-install script...'); +podInstallIfRequired(); +console.log('[POST-INSTALL] Completed!'); diff --git a/examples/demo-react-native/scripts/postinstall.js b/examples/demo-react-native/scripts/postinstall.js index 9843f34c53..90423234da 100644 --- a/examples/demo-react-native/scripts/postinstall.js +++ b/examples/demo-react-native/scripts/postinstall.js @@ -1,29 +1,34 @@ -const rnVersion = function() { - const rnPackageJson = require('react-native/package.json'); - return rnPackageJson.version; -}(); +const fs = require('fs-extra'); +const path = require('path'); +const cp = require('child_process'); -function patchHermesLocationForRN60Android() { - const semver = require('semver'); - const fs = require('fs-extra'); - const path = require('path'); +function patchBoostPodspec() { + // Patch boost.podspec.json to use a different source URL due to an issue with the original + // one https://github.com/boostorg/boost/issues/843 + console.log('[POST-INSTALL] Applying boost.podspec patch...'); - if (semver.minor(rnVersion) === 60) { - console.log('Detox post-install: Detected RN .60...'); + const boostPodspecPath = path.join(process.cwd(), 'node_modules', 'react-native', 'third-party-podspecs', 'boost.podspec'); + const boostPodspec = fs.readFileSync(boostPodspecPath, 'utf8'); + const boostPodspecPatched = boostPodspec.replace( + 'https://boostorg.jfrog.io/artifactory/main/release/1.76.0/source/boost_1_76_0.tar.bz2', + 'https://sourceforge.net/projects/boost/files/boost/1.76.0/boost_1_76_0.tar.bz2' + ); - const HERMES_PATH_ROOT = path.join('node_modules', 'hermesvm'); - const HERMES_PATH_RN = path.join('node_modules', 'react-native', 'node_modules', 'hermesvm'); + fs.writeFileSync(boostPodspecPath, boostPodspecPatched, 'utf8'); +} - const hermesIsInRoot = fs.existsSync(HERMES_PATH_ROOT); - const hermesIsInRN = fs.existsSync(HERMES_PATH_RN); +function podInstallIfRequired() { + if (process.platform === 'darwin' && !process.env.DETOX_DISABLE_POD_INSTALL) { + console.log('[POST-INSTALL] Running pod install...'); + patchBoostPodspec(); - if (hermesIsInRoot && !hermesIsInRN) { - console.log('Detox post-install: Applying hermes-vm patch for RN .60...'); - fs.ensureDirSync(path.join(HERMES_PATH_RN, 'android')); - fs.copySync(path.join(HERMES_PATH_ROOT, 'android'), path.join(HERMES_PATH_RN, 'android')); - } else { - console.log('Detox post-install: hermes-vm patch not needed:', hermesIsInRoot, hermesIsInRN); - } + cp.execSync('pod install', { + cwd: path.join(process.cwd(), 'ios'), + stdio: 'inherit' + }); } } -patchHermesLocationForRN60Android(); + +console.log('[POST-INSTALL] Running Detox\'s example-app post-install script...'); +podInstallIfRequired(); +console.log('[POST-INSTALL] Completed!'); diff --git a/package.json b/package.json index 119fac57a6..a54ff7c976 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,7 @@ "test:e2e:android": "pushd detox/test && npm run e2e:android && popd", "release": "node scripts/ci.release.js", "package:android": "node scripts/ci.android-release.js", - "package:ios": "scripts/ci.ios-release.sh", - "postinstall": "node scripts/postinstall.js" + "package:ios": "scripts/ci.ios-release.sh" }, "repository": { "type": "git", diff --git a/scripts/postinstall.js b/scripts/postinstall.js deleted file mode 100755 index 6567490180..0000000000 --- a/scripts/postinstall.js +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env node - -const cp = require('child_process'); -const path = require('path'); - -if (process.platform === 'darwin' && !process.env.DETOX_DISABLE_POD_INSTALL) { - cp.execSync('pod install', { - cwd: path.join(process.cwd(), 'detox/test/ios'), - stdio: 'inherit' - }); -} From d8074c60901290fe92cca1afabe3f228cc0c2177 Mon Sep 17 00:00:00 2001 From: Asaf Korem Date: Mon, 1 Jan 2024 17:14:01 +0200 Subject: [PATCH 074/185] refactor: postinstall scripts. add some checks --- detox/test/scripts/postinstall.js | 38 +++++++++++-------- .../demo-react-native/scripts/postinstall.js | 38 +++++++++++-------- 2 files changed, 46 insertions(+), 30 deletions(-) diff --git a/detox/test/scripts/postinstall.js b/detox/test/scripts/postinstall.js index 41322c42f1..224622f2e2 100644 --- a/detox/test/scripts/postinstall.js +++ b/detox/test/scripts/postinstall.js @@ -2,20 +2,28 @@ const fs = require('fs-extra'); const path = require('path'); const cp = require('child_process'); -function patchBoostPodspec() { - // Patch boost.podspec.json to use a different source URL due to an issue with the original - // one https://github.com/boostorg/boost/issues/843 - console.log('[POST-INSTALL] Applying boost.podspec patch...'); - - const boostPodspecPath = path.join(process.cwd(), 'node_modules', 'react-native', 'third-party-podspecs', 'boost.podspec'); - const boostPodspec = fs.readFileSync(boostPodspecPath, 'utf8'); - const boostPodspecPatched = boostPodspec.replace( - 'https://boostorg.jfrog.io/artifactory/main/release/1.76.0/source/boost_1_76_0.tar.bz2', - 'https://sourceforge.net/projects/boost/files/boost/1.76.0/boost_1_76_0.tar.bz2' - ); - - fs.writeFileSync(boostPodspecPath, boostPodspecPatched, 'utf8'); -} +const patchBoostPodspec = () => { + const log = message => console.log(`[POST-INSTALL] ${message}`); + const boostPodspecPath = `${process.cwd()}/node_modules/react-native/third-party-podspecs/boost.podspec`; + const originalUrl = 'https://boostorg.jfrog.io/artifactory/main/release/1.76.0/source/boost_1_76_0.tar.bz2'; + const patchedUrl = 'https://sourceforge.net/projects/boost/files/boost/1.76.0/boost_1_76_0.tar.bz2'; + + if (!fs.existsSync(boostPodspecPath)) { + log('boost.podspec does not exist, skipping patch...'); + return; + } + + let boostPodspec = fs.readFileSync(boostPodspecPath, 'utf8'); + + if (!boostPodspec.includes(originalUrl)) { + log('boost.podspec is already patched or the URL is different, skipping patch...'); + return; + } + + log('Applying boost.podspec patch...'); + boostPodspec = boostPodspec.replace(originalUrl, patchedUrl); + fs.writeFileSync(boostPodspecPath, boostPodspec, 'utf8'); +}; function podInstallIfRequired() { if (process.platform === 'darwin' && !process.env.DETOX_DISABLE_POD_INSTALL) { @@ -23,7 +31,7 @@ function podInstallIfRequired() { patchBoostPodspec(); cp.execSync('pod install', { - cwd: path.join(process.cwd(), 'ios'), + cwd: `${process.cwd()}/ios`, stdio: 'inherit' }); } diff --git a/examples/demo-react-native/scripts/postinstall.js b/examples/demo-react-native/scripts/postinstall.js index 90423234da..047a4c9b78 100644 --- a/examples/demo-react-native/scripts/postinstall.js +++ b/examples/demo-react-native/scripts/postinstall.js @@ -2,20 +2,28 @@ const fs = require('fs-extra'); const path = require('path'); const cp = require('child_process'); -function patchBoostPodspec() { - // Patch boost.podspec.json to use a different source URL due to an issue with the original - // one https://github.com/boostorg/boost/issues/843 - console.log('[POST-INSTALL] Applying boost.podspec patch...'); - - const boostPodspecPath = path.join(process.cwd(), 'node_modules', 'react-native', 'third-party-podspecs', 'boost.podspec'); - const boostPodspec = fs.readFileSync(boostPodspecPath, 'utf8'); - const boostPodspecPatched = boostPodspec.replace( - 'https://boostorg.jfrog.io/artifactory/main/release/1.76.0/source/boost_1_76_0.tar.bz2', - 'https://sourceforge.net/projects/boost/files/boost/1.76.0/boost_1_76_0.tar.bz2' - ); - - fs.writeFileSync(boostPodspecPath, boostPodspecPatched, 'utf8'); -} +const patchBoostPodspec = () => { + const log = message => console.log(`[POST-INSTALL] ${message}`); + const boostPodspecPath = `${process.cwd()}/node_modules/react-native/third-party-podspecs/boost.podspec`; + const originalUrl = 'https://boostorg.jfrog.io/artifactory/main/release/1.76.0/source/boost_1_76_0.tar.bz2'; + const patchedUrl = 'https://sourceforge.net/projects/boost/files/boost/1.76.0/boost_1_76_0.tar.bz2'; + + if (!fs.existsSync(boostPodspecPath)) { + log('boost.podspec does not exist, skipping patch...'); + return; + } + + let boostPodspec = fs.readFileSync(boostPodspecPath, 'utf8'); + + if (!boostPodspec.includes(originalUrl)) { + log('boost.podspec is already patched or the URL is different, skipping patch...'); + return; + } + + log('Applying boost.podspec patch...'); + boostPodspec = boostPodspec.replace(originalUrl, patchedUrl); + fs.writeFileSync(boostPodspecPath, boostPodspec, 'utf8'); +}; function podInstallIfRequired() { if (process.platform === 'darwin' && !process.env.DETOX_DISABLE_POD_INSTALL) { @@ -23,7 +31,7 @@ function podInstallIfRequired() { patchBoostPodspec(); cp.execSync('pod install', { - cwd: path.join(process.cwd(), 'ios'), + cwd: `${process.cwd()}/ios`, stdio: 'inherit' }); } From 8568fd6b7e82cb38e671c3a6241d5d62c7154e75 Mon Sep 17 00:00:00 2001 From: Asaf Korem Date: Tue, 2 Jan 2024 09:45:53 +0200 Subject: [PATCH 075/185] fix(iOS): locate all windows before hitability test. The resolved issue mainly affected iOS 17 (regression due to UI changes in iOS). --- .../ios/Detox/Utilities/UIWindow+DetoxUtils.m | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/detox/ios/Detox/Utilities/UIWindow+DetoxUtils.m b/detox/ios/Detox/Utilities/UIWindow+DetoxUtils.m index 578a9cb21f..8bafa95688 100644 --- a/detox/ios/Detox/Utilities/UIWindow+DetoxUtils.m +++ b/detox/ios/Detox/Utilities/UIWindow+DetoxUtils.m @@ -224,37 +224,37 @@ - (NSString *)dtx_shortDescription } + (nullable UIWindow *)dtx_topMostWindowAtPoint:(CGPoint)point { - NSArray *windows = UIApplication.sharedApplication.windows; - - NSArray *visibleWindowsAtPoint = [windows - filteredArrayUsingPredicate:[NSPredicate - predicateWithBlock:^BOOL(UIWindow *window, NSDictionary * _Nullable __unused bindings) { - if (!CGRectContainsPoint(window.frame, point)) { - return NO; - } + NSArray *windows = [self dtx_allWindows]; + + NSArray *visibleWindowsAtPoint = [windows filteredArrayUsingPredicate: + [NSPredicate predicateWithBlock:^BOOL( + UIWindow *window, + NSDictionary * _Nullable __unused bindings + ) { + if (!CGRectContainsPoint(window.frame, point)) { + return NO; + } - if (![window isVisibleAroundPoint:point]) { - return NO; - } + if (![window isVisibleAroundPoint:point]) { + return NO; + } - UIView * _Nullable hit = [window hitTest:point withEvent:nil]; - if (!hit) { - // The point lies completely outside the windos's hierarchy. - return NO; - } + if (![window hitTest:point withEvent:nil]) { + // The point lies completely outside the window's hierarchy. + return NO; + } - return YES; - }]]; + return YES; + }]]; - if (!visibleWindowsAtPoint) { - return nil; + if (!visibleWindowsAtPoint) { + return nil; } return [[visibleWindowsAtPoint - sortedArrayUsingComparator:^NSComparisonResult(UIWindow *window1, UIWindow *window2) { - return window1.windowLevel - window2.windowLevel; - }] - lastObject]; + sortedArrayUsingComparator:^NSComparisonResult(UIWindow *window1, UIWindow *window2) { + return window1.windowLevel - window2.windowLevel; + }] lastObject]; } @end From 0d24b88d6c9419f8e0aff2f47c4c68851f23ccd3 Mon Sep 17 00:00:00 2001 From: Asaf Korem Date: Tue, 2 Jan 2024 09:47:43 +0200 Subject: [PATCH 076/185] chore(iOS): revert boost.podspec patch from #4317. --- detox/test/scripts/postinstall.js | 32 ++----------------- .../demo-react-native/scripts/postinstall.js | 32 ++----------------- 2 files changed, 6 insertions(+), 58 deletions(-) diff --git a/detox/test/scripts/postinstall.js b/detox/test/scripts/postinstall.js index 224622f2e2..a7b2548644 100644 --- a/detox/test/scripts/postinstall.js +++ b/detox/test/scripts/postinstall.js @@ -1,42 +1,16 @@ -const fs = require('fs-extra'); -const path = require('path'); const cp = require('child_process'); -const patchBoostPodspec = () => { - const log = message => console.log(`[POST-INSTALL] ${message}`); - const boostPodspecPath = `${process.cwd()}/node_modules/react-native/third-party-podspecs/boost.podspec`; - const originalUrl = 'https://boostorg.jfrog.io/artifactory/main/release/1.76.0/source/boost_1_76_0.tar.bz2'; - const patchedUrl = 'https://sourceforge.net/projects/boost/files/boost/1.76.0/boost_1_76_0.tar.bz2'; - - if (!fs.existsSync(boostPodspecPath)) { - log('boost.podspec does not exist, skipping patch...'); - return; - } - - let boostPodspec = fs.readFileSync(boostPodspecPath, 'utf8'); - - if (!boostPodspec.includes(originalUrl)) { - log('boost.podspec is already patched or the URL is different, skipping patch...'); - return; - } - - log('Applying boost.podspec patch...'); - boostPodspec = boostPodspec.replace(originalUrl, patchedUrl); - fs.writeFileSync(boostPodspecPath, boostPodspec, 'utf8'); -}; - function podInstallIfRequired() { if (process.platform === 'darwin' && !process.env.DETOX_DISABLE_POD_INSTALL) { - console.log('[POST-INSTALL] Running pod install...'); - patchBoostPodspec(); + console.log('[POST-INSTALL] Running test-app pod install...'); cp.execSync('pod install', { cwd: `${process.cwd()}/ios`, stdio: 'inherit' }); + + console.log('[POST-INSTALL] test-app pod install completed') } } -console.log('[POST-INSTALL] Running Detox\'s test-app post-install script...'); podInstallIfRequired(); -console.log('[POST-INSTALL] Completed!'); diff --git a/examples/demo-react-native/scripts/postinstall.js b/examples/demo-react-native/scripts/postinstall.js index 047a4c9b78..125f730ae0 100644 --- a/examples/demo-react-native/scripts/postinstall.js +++ b/examples/demo-react-native/scripts/postinstall.js @@ -1,42 +1,16 @@ -const fs = require('fs-extra'); -const path = require('path'); const cp = require('child_process'); -const patchBoostPodspec = () => { - const log = message => console.log(`[POST-INSTALL] ${message}`); - const boostPodspecPath = `${process.cwd()}/node_modules/react-native/third-party-podspecs/boost.podspec`; - const originalUrl = 'https://boostorg.jfrog.io/artifactory/main/release/1.76.0/source/boost_1_76_0.tar.bz2'; - const patchedUrl = 'https://sourceforge.net/projects/boost/files/boost/1.76.0/boost_1_76_0.tar.bz2'; - - if (!fs.existsSync(boostPodspecPath)) { - log('boost.podspec does not exist, skipping patch...'); - return; - } - - let boostPodspec = fs.readFileSync(boostPodspecPath, 'utf8'); - - if (!boostPodspec.includes(originalUrl)) { - log('boost.podspec is already patched or the URL is different, skipping patch...'); - return; - } - - log('Applying boost.podspec patch...'); - boostPodspec = boostPodspec.replace(originalUrl, patchedUrl); - fs.writeFileSync(boostPodspecPath, boostPodspec, 'utf8'); -}; - function podInstallIfRequired() { if (process.platform === 'darwin' && !process.env.DETOX_DISABLE_POD_INSTALL) { - console.log('[POST-INSTALL] Running pod install...'); - patchBoostPodspec(); + console.log('[POST-INSTALL] Running example-app pod install...'); cp.execSync('pod install', { cwd: `${process.cwd()}/ios`, stdio: 'inherit' }); + + console.log('[POST-INSTALL] example-app pod install completed') } } -console.log('[POST-INSTALL] Running Detox\'s example-app post-install script...'); podInstallIfRequired(); -console.log('[POST-INSTALL] Completed!'); From e9873b2115f9af8d7eb482f8f399641e39bf5edb Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Tue, 2 Jan 2024 11:56:43 +0200 Subject: [PATCH 077/185] chore: update iOS simulators to 14th gen (#4316) --- .nvmrc | 2 +- detox/test/e2e/30.custom-keyboard.test.js | 4 +-- .../assets/elementScreenshot.ios.horiz.png | Bin 11558 -> 11288 bytes .../e2e/assets/elementScreenshot.ios.vert.png | Bin 36347 -> 37148 bytes detox/test/e2e/detox.config.js | 2 +- .../Screens/VisibilityExpectationScreen.js | 24 ++++++++++-------- examples/demo-native-ios/package.json | 2 +- .../package.json | 2 +- examples/demo-react-native/detox.config.js | 2 +- 9 files changed, 21 insertions(+), 17 deletions(-) diff --git a/.nvmrc b/.nvmrc index 53d838af21..9de2256827 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -lts/gallium +lts/iron diff --git a/detox/test/e2e/30.custom-keyboard.test.js b/detox/test/e2e/30.custom-keyboard.test.js index 6e902c60ec..5eea6ab766 100644 --- a/detox/test/e2e/30.custom-keyboard.test.js +++ b/detox/test/e2e/30.custom-keyboard.test.js @@ -3,7 +3,7 @@ describe(':ios: Custom Keyboard', () => { await device.reloadReactNative(); await element(by.text('Custom Keyboard')).tap(); }); - + afterEach(async () => { await element(by.id('closeButton')).tap(); }); @@ -17,6 +17,6 @@ describe(':ios: Custom Keyboard', () => { it('should obscure elements at bottom of screen when visible', async () => { await expect(element(by.text('Obscured by keyboard'))).toBeVisible(); await element(by.id('textWithCustomInput')).tap(); - await expect(element(by.text('Obscured by keyboard'))).toBeNotVisible(); + await expect(element(by.text('Obscured by keyboard'))).not.toBeVisible(); }); }); diff --git a/detox/test/e2e/assets/elementScreenshot.ios.horiz.png b/detox/test/e2e/assets/elementScreenshot.ios.horiz.png index 79874aa34f41420dd42d632c4feb01363c018516..eafb96ec814e7b6dffd86b3235ba09373af40aad 100644 GIT binary patch literal 11288 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 literal 11558 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 Half Visible Element Element should be only half-visible. - - Half Visible Element - - + Move Element Button Element should be 1/4 visible after button is pressed.