;
-
- /**
- * 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/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/ios/Detox/Actions/UIScrollView+DetoxActions.h b/detox/ios/Detox/Actions/UIScrollView+DetoxActions.h
index c2c0638fd0..5719a0ca57 100644
--- a/detox/ios/Detox/Actions/UIScrollView+DetoxActions.h
+++ b/detox/ios/Detox/Actions/UIScrollView+DetoxActions.h
@@ -13,6 +13,8 @@ NS_ASSUME_NONNULL_BEGIN
@interface UIScrollView (DetoxActions)
- (void)dtx_scrollToEdge:(UIRectEdge)edge NS_SWIFT_NAME(dtx_scroll(to:));
+- (void)dtx_scrollToEdge:(UIRectEdge)edge
+ normalizedStartingPoint:(CGPoint)normalizedStartingPoint;
- (void)dtx_scrollWithOffset:(CGPoint)offset;
- (void)dtx_scrollWithOffset:(CGPoint)offset normalizedStartingPoint:(CGPoint)normalizedStartingPoint NS_SWIFT_NAME(dtx_scroll(withOffset:normalizedStartingPoint:));
diff --git a/detox/ios/Detox/Actions/UIScrollView+DetoxActions.m b/detox/ios/Detox/Actions/UIScrollView+DetoxActions.m
index 5bcd147473..d78c470e48 100644
--- a/detox/ios/Detox/Actions/UIScrollView+DetoxActions.m
+++ b/detox/ios/Detox/Actions/UIScrollView+DetoxActions.m
@@ -83,7 +83,7 @@ @implementation UIScrollView (DetoxActions)
[self setContentOffset:pointMakeMacro(target) animated:YES]; \
[NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:[[self valueForKeyPath:@"animation.duration"] doubleValue] + 0.05]];
-- (void)dtx_scrollToEdge:(UIRectEdge)edge
+- (CGPoint)_edgeToNormalizedEdge:(UIRectEdge)edge
{
CGPoint normalizedEdge;
switch (edge) {
@@ -100,10 +100,19 @@ - (void)dtx_scrollToEdge:(UIRectEdge)edge
normalizedEdge = CGPointMake(1, 0);
break;
default:
+ normalizedEdge= CGPointMake(0, 0);
DTXAssert(NO, @"Incorect edge provided.");
- return;
}
-
+ return normalizedEdge;
+}
+
+
+- (void)dtx_scrollToEdge:(UIRectEdge)edge
+{
+ CGPoint normalizedEdge = [self _edgeToNormalizedEdge:edge];
+ if(normalizedEdge.x == 0 && normalizedEdge.y == 0)
+ return;
+
[self _dtx_scrollToNormalizedEdge:normalizedEdge];
}
@@ -121,6 +130,23 @@ - (void)_dtx_scrollToNormalizedEdge:(CGPoint)edge
[self _dtx_scrollWithOffset:CGPointMake(- edge.x * CGFLOAT_MAX, - edge.y * CGFLOAT_MAX) normalizedStartingPoint:CGPointMake(NAN, NAN) strict:NO];
}
+- (void)dtx_scrollToEdge:(UIRectEdge)edge
+ normalizedStartingPoint:(CGPoint)normalizedStartingPoint
+{
+ CGPoint normalizedEdge = [self _edgeToNormalizedEdge:edge];
+ if(normalizedEdge.x == 0 && normalizedEdge.y == 0)
+ return;
+
+ [self _dtx_scrollToNormalizedEdge:normalizedEdge normalizedStartingPoint: normalizedStartingPoint ];
+}
+
+- (void)_dtx_scrollToNormalizedEdge:(CGPoint)edge
+ normalizedStartingPoint:(CGPoint)normalizedStartingPoint
+{
+ [self _dtx_scrollWithOffset:CGPointMake(- edge.x * CGFLOAT_MAX, - edge.y * CGFLOAT_MAX) normalizedStartingPoint:normalizedStartingPoint strict:NO];
+}
+
+
DTX_ALWAYS_INLINE
static NSString* _DTXScrollDirectionDescriptionWithOffset(CGPoint offset)
{
diff --git a/detox/ios/Detox/Invocation/Action.swift b/detox/ios/Detox/Invocation/Action.swift
index a4ac09db30..1a4470d741 100644
--- a/detox/ios/Detox/Invocation/Action.swift
+++ b/detox/ios/Detox/Invocation/Action.swift
@@ -129,6 +129,15 @@ class Action : CustomStringConvertible {
}
}
+ func startPosition(forIndex index: Int, in params: [Any]?) -> Double {
+ guard params?.count ?? 0 > index,
+ let param = params?[index] as? Double,
+ param.isNaN == false else {
+ return Double.nan
+ }
+ return param
+ }
+
var description: String {
let paramsDescription: String
if let params = params {
@@ -434,9 +443,13 @@ class ScrollToEdgeAction : Action {
fatalError("Unknown scroll direction")
break;
}
-
- element.scroll(to: targetEdge)
-
+
+ let startPositionX = startPosition(forIndex: 1, in: params)
+ let startPositionY = startPosition(forIndex: 2, in: params)
+ let normalizedStartingPoint = CGPoint(x: startPositionX, y: startPositionY)
+
+ element.scroll(to: targetEdge, normalizedStartingPoint: normalizedStartingPoint)
+
return nil
}
}
@@ -487,18 +500,9 @@ class SwipeAction : Action {
targetNormalizedOffset.x *= CGFloat(appliedPercentage)
targetNormalizedOffset.y *= CGFloat(appliedPercentage)
- let startPositionX : Double
- if params?.count ?? 0 > 3, let param2 = params?[3] as? Double, param2.isNaN == false {
- startPositionX = param2
- } else {
- startPositionX = Double.nan
- }
- let startPositionY : Double
- if params?.count ?? 0 > 4, let param3 = params?[4] as? Double, param3.isNaN == false {
- startPositionY = param3
- } else {
- startPositionY = Double.nan
- }
+
+ let startPositionX = startPosition(forIndex: 3, in: params)
+ let startPositionY = startPosition(forIndex: 4, in: params)
let normalizedStartingPoint = CGPoint(x: startPositionX, y: startPositionY)
element.swipe(normalizedOffset: targetNormalizedOffset, velocity: velocity, normalizedStartingPoint: normalizedStartingPoint)
diff --git a/detox/ios/Detox/Invocation/Element.swift b/detox/ios/Detox/Invocation/Element.swift
index 5570ab5ba5..378608b8af 100644
--- a/detox/ios/Detox/Invocation/Element.swift
+++ b/detox/ios/Detox/Invocation/Element.swift
@@ -140,10 +140,13 @@ class Element : NSObject {
view.dtx_pinch(withScale: scale, velocity: velocity, angle: angle)
}
- func scroll(to edge: UIRectEdge) {
+ func scroll(to edge: UIRectEdge, normalizedStartingPoint: CGPoint? = nil) {
let scrollView = extractScrollView()
-
- scrollView.dtx_scroll(to: edge)
+ if let normalizedStartingPoint = normalizedStartingPoint {
+ scrollView.dtx_scroll(to: edge, normalizedStarting: normalizedStartingPoint)
+ } else {
+ scrollView.dtx_scroll(to: edge)
+ }
}
func scroll(withOffset offset: CGPoint, normalizedStartingPoint: CGPoint? = nil) {
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
diff --git a/detox/ios/DetoxSync b/detox/ios/DetoxSync
index 526f2507e2..5b26331610 160000
--- a/detox/ios/DetoxSync
+++ b/detox/ios/DetoxSync
@@ -1 +1 @@
-Subproject commit 526f2507e2e41c744e8286a83fe9325e2b4bda8d
+Subproject commit 5b26331610dc358fc48894023b5d172c238e54b8
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/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: {
diff --git a/detox/local-cli/test.test.js b/detox/local-cli/test.test.js
index c29ce59959..8ea5ac19fe 100644
--- a/detox/local-cli/test.test.js
+++ b/detox/local-cli/test.test.js
@@ -1,10 +1,11 @@
// @ts-nocheck
if (process.platform === 'win32') {
- jest.retryTimes(1); // TODO: investigate why it gets stuck for the 1st time on Windows
+ jest.retryTimes(1); // TODO [2024-12-01]: investigate why it gets stuck for the 1st time on Windows
}
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/package.json b/detox/package.json
index c8102f4a6b..9a9293b5f4 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.18.1",
"bin": {
"detox": "local-cli/cli.js"
},
@@ -34,29 +34,34 @@
"postinstall": "node scripts/postinstall.js"
},
"devDependencies": {
+ "@react-native/babel-preset": "0.73.19",
+ "@react-native/eslint-config": "^0.73.2",
+ "@react-native/metro-config": "^0.73.3",
+ "@react-native/typescript-config": "0.73.1",
+ "@tsconfig/react-native": "^3.0.0",
"@types/bunyan": "^1.8.8",
"@types/child-process-promise": "^2.2.1",
- "@types/fs-extra": "^9.0.13",
- "@types/jest": "^28.1.8",
+ "@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",
- "jest": "^28.1.3",
- "jest-allure2-reporter": "2.0.0-alpha.6",
- "mockdate": "^2.0.1",
- "prettier": "^2.4.1",
- "react-native": "0.71.10",
+ "eslint-plugin-unicorn": "^50.0.1",
+ "jest": "^29.6.3",
+ "jest-allure2-reporter": "^2.0.0-beta.9",
+ "metro-react-native-babel-preset": "0.76.8",
+ "prettier": "^3.1.1",
+ "react-native": "0.73.2",
"react-native-codegen": "^0.0.8",
- "typescript": "^4.5.2",
+ "typescript": "^5.3.3",
"wtfnode": "^0.9.1"
},
"dependencies": {
@@ -72,6 +77,7 @@
"funpermaproxy": "^1.1.0",
"glob": "^8.0.3",
"ini": "^1.3.4",
+ "jest-environment-emit": "^1.0.5",
"json-cycle": "^1.3.0",
"lodash": "^4.17.11",
"multi-sort-stream": "^1.0.3",
@@ -104,9 +110,9 @@
}
},
"engines": {
- "node": ">=14.5.0"
+ "node": ">=18"
},
"browserslist": [
- "node 14"
+ "node 18"
]
}
diff --git a/detox/runners/jest/testEnvironment/index.js b/detox/runners/jest/testEnvironment/index.js
index cddafe5e85..1c85efffdd 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').default;
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,9 @@ class DetoxCircusEnvironment extends NodeEnvironment {
SpecReporter,
WorkerAssignReporter,
});
+
+ // Artifacts flushing should be delayed to avoid conflicts with third-party reporters
+ this.testEvents.on('*', this._onTestEvent.bind(this), 1e6);
}
/** @override */
@@ -72,16 +76,13 @@ 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');
- }
-
- this._timer.schedule(state.testTimeout != null ? state.testTimeout : this.setupTimeout);
+ // @ts-expect-error TS2855
+ await super.handleTestEvent(event, state);
- if (SYNC_CIRCUS_EVENTS.has(event.name)) {
- this._handleTestEventSync(event, state);
- } else {
- await this._handleTestEventAsync(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');
+ }
}
}
@@ -107,6 +108,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}`,
@@ -141,6 +146,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/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/detox/scripts/postinstall.js b/detox/scripts/postinstall.js
index e448f0ce4a..83e1b65872 100755
--- a/detox/scripts/postinstall.js
+++ b/detox/scripts/postinstall.js
@@ -1,5 +1,8 @@
+const { setGradleVersionByRNVersion } = require('./updateGradle');
if (process.platform === 'darwin' && !process.env.DETOX_DISABLE_POSTINSTALL) {
require('child_process').execFileSync(`${__dirname}/build_framework.ios.sh`, {
stdio: 'inherit'
});
+
}
+setGradleVersionByRNVersion();
diff --git a/detox/scripts/updateGradle.js b/detox/scripts/updateGradle.js
new file mode 100644
index 0000000000..43542fc0e1
--- /dev/null
+++ b/detox/scripts/updateGradle.js
@@ -0,0 +1,47 @@
+const fs = require('fs');
+const path = require('path');
+
+const rnMinor = require('../src/utils/rn-consts/rn-consts').rnVersion.minor;
+
+function getGradleVersionByRNVersion() {
+ switch (rnMinor) {
+ default:
+ return '8.3';
+ case '72':
+ return '8.0';
+ case '71':
+ return '7.6.1';
+ }
+}
+
+/**
+ * Update the Gradle wrapper to the version that matches the React Native version.
+ */
+function setGradleVersionByRNVersion() {
+ const gradleVersion = getGradleVersionByRNVersion();
+ updateGradleWrapperSync(gradleVersion);
+}
+
+/**
+ * Update the Gradle wrapper to the specified version.
+ *
+ * @param {string} newVersion - the new Gradle wrapper version
+ */
+function updateGradleWrapperSync(newVersion) {
+ const gradleWrapperPath = path.join(process.cwd(), 'android', 'gradle', 'wrapper', 'gradle-wrapper.properties');
+ console.log(`Updating Gradle wrapper to version${newVersion}. File: ${gradleWrapperPath}`);
+
+ try {
+ let data = fs.readFileSync(gradleWrapperPath, 'utf8');
+ let updatedData = data.replace(/distributionUrl=.+\n/, `distributionUrl=https\\://services.gradle.org/distributions/gradle-${newVersion}-bin.zip\n`);
+
+ fs.writeFileSync(gradleWrapperPath, updatedData, 'utf8');
+ console.log(`Gradle wrapper updated successfully to version ${newVersion}.`);
+ } catch (err) {
+ console.error('Error:', err);
+ }
+}
+
+module.exports = {
+ setGradleVersionByRNVersion
+};
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/android/AndroidExpect.test.js b/detox/src/android/AndroidExpect.test.js
index 8e986dd9bf..667a931179 100644
--- a/detox/src/android/AndroidExpect.test.js
+++ b/detox/src/android/AndroidExpect.test.js
@@ -531,7 +531,7 @@ describe('AndroidExpect', () => {
const script = 'function named(el) { return el.textContent; }';
await e.web.element(e.by.web.id('id')).runScript(function () {});
await e.web.element(e.by.web.className('className')).runScript((el) => el.textContent);
- await e.web.element(e.by.web.cssSelector('cssSelector')).runScript(function named(...args) {});
+ await e.web.element(e.by.web.cssSelector('cssSelector')).runScript(function named(..._args) {});
await e.web.element(e.by.web.name('name')).runScript(script);
await e.web.element(e.by.web.xpath('xpath')).runScript(script);
await e.web.element(e.by.web.href('linkText')).runScript(script);
diff --git a/detox/src/android/actions/native.js b/detox/src/android/actions/native.js
index 601bef562d..7b28efbed5 100644
--- a/detox/src/android/actions/native.js
+++ b/detox/src/android/actions/native.js
@@ -81,10 +81,10 @@ class ScrollAmountStopAtEdgeAction extends Action {
}
class ScrollEdgeAction extends Action {
- constructor(edge) {
+ constructor(edge, startPositionX = -1, startPositionY = -1) {
super();
- this._call = invoke.callDirectly(DetoxActionApi.scrollToEdge(edge));
+ this._call = invoke.callDirectly(DetoxActionApi.scrollToEdge(edge, startPositionX, startPositionY));
}
}
diff --git a/detox/src/android/core/NativeElement.js b/detox/src/android/core/NativeElement.js
index bcca79df83..d493a83e8c 100644
--- a/detox/src/android/core/NativeElement.js
+++ b/detox/src/android/core/NativeElement.js
@@ -93,12 +93,12 @@ class NativeElement {
return await new ActionInteraction(this._invocationManager, this._matcher, action, traceDescription).execute();
}
- async scrollTo(edge) {
+ async scrollTo(edge, startPositionX, startPositionY) {
// override the user's element selection with an extended matcher that looks for UIScrollView children
this._matcher = this._matcher._extendToDescendantScrollViews();
- const action = new actions.ScrollEdgeAction(edge);
- const traceDescription = actionDescription.scrollTo(edge);
+ const action = new actions.ScrollEdgeAction(edge, startPositionX, startPositionY);
+ const traceDescription = actionDescription.scrollTo(edge, startPositionX, startPositionY);
return await new ActionInteraction(this._invocationManager, this._matcher, action, traceDescription).execute();
}
@@ -140,7 +140,6 @@ class NativeElement {
}
async takeScreenshot(screenshotName) {
- // TODO this should be moved to a lower-layer handler of this use-case
const action = new actions.TakeElementScreenshot();
const traceDescription = actionDescription.takeScreenshot(screenshotName);
const resultBase64 = await new ActionInteraction(this._invocationManager, this._matcher, action, traceDescription).execute();
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/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/src/android/espressoapi/DetoxAction.js b/detox/src/android/espressoapi/DetoxAction.js
index 6ed2d213f8..162a091640 100644
--- a/detox/src/android/espressoapi/DetoxAction.js
+++ b/detox/src/android/espressoapi/DetoxAction.js
@@ -68,8 +68,10 @@ class DetoxAction {
};
}
- static scrollToEdge(edge) {
+ static scrollToEdge(edge, startOffsetPercentX, startOffsetPercentY) {
if (typeof edge !== "string") throw new Error("edge should be a string, but got " + (edge + (" (" + (typeof edge + ")"))));
+ if (typeof startOffsetPercentX !== "number") throw new Error("startOffsetPercentX should be a number, but got " + (startOffsetPercentX + (" (" + (typeof startOffsetPercentX + ")"))));
+ if (typeof startOffsetPercentY !== "number") throw new Error("startOffsetPercentY should be a number, but got " + (startOffsetPercentY + (" (" + (typeof startOffsetPercentY + ")"))));
return {
target: {
type: "Class",
@@ -79,6 +81,12 @@ class DetoxAction {
args: [{
type: "Integer",
value: sanitize_android_edge(edge)
+ }, {
+ type: "Double",
+ value: startOffsetPercentX
+ }, {
+ type: "Double",
+ value: startOffsetPercentY
}]
};
}
@@ -208,7 +216,6 @@ class DetoxAction {
}
static adjustSliderToPosition(newPosition) {
- if (typeof newPosition !== "number") throw new Error("newPosition should be a number, but got " + (newPosition + (" (" + (typeof newPosition + ")"))));
return {
target: {
type: "Class",
@@ -216,7 +223,7 @@ class DetoxAction {
},
method: "adjustSliderToPosition",
args: [{
- type: "Double",
+ type: "Float",
value: newPosition
}]
};
diff --git a/detox/src/android/interactions/native.js b/detox/src/android/interactions/native.js
index a7863c526b..d93e2b5530 100644
--- a/detox/src/android/interactions/native.js
+++ b/detox/src/android/interactions/native.js
@@ -28,7 +28,7 @@ class ActionInteraction extends Interaction {
constructor(invocationManager, matcher, action, traceDescription) {
super(invocationManager, traceDescription);
this._call = EspressoDetoxApi.perform(matcher, action._call);
- // TODO: move this.execute() here from the caller
+ // TODO [2024-12-01]: move this.execute() here from the caller
}
}
@@ -39,7 +39,7 @@ class MatcherAssertionInteraction extends Interaction {
matcher = notCondition ? matcher.not : matcher;
this._call = DetoxAssertionApi.assertMatcher(call(element._call), matcher._call.value);
- // TODO: move this.execute() here from the caller
+ // TODO [2024-12-01]: move this.execute() here from the caller
}
}
diff --git a/detox/src/android/matchers/native.js b/detox/src/android/matchers/native.js
index 2a3bef214b..d7beadc975 100644
--- a/detox/src/android/matchers/native.js
+++ b/detox/src/android/matchers/native.js
@@ -76,7 +76,7 @@ class ToggleMatcher extends NativeMatcher {
}
}
-// TODO: Please be aware, that this is just a dummy matcher
+// NOTE: Please be aware, that this is just a dummy matcher
class TraitsMatcher extends NativeMatcher {
constructor(value) {
super();
diff --git a/detox/src/artifacts/providers/index.js b/detox/src/artifacts/providers/index.js
index 3a8febcebc..98561e4874 100644
--- a/detox/src/artifacts/providers/index.js
+++ b/detox/src/artifacts/providers/index.js
@@ -1,5 +1,5 @@
class ArtifactPluginsProvider {
- declareArtifactPlugins({ client }) {} // eslint-disable-line no-unused-vars
+ declareArtifactPlugins({ client }) {} // eslint-disable-line no-unused-vars,@typescript-eslint/no-unused-vars
}
class AndroidArtifactPluginsProvider extends ArtifactPluginsProvider {
diff --git a/detox/src/artifacts/screenshot/SimulatorScreenshotPlugin.js b/detox/src/artifacts/screenshot/SimulatorScreenshotPlugin.js
index 31f121c6ac..7770e21c5f 100644
--- a/detox/src/artifacts/screenshot/SimulatorScreenshotPlugin.js
+++ b/detox/src/artifacts/screenshot/SimulatorScreenshotPlugin.js
@@ -1,7 +1,6 @@
const path = require('path');
const fs = require('../../utils/fsext');
-const log = require('../../utils/logger').child({ cat: 'artifacts-plugin,artifacts' });
const FileArtifact = require('../templates/artifact/FileArtifact');
const temporaryPath = require('../utils/temporaryPath');
diff --git a/detox/src/artifacts/templates/artifact/Artifact.js b/detox/src/artifacts/templates/artifact/Artifact.js
index 2613bf6853..434e7f7b72 100644
--- a/detox/src/artifacts/templates/artifact/Artifact.js
+++ b/detox/src/artifacts/templates/artifact/Artifact.js
@@ -107,7 +107,7 @@ class Artifact {
async doStop() {}
- async doSave(artifactPath) {} // eslint-disable-line no-unused-vars
+ async doSave(_artifactPath) {}
async doDiscard() {}
}
diff --git a/detox/src/artifacts/templates/artifact/Artifact.test.js b/detox/src/artifacts/templates/artifact/Artifact.test.js
index dd5428bf10..2e425ff8f1 100644
--- a/detox/src/artifacts/templates/artifact/Artifact.test.js
+++ b/detox/src/artifacts/templates/artifact/Artifact.test.js
@@ -110,7 +110,6 @@ describe('Artifact', () => {
it('should wait till .save() ends and .start() again', async () => {
await artifact.start(1, 2, 3, 4);
expect(artifact.doStart).toHaveBeenCalledTimes(2);
- // TODO: assert the correct execution order
});
});
@@ -137,7 +136,6 @@ describe('Artifact', () => {
it('should wait till .discard() ends and .start() again', async () => {
await artifact.start(1, 2, 3, 4);
expect(artifact.doStart).toHaveBeenCalledTimes(2);
- // TODO: assert the correct execution order
});
});
diff --git a/detox/src/artifacts/uiHierarchy/IosUIHierarchyPlugin.js b/detox/src/artifacts/uiHierarchy/IosUIHierarchyPlugin.js
index f20d456016..031270984f 100644
--- a/detox/src/artifacts/uiHierarchy/IosUIHierarchyPlugin.js
+++ b/detox/src/artifacts/uiHierarchy/IosUIHierarchyPlugin.js
@@ -51,6 +51,7 @@ class IosUIHierarchyPlugin extends ArtifactPlugin {
if (this.enabled) {
const scope = this.context.testSummary ? 'perTest' : 'perSession';
setUniqueProperty(this._artifacts[scope], name, artifact);
+ this.api.trackArtifact(artifact);
} else {
this._pendingDeletions.push(artifact.discard());
}
@@ -97,6 +98,7 @@ class IosUIHierarchyPlugin extends ArtifactPlugin {
.map(async ([key, artifact]) => {
const destination = await this.api.preparePathForArtifact(`${key}.viewhierarchy`, testSummary);
await artifact.save(destination);
+ this.api.untrackArtifact(artifact);
})
.value();
diff --git a/detox/src/artifacts/uiHierarchy/IosUIHierarchyPlugin.test.js b/detox/src/artifacts/uiHierarchy/IosUIHierarchyPlugin.test.js
index 340422bb7a..06dcf0b4ea 100644
--- a/detox/src/artifacts/uiHierarchy/IosUIHierarchyPlugin.test.js
+++ b/detox/src/artifacts/uiHierarchy/IosUIHierarchyPlugin.test.js
@@ -32,6 +32,8 @@ describe('IosUIHierarchyPlugin', () => {
requestIdleCallback: jest.fn(async (callback) => {
await callback();
}),
+ trackArtifact: jest.fn(),
+ untrackArtifact: jest.fn(),
userConfig: {
enabled: true,
keepOnlyFailedTestsArtifacts: false,
@@ -65,6 +67,8 @@ describe('IosUIHierarchyPlugin', () => {
expect(session2.save).toHaveBeenCalledWith('artifacts/ui2.viewhierarchy');
expect(test1.save).toHaveBeenCalledWith('artifacts/test/ui.viewhierarchy');
expect(test2.save).toHaveBeenCalledWith('artifacts/test/ui2.viewhierarchy');
+ expect(api.trackArtifact).toHaveBeenCalledTimes(4);
+ expect(api.untrackArtifact).toHaveBeenCalledTimes(4);
});
it('should relocate existing artifacts before the app gets uninstalled', async () => {
diff --git a/detox/src/client/AsyncWebSocket.js b/detox/src/client/AsyncWebSocket.js
index d2d1a117b4..cc816a2fb3 100644
--- a/detox/src/client/AsyncWebSocket.js
+++ b/detox/src/client/AsyncWebSocket.js
@@ -134,7 +134,7 @@ class AsyncWebSocket {
}
}
- // TODO: handle this leaked abstraction some day
+ // TODO [2024-12-01]: handle this leaked abstraction some day
hasPendingActions() {
return _.some(this.inFlightPromises, p => p.message.type !== 'currentStatus');
}
@@ -168,7 +168,7 @@ class AsyncWebSocket {
case WebSocket.CONNECTING: return 'opening';
case WebSocket.OPEN: return 'open';
/* istanbul ignore next */
- default: // TODO: [2021-12-01] throw new DetoxInternalError('...'); instead
+ default:
return undefined;
}
}
diff --git a/detox/src/client/Client.js b/detox/src/client/Client.js
index c472544927..4fcc31139d 100644
--- a/detox/src/client/Client.js
+++ b/detox/src/client/Client.js
@@ -187,14 +187,14 @@ class Client {
this._whenAppIsReady = new Deferred();
await this._whenAppIsConnected.promise;
- // TODO: optimize traffic (!) - we can just listen for 'ready' event
+ // TODO [2024-12-01]: optimize traffic (!) - we can just listen for 'ready' event
// if app always sends it upon load completion. On iOS it works,
// but not on Android. Afterwards, this will suffice:
//
// await this._whenAppIsReady.promise;
}
- // TODO: move to else branch after the optimization
+ // TODO [2024-12-01]: move to else branch after the optimization โโ
if (!this._whenAppIsReady.isResolved()) {
this._whenAppIsReady = new Deferred();
await this.sendAction(new actions.Ready());
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/configuration/__mocks__/configuration/cjs/.detoxrc.cjs b/detox/src/configuration/__mocks__/configuration/cjs/.detoxrc.cjs
new file mode 100644
index 0000000000..0580f07c04
--- /dev/null
+++ b/detox/src/configuration/__mocks__/configuration/cjs/.detoxrc.cjs
@@ -0,0 +1,11 @@
+module.exports = {
+ configurations: {
+ simple: {
+ device: {
+ type: "android.attached",
+ device: "Hello from .detoxrc",
+ },
+ apps: [],
+ },
+ },
+};
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/configuration/loadExternalConfig.js b/detox/src/configuration/loadExternalConfig.js
index ab0fdc0405..ccf0bc9656 100644
--- a/detox/src/configuration/loadExternalConfig.js
+++ b/detox/src/configuration/loadExternalConfig.js
@@ -10,9 +10,11 @@ const log = require('../utils/logger').child({ cat: 'config' });
async function locateExternalConfig(cwd) {
return findUp([
+ '.detoxrc.cjs',
'.detoxrc.js',
'.detoxrc.json',
'.detoxrc',
+ 'detox.config.cjs',
'detox.config.js',
'detox.config.json',
'package.json',
@@ -20,7 +22,7 @@ async function locateExternalConfig(cwd) {
}
async function loadConfig(configPath) {
- let config = path.extname(configPath) === '.js'
+ let config = isJS(path.extname(configPath))
? require(configPath)
: JSON.parse(await fs.readFile(configPath, 'utf8'));
@@ -34,6 +36,10 @@ async function loadConfig(configPath) {
};
}
+function isJS(ext) {
+ return ext === '.js' || ext === '.cjs';
+}
+
async function resolveConfigPath(configPath, cwd) {
if (!configPath) {
return locateExternalConfig(cwd);
diff --git a/detox/src/configuration/loadExternalConfig.test.js b/detox/src/configuration/loadExternalConfig.test.js
index b7a7a81622..2b31f4800b 100644
--- a/detox/src/configuration/loadExternalConfig.test.js
+++ b/detox/src/configuration/loadExternalConfig.test.js
@@ -2,6 +2,7 @@ const os = require('os');
const path = require('path');
describe('loadExternalConfig', () => {
+ const DIR_CJS = path.join(__dirname, '__mocks__/configuration/cjs');
const DIR_PACKAGEJSON = path.join(__dirname, '__mocks__/configuration/packagejson');
const DIR_PRIORITY = path.join(__dirname, '__mocks__/configuration/priority');
const DIR_EXTENDS = path.join(__dirname, '__mocks__/configuration/extends');
@@ -35,6 +36,13 @@ describe('loadExternalConfig', () => {
expect(logger.warn).not.toHaveBeenCalled();
});
+ it('should implicitly use .detoxrc.cjs', async () => {
+ const { filepath, config } = await loadExternalConfig({ cwd: DIR_CJS });
+ expect(filepath).toBe(path.join(DIR_CJS, '.detoxrc.cjs'));
+ expect(config).toMatchObject({ configurations: expect.anything() });
+ expect(logger.warn).not.toHaveBeenCalled();
+ });
+
it('should implicitly use package.json, even if there is no .detoxrc', async () => {
const { filepath, config } = await loadExternalConfig({ cwd: DIR_PACKAGEJSON });
diff --git a/detox/src/devices/allocation/drivers/android/attached/AttachedAndroidAllocDriver.js b/detox/src/devices/allocation/drivers/android/attached/AttachedAndroidAllocDriver.js
index 552eb2a5f1..466f91ceee 100644
--- a/detox/src/devices/allocation/drivers/android/attached/AttachedAndroidAllocDriver.js
+++ b/detox/src/devices/allocation/drivers/android/attached/AttachedAndroidAllocDriver.js
@@ -41,7 +41,6 @@ class AttachedAndroidAllocDriver {
async postAllocate(deviceCookie) {
const { adbName } = deviceCookie;
- // TODO Also disable native animations?
await this._adb.apiLevel(adbName);
await this._adb.unlockScreen(adbName);
}
diff --git a/detox/src/devices/allocation/drivers/android/emulator/AVDValidator.test.js b/detox/src/devices/allocation/drivers/android/emulator/AVDValidator.test.js
index 127bce7d66..d8f07946c9 100644
--- a/detox/src/devices/allocation/drivers/android/emulator/AVDValidator.test.js
+++ b/detox/src/devices/allocation/drivers/android/emulator/AVDValidator.test.js
@@ -54,7 +54,7 @@ describe('AVD validator', () => {
try {
await uut.validate();
- fail('expected to throw');
+ throw 'expected to throw';
} catch (err) {
const DetoxRuntimeError = require('../../../../../errors/DetoxRuntimeError');
expect(err).toBeInstanceOf(DetoxRuntimeError);
@@ -68,7 +68,7 @@ describe('AVD validator', () => {
try {
await uut.validate('mock-avd-name');
- fail('expected to throw');
+ throw 'expected to throw';
} catch (err) {
const DetoxRuntimeError = require('../../../../../errors/DetoxRuntimeError');
expect(err).toBeInstanceOf(DetoxRuntimeError);
diff --git a/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.js b/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.js
index 4c215d99cc..2d657d966b 100644
--- a/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.js
+++ b/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.js
@@ -5,7 +5,6 @@
const _ = require('lodash');
-const Deferred = require('../../../../../utils/Deferred');
const log = require('../../../../../utils/logger').child({ cat: 'device,device-allocation' });
const { patchAvdSkinConfig } = require('./patchAvdSkinConfig');
diff --git a/detox/src/devices/allocation/drivers/android/emulator/EmulatorVersionResolver.js b/detox/src/devices/allocation/drivers/android/emulator/EmulatorVersionResolver.js
index 9bc9335ac3..8d06cc2cc3 100644
--- a/detox/src/devices/allocation/drivers/android/emulator/EmulatorVersionResolver.js
+++ b/detox/src/devices/allocation/drivers/android/emulator/EmulatorVersionResolver.js
@@ -7,7 +7,7 @@ class EmulatorVersionResolver {
this.version = undefined;
}
- async resolve(isHeadless = false) { // TODO Make isHeadless a config arg (i.e. through c'tor)?
+ async resolve(isHeadless = false) {
if (!this.version) {
this.version = await this._resolve(isHeadless);
}
diff --git a/detox/src/devices/allocation/drivers/android/genycloud/GenyInstanceLauncher.test.js b/detox/src/devices/allocation/drivers/android/genycloud/GenyInstanceLauncher.test.js
index 7248045c6f..62942400aa 100644
--- a/detox/src/devices/allocation/drivers/android/genycloud/GenyInstanceLauncher.test.js
+++ b/detox/src/devices/allocation/drivers/android/genycloud/GenyInstanceLauncher.test.js
@@ -33,11 +33,9 @@ describe('Genymotion-Cloud instance launcher', () => {
let instanceLifecycleService;
let uut;
let retry;
- let logger;
beforeEach(() => {
jest.mock('../../../../../utils/logger');
- logger = jest.requireMock('../../../../../utils/logger');
jest.mock('../../../../../utils/retry');
retry = jest.requireMock('../../../../../utils/retry');
diff --git a/detox/src/devices/allocation/drivers/ios/SimulatorAllocDriver.js b/detox/src/devices/allocation/drivers/ios/SimulatorAllocDriver.js
index 7d7626cfb2..8af89b600d 100644
--- a/detox/src/devices/allocation/drivers/ios/SimulatorAllocDriver.js
+++ b/detox/src/devices/allocation/drivers/ios/SimulatorAllocDriver.js
@@ -39,7 +39,6 @@ class SimulatorAllocDriver {
async allocate(deviceConfig) {
const deviceQuery = new SimulatorQuery(deviceConfig.device);
- // TODO Delegate this onto a well tested allocator class
const udid = await this._deviceRegistry.registerDevice(async () => {
return await this._findOrCreateDevice(deviceQuery);
});
diff --git a/detox/src/devices/allocation/factories/android.js b/detox/src/devices/allocation/factories/android.js
index 3eefb21ae8..d352bd1f45 100644
--- a/detox/src/devices/allocation/factories/android.js
+++ b/detox/src/devices/allocation/factories/android.js
@@ -42,7 +42,7 @@ class AndroidEmulator extends DeviceAllocatorFactory {
}
class AndroidAttached extends DeviceAllocatorFactory {
- _createDriver({ detoxSession, detoxConfig }) {
+ _createDriver({ detoxSession }) {
const serviceLocator = require('../../servicelocator/android');
const adb = serviceLocator.adb;
const DeviceRegistry = require('../../allocation/DeviceRegistry');
diff --git a/detox/src/devices/allocation/factories/ios.js b/detox/src/devices/allocation/factories/ios.js
index f41257c8dc..af141fc339 100644
--- a/detox/src/devices/allocation/factories/ios.js
+++ b/detox/src/devices/allocation/factories/ios.js
@@ -2,7 +2,7 @@
const DeviceAllocatorFactory = require('./base');
class IosSimulator extends DeviceAllocatorFactory {
- _createDriver({ detoxConfig, detoxSession, eventEmitter }) {
+ _createDriver({ detoxConfig, detoxSession }) {
const AppleSimUtils = require('../../../devices/common/drivers/ios/tools/AppleSimUtils');
const applesimutils = new AppleSimUtils();
diff --git a/detox/src/devices/common/drivers/android/exec/ADB.js b/detox/src/devices/common/drivers/android/exec/ADB.js
index 445c054107..3600972fac 100644
--- a/detox/src/devices/common/drivers/android/exec/ADB.js
+++ b/detox/src/devices/common/drivers/android/exec/ADB.js
@@ -8,7 +8,7 @@ const { escape } = require('../../../../../utils/pipeCommands');
const DeviceHandle = require('../tools/DeviceHandle');
const EmulatorHandle = require('../tools/EmulatorHandle');
-const INSTALL_TIMEOUT = 45000; // TODO Double check 45s makes sense
+const INSTALL_TIMEOUT = 45000;
class ADB {
constructor() {
@@ -345,7 +345,6 @@ class ADB {
return this.adbCmd(deviceId, `emu kill`);
}
- // TODO refactor the whole thing so as to make usage of BinaryExec -- similar to EmulatorExec
async adbCmd(deviceId, params, options = {}) {
const serial = `${deviceId ? `-s ${deviceId}` : ''}`;
const cmd = `"${this.adbBin}" ${serial} ${params}`;
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/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/AppInstallHelper.js b/detox/src/devices/common/drivers/android/tools/AppInstallHelper.js
index c21dfafa00..ba621d322d 100644
--- a/detox/src/devices/common/drivers/android/tools/AppInstallHelper.js
+++ b/detox/src/devices/common/drivers/android/tools/AppInstallHelper.js
@@ -1,5 +1,3 @@
-// TODO Tweak such that if apk's already exist on the device (need to store uniquely), they will not be resent (would optimize cloud, for example)
-
class AppInstallHelper {
constructor(adb, fileTransfer) {
this._adb = adb;
diff --git a/detox/src/devices/common/drivers/android/tools/AppInstallHelper.test.js b/detox/src/devices/common/drivers/android/tools/AppInstallHelper.test.js
index aedb4586a7..1bb199a4c6 100644
--- a/detox/src/devices/common/drivers/android/tools/AppInstallHelper.test.js
+++ b/detox/src/devices/common/drivers/android/tools/AppInstallHelper.test.js
@@ -28,10 +28,7 @@ describe('Android app installation helper', () => {
it('should throw if transient dir prep fails', async () => {
fileTransfer.prepareDestinationDir.mockRejectedValue(new Error('mocked error in adb-shell'));
- try {
- await uut.install(deviceId, appBinaryPath, testBinaryPath);
- fail('expected to throw');
- } catch (err) {}
+ await expect(uut.install(deviceId, appBinaryPath, testBinaryPath)).rejects.toThrow();
});
it('should push app-binary file to the device', async () => {
@@ -47,10 +44,7 @@ describe('Android app installation helper', () => {
it('should break if file push fails', async () => {
fileTransfer.send.mockRejectedValue(new Error('mocked error in adb-push'));
- try {
- await uut.install(deviceId, appBinaryPath, testBinaryPath);
- fail('expected to throw');
- } catch(err) {}
+ await expect(uut.install(deviceId, appBinaryPath, testBinaryPath)).rejects.toThrow();
});
it('should remote-install both binaries via shell', async () => {
@@ -66,10 +60,7 @@ describe('Android app installation helper', () => {
it('should break if remote-install fails', async () => {
adb.remoteInstall.mockRejectedValue(new Error('mocked error in remote-install'));
- try {
- await uut.install(deviceId, appBinaryPath, testBinaryPath);
- fail('expected to throw');
- } catch(err) {}
+ await expect(uut.install(deviceId, appBinaryPath, testBinaryPath)).rejects.toThrow();
});
it('should allow for an install with no test binary', async () => {
diff --git a/detox/src/devices/common/drivers/android/tools/Instrumentation.test.js b/detox/src/devices/common/drivers/android/tools/Instrumentation.test.js
index fcfacd6ca1..30f9bc72d4 100644
--- a/detox/src/devices/common/drivers/android/tools/Instrumentation.test.js
+++ b/detox/src/devices/common/drivers/android/tools/Instrumentation.test.js
@@ -131,11 +131,7 @@ describe('Instrumentation', () => {
childProcessUtils.interruptProcess.mockRejectedValue(new Error());
await uut.launch(deviceId, bundleId, []);
-
- try {
- await invokeTerminationCallback();
- fail();
- } catch(error) {}
+ await expect(invokeTerminationCallback()).rejects.toThrowError();
});
it('should not terminate if dispatched twice', async () => {
@@ -162,11 +158,7 @@ describe('Instrumentation', () => {
it('should break if process interruption fails', async () => {
childProcessUtils.interruptProcess.mockRejectedValue(new Error());
await uut.launch(deviceId, bundleId, []);
-
- try {
- await uut.terminate();
- fail();
- } catch(error) {}
+ await expect(uut.terminate()).rejects.toThrowError();
});
it('should not terminate if not running', async () => {
diff --git a/detox/src/devices/common/drivers/android/tools/MonitoredInstrumentation.test.js b/detox/src/devices/common/drivers/android/tools/MonitoredInstrumentation.test.js
index a718cc559a..dfbab5790b 100644
--- a/detox/src/devices/common/drivers/android/tools/MonitoredInstrumentation.test.js
+++ b/detox/src/devices/common/drivers/android/tools/MonitoredInstrumentation.test.js
@@ -52,10 +52,7 @@ describe('Monitored instrumentation', () => {
it('should break if underlying launch fails', async () => {
instrumentationObj().launch.mockRejectedValue(new Error());
- try {
- await uut.launch(deviceId, bundleId, {});
- fail();
- } catch (e) {}
+ await expect(uut.launch(deviceId, bundleId, {})).rejects.toThrowError();
});
});
@@ -81,10 +78,7 @@ describe('Monitored instrumentation', () => {
await uut.launch(deviceId, bundleId, {});
- try {
- await uut.terminate();
- fail();
- } catch (e) {}
+ await expect(uut.terminate()).rejects.toThrowError();
});
it('should allow for termination without launch', async () => {
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/common/drivers/ios/tools/AppleSimUtils.js b/detox/src/devices/common/drivers/ios/tools/AppleSimUtils.js
index 06ce77e488..3a8b7b86d1 100644
--- a/detox/src/devices/common/drivers/ios/tools/AppleSimUtils.js
+++ b/detox/src/devices/common/drivers/ios/tools/AppleSimUtils.js
@@ -10,21 +10,123 @@ const environment = require('../../../../../utils/environment');
const log = require('../../../../../utils/logger').child({ cat: 'device' });
const { quote } = require('../../../../../utils/shellQuote');
+const PERMISSIONS_VALUES = {
+ YES: 'YES',
+ NO: 'NO',
+ UNSET: 'unset',
+ LIMITED: 'limited',
+};
+
+const SIMCTL_SET_PERMISSION_ACTIONS ={
+ GRANT: 'grant',
+ REVOKE: 'revoke',
+ RESET: 'reset',
+};
+
class AppleSimUtils {
async setPermissions(udid, bundleId, permissionsObj) {
- let permissions = [];
- _.forEach(permissionsObj, function (shouldAllow, permission) {
- permissions.push(permission + '=' + shouldAllow);
- });
+ for (const [service, value] of Object.entries(permissionsObj)) {
+ switch (service) {
+ case 'location':
+ switch (value) {
+ case 'always':
+ await this.setPermissionWithSimctl(udid, bundleId, SIMCTL_SET_PERMISSION_ACTIONS.GRANT, 'location-always');
+ break;
+
+ case 'inuse':
+ await this.setPermissionWithSimctl(udid, bundleId, SIMCTL_SET_PERMISSION_ACTIONS.GRANT, 'location');
+ break;
+
+ case 'never':
+ await this.setPermissionWithSimctl(udid, bundleId, SIMCTL_SET_PERMISSION_ACTIONS.REVOKE, 'location');
+ break;
+
+ case 'unset':
+ await this.setPermissionWithSimctl(udid, bundleId, SIMCTL_SET_PERMISSION_ACTIONS.RESET, 'location');
+ break;
+ }
+
+ break;
+
+ case 'contacts':
+ if (value === PERMISSIONS_VALUES.LIMITED) {
+ await this.setPermissionWithSimctl(udid, bundleId, SIMCTL_SET_PERMISSION_ACTIONS.GRANT, 'contacts-limited');
+ } else {
+ await this.setPermissionWithAppleSimUtils(udid, bundleId, service, value);
+ }
+ break;
+
+ case 'photos':
+ if (value === PERMISSIONS_VALUES.LIMITED) {
+ await this.setPermissionWithSimctl(udid, bundleId, SIMCTL_SET_PERMISSION_ACTIONS.GRANT, 'photos-add');
+ } else {
+ await this.setPermissionWithAppleSimUtils(udid, bundleId, service, value);
+ }
+ break;
+
+ // eslint-disable-next-line no-fallthrough
+ case 'calendar':
+ case 'camera':
+ case 'medialibrary':
+ case 'microphone':
+ case 'motion':
+ case 'reminders':
+ case 'siri':
+ // Simctl uses kebab-case for service names.
+ const simctlService = service.replace('medialibrary', 'media-library');
+ await this.setPermissionWithSimctl(udid, bundleId, this.basicPermissionValueToSimctlAction(value), simctlService);
+ break;
+
+ // Requires AppleSimUtils: unsupported by latest Simctl at the moment of writing this code.
+ // eslint-disable-next-line no-fallthrough
+ case 'notifications':
+ case 'health':
+ case 'homekit':
+ case 'speech':
+ case 'faceid':
+ case 'userTracking':
+ await this.setPermissionWithAppleSimUtils(udid, bundleId, service, value);
+ break;
+ }
+ }
+ }
+
+ basicPermissionValueToSimctlAction(value) {
+ switch (value) {
+ case PERMISSIONS_VALUES.YES:
+ return SIMCTL_SET_PERMISSION_ACTIONS.GRANT;
+
+ case PERMISSIONS_VALUES.NO:
+ return SIMCTL_SET_PERMISSION_ACTIONS.REVOKE;
+
+ case PERMISSIONS_VALUES.UNSET:
+ return SIMCTL_SET_PERMISSION_ACTIONS.RESET;
+ }
+ }
+ async setPermissionWithSimctl(udid, bundleId, action, service) {
const options = {
- args: `--byId ${udid} --bundle ${bundleId} --restartSB --setPermissions ${_.join(permissions, ',')}`,
+ cmd: `privacy ${udid} ${action} ${service} ${bundleId}`,
statusLogs: {
- trying: `Trying to set permissions...`,
- successful: 'Permissions are set'
+ trying: `Trying to set permissions with Simctl: ${action} ${service}...`,
+ successful: `${service} permissions are set`
},
retries: 1,
};
+
+ await this._execSimctl(options);
+ }
+
+ async setPermissionWithAppleSimUtils(udid, bundleId, service, value) {
+ const options = {
+ args: `--byId ${udid} --bundle ${bundleId} --restartSB --setPermissions ${service}=${value}`,
+ statusLogs: {
+ trying: `Trying to set permissions with AppleSimUtils: ${service}=${value}...`,
+ successful: `${service} permissions are set`
+ },
+ retries: 1,
+ };
+
await this._execAppleSimUtils(options);
}
@@ -33,6 +135,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);
@@ -318,14 +421,7 @@ class AppleSimUtils {
}
async setLocation(udid, lat, lon) {
- const result = await childProcess.execWithRetriesAndLogs(`which fbsimctl`, { retries: 1 });
- if (_.get(result, 'stdout')) {
- await childProcess.execWithRetriesAndLogs(`fbsimctl ${udid} set_location ${lat} ${lon}`, { retries: 1 });
- } else {
- throw new DetoxRuntimeError(`setLocation currently supported only through fbsimctl.
- Install fbsimctl using:
- "brew tap facebook/fb && export CODE_SIGNING_REQUIRED=NO && brew install fbsimctl"`);
- }
+ await this._execSimctl({ cmd: `location ${udid} set ${lat},${lon}` });
}
async resetContentAndSettings(udid) {
diff --git a/detox/src/devices/runtime/RuntimeDevice.test.js b/detox/src/devices/runtime/RuntimeDevice.test.js
index c6a7438345..f32bcb5772 100644
--- a/detox/src/devices/runtime/RuntimeDevice.test.js
+++ b/detox/src/devices/runtime/RuntimeDevice.test.js
@@ -402,12 +402,7 @@ describe('Device', () => {
it(`(relaunch) with url and userNofitication should throw`, async () => {
const device = await aValidDevice();
- try {
- await device.relaunchApp({ url: 'scheme://some.url', userNotification: 'notif' });
- fail('should fail');
- } catch (ex) {
- expect(ex).toBeDefined();
- }
+ await expect(device.relaunchApp({ url: 'scheme://some.url', userNotification: 'notif' })).rejects.toThrowError();
});
it(`(relaunch) with permissions should send trigger setpermissions before app starts`, async () => {
@@ -537,13 +532,7 @@ describe('Device', () => {
const device = await aValidDevice();
- try {
- await device.launchApp(launchParams);
- fail('should throw');
- } catch (ex) {
- expect(ex).toBeDefined();
- }
-
+ await expect(device.launchApp(launchParams)).rejects.toThrowError();
expect(device.deviceDriver.deliverPayload).not.toHaveBeenCalled();
});
@@ -810,12 +799,7 @@ describe('Device', () => {
it(`openURL(notAnObject) should pass to device driver`, async () => {
const device = await aValidDevice();
- try {
- await device.openURL('url');
- fail('should throw');
- } catch (ex) {
- expect(ex).toBeDefined();
- }
+ await expect(device.openURL('url')).rejects.toThrowError();
});
it(`reloadReactNative() should pass to device driver`, async () => {
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/AndroidDriver.test.js b/detox/src/devices/runtime/drivers/android/AndroidDriver.test.js
index cccb788cae..1def20be00 100644
--- a/detox/src/devices/runtime/drivers/android/AndroidDriver.test.js
+++ b/detox/src/devices/runtime/drivers/android/AndroidDriver.test.js
@@ -8,7 +8,7 @@ describe('Android driver', () => {
const mockNotificationDataTargetPath = '/ondevice/path/to/notification.json';
let logger;
- let fs; // TODO don't mock
+ let fs; // on the next rewrite, do your best not to mock fs
let client;
let getAbsoluteBinaryPath;
let eventEmitter;
@@ -21,7 +21,6 @@ describe('Android driver', () => {
let appInstallHelper;
let appUninstallHelper;
let instrumentation;
- let DeviceRegistryClass;
let uut;
beforeEach(() => {
@@ -53,12 +52,9 @@ describe('Android driver', () => {
});
it('should break if instrumentation launch fails', async () => {
- instrumentation.launch.mockRejectedValue(new Error());
+ instrumentation.launch.mockRejectedValue(new Error('Simulated failure'));
- try {
- await uut.launchApp(bundleId, {}, '');
- fail();
- } catch (e) {}
+ await expect(uut.launchApp(bundleId, {}, '')).rejects.toThrowError('Simulated failure');
});
it('should set a termination callback function', async () => {
@@ -354,12 +350,8 @@ describe('Android driver', () => {
instrumentation.waitForCrash.mockRejectedValue(new Error());
await uut.launchApp(bundleId, {}, '');
- try {
- await uut.waitUntilReady();
- fail();
- } catch (e) {
- expect(instrumentation.abortWaitForCrash).toHaveBeenCalled();
- }
+ await expect(uut.waitUntilReady()).rejects.toThrowError();
+ expect(instrumentation.abortWaitForCrash).toHaveBeenCalled();
});
});
@@ -440,14 +432,10 @@ describe('Android driver', () => {
.mockRejectedValueOnce(new Error())
.mockResolvedValueOnce();
- try {
- await uut.installUtilBinaries(binaryPaths);
- fail();
- } catch (e) {
- expect(appInstallHelper.install).toHaveBeenCalledWith(adbName, binaryPaths[0]);
- expect(appInstallHelper.install).toHaveBeenCalledWith(adbName, binaryPaths[1]);
- expect(appInstallHelper.install).toHaveBeenCalledTimes(2);
- }
+ await expect(uut.installUtilBinaries(binaryPaths)).rejects.toThrowError();
+ expect(appInstallHelper.install).toHaveBeenCalledWith(adbName, binaryPaths[0]);
+ expect(appInstallHelper.install).toHaveBeenCalledWith(adbName, binaryPaths[1]);
+ expect(appInstallHelper.install).toHaveBeenCalledTimes(2);
});
it('should not install if already installed', async () => {
@@ -595,7 +583,6 @@ describe('Android driver', () => {
jest.mock('../../../allocation/DeviceRegistry');
- DeviceRegistryClass = require('../../../allocation/DeviceRegistry');
};
const mockGetAbsoluteBinaryPathImpl = (x) => `absolutePathOf(${x})`;
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/detox/src/devices/runtime/drivers/android/emulator/EmulatorDriver.js b/detox/src/devices/runtime/drivers/android/emulator/EmulatorDriver.js
index da73dcfcf3..1b1333dbfb 100644
--- a/detox/src/devices/runtime/drivers/android/emulator/EmulatorDriver.js
+++ b/detox/src/devices/runtime/drivers/android/emulator/EmulatorDriver.js
@@ -11,7 +11,6 @@ const AndroidDriver = require('../AndroidDriver');
* @property forceAdbInstall { Boolean }
*/
-// TODO Unit test coverage
class EmulatorDriver extends AndroidDriver {
/**
* @param deps { EmulatorDriverDeps }
diff --git a/detox/src/devices/runtime/factories/ios.js b/detox/src/devices/runtime/factories/ios.js
index 5027684cb9..f11f69aad9 100644
--- a/detox/src/devices/runtime/factories/ios.js
+++ b/detox/src/devices/runtime/factories/ios.js
@@ -2,8 +2,6 @@ const RuntimeDeviceFactory = require('./base');
class RuntimeDriverFactoryIos extends RuntimeDeviceFactory {
_createDriverDependencies(commonDeps) {
- const { eventEmitter } = commonDeps;
-
const AppleSimUtils = require('../../../devices/common/drivers/ios/tools/AppleSimUtils');
const applesimutils = new AppleSimUtils();
diff --git a/detox/src/ios/expectTwo.js b/detox/src/ios/expectTwo.js
index b2153b4386..f89b7f26ae 100644
--- a/detox/src/ios/expectTwo.js
+++ b/detox/src/ios/expectTwo.js
@@ -247,10 +247,13 @@ class Element {
return this.withAction('scroll', traceDescription, pixels, direction, startPositionX, startPositionY);
}
- scrollTo(edge) {
+ scrollTo(edge, startPositionX = NaN, startPositionY = NaN) {
if (!['left', 'right', 'top', 'bottom'].some(option => option === edge)) throw new Error('edge should be one of [left, right, top, bottom], but got ' + edge);
- const traceDescription = actionDescription.scrollTo(edge);
- return this.withAction('scrollTo', traceDescription, edge);
+ if (typeof startPositionX !== 'number') throw new Error('startPositionX should be a number, but got ' + (startPositionX + (' (' + (typeof startPositionX + ')'))));
+ if (typeof startPositionY !== 'number') throw new Error('startPositionY should be a number, but got ' + (startPositionY + (' (' + (typeof startPositionY + ')'))));
+
+ const traceDescription = actionDescription.scrollTo(edge, startPositionX, startPositionY);
+ return this.withAction('scrollTo', traceDescription, edge, startPositionX, startPositionY);
}
swipe(direction, speed = 'fast', normalizedSwipeOffset = NaN, normalizedStartingPointX = NaN, normalizedStartingPointY = NaN) {
diff --git a/detox/src/ios/expectTwoApiCoverage.test.js b/detox/src/ios/expectTwoApiCoverage.test.js
index 53ab4dd6b3..442366d75b 100644
--- a/detox/src/ios/expectTwoApiCoverage.test.js
+++ b/detox/src/ios/expectTwoApiCoverage.test.js
@@ -181,6 +181,8 @@ describe('expectTwo API Coverage', () => {
await expectToThrow(() => e.element(e.by.id('someId')).scrollTo(0));
await expectToThrow(() => e.element(e.by.id('someId')).scrollTo('noDirection'));
+ await expectToThrow(() => e.element(e.by.id('someId')).scrollTo('top','Nan', 0.5));
+ await expectToThrow(() => e.element(e.by.id('someId')).scrollTo('top', 0.5, 'Nan'));
await expectToThrow(() => e.element(e.by.id('someId')).swipe(4, 'fast'));
await expectToThrow(() => e.element(e.by.id('someId')).swipe('left', 'fast', 20));
@@ -266,6 +268,7 @@ describe('expectTwo API Coverage', () => {
await e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).scroll(50, 'down');
await e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).scroll(50, 'down', 0, 0);
await e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).scrollTo('left');
+ await e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).scrollTo('left', 0.1, 0.1);
await e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).swipe('left');
await e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).swipe('left', 'fast');
await e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).swipe('left', 'slow', 0.1);
@@ -283,12 +286,8 @@ describe('expectTwo API Coverage', () => {
});
async function expectToThrow(func) {
- try {
- await func();
- fail('should throw');
- } catch (ex) {
- expect(ex).toBeDefined();
- }
+ const asyncWrapper = async () => await func();
+ await expect(asyncWrapper()).rejects.toThrow();
}
class MockExecutor {
diff --git a/detox/src/ipc/IPCClient.js b/detox/src/ipc/IPCClient.js
index 5033d1804f..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;
}
@@ -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..203ccd597a 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 }) {
@@ -40,7 +40,7 @@ class IPCServer {
this._ipc.config.logger = (msg) => this._logger.trace(msg);
await new Promise((resolve) => {
- // TODO: handle reject
+ // It is worth to handle rejection here some day
this._ipc.serve(() => resolve());
this._ipc.server.on('conductEarlyTeardown', this.onConductEarlyTeardown.bind(this));
this._ipc.server.on('registerContext', this.onRegisterContext.bind(this));
@@ -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);
@@ -102,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 4414935ffe..6e33cd97fc 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 }));
@@ -332,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/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/realms/DetoxContext.js b/detox/src/realms/DetoxContext.js
index 28d2f6d870..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}`),
@@ -102,7 +102,7 @@ class DetoxContext {
/** @abstract */
[symbols.reportTestResults](_testResults) {}
/** @abstract */
- [symbols.conductEarlyTeardown]() {}
+ [symbols.conductEarlyTeardown](_permanent) {}
/**
* @abstract
* @param {Partial} _opts
@@ -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 bec924eda2..e2f634138b 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] = '';
@@ -51,9 +54,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 });
}
};
@@ -85,7 +88,6 @@ class DetoxPrimaryContext extends DetoxContext {
const detoxConfig = await this[symbols.resolveConfig](opts);
const {
- device: deviceConfig,
logger: loggerConfig,
session: sessionConfig
} = detoxConfig;
@@ -96,7 +98,6 @@ class DetoxPrimaryContext extends DetoxContext {
data: this[$sessionState],
}, getCurrentCommand());
- // TODO: IPC Server creation ought to be delegated to a generator/factory.
const IPCServer = require('../ipc/IPCServer');
this[_ipcServer] = new IPCServer({
sessionState: this[$sessionState],
@@ -109,17 +110,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) {
this[_wss] = new DetoxServer({
@@ -132,7 +122,6 @@ class DetoxPrimaryContext extends DetoxContext {
await this[_wss].open();
}
- // TODO: double check that this config is indeed propogated onto the client create at the detox-worker side
if (!sessionConfig.server && this[_wss]) {
// @ts-ignore
sessionConfig.server = `ws://localhost:${this[_wss].port}`;
@@ -162,17 +151,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 +173,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 +193,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 +236,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 +269,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 0a74e9a25a..e5795f1ef8 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.');
}
@@ -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).
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/detox/src/utils/Deferred.test.js b/detox/src/utils/Deferred.test.js
index e6bf3ac1ab..629feef92c 100644
--- a/detox/src/utils/Deferred.test.js
+++ b/detox/src/utils/Deferred.test.js
@@ -105,12 +105,7 @@ describe('Deferred', () => {
deferred = Deferred.rejected(new Error('error mock'));
expect(deferred.status).toBe(Deferred.REJECTED);
- try {
- await deferred.promise;
- fail();
- } catch (e) {
- expect(e.message).toEqual('error mock');
- }
+ await expect(deferred.promise).rejects.toThrowError('error mock');
});
});
});
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..cd5bcff9a7 100644
--- a/detox/src/utils/childProcess/exec.test.js
+++ b/detox/src/utils/childProcess/exec.test.js
@@ -13,6 +13,16 @@ describe('Exec utils', () => {
exec = require('./exec');
});
+ const execWithRetriesAndLogs = async (command, options) => {
+ try {
+ const result = await exec.execWithRetriesAndLogs(command, options);
+ return result;
+ } catch (e) {
+ // Workaround for Jest's expect(...).rejects.toThrowError() not working with thrown plain objects
+ throw new Error(e);
+ }
+ };
+
const advanceOpsCounter = (count) => {
const opsCounter = require('./opsCounter');
for (let i = 0; i < count; i++) opsCounter.inc();
@@ -20,22 +30,22 @@ 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 });
+ await execWithRetriesAndLogs('bin');
+ expect(cpp.exec).toHaveBeenCalledWith(`bin`, {});
});
it(`exec command with arguments ends successfully`, async () => {
mockCppSuccessful(cpp);
const options = { args: `--argument 123` };
- await exec.execWithRetriesAndLogs('bin', options);
+ await 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 () => {
mockCppSuccessful(cpp);
- await exec.execWithRetriesAndLogs('bin');
+ await execWithRetriesAndLogs('bin');
const usedOptions = cpp.exec.mock.calls[0][1];
expect(usedOptions).not.toHaveProperty('env');
expect(cpp.exec).toHaveBeenCalledTimes(1);
@@ -48,18 +58,18 @@ describe('Exec utils', () => {
args: `--argument 123`,
prefix: `export MY_PREFIX`
};
- await exec.execWithRetriesAndLogs('bin', options);
+ await 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 () => {
mockCppSuccessful(cpp);
const options = { prefix: `export MY_PREFIX` };
- await exec.execWithRetriesAndLogs('bin', options);
+ await 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 () => {
@@ -68,7 +78,7 @@ describe('Exec utils', () => {
jest.spyOn(logger, 'child');
mockCppSuccessful(cpp);
- await exec.execWithRetriesAndLogs('bin');
+ await execWithRetriesAndLogs('bin');
expect(logger.child).toHaveBeenCalledWith({ fn: 'execWithRetriesAndLogs', trackingId, cmd: 'bin' });
});
@@ -84,9 +94,9 @@ describe('Exec utils', () => {
successful: 'successful status log',
},
};
- await exec.execWithRetriesAndLogs('bin', options);
+ await 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');
@@ -106,9 +116,9 @@ describe('Exec utils', () => {
logger.debug.mockClear();
- await exec.execWithRetriesAndLogs('bin', options);
+ await 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');
@@ -116,7 +126,7 @@ describe('Exec utils', () => {
it(`exec command should output success and err logs`, async () => {
mockCppSuccessful(cpp);
- await exec.execWithRetriesAndLogs('bin');
+ await execWithRetriesAndLogs('bin');
expect(logger.trace).toHaveBeenCalledWith({ event: 'EXEC_SUCCESS', stdout: true }, '"successful result"');
expect(logger.trace).toHaveBeenCalledWith({ event: 'EXEC_SUCCESS', stderr: true }, 'err');
@@ -130,7 +140,7 @@ describe('Exec utils', () => {
};
cpp.exec.mockResolvedValueOnce(cppResult);
- await exec.execWithRetriesAndLogs('bin');
+ await execWithRetriesAndLogs('bin');
expect(logger.trace).toHaveBeenCalledWith({ event: 'EXEC_SUCCESS' }, '');
expect(logger.trace).toHaveBeenCalledTimes(1);
@@ -138,7 +148,7 @@ describe('Exec utils', () => {
it(`exec command should output success with high severity if verbosity set to high`, async () => {
mockCppSuccessful(cpp);
- await exec.execWithRetriesAndLogs('bin', { verbosity: 'high' });
+ await execWithRetriesAndLogs('bin', { verbosity: 'high' });
expect(logger.debug).toHaveBeenCalledWith({ event: 'EXEC_SUCCESS', stdout: true }, '"successful result"');
expect(logger.debug).toHaveBeenCalledWith({ event: 'EXEC_SUCCESS', stderr: true }, 'err');
@@ -147,50 +157,40 @@ describe('Exec utils', () => {
it(`exec command with undefined return should throw`, async () => {
cpp.exec.mockReturnValueOnce(undefined);
- try {
- await exec.execWithRetriesAndLogs('bin');
- fail('should throw');
- } catch (ex) {
- expect(ex).toBeDefined();
- }
+ await expect(execWithRetriesAndLogs('bin')).rejects.toThrowError();
});
it(`exec command and fail with error code`, async () => {
mockCppFailure(cpp);
- try {
- await exec.execWithRetriesAndLogs('bin', { retries: 0, interval: 1 });
- fail('expected execWithRetriesAndLogs() to throw');
- } catch (object) {
- expect(cpp.exec).toHaveBeenCalledWith(`bin`, { timeout: 0 });
- expect(logger.error.mock.calls).toHaveLength(3);
- expect(logger.error).toHaveBeenCalledWith(expect.objectContaining({ event: 'EXEC_FAIL' }), expect.anything());
- }
+ await expect(execWithRetriesAndLogs('bin', { retries: 0, interval: 1 })).rejects.toThrowError();
+ expect(cpp.exec).toHaveBeenCalledWith(`bin`, {});
+ expect(logger.error.mock.calls).toHaveLength(3);
+ expect(logger.error).toHaveBeenCalledWith(expect.objectContaining({ event: 'EXEC_FAIL' }), expect.anything());
});
it(`exec command and fail with error code, report only to debug log if verbosity is low`, async () => {
mockCppFailure(cpp);
- try {
- 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(logger.error).not.toHaveBeenCalled();
- expect(logger.debug.mock.calls).toHaveLength(4);
- }
+ await expect(execWithRetriesAndLogs('bin', { verbosity: 'low', retries: 0, interval: 1 })).rejects.toThrowError();
+ expect(cpp.exec).toHaveBeenCalledWith(`bin`, {});
+ expect(logger.error).not.toHaveBeenCalled();
+ expect(logger.debug.mock.calls).toHaveLength(4);
});
it(`exec command and fail with timeout`, async () => {
mockCppFailure(cpp);
- try {
- await exec.execWithRetriesAndLogs('bin', { timeout: 1, retries: 0, interval: 1 });
- fail('expected execWithRetriesAndLogs() to throw');
- } catch (object) {
- expect(cpp.exec).toHaveBeenCalledWith(`bin`, { timeout: 1 });
- expect(logger.error.mock.calls).toHaveLength(3);
- }
+ await expect(execWithRetriesAndLogs('bin', { timeout: 1, retries: 0, interval: 1 })).rejects.toThrowError();
+ expect(cpp.exec).toHaveBeenCalledWith(`bin`, { timeout: 1 });
+ expect(logger.error.mock.calls).toHaveLength(3);
+ });
+
+ it(`exec command with a given maxBuffer`, async () => {
+ mockCppSuccessful(cpp);
+
+ await execWithRetriesAndLogs('bin', { maxBuffer: 1000 });
+ expect(cpp.exec).toHaveBeenCalledWith(`bin`, { maxBuffer: 1000 });
});
it(`exec command with multiple failures`, async () => {
@@ -203,14 +203,9 @@ describe('Exec utils', () => {
.mockRejectedValueOnce(errorResult)
.mockRejectedValueOnce(errorResult);
- try {
- 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).toHaveBeenCalledTimes(6);
- expect(object).toBeDefined();
- }
+ await expect(execWithRetriesAndLogs('bin', { retries: 5, interval: 1 })).rejects.toThrowError();
+ expect(cpp.exec).toHaveBeenCalledWith(`bin`, {});
+ expect(cpp.exec).toHaveBeenCalledTimes(6);
});
it(`exec command with multiple failures and then a success`, async () => {
@@ -225,8 +220,8 @@ describe('Exec utils', () => {
.mockRejectedValueOnce(errorResult)
.mockResolvedValueOnce(successfulResult);
- await exec.execWithRetriesAndLogs('bin', { retries: 6, interval: 1 });
- expect(cpp.exec).toHaveBeenCalledWith(`bin`, { timeout: 0 });
+ await execWithRetriesAndLogs('bin', { retries: 6, interval: 1 });
+ expect(cpp.exec).toHaveBeenCalledWith(`bin`, {});
expect(cpp.exec).toHaveBeenCalledTimes(6);
});
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(() => {
diff --git a/detox/src/utils/invocationTraceDescriptions.js b/detox/src/utils/invocationTraceDescriptions.js
index a9168327ae..63969bf635 100644
--- a/detox/src/utils/invocationTraceDescriptions.js
+++ b/detox/src/utils/invocationTraceDescriptions.js
@@ -12,8 +12,9 @@ module.exports = {
pinchWithAngle: (direction, speed, angle) => `pinch with direction ${direction}, speed ${speed}, and angle ${angle}`,
replaceText: (value) => `replace input text: "${value}"`,
scroll: (amount, direction, startPositionX, startPositionY) =>
- `scroll ${amount} pixels ${direction}${startPositionX !== undefined && startPositionY !== undefined ? ` from normalized position (${startPositionX}, ${startPositionY})` : ''}`,
- scrollTo: (edge) => `scroll to ${edge}`,
+ `scroll ${amount} pixels ${direction}${startPositionX !== undefined || startPositionY !== undefined ? ` from normalized position (${startPositionX}, ${startPositionY})` : ''}`,
+ scrollTo: (edge, startPositionX, startPositionY) =>
+ `scroll to ${edge} ${startPositionX !== undefined || startPositionY !== undefined ? ` from normalized position (${startPositionX}, ${startPositionY})` : ''}`,
scrollToIndex: (index) => `scroll to index #${index}`,
setColumnToValue: (column, value) => `set column ${column} to value ${value}`,
setDatePickerDate: (dateString, dateFormat) => `set date picker date to ${dateString} using format ${dateFormat}`,
diff --git a/detox/src/utils/retry.test.js b/detox/src/utils/retry.test.js
index e00ebdd49d..82baa6648f 100644
--- a/detox/src/utils/retry.test.js
+++ b/detox/src/utils/retry.test.js
@@ -4,12 +4,12 @@ describe('retry', () => {
const mockFailingUserFn = () => jest.fn().mockReturnValue(Promise.reject(new Error('a thing')));
const mockFailingOnceUserFn = () => jest.fn()
- .mockReturnValueOnce(Promise.reject())
- .mockReturnValueOnce(Promise.resolve());
+ .mockRejectedValueOnce(new Error('once'))
+ .mockReturnValueOnce();
const mockFailingTwiceUserFn = () => jest.fn()
- .mockReturnValueOnce(Promise.reject(new Error('once')))
- .mockReturnValueOnce(Promise.reject(new Error('twice')))
- .mockReturnValueOnce(Promise.resolve());
+ .mockRejectedValueOnce(new Error('once'))
+ .mockRejectedValueOnce(new Error('twice'))
+ .mockReturnValueOnce();
beforeEach(() => {
jest.mock('./sleep', () => jest.fn().mockReturnValue(Promise.resolve()));
@@ -21,11 +21,7 @@ describe('retry', () => {
it('should retry once over a function that fails once', async () => {
const mockFn = mockFailingOnceUserFn();
- try {
- await retry({ retries: 999, interval: 0 }, mockFn);
- } catch (e) {
- fail('expected retry not to fail');
- }
+ await expect(retry({ retries: 999, interval: 0 }, mockFn)).resolves.not.toThrow();
expect(mockFn).toHaveBeenCalledTimes(2);
});
@@ -33,11 +29,7 @@ describe('retry', () => {
it('should sleep before calling a function if initialSleep is set', async () => {
const mockFn = jest.fn();
- try {
- await retry({ initialSleep: 1234, retries: 999, interval: 0 }, mockFn);
- } catch (e) {
- fail('expected retry not to fail');
- }
+ await expect(retry({ initialSleep: 1234, retries: 999, interval: 0 }, mockFn)).resolves.not.toThrow();
expect(mockFn).toHaveBeenCalledTimes(1);
expect(sleep).toHaveBeenCalledTimes(1);
@@ -47,16 +39,7 @@ describe('retry', () => {
it('should call sleep() with { shouldUnref: true } if set', async () => {
const mockFn = mockFailingTwiceUserFn();
- try {
- await retry({
- initialSleep: 1000,
- retries: 2,
- interval: 0,
- shouldUnref: true,
- }, mockFn);
- } catch (e) {
- fail('expected retry not to fail');
- }
+ await expect(retry({ initialSleep: 1000, retries: 2, interval: 0, shouldUnref: true, }, mockFn)).resolves.not.toThrow();
expect(mockFn).toHaveBeenCalledTimes(3);
expect(sleep).toHaveBeenCalledTimes(3);
@@ -80,23 +63,14 @@ describe('retry', () => {
it('should adhere to retries parameter', async () => {
const mockFn = mockFailingUserFn();
- try {
- await retry({ retries: 2, interval: 1 }, mockFn);
- fail('expected retry to fail and throw');
- } catch (error) {
- expect(mockFn).toHaveBeenCalledTimes(3);
- expect(error).toBeDefined();
- }
+ await expect(retry({ retries: 2, interval: 1 }, mockFn)).rejects.toThrowError();
});
it('should adhere to interval parameter, and sleep for increasingly long intervals (i.e. the default backoff mode)', async () => {
const mockFn = mockFailingUserFn();
const baseInterval = 111;
- try {
- await retry({ retries: 2, interval: baseInterval }, mockFn);
- fail('expected retry to fail and throw');
- } catch (error) {}
+ await expect(retry({ retries: 2, interval: baseInterval }, mockFn)).rejects.toThrowError();
expect(sleep).toHaveBeenCalledTimes(2);
expect(sleep).toHaveBeenCalledWith(baseInterval, undefined);
@@ -112,10 +86,7 @@ describe('retry', () => {
backoff: 'none',
};
- try {
- await retry(options, mockFn);
- fail('expected retry to fail and throw');
- } catch (error) {}
+ await expect(retry(options, mockFn)).rejects.toThrow();
expect(sleep).toHaveBeenCalledTimes(2);
expect(sleep).toHaveBeenNthCalledWith(1, baseInterval, undefined);
@@ -131,10 +102,7 @@ describe('retry', () => {
backoff: 'linear',
};
- try {
- await retry(options, mockFn);
- fail('expected retry to fail and throw');
- } catch (error) {}
+ await expect(retry(options, mockFn)).rejects.toThrow();
expect(sleep).toHaveBeenCalledTimes(2);
expect(sleep).toHaveBeenCalledWith(baseInterval, undefined);
@@ -147,10 +115,7 @@ describe('retry', () => {
.mockReturnValueOnce(true)
.mockReturnValueOnce(false);
- try {
- await retry({ retries: 999, interval: 1, conditionFn }, mockFn);
- fail('expected retry to fail and throw');
- } catch (error) {}
+ await expect(retry({ retries: 999, interval: 1, conditionFn }, mockFn)).rejects.toThrowError();
expect(mockFn).toHaveBeenCalledTimes(2);
});
@@ -160,10 +125,7 @@ describe('retry', () => {
const defaultRetries = 9;
const defaultInterval = 500;
- try {
- await retry(mockFn);
- fail('expected retry to fail and throw');
- } catch (error) {}
+ await expect(retry(mockFn)).rejects.toThrowError();
expect(mockFn).toHaveBeenCalledTimes(defaultRetries + 1);
expect(sleep).toHaveBeenCalledWith(defaultInterval, undefined);
diff --git a/detox/test/e2e/utils/rn-consts.js b/detox/src/utils/rn-consts/rn-consts.js
similarity index 100%
rename from detox/test/e2e/utils/rn-consts.js
rename to detox/src/utils/rn-consts/rn-consts.js
diff --git a/detox/test/.eslintrc.js b/detox/test/.eslintrc.js
index a8cb5f932a..997e2b602a 100644
--- a/detox/test/.eslintrc.js
+++ b/detox/test/.eslintrc.js
@@ -1,7 +1,7 @@
module.exports = {
root: true,
extends: [
- '@react-native-community',
+ '@react-native',
],
plugins: [
'unicorn',
@@ -16,8 +16,7 @@ module.exports = {
// disabled due to styling conflicts between eslint and prettier
'prettier/prettier': 0,
- // TODO: enable this with argsIgnorePattern
- '@typescript-eslint/no-unused-vars': 0, // ['error', {argsIgnorePattern: '^_'}],
+ '@typescript-eslint/no-unused-vars': ['error', {argsIgnorePattern: '^_'}],
// TODO: enable these rules gradually
'comma-dangle': 0,
@@ -26,7 +25,6 @@ module.exports = {
'eqeqeq': 0,
'jsx-quotes': 0,
'keyword-spacing': 0,
- 'no-extra-semi': 0,
'no-sequences': 0,
'no-trailing-spaces': 0,
'no-useless-escape': 0,
diff --git a/detox/test/android/app/build.gradle b/detox/test/android/app/build.gradle
index 022f8e8cb7..8eba840a8a 100644
--- a/detox/test/android/app/build.gradle
+++ b/detox/test/android/app/build.gradle
@@ -1,23 +1,33 @@
apply plugin: 'com.android.application'
+apply plugin: 'com.facebook.react'
+apply plugin: 'kotlin-android'
apply from: '../../../android/rninfo.gradle'
-if (rnInfo.isRN71OrHigher) {
- apply plugin: 'com.facebook.react'
-} else {
- project.ext.react = [
- enableHermes: true
- ]
- apply from: '../../node_modules/react-native/react.gradle'
-}
android {
namespace 'com.example'
- compileSdkVersion rootProject.ext.compileSdkVersion
+ buildToolsVersion = rootProject.ext.buildToolsVersion
+ compileSdk = rootProject.ext.compileSdkVersion
+
+ if (rnInfo.isRN72OrHigher) {
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = '17'
+ }
+ } else {
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_11
+ targetCompatibility JavaVersion.VERSION_11
+ }
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
+ kotlinOptions {
+ jvmTarget = '11'
+ }
}
defaultConfig {
@@ -61,19 +71,9 @@ android {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro', "${project(':detox').projectDir}/proguard-rules-app.pro"
-
signingConfig signingConfigs.release
}
}
- productFlavors {
- flavorDimensions 'reactNativeVersion'
- rnDefault {
- dimension 'reactNativeVersion'
- }
- rnLegacy { // For RN < 71
- dimension 'reactNativeVersion'
- }
- }
packagingOptions {
pickFirst '**/libc++_shared.so'
@@ -83,16 +83,19 @@ android {
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/NOTICE.txt'
}
+
+ buildFeatures {
+ buildConfig = true
+ }
}
dependencies {
// The version of react-native is set by the React Native Gradle Plugin
- rnDefaultImplementation('com.facebook.react:react-android')
- // noinspection GradleDynamicVersion
- rnLegacyImplementation('com.facebook.react:react-native:+')
+ implementation('com.facebook.react:react-android')
implementation "androidx.appcompat:appcompat:${rootProject.ext.appCompatVersion}"
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
+
implementation project(path: ':@react-native-community_slider')
implementation project(':AsyncStorage')
implementation project(':react-native-webview')
@@ -100,6 +103,7 @@ dependencies {
implementation project(':react-native-community-geolocation')
implementation project(':react-native-datetimepicker')
implementation project(':react-native-launcharguments')
+ implementation project(':react-native-permissions')
androidTestImplementation(project(path: ':detox'))
androidTestImplementation 'com.linkedin.testbutler:test-butler-library:2.2.1'
@@ -107,11 +111,7 @@ dependencies {
// Apply Hermes as our JS engine
dependencies {
- rnDefaultImplementation('com.facebook.react:hermes-android')
- // noinspection GradleDynamicVersion
- rnLegacyImplementation('com.facebook.react:hermes-engine:+') {
- exclude group: 'com.facebook.fbjni'
- }
+ implementation('com.facebook.react:hermes-android')
}
// Run this once to be able to run the application with BUCK
diff --git a/detox/test/android/app/src/rnDefault/java/com/example/DetoxRNHost.java b/detox/test/android/app/src/main/java/com/example/DetoxRNHost.java
similarity index 100%
rename from detox/test/android/app/src/rnDefault/java/com/example/DetoxRNHost.java
rename to detox/test/android/app/src/main/java/com/example/DetoxRNHost.java
diff --git a/detox/test/android/app/src/main/java/com/example/ReactNativeAdapter.java b/detox/test/android/app/src/main/java/com/example/ReactNativeAdapter.java
index e0d2552500..fe61cee526 100644
--- a/detox/test/android/app/src/main/java/com/example/ReactNativeAdapter.java
+++ b/detox/test/android/app/src/main/java/com/example/ReactNativeAdapter.java
@@ -9,6 +9,7 @@
import com.reactnativecommunity.slider.ReactSliderPackage;
import com.reactnativecommunity.webview.RNCWebViewPackage;
import com.reactnativelauncharguments.LaunchArgumentsPackage;
+import com.zoontek.rnpermissions.RNPermissionsPackage;
import java.util.Arrays;
import java.util.List;
@@ -24,7 +25,8 @@ public static List getManualLinkPackages() {
new AsyncStoragePackage(),
new ReactCheckBoxPackage(),
new RNDateTimePickerPackage(),
- new LaunchArgumentsPackage()
+ new LaunchArgumentsPackage(),
+ new RNPermissionsPackage()
);
}
}
diff --git a/detox/test/android/app/src/rnLegacy/java/com/example/DetoxRNHost.java b/detox/test/android/app/src/rnLegacy/java/com/example/DetoxRNHost.java
deleted file mode 100644
index 1fc65e9d3c..0000000000
--- a/detox/test/android/app/src/rnLegacy/java/com/example/DetoxRNHost.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.example;
-
-import android.app.Application;
-
-import com.facebook.react.ReactPackage;
-import com.facebook.react.ReactNativeHost;
-
-import java.util.List;
-
-class DetoxRNHost extends ReactNativeHost {
- protected DetoxRNHost(Application application) {
- super(application);
- }
-
- @Override
- public boolean getUseDeveloperSupport() {
- return BuildConfig.DEBUG;
- }
-
- @Override
- protected List getPackages() {
- // Packages that cannot be autolinked yet can be added manually here, for example:
- // packages.add(new MyReactNativePackage());
- // TurboModules must also be loaded here providing a valid TurboReactPackage implementation:
- // packages.add(new TurboReactPackage() { ... });
- // If you have custom Fabric Components, their ViewManagers should also be loaded here
- // inside a ReactPackage.
- return ReactNativeAdapter.getManualLinkPackages();
- }
-}
diff --git a/detox/test/android/build.gradle b/detox/test/android/build.gradle
index cbcbe1ea34..be7799e17e 100644
--- a/detox/test/android/build.gradle
+++ b/detox/test/android/build.gradle
@@ -1,22 +1,14 @@
buildscript {
apply from: '../../android/rninfo.gradle'
- def androidGradlePluginVersion =
- rnInfo.isRN71OrHigher ? '7.3.1' :
- rnInfo.isRN70OrHigher ? '7.2.1' :
- rnInfo.isRN69OrHigher ? '7.1.1' :
- '7.0.4'
-
- println "[$project] Resorted to Android Gradle-plugin version $androidGradlePluginVersion"
-
ext {
isOfficialDetoxApp = true
- kotlinVersion = '1.6.21'
- buildToolsVersion = '33.0.0'
- compileSdkVersion = 33
- targetSdkVersion = 33
+ kotlinVersion = '1.8.0'
+ buildToolsVersion = '34.0.0'
+ compileSdkVersion = 34
+ targetSdkVersion = 34
minSdkVersion = 21
- appCompatVersion = '1.4.2'
+ appCompatVersion = '1.6.1'
}
ext.detoxKotlinVersion = ext.kotlinVersion
@@ -27,18 +19,8 @@ buildscript {
}
dependencies {
- classpath "com.android.tools.build:gradle:$androidGradlePluginVersion"
-
- // In RN .71, they've switched to the gradle plugin they've uploaded to maven-central
- if (rnInfo.isRN71OrHigher) {
- classpath 'com.facebook.react:react-native-gradle-plugin'
- }
-
- // Gradle task downloader seems to come built-in in newer versions of RN/Gradle
- if (!rnInfo.isRN71OrHigher) {
- classpath 'de.undercouch:gradle-download-task:5.0.1'
- }
-
+ classpath "com.android.tools.build:gradle"
+ classpath 'com.facebook.react:react-native-gradle-plugin'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
// Needed by Spek (https://spekframework.org/setup-android)
@@ -49,23 +31,8 @@ buildscript {
allprojects {
repositories {
- // In RN below 71, we want RN and hermes/js-core native code to come from node_modules/ rather
- // than from maven-central, because there are also older versions over there.
- if (rnInfo.isRN71OrHigher) {
- google()
- mavenCentral()
- } else {
- maven {
- url "$rootDir/../../node_modules/react-native/android"
- }
- google()
- mavenCentral() {
- content {
- excludeGroup 'com.facebook.react'
- }
- }
- }
-
+ google()
+ mavenCentral()
mavenLocal()
}
}
@@ -79,3 +46,7 @@ subprojects {
}
}
}
+
+if (ext.rnInfo.isRN73OrHigher) {
+ apply plugin: "com.facebook.react.rootproject"
+}
diff --git a/detox/test/android/gradle/wrapper/gradle-wrapper.jar b/detox/test/android/gradle/wrapper/gradle-wrapper.jar
index 7454180f2a..7f93135c49 100644
Binary files a/detox/test/android/gradle/wrapper/gradle-wrapper.jar and b/detox/test/android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/detox/test/android/gradle/wrapper/gradle-wrapper.properties b/detox/test/android/gradle/wrapper/gradle-wrapper.properties
index e81f2af1a1..ac72c34e8a 100644
--- a/detox/test/android/gradle/wrapper/gradle-wrapper.properties
+++ b/detox/test/android/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,7 @@
-#Thu Feb 09 21:36:01 IST 2023
distributionBase=GRADLE_USER_HOME
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
distributionPath=wrapper/dists
-zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/detox/test/android/gradlew b/detox/test/android/gradlew
index 3447eeb0cb..0adc8e1a53 100755
--- a/detox/test/android/gradlew
+++ b/detox/test/android/gradlew
@@ -1,4 +1,4 @@
-#!/usr/bin/env bash
+#!/bin/sh
#
# Copyright ยฉ 2015-2021 the original authors.
@@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
-# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -80,13 +80,11 @@ do
esac
done
-APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
-
-APP_NAME="Gradle"
+# This is normally unused
+# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -133,22 +131,29 @@ location of your Java installation."
fi
else
JAVACMD=java
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
+ fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -193,6 +198,10 @@ if "$cygwin" || "$msys" ; then
done
fi
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
@@ -205,6 +214,12 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \
"$@"
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
diff --git a/detox/test/android/gradlew.bat b/detox/test/android/gradlew.bat
index aec99730b4..6689b85bee 100644
--- a/detox/test/android/gradlew.bat
+++ b/detox/test/android/gradlew.bat
@@ -1,4 +1,20 @@
-@if "%DEBUG%" == "" @echo off
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -8,20 +24,24 @@
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS=
-
set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto init
+if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -35,7 +55,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-if exist "%JAVA_EXE%" goto init
+if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@@ -45,44 +65,26 @@ echo location of your Java installation.
goto fail
-:init
-@rem Get command-line arguments, handling Windowz variants
-
-if not "%OS%" == "Windows_NT" goto win9xME_args
-if "%@eval[2+2]" == "4" goto 4NT_args
-
-:win9xME_args
-@rem Slurp the command line arguments.
-set CMD_LINE_ARGS=
-set _SKIP=2
-
-:win9xME_args_slurp
-if "x%~1" == "x" goto execute
-
-set CMD_LINE_ARGS=%*
-goto execute
-
-:4NT_args
-@rem Get arguments from the 4NT Shell from JP Software
-set CMD_LINE_ARGS=%$
-
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
+if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
diff --git a/detox/test/android/settings.gradle b/detox/test/android/settings.gradle
index ca5d4b00fc..118655f99b 100644
--- a/detox/test/android/settings.gradle
+++ b/detox/test/android/settings.gradle
@@ -1,7 +1,17 @@
+apply from: file("../../android/rninfo.gradle")
rootProject.name = 'DetoxTest'
include ':app'
-includeBuild('../node_modules/react-native-gradle-plugin')
+def rnMajorVer = getRnMajorVersion(rootDir)
+println "[settings] RNInfo: detected React Native version: (major=$rnMajorVer)"
+
+if (rnMajorVer < 72) {
+ includeBuild('../node_modules/react-native-gradle-plugin')
+} else {
+ includeBuild('../node_modules/@react-native/gradle-plugin')
+}
+
+
include ':detox'
project(':detox').projectDir = new File(rootProject.projectDir, '../../android/detox')
@@ -26,3 +36,6 @@ project(':react-native-datetimepicker').projectDir = new File(rootProject.projec
include ':react-native-launcharguments'
project(':react-native-launcharguments').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-launch-arguments/android')
+
+include ':react-native-permissions'
+project(':react-native-permissions').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-permissions/android')
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/e2e/03.actions-scroll.test.js b/detox/test/e2e/03.actions-scroll.test.js
index e4cb32f444..6615d6ea4b 100644
--- a/detox/test/e2e/03.actions-scroll.test.js
+++ b/detox/test/e2e/03.actions-scroll.test.js
@@ -46,6 +46,32 @@ describe('Actions - Scroll', () => {
await expect(element(by.text('HText1'))).toBeVisible();
});
+ it('should scroll to edge from a custom start-position ratio', async () => {
+ await expect(element(by.text('Text12'))).not.toBeVisible();
+ await element(by.id('toggleScrollOverlays')).tap();
+ await element(by.id('ScrollView161')).scrollTo('bottom', 0.2, 0.4);
+ await element(by.id('toggleScrollOverlays')).tap();
+ await expect(element(by.text('Text12'))).toBeVisible();
+
+ await element(by.id('toggleScrollOverlays')).tap();
+ await element(by.id('ScrollView161')).scrollTo('top', 0.8, 0.6);
+ await element(by.id('toggleScrollOverlays')).tap();
+ await expect(element(by.text('Text1'))).toBeVisible();
+ });
+
+ it('should scroll to edge horizontally from a custom start-position ratio', async () => {
+ await expect(element(by.text('HText8'))).not.toBeVisible();
+ await element(by.id('toggleScrollOverlays')).tap();
+ await element(by.id('ScrollViewH')).scrollTo('right', 0.8, 0.6);
+ await element(by.id('toggleScrollOverlays')).tap();
+ await expect(element(by.text('HText8'))).toBeVisible();
+
+ await element(by.id('toggleScrollOverlays')).tap();
+ await element(by.id('ScrollViewH')).scrollTo('left',0.2, 0.4);
+ await element(by.id('toggleScrollOverlays')).tap();
+ await expect(element(by.text('HText1'))).toBeVisible();
+ });
+
it('should scroll from a custom start-position ratio', async () => {
await expect(element(by.text('Text12'))).not.toBeVisible();
await element(by.id('toggleScrollOverlays')).tap();
diff --git a/detox/test/e2e/03.actions.test.js b/detox/test/e2e/03.actions.test.js
index 5ef6698e08..315db569f0 100644
--- a/detox/test/e2e/03.actions.test.js
+++ b/detox/test/e2e/03.actions.test.js
@@ -1,4 +1,5 @@
const driver = require('./drivers/actions-driver').actionsScreenDriver;
+const custom = require('./utils/custom-it');
describe('Actions', () => {
beforeEach(async () => {
@@ -212,7 +213,7 @@ describe('Actions', () => {
await expect(element(by.id('UniqueId007'))).toBeVisible();
});
- it('should adjust legacy slider and assert its value', async () => {
+ custom.it.skipFromRNVersion(71)('should adjust legacy slider and assert its value', async () => {
const reactSliderId = 'legacySliderWithASimpleID';
await expect(element(by.id(reactSliderId))).toHaveSliderPosition(0.25);
await element(by.id(reactSliderId)).adjustSliderToPosition(0.75);
diff --git a/detox/test/e2e/13.permissions.test.js b/detox/test/e2e/13.permissions.test.js
index 572057e7b0..f2b443e21c 100644
--- a/detox/test/e2e/13.permissions.test.js
+++ b/detox/test/e2e/13.permissions.test.js
@@ -1,14 +1,267 @@
+const { RESULTS } = require('react-native-permissions');
+
+const BASIC_PERMISSIONS_TO_CHECK = [
+ 'userTracking',
+ 'calendar',
+ 'camera',
+ 'contacts',
+ 'microphone',
+ 'reminders',
+ 'siri',
+ 'speech',
+ 'medialibrary'
+];
+
+const LOCATION_ALWAYS = 'location_always';
+const LOCATION_WHEN_IN_USE = 'location_when_in_use';
+
+const PHOTO_LIBRARY = 'photo_library';
+const PHOTO_LIBRARY_ADD_ONLY = 'photo_library_add_only';
+
describe(':ios: Permissions', () => {
+ BASIC_PERMISSIONS_TO_CHECK.forEach((name) => {
+ describe(name, () => {
+ const authorizationStatus = element(by.id(name));
- it('Permissions is granted', async () => {
- await device.launchApp({permissions: {calendar: 'YES'}, newInstance: true});
- await element(by.text('Permissions')).tap();
- await expect(element(by.text('granted'))).toBeVisible();
+ it('should find element with test-id: ' + name, async () => {
+ await device.launchApp({delete: true});
+ await element(by.text('Permissions')).tap();
+
+ await expect(authorizationStatus).toBeVisible();
+ });
+
+ it('should show default permissions when undefined', async () => {
+ await device.launchApp({delete: true});
+ await element(by.text('Permissions')).tap();
+
+ await expect(authorizationStatus).toHaveText(RESULTS.DENIED);
+ });
+
+ it('should show default permissions when defined to `unset`', async () => {
+ const permissions = {[name]: 'unset'};
+
+ await device.launchApp({permissions, delete: true});
+ await element(by.text('Permissions')).tap();
+
+ await expect(authorizationStatus).toHaveText(RESULTS.DENIED);
+ });
+
+ it('should grant permission', async () => {
+ const permissions = {[name]: 'YES'};
+
+ await device.launchApp({permissions, delete: true});
+ await element(by.text('Permissions')).tap();
+
+ await expect(authorizationStatus).toHaveText('granted');
+ });
+
+ it('should block permissions', async () => {
+ const permissions = {[name]: 'NO'};
+
+ await device.launchApp({permissions, delete: true});
+ await element(by.text('Permissions')).tap();
+
+ await expect(authorizationStatus).toHaveText(RESULTS.BLOCKED);
+ });
+ });
+ });
+
+ describe("location", () => {
+ const locationAlways = element(by.id(LOCATION_ALWAYS));
+ const locationInuse = element(by.id(LOCATION_WHEN_IN_USE));
+
+ it('should find status elements', async () => {
+ await device.launchApp({delete: true});
+ await element(by.text('Permissions')).tap();
+
+ await expect(locationAlways).toBeVisible();
+ await expect(locationInuse).toBeVisible();
+ });
+
+ it('should show default permissions when undefined', async () => {
+ await device.launchApp({delete: true});
+ await element(by.text('Permissions')).tap();
+
+ await expect(locationAlways).toHaveText(RESULTS.DENIED);
+ await expect(locationInuse).toHaveText(RESULTS.DENIED);
+ });
+
+ it('should show default permissions when defined to `unset`', async () => {
+ const permissions = {location: 'unset'};
+
+ await device.launchApp({permissions, delete: true});
+ await element(by.text('Permissions')).tap();
+
+ await expect(locationAlways).toHaveText(RESULTS.DENIED);
+ await expect(locationInuse).toHaveText(RESULTS.DENIED);
+ });
+
+ it('should grant permission `inuse`', async () => {
+ const permissions = {location: 'inuse'};
+
+ await device.launchApp({permissions, delete: true});
+ await element(by.text('Permissions')).tap();
+
+ await expect(locationAlways).toHaveText(RESULTS.BLOCKED);
+ await expect(locationInuse).toHaveText(RESULTS.GRANTED);
+ });
+
+ it('should grant permission `always`', async () => {
+ const permissions = {location: 'always'};
+
+ await device.launchApp({permissions, delete: true});
+ await element(by.text('Permissions')).tap();
+
+ await expect(locationAlways).toHaveText(RESULTS.GRANTED);
+ await expect(locationInuse).toHaveText(RESULTS.GRANTED);
+ });
+
+ it('should block permissions', async () => {
+ const permissions = {location: 'never'};
+
+ await device.launchApp({permissions, delete: true});
+ await element(by.text('Permissions')).tap();
+
+ await expect(locationAlways).toHaveText(RESULTS.BLOCKED);
+ await expect(locationInuse).toHaveText(RESULTS.BLOCKED);
+ });
});
- it('Permissions denied', async () => {
- await device.launchApp({permissions: {calendar: 'NO'}, newInstance: true});
+ describe("faceid", () => {
+ const faceid = element(by.id('faceid'));
+
+ it('should find status elements', async () => {
+ await device.launchApp({ delete: true });
+ await element(by.text('Permissions')).tap();
+
+ await expect(faceid).toBeVisible();
+ });
+
+ it('should get unavailable status when biometrics are not enrolled', async () => {
+ await device.setBiometricEnrollment(false);
+
+ await device.launchApp({ delete: true });
+ await element(by.text('Permissions')).tap();
+
+ await expect(faceid).toHaveText(RESULTS.UNAVAILABLE);
+ });
+
+ describe("when biometrics are enrolled", () => {
+ beforeEach(async () => {
+ await device.setBiometricEnrollment(true);
+ });
+
+ it('should show default permissions when undefined', async () => {
+ await device.launchApp({ delete: true });
+ await element(by.text('Permissions')).tap();
+
+ await expect(faceid).toHaveText(RESULTS.DENIED);
+ });
+
+ it('should show default permissions when defined to `unset`', async () => {
+ const permissions = { faceid: 'unset' };
+
+ await device.launchApp({ permissions, delete: true });
+ await element(by.text('Permissions')).tap();
+
+ await expect(faceid).toHaveText(RESULTS.DENIED);
+ });
+
+ // todo: Skipped due to an error coming from react-native-permissions. Fix or implement a custom check.
+ it.skip('should grant permission', async () => {
+ const permissions = { faceid: 'YES' };
+
+ await device.launchApp({ permissions, delete: true });
+ await element(by.text('Permissions')).tap();
+
+ await expect(faceid).toHaveText('granted');
+ });
+
+ it('should block permissions', async () => {
+ const permissions = { faceid: 'NO' };
+
+ await device.launchApp({ permissions, delete: true });
+ await element(by.text('Permissions')).tap();
+
+ await expect(faceid).toHaveText(RESULTS.BLOCKED);
+ });
+ });
+ });
+
+ describe("photos", () => {
+ const photoLibrary = element(by.id(PHOTO_LIBRARY));
+ const photoLibraryAddOnly = element(by.id(PHOTO_LIBRARY_ADD_ONLY));
+
+ it('should find status elements', async () => {
+ await device.launchApp({delete: true});
+ await element(by.text('Permissions')).tap();
+
+ await expect(photoLibrary).toBeVisible();
+ await expect(photoLibraryAddOnly).toBeVisible();
+ });
+
+ it('should show default permissions when undefined', async () => {
+ await device.launchApp({delete: true});
+ await element(by.text('Permissions')).tap();
+
+ await expect(photoLibrary).toHaveText(RESULTS.DENIED);
+ await expect(photoLibraryAddOnly).toHaveText(RESULTS.DENIED);
+ });
+
+ it('should show default permissions when defined to `unset`', async () => {
+ const permissions = {photos: 'unset'};
+
+ await device.launchApp({permissions, delete: true});
+ await element(by.text('Permissions')).tap();
+
+ await expect(photoLibrary).toHaveText(RESULTS.DENIED);
+ await expect(photoLibraryAddOnly).toHaveText(RESULTS.DENIED);
+ });
+
+ it('should grant permission `limited`', async () => {
+ const permissions = {photos: 'limited'};
+
+ await device.launchApp({permissions, delete: true});
+ await element(by.text('Permissions')).tap();
+
+ await expect(photoLibrary).toHaveText(RESULTS.DENIED);
+ await expect(photoLibraryAddOnly).toHaveText(RESULTS.GRANTED);
+ });
+
+ it('should grant permission', async () => {
+ const permissions = {photos: 'YES'};
+
+ await device.launchApp({permissions, delete: true});
+ await element(by.text('Permissions')).tap();
+
+ await expect(photoLibrary).toHaveText(RESULTS.GRANTED);
+ await expect(photoLibraryAddOnly).toHaveText(RESULTS.GRANTED);
+ });
+
+ it('should block permissions', async () => {
+ const permissions = {photos: 'NO'};
+
+ await device.launchApp({permissions, delete: true});
+ await element(by.text('Permissions')).tap();
+
+ await expect(photoLibrary).toHaveText(RESULTS.BLOCKED);
+ await expect(photoLibraryAddOnly).toHaveText(RESULTS.DENIED);
+ });
+ });
+
+ it('should grant or block multiple permissions', async () => {
+ const permissions = {
+ photos: 'YES',
+ camera: 'YES',
+ location: 'never'
+ };
+
+ await device.launchApp({permissions, delete: true});
await element(by.text('Permissions')).tap();
- await expect(element(by.text('denied'))).toBeVisible();
+
+ await expect(element(by.id('photo_library'))).toHaveText(RESULTS.GRANTED);
+ await expect(element(by.id('camera'))).toHaveText(RESULTS.GRANTED);
+ await expect(element(by.id(LOCATION_ALWAYS))).toHaveText(RESULTS.BLOCKED);
});
-});
\ No newline at end of file
+});
+
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/16.location.test.js b/detox/test/e2e/16.location.test.js
index 39f5c5e472..7fc4e3225e 100644
--- a/detox/test/e2e/16.location.test.js
+++ b/detox/test/e2e/16.location.test.js
@@ -1,45 +1,91 @@
-const exec = require('child-process-promise').exec;
-
-//TODO: Ignoring the test in CI until fbsimctl supports Xcode 9
-async function isFbsimctlInstalled() {
- try {
- await exec(`which fbsimctl`);
- return true;
- } catch (e) {
- console.log(`setLocation only works through fbsimctl currently`);
- return false;
+const LOCATION_SCREEN_BUTTON_TEXT = 'Location';
+const LOCATION_LATITUDE_TEST_ID = 'location_latitude';
+const LOCATION_LONGITUDE_TEST_ID = 'location_longitude';
+const LOCATION_ERROR_TEST_ID = 'location_error';
+const GET_LOCATION_BUTTON_TEST_ID = 'get_location_button';
+
+const DUMMY_COORDINATE_1 = -80.125;
+const DUMMY_COORDINATE_2 = 66.5;
+
+describe('set location', () => {
+ const enterLocationScreen = async (location) => {
+ await device.launchApp({
+ delete: true,
+ ...(location !== undefined && { permissions: { location: location } }),
+ });
+
+ await element(by.text(LOCATION_SCREEN_BUTTON_TEXT)).tap();
}
-}
-
-describe('location', () => {
- const lat = -80.125;
- const lon = 66.5;
-
- // Skipped on Android because there is no Android permissions support yet
- it(':ios: Location should be unavailable', async () => {
- if (!await isFbsimctlInstalled()) {
- return;
- }
- await device.relaunchApp({ permissions: { location: 'never' } });
- await element(by.text('Location')).tap();
- await element(by.id('getLocationButton')).tap();
- await expect(element(by.id('error'))).toBeVisible();
+
+ const updateLocationInfo = async () => {
+ await element(by.id(GET_LOCATION_BUTTON_TEST_ID)).tap();
+ }
+
+ const expectLocationToAppear = async (latitude, longitude) => {
+ await waitFor(element(by.id(LOCATION_LATITUDE_TEST_ID))).toHaveText(`Latitude: ${latitude}`).withTimeout(3000);
+ await expect(element(by.id(LOCATION_LONGITUDE_TEST_ID))).toHaveText(`Longitude: ${longitude}`);
+ }
+
+ const expectErrorToAppear = async () => {
+ await waitFor(element(by.id(LOCATION_ERROR_TEST_ID))).toBeVisible().withTimeout(3000);
+ await expect(element(by.id(LOCATION_LATITUDE_TEST_ID))).not.toBeVisible();
+ await expect(element(by.id(LOCATION_LONGITUDE_TEST_ID))).not.toBeVisible();
+ }
+
+ describe(':android: permission granted in the app manifest', () => {
+ it('should set location', async () => {
+ await enterLocationScreen();
+
+ await device.setLocation(DUMMY_COORDINATE_1, DUMMY_COORDINATE_2);
+ await updateLocationInfo();
+
+ await expectLocationToAppear(DUMMY_COORDINATE_1, DUMMY_COORDINATE_2);
+ });
+
+ it('should set location multiple times', async () => {
+ await enterLocationScreen();
+
+ await device.setLocation(DUMMY_COORDINATE_1, DUMMY_COORDINATE_2);
+ await device.setLocation(DUMMY_COORDINATE_2, DUMMY_COORDINATE_1);
+ await updateLocationInfo();
+
+ await expectLocationToAppear(DUMMY_COORDINATE_2, DUMMY_COORDINATE_1);
+ });
});
- it('Should accept a location', async () => {
- const isIOS = device.getPlatform() === 'ios';
+ describe(':ios: permission set on launch config', () => {
+ it('should show error when permission defined as `never`', async () => {
+ await enterLocationScreen('never');
+ await updateLocationInfo();
+ await expectErrorToAppear();
+ });
+
+ it('should set location when permission defined as `inuse`', async () => {
+ await enterLocationScreen('inuse');
+
+ await device.setLocation(DUMMY_COORDINATE_1, DUMMY_COORDINATE_2);
+ await updateLocationInfo();
+
+ await expectLocationToAppear(DUMMY_COORDINATE_1, DUMMY_COORDINATE_2);
+ });
+
+ it('should set location when permission defined as `always`', async () => {
+ await enterLocationScreen('always');
+
+ await device.setLocation(DUMMY_COORDINATE_1, DUMMY_COORDINATE_2);
+ await updateLocationInfo();
+
+ await expectLocationToAppear(DUMMY_COORDINATE_1, DUMMY_COORDINATE_2);
+ });
- if (isIOS && !await isFbsimctlInstalled()) {
- return;
- }
+ it('should set location multiple times', async () => {
+ await enterLocationScreen('always');
- await device.relaunchApp({ permissions: { location: 'always' } });
- await device.setLocation(lat, lon);
- await element(by.text('Location')).tap();
- await element(by.id('getLocationButton')).tap();
- await waitFor(element(by.text(`Latitude: ${lat}`))).toBeVisible().withTimeout(5500);
+ await device.setLocation(DUMMY_COORDINATE_1, DUMMY_COORDINATE_2);
+ await device.setLocation(DUMMY_COORDINATE_2, DUMMY_COORDINATE_1);
+ await updateLocationInfo();
- await expect(element(by.text(`Latitude: ${lat}`))).toBeVisible();
- await expect(element(by.text(`Longitude: ${lon}`))).toBeVisible();
+ await expectLocationToAppear(DUMMY_COORDINATE_2, DUMMY_COORDINATE_1);
+ });
});
});
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/21.artifacts.test.js b/detox/test/e2e/21.artifacts.test.js
index ee13dd15e2..e92eb00491 100644
--- a/detox/test/e2e/21.artifacts.test.js
+++ b/detox/test/e2e/21.artifacts.test.js
@@ -1,6 +1,7 @@
const fs = require('fs');
const path = require('path');
+const jestExpect = require('expect').default;
const { PNG } = require('pngjs');
const {
@@ -83,11 +84,7 @@ describe('Artifacts', () => {
it('should capture hierarchy upon multiple invocation failures', async () => {
for (let i = 0; i < 2; i++) {
- try {
- await element(by.id('nonExistentId')).tap();
- fail('should have failed');
- } catch (e) {
- }
+ await jestExpect(element(by.id('nonExistentId')).tap()).rejects.toThrow();
}
});
@@ -104,12 +101,8 @@ describe('Artifacts', () => {
describe('edge uninstall case', () => {
it('should capture hierarchy regardless', async () => {
- try {
- await element(by.id('nonExistentId')).tap();
- fail('should have failed');
- } catch (e) {
- await device.uninstallApp();
- }
+ await jestExpect(element(by.id('nonExistentId')).tap()).rejects.toThrow();
+ await device.uninstallApp();
});
afterAll(async () => {
diff --git a/detox/test/e2e/22.launch-args.test.js b/detox/test/e2e/22.launch-args.test.js
index a87bc5ab03..c8921f5ac1 100644
--- a/detox/test/e2e/22.launch-args.test.js
+++ b/detox/test/e2e/22.launch-args.test.js
@@ -1,5 +1,4 @@
-/* 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 +12,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 +46,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 +62,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 +77,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 +96,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 +110,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/28.drag-and-drop.test.js b/detox/test/e2e/28.drag-and-drop.test.js
index f6a7b1442b..f326257a6e 100644
--- a/detox/test/e2e/28.drag-and-drop.test.js
+++ b/detox/test/e2e/28.drag-and-drop.test.js
@@ -3,7 +3,7 @@ describe(':ios: Drag And Drop', () => {
await device.reloadReactNative();
await element(by.text('Drag And Drop')).tap();
});
-
+
afterEach(async () => {
await element(by.id('closeButton')).tap();
});
@@ -14,20 +14,22 @@ describe(':ios: Drag And Drop', () => {
await assertCellText(2, '10');
});
- it('should drag the second cell and drop before the ten cell position', async () => {
- await assertCellText(9, '9');
- await element(by.id('cell2')).longPressAndDrag(1000, 0.9, NaN, element(by.id('cell10')), 0.9, 0.01, 'slow', 0);
+ it('should drag the second cell and drop on the ten cell position', async () => {
+ await assertCellText(2, '2');
await assertCellText(10, '10');
- //Because we used 0.001 as the drop Y point, the `cell2` actually landed at cell9, not cell10.
- await assertCellText(9, '2');
+
+ await element(by.id('cell2')).longPressAndDrag(1000, 0.9, NaN, element(by.id('cell10')), 0.9, 0.01, 'slow', 0);
+
+ await assertCellText(2, '3');
+ await assertCellText(10, '2');
});
-
+
async function assertCellText(idx, value) {
const attribs = await element(by.id('cellTextLabel')).getAttributes();
const cellStrings = attribs.elements.map(x => x.text);
-
+
if(cellStrings[idx - 1] !== value) {
throw new Error("Failed!");
}
}
-});
\ No newline at end of file
+});
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/33.attributes.test.js b/detox/test/e2e/33.attributes.test.js
index c1a278ee0e..2ddaf6b6e5 100644
--- a/detox/test/e2e/33.attributes.test.js
+++ b/detox/test/e2e/33.attributes.test.js
@@ -1,5 +1,6 @@
const { device, element, by } = require('detox');
const expect = require('expect').default;
+const custom = require('./utils/custom-it');
describe('Attributes', () => {
/** @type {Detox.IndexableNativeElement} */
@@ -156,7 +157,7 @@ describe('Attributes', () => {
});
});
- describe('of a legacy slider', () => {
+ custom.describe.skipFromRNVersion(71)('of a legacy slider', () => {
beforeAll(() => useMatcher(by.id('legacySliderId')));
it(':ios: should have a string percent .value, and .normalizedSliderPosition', () => {
diff --git a/detox/test/e2e/assets/elementScreenshot.ios.horiz.png b/detox/test/e2e/assets/elementScreenshot.ios.horiz.png
index 79874aa34f..062771b1e4 100644
Binary files a/detox/test/e2e/assets/elementScreenshot.ios.horiz.png and b/detox/test/e2e/assets/elementScreenshot.ios.horiz.png differ
diff --git a/detox/test/e2e/assets/elementScreenshot.ios.vert.png b/detox/test/e2e/assets/elementScreenshot.ios.vert.png
index caeacd1029..93fbc3953c 100644
Binary files a/detox/test/e2e/assets/elementScreenshot.ios.vert.png and b/detox/test/e2e/assets/elementScreenshot.ios.vert.png differ
diff --git a/detox/test/e2e/customPathBuilder.js b/detox/test/e2e/customPathBuilder.js
index f04b01aa77..bfa4932d49 100644
--- a/detox/test/e2e/customPathBuilder.js
+++ b/detox/test/e2e/customPathBuilder.js
@@ -3,7 +3,7 @@ module.exports = ({ rootDir }) => {
const sanitize = require('sanitize-filename');
return {
- buildPathForTestArtifact(artifactName, testSummary = null) {
+ buildPathForTestArtifact(artifactName, _testSummary) {
return path.join(rootDir, sanitize(artifactName));
}
};
diff --git a/detox/test/e2e/detox.config-android.js b/detox/test/e2e/detox.config-android.js
index 99044dd83c..4e36353192 100644
--- a/detox/test/e2e/detox.config-android.js
+++ b/detox/test/e2e/detox.config-android.js
@@ -1,20 +1,12 @@
-const _ = require('lodash');
-const { rnVersion } = require('./utils/rn-consts');
-
const capitalizeFirstLetter = (str) => str.charAt(0).toUpperCase() + str.slice(1);
-const isLegacyRNVersion = (rnVersion.minor < 71);
-const warnOnce = _.once((...args) => console.warn(...args));
function androidBaseAppConfig(buildType /* 'debug' | 'release' */) {
- const warnRNLegacy = () => warnOnce(`โ ๏ธ Detected a legacy RN version (v${rnVersion.raw}) - Using legacy build-flavor for Android config! ๐ค๐ \n`);
-
- const appFlavor = (isLegacyRNVersion ? warnRNLegacy() || 'rnLegacy' : 'rnDefault');
- const appFlavorUC = capitalizeFirstLetter(appFlavor);
const buildTypeUC = capitalizeFirstLetter(buildType);
+
return {
type: 'android.apk',
- binaryPath: `android/app/build/outputs/apk/${appFlavor}/${buildType}/app-${appFlavor}-${buildType}.apk`,
- build: `cd android && ./gradlew assemble${appFlavorUC}${buildTypeUC} assemble${appFlavorUC}${buildTypeUC}AndroidTest -DtestBuildType=${buildType} && cd ..`,
+ binaryPath: `android/app/build/outputs/apk/${buildType}/app-${buildType}.apk`,
+ build: `cd android && ./gradlew assemble${buildTypeUC} assemble${buildTypeUC}AndroidTest -DtestBuildType=${buildType} && cd ..`,
};
}
diff --git a/detox/test/e2e/detox.config.js b/detox/test/e2e/detox.config.js
index bd6a5a4c57..e8762e625d 100644
--- a/detox/test/e2e/detox.config.js
+++ b/detox/test/e2e/detox.config.js
@@ -10,10 +10,12 @@ const launchArgs = {
const config = {
testRunner: {
args: {
- $0: 'nyc jest',
+ $0: process.env.CI ? 'nyc jest' : '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}`,
@@ -97,7 +99,8 @@ const config = {
type: 'ios.simulator',
headless: Boolean(process.env.CI),
device: {
- type: 'iPhone 12 Pro Max',
+ type: 'iPhone 15 Pro Max',
+ os: "17.2",
},
},
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/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/e2e/utils/custom-it.js b/detox/test/e2e/utils/custom-it.js
index 1b2037242d..6c625f2d9b 100644
--- a/detox/test/e2e/utils/custom-it.js
+++ b/detox/test/e2e/utils/custom-it.js
@@ -1,13 +1,18 @@
const _ = require('lodash');
-const rnMinorVer = require('./rn-consts').rnVersion.minor;
+const rnMinorVer = require('../../../src/utils/rn-consts/rn-consts').rnVersion.minor;
const _it = {
withFailureIf: {
android: (spec, specFn) => runOrExpectFailByPredicates(spec, specFn, platformIs('android')),
iOSWithRNLessThan67: (spec, specFn) => runOrExpectFailByPredicates(spec, specFn, platformIs('ios'), rnVerLessThan(67)),
},
+ skipFromRNVersion: (version) => skipFromRNVersion(version),
};
+const _describe = {
+ skipFromRNVersion: (version) => describeFromRNVersion(version),
+}
+
function runOrExpectFailByPredicates(spec, specFn, ...predicateFuncs) {
it(spec, async function() {
if (allPredicatesTrue(predicateFuncs)) {
@@ -18,6 +23,32 @@ function runOrExpectFailByPredicates(spec, specFn, ...predicateFuncs) {
});
}
+
+/**
+ * Run the test only if the RN version is {version} or below. Otherwise, skip it.
+ * @returns it or it.skip functions
+ */
+function skipFromRNVersion(version) {
+ if (parseInt(rnMinorVer) <= version) {
+ return it;
+ } else {
+ return it.skip;
+ }
+}
+
+/**
+ * Run the test only if the RN version is {version} or below. Otherwise, skip it.
+ * @param version
+ * @returns describe or describe.skip functions
+ */
+function describeFromRNVersion(version) {
+ if (parseInt(rnMinorVer) <= version) {
+ return describe;
+ } else {
+ return describe.skip;
+ }
+}
+
const platformIs = (platform) => () => (device.getPlatform() === platform);
const rnVerLessThan = (rnVer) => () => (rnMinorVer < rnVer);
const allPredicatesTrue = (predicateFuncs) => _.reduce(predicateFuncs, (result, predicate) => (result && predicate()), true);
@@ -36,4 +67,5 @@ const runSpec = (specFn) => specFn();
module.exports = {
it: _it,
+ describe: _describe,
};
diff --git a/detox/test/integration/__snapshots__/timeline-artifact.test.js.snap b/detox/test/integration/__snapshots__/timeline-artifact.test.js.snap
index e99e840c78..bc04e54ad7 100644
--- a/detox/test/integration/__snapshots__/timeline-artifact.test.js.snap
+++ b/detox/test/integration/__snapshots__/timeline-artifact.test.js.snap
@@ -35,7 +35,7 @@ exports[`Timeline integration test Flaky test should deterministically produce a
"v": 0,
},
"cat": "lifecycle,cli",
- "name": "nyc jest --config integration/e2e/config.js flaky",
+ "name": "jest --config integration/e2e/config.js flaky",
"ph": "B",
"pid": 0,
"tid": 0,
@@ -432,7 +432,7 @@ Detox CLI is going to restart the test runner with those files...
"v": 0,
},
"cat": "lifecycle,cli",
- "name": "nyc jest --config integration/e2e/config.js $CWD/integration/e2e/flaky.test.js",
+ "name": "jest --config integration/e2e/config.js $CWD/integration/e2e/flaky.test.js",
"ph": "B",
"pid": 0,
"tid": 0,
@@ -851,7 +851,7 @@ exports[`Timeline integration test Focused test should deterministically produce
"v": 0,
},
"cat": "lifecycle,cli",
- "name": "nyc jest --config integration/e2e/config.js focused",
+ "name": "jest --config integration/e2e/config.js focused",
"ph": "B",
"pid": 0,
"tid": 0,
@@ -1250,7 +1250,7 @@ exports[`Timeline integration test Skipped test should deterministically produce
"v": 0,
},
"cat": "lifecycle,cli",
- "name": "nyc jest --config integration/e2e/config.js skipped",
+ "name": "jest --config integration/e2e/config.js skipped",
"ph": "B",
"pid": 0,
"tid": 0,
diff --git a/detox/test/integration/e2e/focused.test.js b/detox/test/integration/e2e/focused.test.js
index 474ff18003..f2dbb0af97 100644
--- a/detox/test/integration/e2e/focused.test.js
+++ b/detox/test/integration/e2e/focused.test.js
@@ -3,6 +3,7 @@ describe('Focused', () => {
// Reproducing when hook_start is called after test_start
});
+ // eslint-disable-next-line jest/no-focused-tests
it.only('Only test', async () => {
// Checking that skipped tests are also traced
});
diff --git a/detox/test/integration/e2e/passing-skipped.test.js b/detox/test/integration/e2e/passing-skipped.test.js
index 5ac513a940..4c157f71d5 100644
--- a/detox/test/integration/e2e/passing-skipped.test.js
+++ b/detox/test/integration/e2e/passing-skipped.test.js
@@ -1,4 +1,5 @@
describe('Suite with skipped tests', () => {
+ // eslint-disable-next-line jest/no-disabled-tests
it.skip('Skipped test', async () => {
// Checking that skipped tests are also traced
});
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/detox/test/integration/stub/StubRuntimeDriver.js b/detox/test/integration/stub/StubRuntimeDriver.js
index 918ec3fab2..bfa400155b 100644
--- a/detox/test/integration/stub/StubRuntimeDriver.js
+++ b/detox/test/integration/stub/StubRuntimeDriver.js
@@ -47,7 +47,7 @@ class StubRuntimeDriver extends DeviceDriverBase {
return process.pid;
}
- async deliverPayload(params) {
+ async deliverPayload(_params) {
await sleepVeryLittle();
}
diff --git a/detox/test/ios/Podfile b/detox/test/ios/Podfile
index 1df16527c3..ed419b26f0 100644
--- a/detox/test/ios/Podfile
+++ b/detox/test/ios/Podfile
@@ -1,14 +1,52 @@
-require_relative '../node_modules/react-native/scripts/react_native_pods'
-require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
+if ENV["REACT_NATIVE_VERSION"] && ENV["REACT_NATIVE_VERSION"].match(/0.(70|71).*/)
+ require_relative '../node_modules/react-native/scripts/react_native_pods'
+ require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
+ require_relative '../node_modules/react-native-permissions/scripts/setup'
+else
+ # Resolve react_native_pods.rb with node to allow for hoisting
+ def node_require(script)
+ # Resolve script with node to allow for hoisting
+ require Pod::Executable.execute_command('node', ['-p',
+ "require.resolve(
+ '#{script}',
+ {paths: [process.argv[1]]},
+ )", __dir__]).strip
+ end
-platform :ios, '13.0'
+ node_require('react-native/scripts/react_native_pods.rb')
+ node_require('react-native-permissions/scripts/setup.rb')
+end
+platform :ios, min_ios_version_supported
install! 'cocoapods', :deterministic_uuids => false
+# Comment unwanted permissions
+setup_permissions([
+ 'AppTrackingTransparency',
+ 'Bluetooth',
+ 'Calendars',
+ 'Camera',
+ 'Contacts',
+ 'FaceID',
+ 'LocationAccuracy',
+ 'LocationAlways',
+ 'LocationWhenInUse',
+ 'MediaLibrary',
+ 'Microphone',
+ 'Motion',
+ 'Notifications',
+ 'PhotoLibrary',
+ 'PhotoLibraryAddOnly',
+ 'Reminders',
+ 'Siri',
+ 'SpeechRecognition',
+ 'StoreKit',
+])
+
def shared_pods
config = use_native_modules!
- if !ENV["REACT_NATIVE_VERSION"] || ENV["REACT_NATIVE_VERSION"].match(/0.(68|69|70).*/)
+ if ENV["REACT_NATIVE_VERSION"] && ENV["REACT_NATIVE_VERSION"].match(/0.(70).*/)
# Flags change depending on the env values.
flags = get_default_flags()
@@ -31,7 +69,6 @@ end
target 'example' do
shared_pods
pod 'react-native-slider', :path => '../node_modules/@react-native-community/slider'
-
end
target 'example_ci' do
@@ -66,19 +103,18 @@ post_install do |installer|
__apply_update_deployment_target_workaround(installer)
__apply_Xcode_15_post_install_workaround(installer)
- if ENV["REACT_NATIVE_VERSION"] && ENV["REACT_NATIVE_VERSION"].match(/0.6[6,7,8,9].*/)
- react_native_post_install(installer)
- else
- react_native_post_install(
- installer,
- # Set `mac_catalyst_enabled` to `true` in order to apply patches
- # necessary for Mac Catalyst builds
- :mac_catalyst_enabled => false
- )
- end
+ config = use_native_modules!
+
+ react_native_post_install(
+ installer,
+ config[:reactNativePath],
+ # Set `mac_catalyst_enabled` to `true` in order to apply patches
+ # necessary for Mac Catalyst builds
+ :mac_catalyst_enabled => false
+ )
# See https://github.com/wix/Detox/pull/3035#discussion_r774747705
- if !ENV["REACT_NATIVE_VERSION"] || ENV["REACT_NATIVE_VERSION"].match(/0.(66|67|68|69|70).*/)
+ if ENV["REACT_NATIVE_VERSION"] && ENV["REACT_NATIVE_VERSION"].match(/0.(70|72).*/)
__apply_Xcode_12_5_M1_post_install_workaround(installer)
end
end
diff --git a/detox/test/ios/example.xcodeproj/project.pbxproj b/detox/test/ios/example.xcodeproj/project.pbxproj
index 1aeefcc485..f9bad7b6f6 100644
--- a/detox/test/ios/example.xcodeproj/project.pbxproj
+++ b/detox/test/ios/example.xcodeproj/project.pbxproj
@@ -7,7 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
- 0BA16EEC4E404A69C77F4E3B /* libPods-example_ci.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CD2D2A7D46E170EAE3BAD452 /* libPods-example_ci.a */; };
+ 03815B1566F43B2C8ACBCCBB /* libPods-example_ci.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6CEAF92A5314EC6824AB3DE3 /* libPods-example_ci.a */; };
13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; };
13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; };
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
@@ -28,10 +28,8 @@
39F5268E25C7429E00BA644D /* DragDropTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 39F5268C25C7429E00BA644D /* DragDropTableViewController.m */; };
4FB97BDF2636490900B7B57C /* CustomKeyboardViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4FB97BDE2636490800B7B57C /* CustomKeyboardViewController.m */; };
4FB97BE02636490900B7B57C /* CustomKeyboardViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4FB97BDE2636490800B7B57C /* CustomKeyboardViewController.m */; };
- 672CB76AF6D68D56259F08FB /* libPods-example.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D77962AECF29A300FFA668B4 /* libPods-example.a */; };
+ A6246A8AF2D035D89BA61CA6 /* libPods-example.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 86172A40F266BB07F101EB18 /* libPods-example.a */; };
CC17D3321D60A24300267B0C /* NativeModule.m in Sources */ = {isa = PBXBuildFile; fileRef = CC17D3311D60A24300267B0C /* NativeModule.m */; };
- E088C8F41F01585500CC48E9 /* CalendarManager.m in Sources */ = {isa = PBXBuildFile; fileRef = E088C8F31F01585500CC48E9 /* CalendarManager.m */; };
- E088C8F51F025DE900CC48E9 /* CalendarManager.m in Sources */ = {isa = PBXBuildFile; fileRef = E088C8F31F01585500CC48E9 /* CalendarManager.m */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -91,7 +89,6 @@
/* Begin PBXFileReference section */
008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = ""; };
- 0FCE2B17362E305AD63D208E /* Pods-example_ci.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-example_ci.debug.xcconfig"; path = "Target Support Files/Pods-example_ci/Pods-example_ci.debug.xcconfig"; sourceTree = ""; };
13B07F961A680F5B00A75B9A /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; };
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = example/AppDelegate.h; sourceTree = ""; tabWidth = 4; };
13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = example/AppDelegate.m; sourceTree = "