Repository: wix/Detox
Pull request #2646 (2021-02-17 19:32:41)

pr2pr: terser web api
# This is a PR to PR

**Description:**

Proposes another syntax for the web actions and expecations:

```diff
-        await web.expect(
-            web.getWebView(by.id("webview")).element(web.by.cssSelector("#cssSel"), 2)
-            ).toExist();

+        await expect(
+            web(by.id("webview")).element(by.cssSelector("#cssSel"), 2)
+        ).toExist();
```

and

```diff
-        await web.expect(web.getWebView().element(web.by.id("btnSave"), 2)).toExist();
+        await web.element(by.htmlId("btnSave")).tap();
```

Further details to be done.

d4vidi: I think it's much better that the `expect` becomes seamless. However, I'm not fond of how two very different type of matchers (e.g. id vs htmlid, type vs classType) all reside in the same level. It'd put too much strain on the users and is rather error-prone.

To return to my suggestion from this discussion of the PR itself, I think I may have figure out a way to improve it thanks to this PR. Please speak your opinion on this suggested API:
```js
await withWebView(({ expect, element, by }) => {
   await expect(element(by.id('testingPar')).toExist();
   
   const editable = await element(by.className('public-DraftEditor-content'));
   await editable.scrollToView();
});
```
With `expect`, `element` and `by` as arguments to the user's callback, they can enforce the actual API and significantly reduce the change of errors due to type checking.
This is the kotlin-ish way to achieve this, as I mentioned privately...

noomorph: I've given it a thought, then one more thought, and my feedback is rather vague.

1. Separate and numerous `expect` realms don't feel like the right solution provided we still want to be compatible with `chai` and `jest` expectation system in the future. The `expect` should be single for all.
2. Provided you have a complex scenario with switching back and forth between the web view and native, the syntax will look clunky:

```js
await withWebView(({ element, by }) => {
   await element(by.id('notReceivedSms')).tap();
});
await expect(element(by.id('notification')).toHaveText('Please wait until SMS');
await withWebView(({ expect, element, by }) => {
   await expect(element(by.id('code')).toExist();
   await element(by.id('code')).type('0451');
   await element(by.id('confirm')).tap();
});
await expect(element(by.id('notification')).not.toExist();
```

compared to:

```js
await web.element(by.id('notReceivedSms')).tap();
await expect(element(by.id('notification')).toHaveText('Please wait until SMS');
await expect(web.element(by.id('code')).toExist();
await web.element(by.id('code')).type('0451');
await web.element(by.id('confirm')).tap();
await expect(element(by.id('notification')).not.toExist();
```

3. > It'd put too much strain on the users and is rather error-prone.

Again, after giving it a thought, I'd rather disagree. In the scenario when you use a non-existing matcher by mistake:

```
await withWebView(({ expect, element, by }) => {
   await expect(element(by.type('input')).toExist();
});
```

what will happen? If `by` object has no `type` property, the value will be resolved to `undefined`, meaning that `element(undefined)` will throw an error, but which one? Instead of telling `Hey, you are using a wrong matcher!` we'll be saying `Dude, no idea what happened, but it seems like you call element() without arguments`, which is far from the truth.

Secondly, we can do a bit better in regards of naming:

```js
by.html.id();
by.html.class();
by.html.name();
by.html.tag();
by.html.querySelector();
by.html.xpath();
by.html.href();
by.html.hrefPartial();
```

Terse enough, and hard to confuse.



Patch:
diff --git a/detox/index.d.ts b/detox/index.d.ts
index 51ec0943b..72c09641d 100644
--- a/detox/index.d.ts
+++ b/detox/index.d.ts
@@ -276,11 +276,11 @@ declare global {
         // Detox exports all methods from detox global and all of the global constants.
         interface DetoxInstance {
             device: Device;
-            element: Element;
-            waitFor: WaitFor;
-            expect: Expect<Expect<Promise<void>>>;
-            by: Matchers;
-            web: Web;
+            element: ElementFacade;
+            waitFor: WaitForFacade;
+            expect: ExpectFacade;
+            by: ByFacade;
+            web: WebFacade;
         }
 
         interface DetoxExportWrapper extends DetoxInstance {
@@ -635,21 +635,27 @@ declare global {
             getUiDevice(): Promise<void>;
         }
 
-        type DetoxAny = Element & Actions<any> & WaitFor;
+        /**
+         * @deprecated
+         */
+        type DetoxAny = NativeElement & WaitFor;
 
-        interface Element {
-            (by: Matchers): DetoxAny;
+        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);
              */
-            atIndex(index: number): DetoxAny;
+            atIndex(index: number): NativeElement;
         }
 
-        interface Matchers {
-            (by: Matchers): Matchers;
+        interface NativeElement extends NativeElementActions {
+        }
 
+        interface ByFacade {
             /**
              * by.id will match an id that is given to the view via testID prop.
              * @example
@@ -658,53 +664,139 @@ declare global {
              * // Then match with by.id:
              * await element(by.id('tap_me'));
              */
-            id(id: string): Matchers;
+            id(id: string): NativeMatcher;
 
             /**
              * Find an element by text, useful for text fields, buttons.
              * @example await element(by.text('Tap Me'));
              */
-            text(text: string): Matchers;
+            text(text: string): NativeMatcher;
 
             /**
              * Find an element by accessibilityLabel on iOS, or by contentDescription on Android.
              * @example await element(by.label('Welcome'));
              */
-            label(label: string): Matchers;
+            label(label: string): NativeMatcher;
 
             /**
              * Find an element by native view type.
              * @example await element(by.type('RCTImageView'));
              */
-            type(nativeViewType: string): Matchers;
+            type(nativeViewType: string): NativeMatcher;
 
             /**
              * Find an element with an accessibility trait. (iOS only)
              * @example await element(by.traits(['button']));
              */
-            traits(traits: string[]): Matchers;
+            traits(traits: string[]): NativeMatcher;
+
+            /**
+             * Find an element on the DOM tree by it's id
+             * @param id
+             * @example
+             * web.element(by.htmlId('testingh1'))
+             */
+            htmlId(id: string): WebMatcher;
+
+            /**
+             * Find an element on the DOM tree by it's className
+             * @param className
+             * @example
+             * web.element(by.className('a'))
+             */
+            className(className: string): WebMatcher;
+
+            /**
+             * Find an element on the DOM tree by it's css selector
+             * @param cssSelector
+             * @example
+             * web.element(by.cssSelector('#cssSelector'))
+             */
+            cssSelector(cssSelector: string): WebMatcher;
+
+            /**
+             * Find an element on the DOM tree by it's name prop
+             * @param name
+             * @example
+             * web.element(by.name('sec_input'))
+             */
+            name(name: string): WebMatcher;
+
+            /**
+             * Find an element on the DOM tree by it's xPath
+             * @param xpath
+             * @example
+             * web.element(by.xpath('//*[@id="testingh1-1"]'))
+             */
+            xpath(xpath: string): WebMatcher;
+
+            /**
+             * Find an <a> element on the DOM tree by it's link text (href content)
+             * @param linkText
+             * @example
+             * web.element(by.linkText('disney.com'))
+             */
+            linkText(linkText: string): WebMatcher;
+
+            /**
+             * Find an <a> element on the DOM tree by it's partial link text (href content)
+             * @param partialLinkText
+             * @example
+             * web.element(by.partialLinkText('disney'))
+             */
+            partialLinkText(partialLinkText: string): WebMatcher;
+
+            /**
+             * Find an element on the DOM tree by it's tag
+             * @param tag
+             * @example
+             * web.element(by.tag('mark'))
+             */
+            tag(tag: 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: Matchers): Matchers;
-
+            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: Matchers): Matchers;
+            withDescendant(childBy: NativeMatcher): NativeMatcher;
+        }
 
+        interface WebMatcher {
+            __web__: any; // prevent type coersion
+        }
+
+        interface ExpectFacade {
+            (element: NativeElement): Expect;
+            (webElement: WebElement): WebExpect;
+        }
+
+        interface WebViewElement {
+            element(webMatcher: WebMatcher, index?: number): WebElement;
+        }
+
+        interface WebFacade extends WebViewElement {
             /**
-             * Find an element by multiple matchers
-             * @example await element(by.text('Product').and(by.id('product_name'));
+             * 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.
              */
-            and(by: Matchers): Matchers;
+            (matcher?: NativeMatcher): WebViewElement;
         }
 
-        interface Expect<R> {
-            (element: Element): Expect<Promise<void>>;
+        interface Expect<R = Promise<void>> {
 
             /**
              * Expect the view to be at least 75% visible.
@@ -716,7 +808,7 @@ declare global {
              * Negate the expectation.
              * @example await expect(element(by.id('UniqueId205'))).not.toBeVisible();
              */
-            not: Expect<Promise<void>>;
+            not: this;
 
             /**
              * Expect the view to not be visible.
@@ -770,14 +862,16 @@ declare global {
             toHaveValue(value: any): R;
         }
 
-        interface WaitFor {
+        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('UniqueId336'))).toExist().withTimeout(2000);
              */
-            (element: Element): Expect<WaitFor>;
+            (element: NativeElement): Expect<WaitFor>;
+        }
 
+        interface WaitFor {
             /**
              * Waits for the condition to be met until the specified time (millis) have elapsed.
              * @example await waitFor(element(by.id('UniqueId336'))).toExist().withTimeout(2000);
@@ -788,71 +882,72 @@ declare global {
              * Performs the action repeatedly on the element until an expectation is met
              * @example await waitFor(element(by.text('Text5'))).toBeVisible().whileElement(by.id('ScrollView630')).scroll(50, 'down');
              */
-            whileElement(by: Matchers): DetoxAny;
+            whileElement(by: NativeMatcher): NativeElement & WaitFor;
+            // TODO: not sure about & WaitFor - check if we can chain whileElement multiple times
         }
 
-        interface Actions<R> {
+        interface NativeElementActions {
             /**
              * Simulate tap on an element
              * @example await element(by.id('tappable')).tap();
              */
-            tap(): Promise<Actions<R>>;
+            tap(): Promise<void>;
 
             /**
              * Simulate long press on an element
              * @example await element(by.id('tappable')).longPress();
              */
-            longPress(): Promise<Actions<R>>;
+            longPress(): Promise<void>;
 
             /**
              * 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: Element,
-                             normalizedTargetPositionX: number, normalizedTargetPositionY: number, speed: Speed, holdDuration: number): Promise<Actions<R>>;
+            longPressAndDrag(duration: number, normalizedPositionX: number, normalizedPositionY: number, targetElement: NativeElement,
+                             normalizedTargetPositionX: number, normalizedTargetPositionY: number, speed: Speed, holdDuration: number): Promise<void>;
             /**
              * 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<Actions<R>>;
+            multiTap(times: number): Promise<void>;
 
             /**
              * 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 });
              */
-            tapAtPoint(point: { x: number; y: number }): Promise<Actions<R>>;
+            tapAtPoint(point: { x: number; y: number }): Promise<void>;
 
             /**
              * Use the builtin keyboard to type text into a text field.
              * @example await element(by.id('textField')).typeText('passcode');
              */
-            typeText(text: string): Promise<Actions<R>>;
+            typeText(text: string): Promise<void>;
 
             /**
              * Paste text into a text field.
              * @example await element(by.id('textField')).replaceText('passcode again');
              */
-            replaceText(text: string): Promise<Actions<R>>;
+            replaceText(text: string): Promise<void>;
 
             /**
              * Clear text from a text field.
              * @example await element(by.id('textField')).clearText();
              */
-            clearText(): Promise<Actions<R>>;
+            clearText(): Promise<void>;
 
             /**
              * Taps the backspace key on the built-in keyboard.
              * @example await element(by.id('textField')).tapBackspaceKey();
              */
-            tapBackspaceKey(): Promise<Actions<R>>;
+            tapBackspaceKey(): Promise<void>;
 
             /**
              * Taps the return key on the built-in keyboard.
              * @example await element(by.id('textField')).tapReturnKey();
              */
-            tapReturnKey(): Promise<Actions<R>>;
+            tapReturnKey(): Promise<void>;
 
             /**
              * Scrolls a given amount of pixels in the provided direction, starting from the provided start positions.
@@ -868,14 +963,14 @@ declare global {
               direction: Direction,
               startPositionX?: number,
               startPositionY?: number,
-            ): Promise<Actions<R>>;
+            ): Promise<void>;
 
             /**
              * Scroll to edge.
              * @example await element(by.id('scrollView')).scrollTo('bottom');
              * @example await element(by.id('scrollView')).scrollTo('top');
              */
-            scrollTo(edge: Direction): Promise<Actions<R>>;
+            scrollTo(edge: Direction): Promise<void>;
 
             /**
              * Swipes in the provided direction at the provided speed, started from percentage.
@@ -889,7 +984,7 @@ declare global {
              * @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<Actions<R>>;
+            swipe(direction: Direction, speed?: Speed, percentage?: number, normalizedStartingPointX?: number, normalizedStartingPointY?: number): Promise<void>;
 
             /**
              * Sets a picker view’s column to the given value. This function supports both date pickers and general picker views. (iOS Only)
@@ -903,7 +998,7 @@ declare global {
              * await element(by.type('UIPickerView')).setColumnToValue(1,"6");
              * await element(by.type('UIPickerView')).setColumnToValue(2,"34");
              */
-            setColumnToValue(column: number, value: string): Promise<Actions<R>>;
+            setColumnToValue(column: number, value: string): Promise<void>;
 
             /**
              * Sets the date of a date picker to a date generated from the provided string and date format. (iOS only)
@@ -913,7 +1008,7 @@ declare global {
              * await expect(element(by.id('datePicker'))).toBeVisible();
              * await element(by.id('datePicker')).setDatePickerDate('2019-02-06T05:10:00-08:00', "yyyy-MM-dd'T'HH:mm:ssZZZZZ");
              */
-            setDatePickerDate(dateString: string, dateFormat: string): Promise<Actions<R>>;
+            setDatePickerDate(dateString: string, dateFormat: string): Promise<void>;
 
             /**
              * Pinches in the given direction with speed and angle. (iOS only)
@@ -922,7 +1017,104 @@ declare global {
              * await expect(element(by.id('PinchableScrollView'))).toBeVisible();
              * await element(by.id('PinchableScrollView')).pinchWithAngle('outward', 'slow', 0);
              */
-            pinchWithAngle(direction: Direction, speed: Speed, angle: number): Promise<Actions<R>>;
+            pinchWithAngle(direction: Direction, speed: Speed, angle: number): Promise<void>;
+        }
+
+        interface WebExpect<R = Promise<void>> {
+            /**
+             * Negate the expectation.
+             * @example await expect(webview.element(by.htmlId('UniqueId205'))).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(webview.element(by.htmlId('UniqueId205'))).toHaveText('ExactText');
+             */
+            toHaveText(text: string): R
+
+            /**
+             * Expect the view to exist in the webview DOM tree.
+             * @example await expect(webview.element(by.htmlId('UniqueId205'))).toExist();
+             */
+            toExist(): R;
+        }
+
+        interface WebElement extends WebElementActions {
+
+        }
+
+        interface WebElementActions {
+            tap(): Promise<void>
+
+            /**
+             * @param text to type
+             * @param isContentEditable whether its a ContentEditable element, default is false.
+             */
+            typeText(text: string, isContentEditable: boolean): Promise<void>
+
+            /**
+             * At the moment not working on content-editable
+             * @param text to replace with the old content.
+             */
+            replaceText(text: string): Promise<void>
+
+            /**
+             * At the moment not working on content-editable
+             */
+            clearText(): Promise<void>
+
+            /**
+             * scrolling to the view, the element top position will be at the top of the screen.
+             */
+            scrollToView(): Promise<void>
+
+            /**
+             * Gets the input content
+             */
+            getText(): Promise<string>
+
+            /**
+             * Calls the focus function on the element
+             */
+            focus(): Promise<void>
+
+            /**
+             * Selects all the input content, works on ContentEditable at the moment.
+             */
+            selectAllText(): Promise<void>
+
+            /**
+             * Moves the input cursor / caret to the end of the content, works on ContentEditable at the moment.
+             */
+            moveCursorToEnd(): Promise<void>
+
+            /**
+             * Running a script on the element
+             * @param script a method that accept the element as its first arg
+             * @example function foo(element) { console.log(element); }
+             */
+            runScript(script: string): Promise<any>
+
+            /**
+             * Running a script on the element that accept args
+             * @param script a method that accept few args, and the element as the last arg.
+             * @param args a list of args to pass to the script
+             * @example function foo(a, b, c, element) { console.log(`${a}, ${b}, ${c}, ${element}`)}
+             */
+            runScriptWithArgs(script: string, args: any[]): Promise<any>;
+
+            /**
+             * Gets the current page url
+             */
+            getCurrentUrl(): Promise<string>;
+
+            /**
+             * Gets the current page title
+             */
+            getTitle(): Promise<string>;
         }
 
         type Direction = 'left' | 'right' | 'top' | 'bottom' | 'up' | 'down';
@@ -1033,183 +1225,6 @@ declare global {
             };
         }
 
-        interface Web {
-            /**
-             * 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.
-             */
-            getWebView(matcher?: Matchers): WebViewElement;
-
-            expect: WebExpect<Promise<void>>;
-            by: WebMatchers;
-        }
-
-        interface WebViewElement {
-            element(webMatcher: WebMatchers, index?: number): WebElement;
-        }
-
-        interface WebExpect<R> {
-            (webElement: WebElement): WebExpect<R>;
-
-            /**
-             * Negate the expectation.
-             * @example await web.expect(webview.element(web.by.id('UniqueId205'))).not.toExist();
-             */
-            not: WebExpect<R>;
-
-            /**
-             * Expect the element content to have the `text` supplied
-             * @param text expected to be on the element content
-             * @example
-             * await web.expect(webview.element(web.by.id('UniqueId205'))).toHaveText('ExactText');
-             */
-            toHaveText(text: string): R
-
-            /**
-             * Expect the view to exist in the webview DOM tree.
-             * @example await web.expect(webview.element(web.by.id('UniqueId205'))).toExist();
-             */
-            toExist(): R;
-        }
-
-        interface WebMatchers {
-            /**
-             * Find an element on the DOM tree by it's id
-             * @param id
-             * @example
-             * webview.element(web.by.id('testingh1'))
-             */
-            id(id: string): WebMatchers;
-
-            /**
-             * Find an element on the DOM tree by it's className
-             * @param className
-             * @example
-             * webview.element(web.by.className('a'))
-             */
-            className(className: string): WebMatchers;
-
-            /**
-             * Find an element on the DOM tree by it's css selector
-             * @param cssSelector
-             * @example
-             * webview.element(web.by.cssSelector('#cssSelector'))
-             */
-            cssSelector(cssSelector: string): WebMatchers;
-
-            /**
-             * Find an element on the DOM tree by it's name prop
-             * @param name
-             * @example
-             * webview.element(web.by.name('sec_input'))
-             */
-            name(name: string): WebMatchers;
-
-            /**
-             * Find an element on the DOM tree by it's xPath
-             * @param xpath
-             * @example
-             * webview.element(web.by.xpath('//*[@id="testingh1-1"]'))
-             */
-            xpath(xpath: string): WebMatchers;
-
-            /**
-             * Find an <a> element on the DOM tree by it's link text (href content)
-             * @param linkText
-             * @example
-             * webview.element(web.by.linkText('disney.com'))
-             */
-            linkText(linkText: string): WebMatchers;
-
-            /**
-             * Find an <a> element on the DOM tree by it's partial link text (href content)
-             * @param partialLinkText
-             * @example
-             * webview.element(web.by.partialLinkText('disney'))
-             */
-            partialLinkText(partialLinkText: string): WebMatchers;
-
-            /**
-             * Find an element on the DOM tree by it's tag
-             * @param tag
-             * @example
-             * webview.element(web.by.tag('mark'))
-             */
-            tag(tag: string): WebMatchers;
-        }
-
-        interface WebElement {
-            tap(): Promise<void>
-
-            /**
-             * @param text to type
-             * @param isContentEditable whether its a ContentEditable element, default is false.
-             */
-            typeText(text: string, isContentEditable: boolean): Promise<void>
-
-            /**
-             * At the moment not working on content-editable
-             * @param text to replace with the old content.
-             */
-            replaceText(text: string): Promise<void>
-
-            /**
-             * At the moment not working on content-editable
-             */
-            clearText(): Promise<void>
-
-            /**
-             * scrolling to the view, the element top position will be at the top of the screen.
-             */
-            scrollToView(): Promise<void>
-
-            /**
-             * Gets the input content
-             */
-            getText(): Promise<string>
-
-            /**
-             * Calls the focus function on the element
-             */
-            focus(): Promise<void>
-
-            /**
-             * Selects all the input content, works on ContentEditable at the moment.
-             */
-            selectAllText(): Promise<void>
-
-            /**
-             * Moves the input cursor / caret to the end of the content, works on ContentEditable at the moment.
-             */
-            moveCursorToEnd(): Promise<void>
-
-            /**
-             * Running a script on the element
-             * @param script a method that accept the element as its first arg
-             * @example function foo(element) { console.log(element); }
-             */
-            runScript(script: string): Promise<any>
-
-            /**
-             * Running a script on the element that accept args
-             * @param script a method that accept few args, and the element as the last arg.
-             * @param args a list of args to pass to the script
-             * @example function foo(a, b, c, element) { console.log(`${a}, ${b}, ${c}, ${element}`)}
-             */
-            runScriptWithArgs(script: string, args: any[]): Promise<any>;
-
-            /**
-             * Gets the current page url
-             */
-            getCurrentUrl(): Promise<string>;
-
-            /**
-             * Gets the current page title
-             */
-            getTitle(): Promise<string>;
-        }
     }
 }
 
diff --git a/detox/src/Detox.js b/detox/src/Detox.js
index 0c725627b..cec32bbc7 100644
--- a/detox/src/Detox.js
+++ b/detox/src/Detox.js
@@ -170,18 +170,16 @@ class Detox {
 
     await this.device.prepare();
 
-    const { matchers, webMatchers } = matchersRegistry.resolve(this.device, {
+    const matchers = matchersRegistry.resolve(this.device, {
       invocationManager,
-      deviceDriver,
+      device: this.device,
       emitter: this._eventEmitter,
     });
     Object.assign(this, matchers);
-    Object.assign(this, { web: webMatchers });
 
     if (behaviorConfig.exposeGlobals) {
       Object.assign(Detox.global, {
         ...matchers,
-        web: webMatchers,
         device: this.device,
       });
     }
diff --git a/detox/src/android/AndroidExpect.js b/detox/src/android/AndroidExpect.js
new file mode 100644
index 000000000..d180ea799
--- /dev/null
+++ b/detox/src/android/AndroidExpect.js
@@ -0,0 +1,52 @@
+const { NativeElement } = require('./core/NativeElement');
+const { NativeMatcher } = require('./core/NativeMatcher');
+const { NativeExpectElement } = require('./core/NativeExpect');
+const { NativeWaitForElement } = require('./core/NativeWaitFor');
+const { WebElement, WebViewElement } = require('./core/WebElement');
+const { WebExpectElement } = require('./core/WebExpect');
+const matchers = require('./matchers');
+
+class AndroidExpect {
+  constructor({ invocationManager, device, emitter }) {
+    this._device = device;
+    this._emitter = emitter;
+    this._invocationManager = invocationManager;
+
+    this.by = matchers;
+    this.element = this.element.bind(this);
+    this.expect = this.expect.bind(this);
+    this.waitFor = this.waitFor.bind(this);
+    this.web = this.web.bind(this);
+    this.web.element = (...args) => this.web().element(...args);
+  }
+
+  element(matcher) {
+    if (matcher instanceof NativeMatcher) {
+      return new NativeElement(this._invocationManager, this._emitter, matcher);
+    }
+
+    throw new Error(`element() argument is invalid, expected a native matcher, but got ${typeof element}`);
+  }
+
+  // Matcher can be null only if there is only one webview on the hierarchy tree.
+  web(matcher) {
+    if (matcher == null || matcher instanceof NativeMatcher) {
+      return new WebViewElement(this._invocationManager, this._device, this._emitter, matcher);
+    }
+
+    throw new Error(`web() argument is invalid, expected a native matcher, but got ${typeof element}`);
+  }
+
+  expect(element) {
+    if (element instanceof WebElement) return new WebExpectElement(this._invocationManager, element);
+    if (element instanceof NativeElement) return new NativeExpectElement(this._invocationManager, element);
+    throw new Error(`expect() argument is invalid, expected a native or web matcher, but got ${typeof element}`);
+  }
+
+  waitFor(element) {
+    if (element instanceof NativeElement) return new NativeWaitForElement(this._invocationManager, element);
+    throw new Error(`waitFor() argument is invalid, got ${typeof element}`);
+  }
+}
+
+module.exports = AndroidExpect;
diff --git a/detox/src/android/actions/native.js b/detox/src/android/actions/native.js
new file mode 100644
index 000000000..b24213c9c
--- /dev/null
+++ b/detox/src/android/actions/native.js
@@ -0,0 +1,135 @@
+const invoke = require('../../invoke');
+const DetoxActionApi = require('../espressoapi/DetoxAction');
+const ViewActionsApi = require('../espressoapi/ViewActions');
+const DetoxViewActionsApi = require('../espressoapi/DetoxViewActions');
+
+const { assertEnum, assertNormalized } = require('../../utils/assertArgument');
+const assertDirection = assertEnum(['left', 'right', 'up', 'down']);
+const assertSpeed = assertEnum(['fast', 'slow']);
+
+class Action {
+}
+
+class TapAction extends Action {
+  constructor(value) {
+    super();
+    this._call = invoke.callDirectly(value ? DetoxActionApi.tapAtLocation(value.x, value.y) : DetoxViewActionsApi.click());
+  }
+}
+
+class TapAtPointAction extends Action {
+  constructor(value) {
+    super();
+    this._call = invoke.callDirectly(DetoxActionApi.tapAtLocation(value.x, value.y));
+  }
+}
+
+class LongPressAction extends Action {
+  constructor() {
+    super();
+    this._call = invoke.callDirectly(ViewActionsApi.longClick());
+  }
+}
+
+class MultiClickAction extends Action {
+  constructor(times) {
+    super();
+    this._call = invoke.callDirectly(DetoxActionApi.multiClick(times));
+  }
+}
+
+class PressKeyAction extends Action {
+  constructor(value) {
+    super();
+    this._call = invoke.callDirectly(ViewActionsApi.pressKey(value));
+  }
+}
+
+class TypeTextAction extends Action {
+  constructor(value) {
+    super();
+    this._call = invoke.callDirectly(DetoxViewActionsApi.typeText(value));
+  }
+}
+
+class ReplaceTextAction extends Action {
+  constructor(value) {
+    super();
+    this._call = invoke.callDirectly(ViewActionsApi.replaceText(value));
+  }
+}
+
+class ClearTextAction extends Action {
+  constructor() {
+    super();
+    this._call = invoke.callDirectly(ViewActionsApi.clearText());
+  }
+}
+
+class ScrollAmountAction extends Action {
+  constructor(direction, amount, startPositionX = -1.0, startPositionY = -1.0) {
+    super();
+    this._call = invoke.callDirectly(DetoxActionApi.scrollInDirection(direction, amount, startPositionX, startPositionY));
+  }
+}
+
+class ScrollAmountStopAtEdgeAction extends Action {
+  constructor(direction, amount, startPositionX = -1.0, startPositionY = -1.0) {
+    super();
+    this._call = invoke.callDirectly(DetoxActionApi.scrollInDirectionStaleAtEdge(direction, amount, startPositionX, startPositionY));
+  }
+}
+
+class ScrollEdgeAction extends Action {
+  constructor(edge) {
+    super();
+
+    this._call = invoke.callDirectly(DetoxActionApi.scrollToEdge(edge));
+  }
+}
+
+class SwipeAction extends Action {
+  constructor(direction, speed, normalizedSwipeOffset, normalizedStartingPointX, normalizedStartingPointY) {
+    super();
+
+    assertDirection({ direction });
+    assertSpeed({ speed });
+    assertNormalized({ normalizedSwipeOffset });
+    assertNormalized({ normalizedStartingPointX });
+    assertNormalized({ normalizedStartingPointY });
+
+    this._call = invoke.callDirectly(
+      DetoxActionApi.swipeInDirection(
+        direction,
+        speed === 'fast',
+        normalizedSwipeOffset,
+        normalizedStartingPointX,
+        normalizedStartingPointY
+      )
+    );
+  }
+}
+
+class TakeElementScreenshot extends Action {
+  constructor() {
+    super();
+    this._call = invoke.callDirectly(DetoxActionApi.takeViewScreenshot());
+  }
+}
+
+module.exports = {
+  Action,
+  TapAction,
+  TapAtPointAction,
+  LongPressAction,
+  MultiClickAction,
+  PressKeyAction,
+  TypeTextAction,
+  ReplaceTextAction,
+  ClearTextAction,
+  ScrollAmountAction,
+  ScrollAmountStopAtEdgeAction,
+  ScrollEdgeAction,
+  SwipeAction,
+  TakeElementScreenshot,
+};
diff --git a/detox/src/android/actions/web.js b/detox/src/android/actions/web.js
new file mode 100644
index 000000000..e6e2e544c
--- /dev/null
+++ b/detox/src/android/actions/web.js
@@ -0,0 +1,138 @@
+const simpleAtoms = require('../espressoapi/web/simpleAtoms');
+const WebElementApi = require('../espressoapi/web/WebElement');
+const WebExpectApi = require('../espressoapi/web/WebExpect');
+
+class WebAction {
+}
+
+class WebTapAction extends WebAction {
+  constructor(element) {
+    super();
+    this._call = WebElementApi.tap(element._call);
+  }
+}
+
+class WebTypeTextAction extends WebAction {
+  constructor(element, text) {
+    super();
+    this._call = WebElementApi.typeText(element._call, text);
+  }
+}
+
+class WebReplaceTextAction extends WebAction {
+  constructor(element, text) {
+    super();
+    this._call = WebElementApi.replaceText(element._call, text);
+  }
+}
+
+class WebClearTextAction extends WebAction {
+  constructor(element) {
+    super();
+    this._call = WebElementApi.clearText(element._call);
+  }
+}
+
+class WebScrollToViewAction extends WebAction {
+  constructor(element) {
+    super();
+    this._call = WebElementApi.scrollToView(element._call);
+  }
+}
+
+class WebGetTextAction extends WebAction {
+  constructor(element) {
+    super();
+    this._call = WebElementApi.getText(element._call);
+  }
+}
+
+class WebRunScriptAction extends WebAction {
+  constructor(element, script) {
+    super();
+    this._call = WebElementApi.runScript(element._call, script);
+  }
+}
+
+class WebRunScriptWithArgsAction extends WebAction {
+  constructor(element, script, args) {
+    super();
+    this._call = WebElementApi.runScriptWithArgs(element._call, script, args);
+  }
+}
+
+class WebGetCurrentUrlAction extends WebAction {
+  constructor(element) {
+    super();
+    this._call = WebElementApi.getCurrentUrl(element._call);
+  }
+}
+
+class WebGetTitleAction extends WebAction {
+  constructor(element) {
+    super();
+    this._call = WebElementApi.getTitle(element._call);
+  }
+}
+
+class WebFocusAction extends WebAction {
+  constructor(element) {
+    super();
+    this._call = WebElementApi.runScript(element._call, simpleAtoms.focus);
+  }
+}
+
+class WebSelectAllText extends WebAction {
+  constructor(element) {
+    super();
+    this._call = WebElementApi.runScript(element._call, simpleAtoms.selectElementContents);
+  }
+}
+
+class WebMoveCursorEnd extends WebAction {
+  constructor(element) {
+    super();
+    this._call = WebElementApi.runScript(element._call, simpleAtoms.moveCursorToEnd);
+  }
+}
+
+class WebExistsAssertion extends WebAction {
+  constructor(webExpect) {
+    super();
+    if (webExpect._notCondition) {
+      this._call = WebExpectApi.toNotExist(webExpect._call);
+    } else {
+      this._call = WebExpectApi.toExist(webExpect._call);
+    }
+  }
+}
+
+class WebHasTextAssertion extends WebAction {
+  constructor(webExpect, text) {
+    super();
+    if (webExpect._notCondition) {
+      this._call = WebExpectApi.toNotHaveText(webExpect._call, text);
+    } else {
+      this._call = WebExpectApi.toHaveText(webExpect._call, text);
+    }
+  }
+}
+
+module.exports = {
+  WebAction,
+  WebTapAction,
+  WebTypeTextAction,
+  WebReplaceTextAction,
+  WebClearTextAction,
+  WebScrollToViewAction,
+  WebGetTextAction,
+  WebRunScriptAction,
+  WebRunScriptWithArgsAction,
+  WebGetCurrentUrlAction,
+  WebGetTitleAction,
+  WebFocusAction,
+  WebSelectAllText,
+  WebMoveCursorEnd,
+  WebExistsAssertion,
+  WebHasTextAssertion,
+};
diff --git a/detox/src/android/core/NativeElement.js b/detox/src/android/core/NativeElement.js
new file mode 100644
index 000000000..61e5cb9ad
--- /dev/null
+++ b/detox/src/android/core/NativeElement.js
@@ -0,0 +1,112 @@
+const fs = require('fs-extra');
+const path = require('path');
+const tempfile = require('tempfile');
+const invoke = require('../../invoke');
+const DetoxMatcherApi = require('../espressoapi/DetoxMatcher');
+const { ActionInteraction } = require('../interactions/native')
+const actions = require('../actions/native')
+
+class NativeElement {
+  constructor(invocationManager, emitter, matcher) {
+    this._invocationManager = invocationManager;
+    this._emitter = emitter;
+    this._originalMatcher = matcher;
+    this._selectElementWithMatcher(this._originalMatcher);
+  }
+
+  _selectElementWithMatcher(matcher) {
+    // if (!(matcher instanceof NativeMatcher)) throw new Error(`Element _selectElementWithMatcher argument must be a valid NativeMatcher, got ${typeof matcher}`);
+    this._call = invoke.call(invoke.Espresso, 'onView', matcher._call);
+  }
+
+  atIndex(index) {
+    if (typeof index !== 'number') throw new Error(`Element atIndex argument must be a number, got ${typeof index}`);
+    const matcher = this._originalMatcher;
+    this._originalMatcher._call = invoke.callDirectly(DetoxMatcherApi.matcherForAtIndex(index, matcher._call.value));
+
+    this._selectElementWithMatcher(this._originalMatcher);
+    return this;
+  }
+
+  async tap(value) {
+    return await new ActionInteraction(this._invocationManager, this, new actions.TapAction(value)).execute();
+  }
+
+  async tapAtPoint(value) {
+    return await new ActionInteraction(this._invocationManager, this, new actions.TapAtPointAction(value)).execute();
+  }
+
+  async longPress() {
+    return await new ActionInteraction(this._invocationManager, this, new actions.LongPressAction()).execute();
+  }
+
+  async multiTap(times) {
+    return await new ActionInteraction(this._invocationManager, this, new actions.MultiClickAction(times)).execute();
+  }
+
+  async tapBackspaceKey() {
+    return await new ActionInteraction(this._invocationManager, this, new actions.PressKeyAction(67)).execute();
+  }
+
+  async tapReturnKey() {
+    return await new ActionInteraction(this._invocationManager, this, new actions.TypeTextAction('\n')).execute();
+  }
+
+  async typeText(value) {
+    return await new ActionInteraction(this._invocationManager, this, new actions.TypeTextAction(value)).execute();
+  }
+
+  async replaceText(value) {
+    return await new ActionInteraction(this._invocationManager, this, new actions.ReplaceTextAction(value)).execute();
+  }
+
+  async clearText() {
+    return await new ActionInteraction(this._invocationManager, this, new actions.ClearTextAction()).execute();
+  }
+
+  async scroll(amount, direction = 'down', startPositionX, startPositionY) {
+    // override the user's element selection with an extended matcher that looks for UIScrollView children
+    // this._selectElementWithMatcher(this._originalMatcher._extendToDescendantScrollViews());
+    return await new ActionInteraction(this._invocationManager, this, new actions.ScrollAmountAction(direction, amount, startPositionX, startPositionY)).execute();
+  }
+
+  async scrollTo(edge) {
+    // override the user's element selection with an extended matcher that looks for UIScrollView children
+    this._selectElementWithMatcher(this._originalMatcher._extendToDescendantScrollViews());
+    return await new ActionInteraction(this._invocationManager, this, new actions.ScrollEdgeAction(edge)).execute();
+  }
+
+  /**
+   * @param {'up' | 'right' | 'down' | 'left'} direction
+   * @param {'slow' | 'fast'} [speed]
+   * @param {number} [normalizedSwipeOffset] - swipe amount relative to the screen width/height
+   * @param {number} [normalizedStartingPointX] - X coordinate of swipe starting point, relative to the view width
+   * @param {number} [normalizedStartingPointY] - Y coordinate of swipe starting point, relative to the view height
+   */
+  async swipe(direction, speed = 'fast', normalizedSwipeOffset = NaN, normalizedStartingPointX = NaN, normalizedStartingPointY = NaN) {
+    normalizedSwipeOffset = Number.isNaN(normalizedSwipeOffset) ? 0.75 : normalizedSwipeOffset;
+
+    // override the user's element selection with an extended matcher that avoids RN issues with RCTScrollView
+    this._selectElementWithMatcher(this._originalMatcher._avoidProblematicReactNativeElements());
+    const action = new actions.SwipeAction(direction, speed, normalizedSwipeOffset, normalizedStartingPointX, normalizedStartingPointY);
+    return await new ActionInteraction(this._invocationManager, this, action).execute();
+  }
+
+  async takeScreenshot(screenshotName) {
+    // TODO this should be moved to a lower-layer handler of this use-case
+    const resultBase64 = await new ActionInteraction(this._invocationManager, this, new actions.TakeElementScreenshot()).execute();
+    const filePath = tempfile('detox.element-screenshot.png');
+    await fs.writeFile(filePath, resultBase64, 'base64');
+
+    await this._emitter.emit('createExternalArtifact', {
+      pluginId: 'screenshot',
+      artifactName: screenshotName || path.basename(filePath, '.png'),
+      artifactPath: filePath,
+    });
+    return filePath;
+  }
+}
+
+module.exports = {
+  NativeElement,
+};
diff --git a/detox/src/android/core/NativeExpect.js b/detox/src/android/core/NativeExpect.js
new file mode 100644
index 000000000..083600681
--- /dev/null
+++ b/detox/src/android/core/NativeExpect.js
@@ -0,0 +1,77 @@
+const { MatcherAssertionInteraction } = require('../interactions/native')
+const matchers = require('../matchers/native')
+
+class NativeExpect {
+  constructor(invocationManager) {
+    this._invocationManager = invocationManager;
+  }
+
+  get not() {
+    this._notCondition = true;
+    return this;
+  }
+}
+
+class NativeExpectElement extends NativeExpect {
+  constructor(invocationManager, element) {
+    super(invocationManager);
+    this._element = element;
+  }
+
+  async toBeVisible() {
+    return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.VisibleMatcher().not : new matchers.VisibleMatcher()).execute();
+  }
+
+  async toBeNotVisible() {
+    return await this.not.toBeVisible();
+  }
+
+  async toExist() {
+    return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.ExistsMatcher().not : new matchers.ExistsMatcher()).execute();
+  }
+
+  async toNotExist() {
+    return await this.not.toExist();
+  }
+
+  async toHaveText(text) {
+    return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.TextMatcher(text).not : new matchers.TextMatcher(text)).execute();
+  }
+
+  async toNotHaveText(text) {
+    return await this.not.toHaveText(text);
+  }
+
+  async toHaveLabel(value) {
+    return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.LabelMatcher(value).not : new matchers.LabelMatcher(value)).execute();
+  }
+
+  async toNotHaveLabel(value) {
+    return await this.not.toHaveLabel(value);
+  }
+
+  async toHaveId(value) {
+    return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.IdMatcher(value).not : new matchers.IdMatcher(value)).execute();
+  }
+
+  async toNotHaveId(value) {
+    return await this.not.toHaveId(value);
+  }
+
+  async toHaveValue(value) {
+    return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.ValueMatcher(value).not : new matchers.ValueMatcher(value)).execute();
+  }
+
+  async toNotHaveValue(value) {
+    return await this.not.toHaveValue(value);
+  }
+
+  async toHaveToggleValue(value) {
+    return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.ToggleMatcher(value).not : new matchers.ToggleMatcher(value)).execute();
+  }
+}
+
+module.exports = {
+  NativeExpect,
+  NativeExpectElement,
+};
diff --git a/detox/src/android/core/NativeMatcher.js b/detox/src/android/core/NativeMatcher.js
new file mode 100644
index 000000000..5c25f4504
--- /dev/null
+++ b/detox/src/android/core/NativeMatcher.js
@@ -0,0 +1,48 @@
+const invoke = require('../../invoke');
+const DetoxMatcherApi = require('../espressoapi/DetoxMatcher');
+
+class NativeMatcher {
+  withAncestor(matcher) {
+    this._call = invoke.callDirectly(DetoxMatcherApi.matcherWithAncestor(this, matcher));
+    return this;
+  }
+
+  withDescendant(matcher) {
+    this._call = invoke.callDirectly(DetoxMatcherApi.matcherWithDescendant(this, matcher));
+    return this;
+  }
+
+  and(matcher) {
+    this._call = invoke.callDirectly(DetoxMatcherApi.matcherForAnd(this, matcher));
+    return this;
+  }
+
+  or(matcher) {
+    this._call = invoke.callDirectly(DetoxMatcherApi.matcherForOr(this, matcher));
+    return this;
+  }
+
+  get not() {
+    this._call = invoke.callDirectly(DetoxMatcherApi.matcherForNot(this));
+    return this;
+  }
+
+  _avoidProblematicReactNativeElements() {
+    /*
+    const _originalMatcherCall = this._call;
+    this._call = invoke.call(invoke.IOS.Class('GREYMatchers'), 'detoxMatcherAvoidingProblematicReactNativeElements:', _originalMatcherCall);
+    */
+    return this;
+  }
+  _extendToDescendantScrollViews() {
+    /*
+    const _originalMatcherCall = this._call;
+    this._call = invoke.call(invoke.IOS.Class('GREYMatchers'), 'detoxMatcherForScrollChildOfMatcher:', _originalMatcherCall);
+    */
+    return this;
+  }
+}
+
+module.exports = {
+  NativeMatcher,
+};
diff --git a/detox/src/android/core/NativeWaitFor.js b/detox/src/android/core/NativeWaitFor.js
new file mode 100644
index 000000000..7008ad7a4
--- /dev/null
+++ b/detox/src/android/core/NativeWaitFor.js
@@ -0,0 +1,73 @@
+const { WaitForInteraction } = require('../interactions/native')
+const matchers = require('../matchers/native')
+
+class NativeWaitFor {
+  constructor(invocationManager) {
+    this._invocationManager = invocationManager;
+  }
+}
+
+class NativeWaitForElement extends NativeWaitFor {
+  constructor(invocationManager, element) {
+    super(invocationManager);
+    this._element = element;
+  }
+
+  get not() {
+    this._notCondition = true;
+    return this;
+  }
+
+  toBeVisible() {
+    return new WaitForInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.VisibleMatcher().not : new matchers.VisibleMatcher());
+  }
+
+  toBeNotVisible() {
+    return this.not.toBeVisible();
+  }
+
+  toExist() {
+    return new WaitForInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.ExistsMatcher().not : new matchers.ExistsMatcher());
+  }
+
+  toNotExist() {
+    return this.not.toExist();
+  }
+
+  toHaveText(text) {
+    return new WaitForInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.TextMatcher(text).not : new matchers.TextMatcher(text));
+  }
+
+  toNotHaveText(text) {
+    return this.not.toHaveText(text);
+  }
+
+  toHaveLabel(value) {
+    return new WaitForInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.LabelMatcher(value).not : new matchers.LabelMatcher(value));
+  }
+
+  toNotHaveLabel(value) {
+    return this.not.toHaveLabel(value);
+  }
+
+  toHaveId(value) {
+    return new WaitForInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.IdMatcher(value).not : new matchers.IdMatcher(value));
+  }
+
+  toNotHaveId(value) {
+    return this.not.toHaveId(value);
+  }
+
+  toHaveValue(value) {
+    return new WaitForInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.ValueMatcher(value).not : new matchers.ValueMatcher(value));
+  }
+
+  toNotHaveValue(value) {
+    return this.not.toHaveValue(value);
+  }
+}
+
+module.exports = {
+  NativeWaitFor,
+  NativeWaitForElement,
+};
diff --git a/detox/src/android/core/WebElement.js b/detox/src/android/core/WebElement.js
new file mode 100644
index 000000000..5fbdc0a72
--- /dev/null
+++ b/detox/src/android/core/WebElement.js
@@ -0,0 +1,102 @@
+const invoke = require('../../invoke');
+const EspressoWebDetoxApi = require('../espressoapi/web/EspressoWebDetox');
+const WebViewElementApi = require('../espressoapi/web/WebViewElement');
+const actions = require('../actions/web');
+const { ActionInteraction } = require('../interactions/web');
+const { WebMatcher } = require('./WebMatcher');
+
+class WebElement {
+  constructor(invocationManager, device, webViewElement, matcher, index) {
+    this._invocationManager = invocationManager;
+    this._device = device;
+    this._call = invoke.callDirectly(WebViewElementApi.element(webViewElement._call, matcher._call.value, index));
+  }
+
+  // At the moment not working on content-editable
+  async tap() {
+    return await new ActionInteraction(this._invocationManager, new actions.WebTapAction(this)).execute();
+  }
+
+  async typeText(text, isContentEditable = false) {
+    if (isContentEditable) {
+      return await this._device.typeText(text);
+    }
+    return await new ActionInteraction(this._invocationManager,  new actions.WebTypeTextAction(this, text)).execute();
+  }
+
+  // At the moment not working on content-editable
+  async replaceText(text) {
+    return await new ActionInteraction(this._invocationManager,  new actions.WebReplaceTextAction(this, text)).execute();
+  }
+
+  // At the moment not working on content-editable
+  async clearText() {
+    return await new ActionInteraction(this._invocationManager,  new actions.WebClearTextAction(this)).execute();
+  }
+
+  async scrollToView() {
+    return await new ActionInteraction(this._invocationManager,  new actions.WebScrollToViewAction(this)).execute();
+  }
+
+  async getText() {
+    return await new ActionInteraction(this._invocationManager,  new actions.WebGetTextAction(this)).execute();
+  }
+
+  async focus() {
+    return await new ActionInteraction(this._invocationManager, new actions.WebFocusAction(this)).execute();
+  }
+
+  async selectAllText() {
+    return await new ActionInteraction(this._invocationManager, new actions.WebSelectAllText(this)).execute();
+  }
+
+  async moveCursorToEnd() {
+    return await new ActionInteraction(this._invocationManager, new actions.WebMoveCursorEnd(this)).execute();
+  }
+
+  async runScript(script) {
+    return await new ActionInteraction(this._invocationManager, new actions.WebRunScriptAction(this, script)).execute();
+  }
+
+  async runScriptWithArgs(script, args) {
+    return await new ActionInteraction(this._invocationManager, new actions.WebRunScriptWithArgsAction(this, script, args)).execute();
+  }
+
+  async getCurrentUrl() {
+    return await new ActionInteraction(this._invocationManager, new actions.WebGetCurrentUrlAction(this)).execute();
+  }
+
+  async getTitle() {
+    return await new ActionInteraction(this._invocationManager, new actions.WebGetTitleAction(this)).execute();
+  }
+}
+
+class WebViewElement {
+  constructor(invocationManager, device, emitter, matcher) {
+    this._invocationManager = invocationManager;
+    this._device = device;
+    this._emitter = emitter;
+    this._matcher = matcher;
+
+    if (matcher !== undefined) {
+      this._call = invoke.callDirectly(EspressoWebDetoxApi.getWebView(matcher._call.value));
+    } else {
+      this._call = invoke.callDirectly(EspressoWebDetoxApi.getWebView());
+    }
+
+    this.element = this.element.bind(this);
+  }
+
+  element(webMatcher, index = 0) {
+    if (webMatcher instanceof WebMatcher) {
+      return new WebElement(this._invocationManager, this._device, this, webMatcher, index);
+    }
+
+    throw new Error(`element() argument is invalid, expected a web matcher, but got ${typeof element}`);
+  }
+}
+
+module.exports = {
+  WebElement,
+  WebViewElement,
+};
diff --git a/detox/src/android/core/WebExpect.js b/detox/src/android/core/WebExpect.js
new file mode 100644
index 000000000..89d5deeb8
--- /dev/null
+++ b/detox/src/android/core/WebExpect.js
@@ -0,0 +1,36 @@
+const invoke = require('../../invoke');
+const EspressoWebDetoxApi = require('../espressoapi/web/EspressoWebDetox');
+const { WebExistsAssertion, WebHasTextAssertion } = require('../actions/web');
+const { WebAssertionInteraction } = require('../interactions/web');
+
+class WebExpect {
+  constructor(invocationManager) {
+    this._invocationManager = invocationManager
+    this._notCondition = false;
+  }
+
+  get not() {
+    this._notCondition = true;
+    return this;
+  }
+}
+
+class WebExpectElement extends WebExpect {
+  constructor(invocationManager, webElement) {
+    super(invocationManager)
+    this._call = invoke.callDirectly(EspressoWebDetoxApi.expect(webElement._call.value));
+  }
+
+  async toHaveText(text) {
+    return await new WebAssertionInteraction(this._invocationManager, new WebHasTextAssertion(this, text)).execute();
+  }
+
+  async toExist() {
+    return await new WebAssertionInteraction(this._invocationManager, new WebExistsAssertion(this)).execute();
+  }
+}
+
+module.exports = {
+  WebExpect,
+  WebExpectElement,
+};
diff --git a/detox/src/android/core/WebMatcher.js b/detox/src/android/core/WebMatcher.js
new file mode 100644
index 000000000..03dca2013
--- /dev/null
+++ b/detox/src/android/core/WebMatcher.js
@@ -0,0 +1,7 @@
+class WebMatcher {
+
+}
+
+module.exports = {
+  WebMatcher,
+};
diff --git a/detox/src/android/expect.js b/detox/src/android/expect.js
deleted file mode 100644
index 0e0b8b1bd..000000000
--- a/detox/src/android/expect.js
+++ /dev/null
@@ -1,494 +0,0 @@
-const fs = require('fs-extra');
-const path = require('path');
-const tempfile = require('tempfile');
-const invoke = require('../invoke');
-const DetoxActionApi = require('./espressoapi/DetoxAction');
-const ViewActionsApi = require('./espressoapi/ViewActions');
-const DetoxViewActionsApi = require('./espressoapi/DetoxViewActions');
-const DetoxAssertionApi = require('./espressoapi/DetoxAssertion');
-const EspressoDetoxApi = require('./espressoapi/EspressoDetox');
-const DetoxMatcherApi = require('./espressoapi/DetoxMatcher');
-const {
-  Matcher,
-  LabelMatcher,
-  IdMatcher,
-  TypeMatcher,
-  TraitsMatcher,
-  VisibleMatcher,
-  ExistsMatcher,
-  TextMatcher,
-  ValueMatcher,
-  ToggleMatcher,
-} = require('./matcher');
-const { assertEnum, assertNormalized, assertNumber } = require('../utils/assertArgument');
-const assertDirection = assertEnum(['left', 'right', 'up', 'down']);
-const assertSpeed = assertEnum(['fast', 'slow']);
-
-function call(maybeAFunction) {
-  return maybeAFunction instanceof Function ? maybeAFunction() : maybeAFunction;
-}
-
-class Action {
-}
-
-class TapAction extends Action {
-  constructor(value) {
-    super();
-    this._call = invoke.callDirectly(value ? DetoxActionApi.tapAtLocation(value.x, value.y) : DetoxViewActionsApi.click());
-  }
-}
-
-class TapAtPointAction extends Action {
-  constructor(value) {
-    super();
-    this._call = invoke.callDirectly(DetoxActionApi.tapAtLocation(value.x, value.y));
-  }
-}
-
-class LongPressAction extends Action {
-  constructor() {
-    super();
-    this._call = invoke.callDirectly(ViewActionsApi.longClick());
-  }
-}
-
-class MultiClickAction extends Action {
-  constructor(times) {
-    super();
-    this._call = invoke.callDirectly(DetoxActionApi.multiClick(times));
-  }
-}
-
-class PressKeyAction extends Action {
-  constructor(value) {
-    super();
-    this._call = invoke.callDirectly(ViewActionsApi.pressKey(value));
-  }
-}
-
-class TypeTextAction extends Action {
-  constructor(value) {
-    super();
-    this._call = invoke.callDirectly(DetoxViewActionsApi.typeText(value));
-  }
-}
-
-class ReplaceTextAction extends Action {
-  constructor(value) {
-    super();
-    this._call = invoke.callDirectly(ViewActionsApi.replaceText(value));
-  }
-}
-
-class ClearTextAction extends Action {
-  constructor() {
-    super();
-    this._call = invoke.callDirectly(ViewActionsApi.clearText());
-  }
-}
-
-class ScrollAmountAction extends Action {
-  constructor(direction, amount, startPositionX = -1.0, startPositionY = -1.0) {
-    super();
-    this._call = invoke.callDirectly(DetoxActionApi.scrollInDirection(direction, amount, startPositionX, startPositionY));
-  }
-}
-
-class ScrollAmountStopAtEdgeAction extends Action {
-  constructor(direction, amount, startPositionX = -1.0, startPositionY = -1.0) {
-    super();
-    this._call = invoke.callDirectly(DetoxActionApi.scrollInDirectionStaleAtEdge(direction, amount, startPositionX, startPositionY));
-  }
-}
-
-class ScrollEdgeAction extends Action {
-  constructor(edge) {
-    super();
-
-    this._call = invoke.callDirectly(DetoxActionApi.scrollToEdge(edge));
-  }
-}
-
-class SwipeAction extends Action {
-  constructor(direction, speed, normalizedSwipeOffset, normalizedStartingPointX, normalizedStartingPointY) {
-    super();
-
-    assertDirection({ direction });
-    assertSpeed({ speed });
-    assertNormalized({ normalizedSwipeOffset });
-    assertNormalized({ normalizedStartingPointX });
-    assertNormalized({ normalizedStartingPointY });
-
-    this._call = invoke.callDirectly(
-      DetoxActionApi.swipeInDirection(
-        direction,
-        speed === 'fast',
-        normalizedSwipeOffset,
-        normalizedStartingPointX,
-        normalizedStartingPointY
-      )
-    );
-  }
-}
-
-class TakeElementScreenshot extends Action {
-  constructor() {
-    super();
-    this._call = invoke.callDirectly(DetoxActionApi.takeViewScreenshot());
-  }
-}
-
-class Interaction {
-  constructor(invocationManager) {
-    this._call = undefined;
-    this._invocationManager = invocationManager;
-  }
-
-  async execute() {
-    const resultObj = await this._invocationManager.execute(this._call);
-    return resultObj ? resultObj.result : undefined;
-  }
-}
-
-class ActionInteraction extends Interaction {
-  constructor(invocationManager, element, action) {
-    super(invocationManager);
-    this._call = EspressoDetoxApi.perform(call(element._call), action._call);
-    // TODO: move this.execute() here from the caller
-  }
-}
-
-class MatcherAssertionInteraction extends Interaction {
-  constructor(invocationManager, element, matcher) {
-    super(invocationManager);
-    this._call = DetoxAssertionApi.assertMatcher(call(element._call), matcher._call.value);
-    // TODO: move this.execute() here from the caller
-  }
-}
-
-class WaitForInteraction extends Interaction {
-  constructor(invocationManager, element, assertionMatcher) {
-    super(invocationManager);
-    this._element = element;
-    this._assertionMatcher = assertionMatcher;
-    this._element._selectElementWithMatcher(this._element._originalMatcher);
-  }
-
-  async withTimeout(timeout) {
-    if (typeof timeout !== 'number') throw new Error(`WaitForInteraction withTimeout argument must be a number, got ${typeof timeout}`);
-    if (timeout < 0) throw new Error('timeout must be larger than 0');
-
-    this._call = DetoxAssertionApi.waitForAssertMatcher(call(this._element._call), this._assertionMatcher._call.value, timeout / 1000);
-    await this.execute();
-  }
-
-  whileElement(searchMatcher) {
-    return new WaitForActionInteraction(this._invocationManager, this._element, this._assertionMatcher, searchMatcher);
-  }
-}
-
-class WaitForActionInteractionBase extends Interaction {
-  constructor(invocationManager, element, matcher, searchMatcher) {
-    super(invocationManager);
-    //if (!(element instanceof Element)) throw new Error(`WaitForActionInteraction ctor 1st argument must be a valid Element, got ${typeof element}`);
-    //if (!(matcher instanceof Matcher)) throw new Error(`WaitForActionInteraction ctor 2nd argument must be a valid Matcher, got ${typeof matcher}`);
-    if (!(searchMatcher instanceof Matcher))
-      throw new Error(`WaitForActionInteraction ctor 3rd argument must be a valid Matcher, got ${typeof searchMatcher}`);
-    this._element = element;
-    this._originalMatcher = matcher;
-    this._searchMatcher = searchMatcher;
-  }
-
-  _prepare(searchAction) {
-    //if (!searchAction instanceof Action) throw new Error(`WaitForActionInteraction _execute argument must be a valid Action, got ${typeof searchAction}`);
-
-    this._call = DetoxAssertionApi.waitForAssertMatcherWithSearchAction(
-      call(this._element._call),
-      call(this._originalMatcher._call).value,
-      call(searchAction._call),
-      call(this._searchMatcher._call).value
-    );
-  }
-}
-
-class WaitForActionInteraction extends WaitForActionInteractionBase {
-  async scroll(amount, direction = 'down', scrollPositionX, scrollPositionY) {
-    this._prepare(new ScrollAmountStopAtEdgeAction(direction, amount, scrollPositionX, scrollPositionY));
-    await this.execute();
-  }
-}
-
-class Element {
-  constructor(invocationManager, emitter, matcher) {
-    this._invocationManager = invocationManager;
-    this._emitter = emitter;
-    this._originalMatcher = matcher;
-    this._selectElementWithMatcher(this._originalMatcher);
-  }
-
-  _selectElementWithMatcher(matcher) {
-    // if (!(matcher instanceof Matcher)) throw new Error(`Element _selectElementWithMatcher argument must be a valid Matcher, got ${typeof matcher}`);
-    this._call = invoke.call(invoke.Espresso, 'onView', matcher._call);
-  }
-
-  atIndex(index) {
-    if (typeof index !== 'number') throw new Error(`Element atIndex argument must be a number, got ${typeof index}`);
-    const matcher = this._originalMatcher;
-    this._originalMatcher._call = invoke.callDirectly(DetoxMatcherApi.matcherForAtIndex(index, matcher._call.value));
-
-    this._selectElementWithMatcher(this._originalMatcher);
-    return this;
-  }
-
-  async tap(value) {
-    return await new ActionInteraction(this._invocationManager, this, new TapAction(value)).execute();
-  }
-
-  async tapAtPoint(value) {
-    return await new ActionInteraction(this._invocationManager, this, new TapAtPointAction(value)).execute();
-  }
-
-  async longPress() {
-    return await new ActionInteraction(this._invocationManager, this, new LongPressAction()).execute();
-  }
-
-  async multiTap(times) {
-    return await new ActionInteraction(this._invocationManager, this, new MultiClickAction(times)).execute();
-  }
-
-  async tapBackspaceKey() {
-    return await new ActionInteraction(this._invocationManager, this, new PressKeyAction(67)).execute();
-  }
-
-  async tapReturnKey() {
-    return await new ActionInteraction(this._invocationManager, this, new TypeTextAction('\n')).execute();
-  }
-
-  async typeText(value) {
-    return await new ActionInteraction(this._invocationManager, this, new TypeTextAction(value)).execute();
-  }
-
-  async replaceText(value) {
-    return await new ActionInteraction(this._invocationManager, this, new ReplaceTextAction(value)).execute();
-  }
-
-  async clearText() {
-    return await new ActionInteraction(this._invocationManager, this, new ClearTextAction()).execute();
-  }
-
-  async scroll(amount, direction = 'down', startPositionX, startPositionY) {
-    // override the user's element selection with an extended matcher that looks for UIScrollView children
-    // this._selectElementWithMatcher(this._originalMatcher._extendToDescendantScrollViews());
-    return await new ActionInteraction(this._invocationManager, this, new ScrollAmountAction(direction, amount, startPositionX, startPositionY)).execute();
-  }
-
-  async scrollTo(edge) {
-    // override the user's element selection with an extended matcher that looks for UIScrollView children
-    this._selectElementWithMatcher(this._originalMatcher._extendToDescendantScrollViews());
-    return await new ActionInteraction(this._invocationManager, this, new ScrollEdgeAction(edge)).execute();
-  }
-
-  /**
-   * @param {'up' | 'right' | 'down' | 'left'} direction
-   * @param {'slow' | 'fast'} [speed]
-   * @param {number} [normalizedSwipeOffset] - swipe amount relative to the screen width/height
-   * @param {number} [normalizedStartingPointX] - X coordinate of swipe starting point, relative to the view width
-   * @param {number} [normalizedStartingPointY] - Y coordinate of swipe starting point, relative to the view height
-   */
-  async swipe(direction, speed = 'fast', normalizedSwipeOffset = NaN, normalizedStartingPointX = NaN, normalizedStartingPointY = NaN) {
-    normalizedSwipeOffset = Number.isNaN(normalizedSwipeOffset) ? 0.75 : normalizedSwipeOffset;
-
-    // override the user's element selection with an extended matcher that avoids RN issues with RCTScrollView
-    this._selectElementWithMatcher(this._originalMatcher._avoidProblematicReactNativeElements());
-    const action = new SwipeAction(direction, speed, normalizedSwipeOffset, normalizedStartingPointX, normalizedStartingPointY);
-    return await new ActionInteraction(this._invocationManager, this, action).execute();
-  }
-
-  async takeScreenshot(screenshotName) {
-    // TODO this should be moved to a lower-layer handler of this use-case
-    const resultBase64 = await new ActionInteraction(this._invocationManager, this, new TakeElementScreenshot()).execute();
-    const filePath = tempfile('detox.element-screenshot.png');
-    await fs.writeFile(filePath, resultBase64, 'base64');
-
-    await this._emitter.emit('createExternalArtifact', {
-      pluginId: 'screenshot',
-      artifactName: screenshotName || path.basename(filePath, '.png'),
-      artifactPath: filePath,
-    });
-    return filePath;
-  }
-}
-
-class Expect {
-  constructor(invocationManager) {
-    this._invocationManager = invocationManager;
-  }
-
-  get not() {
-    this._notCondition = true;
-    return this;
-  }
-}
-
-class ExpectElement extends Expect {
-  constructor(invocationManager, element) {
-    super(invocationManager);
-    this._element = element;
-  }
-
-  async toBeVisible() {
-    return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new VisibleMatcher().not : new VisibleMatcher()).execute();
-  }
-
-  async toBeNotVisible() {
-    return await this.not.toBeVisible();
-  }
-
-  async toExist() {
-    return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new ExistsMatcher().not : new ExistsMatcher()).execute();
-  }
-
-  async toNotExist() {
-    return await this.not.toExist();
-  }
-
-  async toHaveText(text) {
-    return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new TextMatcher(text).not : new TextMatcher(text)).execute();
-  }
-
-  async toNotHaveText(text) {
-    return await this.not.toHaveText(text);
-  }
-
-  async toHaveLabel(value) {
-    return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new LabelMatcher(value).not : new LabelMatcher(value)).execute();
-  }
-
-  async toNotHaveLabel(value) {
-    return await this.not.toHaveLabel(value);
-  }
-
-  async toHaveId(value) {
-    return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new IdMatcher(value).not : new IdMatcher(value)).execute();
-  }
-
-  async toNotHaveId(value) {
-    return await this.not.toHaveId(value);
-  }
-
-  async toHaveValue(value) {
-    return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new ValueMatcher(value).not : new ValueMatcher(value)).execute();
-  }
-
-  async toNotHaveValue(value) {
-    return await this.not.toHaveValue(value);
-  }
-
-  async toHaveToggleValue(value) {
-    return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new ToggleMatcher(value).not : new ToggleMatcher(value)).execute()
-  }
-}
-
-class WaitFor {
-  constructor(invocationManager) {
-    this._invocationManager = invocationManager;
-  }
-}
-
-class WaitForElement extends WaitFor {
-  constructor(invocationManager, element) {
-    super(invocationManager);
-    this._element = element;
-  }
-
-  get not() {
-    this._notCondition = true;
-    return this;
-  }
-
-  toBeVisible() {
-    return new WaitForInteraction(this._invocationManager, this._element, this._notCondition ? new VisibleMatcher().not : new VisibleMatcher());
-  }
-
-  toBeNotVisible() {
-    return this.not.toBeVisible();
-  }
-
-  toExist() {
-    return new WaitForInteraction(this._invocationManager, this._element, this._notCondition ? new ExistsMatcher().not : new ExistsMatcher());
-  }
-
-  toNotExist() {
-    return this.not.toExist();
-  }
-
-  toHaveText(text) {
-    return new WaitForInteraction(this._invocationManager, this._element, this._notCondition ? new TextMatcher(text).not : new TextMatcher(text));
-  }
-
-  toNotHaveText(text) {
-    return this.not.toHaveText(text);
-  }
-
-  toHaveLabel(value) {
-    return new WaitForInteraction(this._invocationManager, this._element, this._notCondition ? new LabelMatcher(value).not : new LabelMatcher(value));
-  }
-
-  toNotHaveLabel(value) {
-    return this.not.toHaveLabel(value);
-  }
-
-  toHaveId(value) {
-    return new WaitForInteraction(this._invocationManager, this._element, this._notCondition ? new IdMatcher(value).not : new IdMatcher(value));
-  }
-
-  toNotHaveId(value) {
-    return this.not.toHaveId(value);
-  }
-
-  toHaveValue(value) {
-    return new WaitForInteraction(this._invocationManager, this._element, this._notCondition ? new ValueMatcher(value).not : new ValueMatcher(value));
-  }
-
-  toNotHaveValue(value) {
-    return this.not.toHaveValue(value);
-  }
-}
-
-class AndroidExpect {
-  constructor({ invocationManager, emitter }) {
-    this._invocationManager = invocationManager;
-    this._emitter = emitter;
-
-    this.by = {
-      accessibilityLabel: value => new LabelMatcher(value),
-      label: value => new LabelMatcher(value),
-      id: value => new IdMatcher(value),
-      type: value => new TypeMatcher(value),
-      traits: value => new TraitsMatcher(value),
-      value: value => new ValueMatcher(value),
-      text: value => new TextMatcher(value)
-    };
-
-    this.element = this.element.bind(this);
-    this.expect = this.expect.bind(this);
-    this.waitFor = this.waitFor.bind(this);
-  }
-
-
-  element(matcher) {
-    return new Element(this._invocationManager, this._emitter, matcher);
-  }
-
-  expect(element) {
-    if (element instanceof Element) return new ExpectElement(this._invocationManager, element);
-    throw new Error(`expect() argument is invalid, got ${typeof element}`);
-  }
-
-  waitFor(element) {
-    if (element instanceof Element) return new WaitForElement(this._invocationManager, element);
-    throw new Error(`waitFor() argument is invalid, got ${typeof element}`);
-  }
-}
-
-module.exports = AndroidExpect;
diff --git a/detox/src/android/interactions/native.js b/detox/src/android/interactions/native.js
new file mode 100644
index 000000000..edd8e3c44
--- /dev/null
+++ b/detox/src/android/interactions/native.js
@@ -0,0 +1,97 @@
+const DetoxAssertionApi = require('../espressoapi/DetoxAssertion');
+const EspressoDetoxApi = require('../espressoapi/EspressoDetox');
+const { NativeMatcher } = require('../core/NativeMatcher');
+const { ScrollAmountStopAtEdgeAction } = require('../actions/native');
+
+function call(maybeAFunction) {
+  return maybeAFunction instanceof Function ? maybeAFunction() : maybeAFunction;
+}
+
+class Interaction {
+  constructor(invocationManager) {
+    this._call = undefined;
+    this._invocationManager = invocationManager;
+  }
+
+  async execute() {
+    const resultObj = await this._invocationManager.execute(this._call);
+    return resultObj ? resultObj.result : undefined;
+  }
+}
+
+class ActionInteraction extends Interaction {
+  constructor(invocationManager, element, action) {
+    super(invocationManager);
+    this._call = EspressoDetoxApi.perform(call(element._call), action._call);
+    // TODO: move this.execute() here from the caller
+  }
+}
+
+class MatcherAssertionInteraction extends Interaction {
+  constructor(invocationManager, element, matcher) {
+    super(invocationManager);
+    this._call = DetoxAssertionApi.assertMatcher(call(element._call), matcher._call.value);
+    // TODO: move this.execute() here from the caller
+  }
+}
+
+class WaitForInteraction extends Interaction {
+  constructor(invocationManager, element, assertionMatcher) {
+    super(invocationManager);
+    this._element = element;
+    this._assertionMatcher = assertionMatcher;
+    this._element._selectElementWithMatcher(this._element._originalMatcher);
+  }
+
+  async withTimeout(timeout) {
+    if (typeof timeout !== 'number') throw new Error(`WaitForInteraction withTimeout argument must be a number, got ${typeof timeout}`);
+    if (timeout < 0) throw new Error('timeout must be larger than 0');
+
+    this._call = DetoxAssertionApi.waitForAssertMatcher(call(this._element._call), this._assertionMatcher._call.value, timeout / 1000);
+    await this.execute();
+  }
+
+  whileElement(searchMatcher) {
+    return new WaitForActionInteraction(this._invocationManager, this._element, this._assertionMatcher, searchMatcher);
+  }
+}
+
+class WaitForActionInteractionBase extends Interaction {
+  constructor(invocationManager, element, matcher, searchMatcher) {
+    super(invocationManager);
+    //if (!(element instanceof NativeElement)) throw new Error(`WaitForActionInteraction ctor 1st argument must be a valid NativeElement, got ${typeof element}`);
+    //if (!(matcher instanceof NativeMatcher)) throw new Error(`WaitForActionInteraction ctor 2nd argument must be a valid NativeMatcher, got ${typeof matcher}`);
+    if (!(searchMatcher instanceof NativeMatcher))
+      throw new Error(`WaitForActionInteraction ctor 3rd argument must be a valid NativeMatcher, got ${typeof searchMatcher}`);
+    this._element = element;
+    this._originalMatcher = matcher;
+    this._searchMatcher = searchMatcher;
+  }
+
+  _prepare(searchAction) {
+    //if (!searchAction instanceof Action) throw new Error(`WaitForActionInteraction _execute argument must be a valid Action, got ${typeof searchAction}`);
+
+    this._call = DetoxAssertionApi.waitForAssertMatcherWithSearchAction(
+      call(this._element._call),
+      call(this._originalMatcher._call).value,
+      call(searchAction._call),
+      call(this._searchMatcher._call).value
+    );
+  }
+}
+
+class WaitForActionInteraction extends WaitForActionInteractionBase {
+  async scroll(amount, direction = 'down', scrollPositionX, scrollPositionY) {
+    this._prepare(new ScrollAmountStopAtEdgeAction(direction, amount, scrollPositionX, scrollPositionY));
+    await this.execute();
+  }
+}
+
+module.exports = {
+  Interaction,
+  ActionInteraction,
+  MatcherAssertionInteraction,
+  WaitForActionInteraction,
+  WaitForActionInteractionBase,
+  WaitForInteraction,
+};
diff --git a/detox/src/android/interactions/web.js b/detox/src/android/interactions/web.js
new file mode 100644
index 000000000..3840c14a8
--- /dev/null
+++ b/detox/src/android/interactions/web.js
@@ -0,0 +1,31 @@
+class WebInteraction {
+  constructor(invocationManager) {
+    this._call = undefined;
+    this._invocationManager = invocationManager;
+  }
+
+  async execute() {
+    const resultObj = await this._invocationManager.execute(this._call);
+    return resultObj ? resultObj.result : undefined;
+  }
+}
+
+class ActionInteraction extends WebInteraction {
+  constructor(invocationManager, action) {
+    super(invocationManager);
+    this._call = action._call;
+  }
+}
+
+class WebAssertionInteraction extends WebInteraction {
+  constructor(invocationManager, assertion) {
+    super(invocationManager);
+    this._call = assertion._call;
+  }
+}
+
+module.exports = {
+  WebInteraction,
+  ActionInteraction,
+  WebAssertionInteraction,
+};
diff --git a/detox/src/android/matcher.js b/detox/src/android/matcher.js
deleted file mode 100644
index b12ef44e5..000000000
--- a/detox/src/android/matcher.js
+++ /dev/null
@@ -1,124 +0,0 @@
-const invoke = require('../invoke');
-const DetoxMatcherApi = require('./espressoapi/DetoxMatcher');
-
-class Matcher {
-  withAncestor(matcher) {
-    this._call = invoke.callDirectly(DetoxMatcherApi.matcherWithAncestor(this, matcher));
-    return this;
-  }
-
-  withDescendant(matcher) {
-    this._call = invoke.callDirectly(DetoxMatcherApi.matcherWithDescendant(this, matcher));
-    return this;
-  }
-
-  and(matcher) {
-    this._call = invoke.callDirectly(DetoxMatcherApi.matcherForAnd(this, matcher));
-    return this;
-  }
-
-  or(matcher) {
-    this._call = invoke.callDirectly(DetoxMatcherApi.matcherForOr(this, matcher));
-    return this;
-  }
-
-  get not() {
-    this._call = invoke.callDirectly(DetoxMatcherApi.matcherForNot(this));
-    return this;
-  }
-
-  _avoidProblematicReactNativeElements() {
-    /*
-    const _originalMatcherCall = this._call;
-    this._call = invoke.call(invoke.IOS.Class('GREYMatchers'), 'detoxMatcherAvoidingProblematicReactNativeElements:', _originalMatcherCall);
-    */
-    return this;
-  }
-  _extendToDescendantScrollViews() {
-    /*
-    const _originalMatcherCall = this._call;
-    this._call = invoke.call(invoke.IOS.Class('GREYMatchers'), 'detoxMatcherForScrollChildOfMatcher:', _originalMatcherCall);
-    */
-    return this;
-  }
-}
-
-class LabelMatcher extends Matcher {
-  constructor(value) {
-    super();
-    this._call = invoke.callDirectly(DetoxMatcherApi.matcherForContentDescription(value));
-  }
-}
-
-class IdMatcher extends Matcher {
-  constructor(value) {
-    super();
-    this._call = invoke.callDirectly(DetoxMatcherApi.matcherForTestId(value));
-  }
-}
-
-class TypeMatcher extends Matcher {
-  constructor(value) {
-    super();
-    this._call = invoke.callDirectly(DetoxMatcherApi.matcherForClass(value));
-  }
-}
-
-class VisibleMatcher extends Matcher {
-  constructor() {
-    super();
-    this._call = invoke.callDirectly(DetoxMatcherApi.matcherForSufficientlyVisible());
-  }
-}
-
-class ExistsMatcher extends Matcher {
-  constructor() {
-    super();
-    this._call = invoke.callDirectly(DetoxMatcherApi.matcherForNotNull());
-  }
-}
-
-class TextMatcher extends Matcher {
-  constructor(value) {
-    super();
-    this._call = invoke.callDirectly(DetoxMatcherApi.matcherForText(value));
-  }
-}
-
-class ValueMatcher extends Matcher {
-  constructor(value) {
-    super();
-    this._call = invoke.callDirectly(DetoxMatcherApi.matcherForContentDescription(value));
-  }
-}
-
-class ToggleMatcher extends Matcher {
-  constructor(toggleState) {
-    super();
-    this._call = invoke.callDirectly(DetoxMatcherApi.matcherForToggleable(toggleState));
-  }
-}
-
-// TODO
-// Please be aware, that this is just a dummy matcher
-class TraitsMatcher extends Matcher {
-  constructor(value) {
-    super();
-    if ((typeof value !== 'object') || (!value instanceof Array)) throw new Error(`TraitsMatcher ctor argument must be an array, got ${typeof value}`);
-
-    this._call = invoke.callDirectly(DetoxMatcherApi.matcherForAnything());
-  }
-}
-
-module.exports = {
-  Matcher,
-  LabelMatcher,
-  IdMatcher,
-  TypeMatcher,
-  TraitsMatcher,
-  VisibleMatcher,
-  ExistsMatcher,
-  TextMatcher,
-  ValueMatcher,
-  ToggleMatcher,
-};
diff --git a/detox/src/android/matchers/index.js b/detox/src/android/matchers/index.js
new file mode 100644
index 000000000..455e5bcee
--- /dev/null
+++ b/detox/src/android/matchers/index.js
@@ -0,0 +1,21 @@
+const native = require('./native');
+const web = require('./web');
+
+module.exports = {
+  accessibilityLabel: (value) => new native.LabelMatcher(value),
+  id: (value) => new native.IdMatcher(value),
+  label: (value) => new native.LabelMatcher(value),
+  text: (value) => new native.TextMatcher(value),
+  traits: (value) => new native.TraitsMatcher(value),
+  type: (value) => new native.TypeMatcher(value),
+  value: (value) => new native.ValueMatcher(value),
+
+  className: (value) => new web.ClassNameMatcher(value),
+  cssSelector: (value) => new web.CssSelectorMatcher(value),
+  htmlId: (value) => new web.IdMatcher(value),
+  linkText: (value) => new web.LinkTextMatcher(value),
+  name: (value) => new web.NameMatcher(value),
+  partialLinkText: (value) => new web.PartialLinkTextMatcher(value),
+  tag: (value) => new web.TagNameMatcher(value),
+  xpath: (value) => new web.XPathMatcher(value),
+};
diff --git a/detox/src/android/matchers/native.js b/detox/src/android/matchers/native.js
new file mode 100644
index 000000000..9a8835211
--- /dev/null
+++ b/detox/src/android/matchers/native.js
@@ -0,0 +1,82 @@
+const invoke = require('../../invoke');
+const DetoxMatcherApi = require('../espressoapi/DetoxMatcher');
+const { NativeMatcher } = require('../core/NativeMatcher');
+
+class LabelMatcher extends NativeMatcher {
+  constructor(value) {
+    super();
+    this._call = invoke.callDirectly(DetoxMatcherApi.matcherForContentDescription(value));
+  }
+}
+
+class IdMatcher extends NativeMatcher {
+  constructor(value) {
+    super();
+    this._call = invoke.callDirectly(DetoxMatcherApi.matcherForTestId(value));
+  }
+}
+
+class TypeMatcher extends NativeMatcher {
+  constructor(value) {
+    super();
+    this._call = invoke.callDirectly(DetoxMatcherApi.matcherForClass(value));
+  }
+}
+
+class VisibleMatcher extends NativeMatcher {
+  constructor() {
+    super();
+    this._call = invoke.callDirectly(DetoxMatcherApi.matcherForSufficientlyVisible());
+  }
+}
+
+class ExistsMatcher extends NativeMatcher {
+  constructor() {
+    super();
+    this._call = invoke.callDirectly(DetoxMatcherApi.matcherForNotNull());
+  }
+}
+
+class TextMatcher extends NativeMatcher {
+  constructor(value) {
+    super();
+    this._call = invoke.callDirectly(DetoxMatcherApi.matcherForText(value));
+  }
+}
+
+class ValueMatcher extends NativeMatcher {
+  constructor(value) {
+    super();
+    this._call = invoke.callDirectly(DetoxMatcherApi.matcherForContentDescription(value));
+  }
+}
+
+class ToggleMatcher extends NativeMatcher {
+  constructor(toggleState) {
+    super();
+    this._call = invoke.callDirectly(DetoxMatcherApi.matcherForToggleable(toggleState));
+  }
+}
+
+// TODO
+// Please be aware, that this is just a dummy matcher
+class TraitsMatcher extends NativeMatcher {
+  constructor(value) {
+    super();
+    if ((typeof value !== 'object') || (!value instanceof Array)) throw new Error(`TraitsMatcher ctor argument must be an array, got ${typeof value}`);
+
+    this._call = invoke.callDirectly(DetoxMatcherApi.matcherForAnything());
+  }
+}
+
+module.exports = {
+  LabelMatcher,
+  IdMatcher,
+  TypeMatcher,
+  TraitsMatcher,
+  VisibleMatcher,
+  ExistsMatcher,
+  TextMatcher,
+  ValueMatcher,
+  ToggleMatcher,
+};
diff --git a/detox/src/android/webMatcher.js b/detox/src/android/matchers/web.js
similarity index 89%
rename from detox/src/android/webMatcher.js
rename to detox/src/android/matchers/web.js
index 78aa0fca6..b22f794e3 100644
--- a/detox/src/android/webMatcher.js
+++ b/detox/src/android/matchers/web.js
@@ -1,9 +1,6 @@
-const invoke = require('../invoke');
-const DetoxWebMatcherApi = require('./espressoapi/web/DetoxWebAtomMatcher');
-
-class WebMatcher {
-
-}
+const invoke = require('../../invoke');
+const DetoxWebMatcherApi = require('../espressoapi/web/DetoxWebAtomMatcher');
+const { WebMatcher } = require('../core/WebMatcher');
 
 class IdMatcher extends WebMatcher {
   constructor(id) {
@@ -61,7 +58,6 @@ class TagNameMatcher extends WebMatcher {
   }
 }
 
-
 module.exports = {
   IdMatcher,
   ClassNameMatcher,
@@ -71,4 +67,4 @@ module.exports = {
   LinkTextMatcher,
   PartialLinkTextMatcher,
   TagNameMatcher
-};
\ No newline at end of file
+};
diff --git a/detox/src/android/webExpect.js b/detox/src/android/webExpect.js
deleted file mode 100644
index e1a25dcc7..000000000
--- a/detox/src/android/webExpect.js
+++ /dev/null
@@ -1,309 +0,0 @@
-const invoke = require('../invoke');
-const EspressoWebDetoxApi = require('./espressoapi/web/EspressoWebDetox');
-const WebViewElementApi = require('./espressoapi/web/WebViewElement');
-const WebElementApi = require('./espressoapi/web/WebElement');
-const WebExpectApi = require('./espressoapi/web/WebExpect');
-const {
-  IdMatcher,
-  ClassNameMatcher,
-  CssSelectorMatcher,
-  NameMatcher,
-  XPathMatcher,
-  LinkTextMatcher,
-  PartialLinkTextMatcher,
-  TagNameMatcher,
-} = require('./webMatcher');
-const {
-  selectElementContents,
-  moveCursorToEnd,
-  focus
- } = require ('./espressoapi/web/simpleAtoms');
-
-class WebInteraction {
-  constructor(invocationManager) {
-    this._call = undefined;
-    this._invocationManager = invocationManager;
-  }
-
-  async execute() {
-    const resultObj = await this._invocationManager.execute(this._call);
-    return resultObj ? resultObj.result : undefined;
-  }
-}
-
-class ActionInteraction extends WebInteraction {
-  constructor(invocationManager, action) {
-    super(invocationManager);
-    this._call = action._call;
-  }
-}
-
-class WebAction {
-}
-
-class WebTapAction extends WebAction {
-  constructor(element) {
-    super();
-    this._call = WebElementApi.tap(element._call);
-  }
-}
-
-class WebTypeTextAction extends WebAction {
-  constructor(element, text) {
-    super();
-    this._call = WebElementApi.typeText(element._call, text);
-  }
-}
-
-class WebReplaceTextAction extends WebAction {
-  constructor(element, text) {
-    super();
-    this._call = WebElementApi.replaceText(element._call, text);
-  }
-}
-
-class WebClearTextAction extends WebAction {
-  constructor(element) {
-    super();
-    this._call = WebElementApi.clearText(element._call);
-  }
-}
-
-class WebScrollToViewAction extends WebAction {
-  constructor(element) {
-    super();
-    this._call = WebElementApi.scrollToView(element._call);
-  }
-}
-
-class WebGetTextAction extends WebAction {
-  constructor(element) {
-    super();
-    this._call = WebElementApi.getText(element._call);
-  }
-}
-
-class WebRunScriptAction extends WebAction {
-  constructor(element, script) {
-    super();
-    this._call = WebElementApi.runScript(element._call, script);
-  }
-}
-
-class WebRunScriptWithArgsAction extends WebAction {
-  constructor(element, script, args) {
-    super();
-    this._call = WebElementApi.runScriptWithArgs(element._call, script, args);
-  }
-}
-
-class WebGetCurrentUrlAction extends WebAction {
-  constructor(element) {
-    super();
-    this._call = WebElementApi.getCurrentUrl(element._call);
-  }
-}
-
-class WebGetTitleAction extends WebAction {
-  constructor(element) {
-    super();
-    this._call = WebElementApi.getTitle(element._call);
-  }
-}
-
-class WebFocusAction extends WebAction {
-  constructor(element) {
-    super();
-    this._call = WebElementApi.runScript(element._call, focus);
-  }
-}
-
-class WebSelectAllText extends WebAction {
-  constructor(element) {
-    super();
-    this._call = WebElementApi.runScript(element._call, selectElementContents);
-  }
-}
-
-class WebMoveCursorEnd extends WebAction {
-  constructor(element) {
-    super();
-    this._call = WebElementApi.runScript(element._call, moveCursorToEnd);
-  }
-}
-class WebViewElement {
-  constructor(invocationManager, deviceDriver, emitter, matcher) {
-    this._invocationManager = invocationManager;
-    this._deviceDriver = deviceDriver;
-    this._emitter = emitter;
-    this._matcher = matcher;
-
-    if (matcher !== undefined) {
-      this._call = invoke.callDirectly(EspressoWebDetoxApi.getWebView(matcher._call.value));
-    } else {
-      this._call = invoke.callDirectly(EspressoWebDetoxApi.getWebView());
-    }
-
-    this.element = this.element.bind(this);
-  }
-
-  element(webMatcher, index = 0) {
-    return new WebElement(this._invocationManager, this._deviceDriver, this, webMatcher, index);
-  }
-}
-
-class WebElement {
-  constructor(invocationManager, deviceDriver, webViewElement, matcher, index) {
-    this._invocationManager = invocationManager;
-    this._deviceDriver = deviceDriver;
-    this._call = invoke.callDirectly(WebViewElementApi.element(webViewElement._call, matcher._call.value, index));
-  }
-
-  // At the moment not working on content-editable
-  async tap() {
-    return await new ActionInteraction(this._invocationManager, new WebTapAction(this)).execute();
-  }
-
-  async typeText(text, isContentEditable = false) {
-    if (isContentEditable) {
-      return await this._deviceDriver.typeText(text);
-    }
-    return await new ActionInteraction(this._invocationManager,  new WebTypeTextAction(this, text)).execute();
-  }
-
-  // At the moment not working on content-editable
-  async replaceText(text) {
-    return await new ActionInteraction(this._invocationManager,  new WebReplaceTextAction(this, text)).execute();
-  }
-
-  // At the moment not working on content-editable
-  async clearText() {
-    return await new ActionInteraction(this._invocationManager,  new WebClearTextAction(this)).execute();
-  }
-
-  async scrollToView() {
-    return await new ActionInteraction(this._invocationManager,  new WebScrollToViewAction(this)).execute();
-  }
-
-  async getText() {
-    return await new ActionInteraction(this._invocationManager,  new WebGetTextAction(this)).execute();
-  }
-
-  async focus() {
-    return await new ActionInteraction(this._invocationManager, new WebFocusAction(this)).execute();
-  }
-
-  async selectAllText() {
-    return await new ActionInteraction(this._invocationManager, new WebSelectAllText(this)).execute();
-  }
-
-  async moveCursorToEnd() {
-    return await new ActionInteraction(this._invocationManager, new WebMoveCursorEnd(this)).execute();
-  }
-
-  async runScript(script) {
-    return await new ActionInteraction(this._invocationManager, new WebRunScriptAction(this, script)).execute();
-  }
-
-  async runScriptWithArgs(script, args) {
-    return await new ActionInteraction(this._invocationManager, new WebRunScriptWithArgsAction(this, script, args)).execute();
-  }
-
-  async getCurrentUrl() {
-    return await new ActionInteraction(this._invocationManager, new WebGetCurrentUrlAction(this)).execute();
-  }
-
-  async getTitle() {
-    return await new ActionInteraction(this._invocationManager, new WebGetTitleAction(this)).execute();
-  }
-}
-
-class WebAssertionInteraction extends WebInteraction {
-  constructor(invocationManager, assertion) {
-    super(invocationManager);
-    this._call = assertion._call;
-  }
-}
-
-class WebExistsAssertion extends WebAction {
-  constructor(webExpect) {
-    super();
-    if (webExpect._notCondition) {
-      this._call = WebExpectApi.toNotExist(webExpect._call);
-    } else {
-      this._call = WebExpectApi.toExist(webExpect._call);
-    }
-  }
-}
-
-class WebHasTextAssertion extends WebAction {
-  constructor(webExpect, text) {
-    super();
-    if (webExpect._notCondition) {
-      this._call = WebExpectApi.toNotHaveText(webExpect._call, text);
-    } else {
-      this._call = WebExpectApi.toHaveText(webExpect._call, text);
-    }
-  }
-}
-
-class WebExpect {
-  constructor(invocationManager) {
-    this._invocationManager = invocationManager
-    this._notCondition = false;
-  }
-
-  get not() {
-    this._notCondition = true;
-    return this;
-  }
-}
-
-class WebExpectElement extends WebExpect {
-  constructor(invocationManager, webElement) {
-    super(invocationManager)
-    this._call = invoke.callDirectly(EspressoWebDetoxApi.expect(webElement._call.value));
-  }
-
-  async toHaveText(text) {
-    return await new WebAssertionInteraction(this._invocationManager, new WebHasTextAssertion(this, text)).execute();
-  }
-
-  async toExist() {
-    return await new WebAssertionInteraction(this._invocationManager, new WebExistsAssertion(this)).execute();
-  }
-}
-class AndroidWebExpect {
-  constructor({ invocationManager, deviceDriver, emitter }) {
-    this._invocationManager = invocationManager;
-    this._deviceDriver = deviceDriver;
-    this._emitter = emitter;
-
-    this.by = {
-      id: (value) => new IdMatcher(value),
-      className: (value) => new ClassNameMatcher(value),
-      cssSelector: (value) => new CssSelectorMatcher(value),
-      name: (value) => new NameMatcher(value),
-      xpath: (value) => new XPathMatcher(value),
-      linkText: (value) => new LinkTextMatcher(value),
-      partialLinkText: (value) => new PartialLinkTextMatcher(value),
-      tag: (value) => new TagNameMatcher(value)
-    };
-    this.getWebView = this.getWebView.bind(this);
-    this.expect = this.expect.bind(this);
-  }
-
-  // Matcher can be null only if there is only one webview on the hierarchy tree.
-  getWebView(webViewMatcher) {
-    const webview = new WebViewElement(this._invocationManager, this._deviceDriver, this._emitter, webViewMatcher);
-    webview.by = this.by;
-    webview.expect = this.expect;
-    return webview;
-  }
-
-  expect(webElement) {
-    return new WebExpectElement(this._invocationManager, webElement)
-  }
-}
-
-module.exports = AndroidWebExpect;
diff --git a/detox/src/devices/Device.js b/detox/src/devices/Device.js
index 045f7003f..16bb0c283 100644
--- a/detox/src/devices/Device.js
+++ b/detox/src/devices/Device.js
@@ -266,6 +266,10 @@ class Device {
     await this.deviceDriver.resetStatusBar(this._deviceId);
   }
 
+  async typeText(text) {
+    await this.deviceDriver.typeText(this._deviceId, text);
+  }
+
   get _bundleId() {
     return this._getCurrentApp().bundleId;
   }
diff --git a/detox/src/matchersRegistry.js b/detox/src/matchersRegistry.js
index 22ee7625a..c9078ea3d 100644
--- a/detox/src/matchersRegistry.js
+++ b/detox/src/matchersRegistry.js
@@ -2,26 +2,12 @@ const resolveModuleFromPath = require('./utils/resolveModuleFromPath');
 
 function resolve(device, opts) {
   let MatcherClass;
-  let WebViewMatcherClass;
   switch (device.getPlatform()) {
-    case 'android':
-      MatcherClass = require('./android/expect');
-      WebViewMatcherClass = require('./android/webExpect');
-      break;
-
-    case 'ios':
-      MatcherClass = require('./ios/expectTwo');
-      break;
-
-    default:
-      MatcherClass = resolveModuleFromPath(device.type).ExpectClass;
-      break;
+    case 'android': MatcherClass = require('./android/AndroidExpect'); break;
+    case 'ios': MatcherClass = require('./ios/expectTwo'); break;
+    default: MatcherClass = resolveModuleFromPath(device.type).ExpectClass; break;
   }
-
-  return {
-    matchers: new MatcherClass(opts),
-    webMatchers: WebViewMatcherClass && new WebViewMatcherClass(opts),
-  };
+  return new MatcherClass(opts);
 }
 
 module.exports = {


Test patch:
diff --git a/detox/src/Detox.test.js b/detox/src/Detox.test.js
index 3f3480f47..b56ceae0a 100644
--- a/detox/src/Detox.test.js
+++ b/detox/src/Detox.test.js
@@ -29,8 +29,6 @@ describe('Detox', () => {
   let Detox;
   let detox;
   let lifecycleSymbols;
-  let mockGlobalMatcher;
-  let mockGlobalWebMatcher;
 
   const client = () => Client.mock.instances[0];
   const device = () => _.last(Device.mock.instances);
@@ -61,27 +59,28 @@ describe('Detox', () => {
     matchersRegistry = require('./matchersRegistry');
     Detox = require('./Detox');
     lifecycleSymbols = require('../runners/integration').lifecycle;
-
-    mockGlobalMatcher = jest.fn();
-    mockGlobalWebMatcher = jest.fn();
-    matchersRegistry.resolve.mockReturnValue({
-      matchers: { globalMatcher: mockGlobalMatcher },
-      webMatchers: mockGlobalWebMatcher
-    });
-  });
-
-  afterEach(() => {
-    // cleanup spilled globals after detox.init()
-    delete global.device;
-    delete global.globalMatcher;
-    delete global.web;
   });
 
   describe('when detox.init() is called', () => {
+    let mockGlobalMatcher;
+
     const init = async () => {
       detox = await new Detox(detoxConfig).init();
     };
 
+    beforeEach(() => {
+      mockGlobalMatcher = jest.fn();
+      matchersRegistry.resolve.mockReturnValue({
+        globalMatcher: mockGlobalMatcher,
+      });
+    });
+
+    afterEach(() => {
+      // cleanup spilled globals after detox.init()
+      delete global.device;
+      delete global.globalMatcher;
+    });
+
     describe('', () => {
       beforeEach(init);
 
@@ -121,7 +120,7 @@ describe('Detox', () => {
         const { emitter } = Device.mock.calls[0][0];
         expect(matchersRegistry.resolve).toHaveBeenCalledWith(device(), {
           invocationManager: invocationManager(),
-          deviceDriver: expect.any(Object),
+          device: device(),
           emitter,
         });
       });
@@ -129,9 +128,6 @@ describe('Detox', () => {
       it('should take the matchers from the matchers-registry to Detox', () =>
         expect(detox.globalMatcher).toBe(mockGlobalMatcher));
 
-      it('should take the web matchers from the matchers-registry to Detox.web', () =>
-        expect(detox.web).toBe(mockGlobalWebMatcher));
-
       it('should take device to Detox', () =>
         expect(detox.device).toBe(device()));
 
@@ -152,9 +148,6 @@ describe('Detox', () => {
       it('should expose matchers to global', () =>
         expect(global.globalMatcher).toBe(mockGlobalMatcher));
 
-      it('should expose webMatchers to global', () =>
-        expect(global.web).toBe(mockGlobalWebMatcher));
-
       it('should create artifacts manager', () =>
         expect(ArtifactsManager).toHaveBeenCalledWith(detoxConfig.artifactsConfig));
 
@@ -240,9 +233,6 @@ describe('Detox', () => {
 
       it('should not expose matchers to globals', () =>
         expect(global.globalMatcher).toBe(undefined));
-
-      it('should not expose webMatchers to globals', () =>
-        expect(global.web).toBe(undefined));
     });
 
     describe('with behaviorConfig.init.reinstallApp = false', () => {
diff --git a/detox/src/android/AndroidExpect.test.js b/detox/src/android/AndroidExpect.test.js
new file mode 100644
index 000000000..94051c68b
--- /dev/null
+++ b/detox/src/android/AndroidExpect.test.js
@@ -0,0 +1,602 @@
+describe('expect', () => {
+  let e;
+
+  let mockExecutor;
+  let emitter;
+  let device;
+
+  beforeEach(() => {
+    jest.mock('tempfile');
+    jest.mock('fs-extra');
+
+    mockExecutor = new MockExecutor();
+
+    const Emitter = jest.genMockFromModule('../utils/AsyncEmitter');
+    emitter = new Emitter();
+
+    device = {
+      typeText: jest.fn(),
+    };
+
+    const AndroidExpect = require('./AndroidExpect');
+    e = new AndroidExpect({
+      invocationManager: mockExecutor,
+      device,
+      emitter,
+    });
+  });
+
+  describe('native', () => {
+    it(`element by accessibilityLabel`, async () => {
+      await e.expect(e.element(e.by.accessibilityLabel('test'))).toBeVisible();
+      await e.expect(e.element(e.by.accessibilityLabel('test'))).toBeNotVisible();
+      await e.expect(e.element(e.by.accessibilityLabel('test'))).not.toBeVisible();
+      await e.expect(e.element(e.by.accessibilityLabel('test'))).toExist();
+      await e.expect(e.element(e.by.accessibilityLabel('test'))).toNotExist();
+      await e.expect(e.element(e.by.accessibilityLabel('test'))).not.toExist();
+      await e.expect(e.element(e.by.accessibilityLabel('test'))).toHaveText('text');
+      await e.expect(e.element(e.by.accessibilityLabel('test'))).toNotHaveText('text');
+      await e.expect(e.element(e.by.accessibilityLabel('test'))).not.toHaveText('text');
+      await e.expect(e.element(e.by.accessibilityLabel('test'))).toHaveLabel('label');
+      await e.expect(e.element(e.by.accessibilityLabel('test'))).toNotHaveLabel('label');
+      await e.expect(e.element(e.by.accessibilityLabel('test'))).not.toHaveLabel('label');
+      await e.expect(e.element(e.by.accessibilityLabel('test'))).toHaveId('id');
+      await e.expect(e.element(e.by.accessibilityLabel('test'))).toNotHaveId('id');
+      await e.expect(e.element(e.by.accessibilityLabel('test'))).not.toHaveId('id');
+      await e.expect(e.element(e.by.accessibilityLabel('test'))).toHaveValue('value');
+      await e.expect(e.element(e.by.accessibilityLabel('test'))).toNotHaveValue('value');
+      await e.expect(e.element(e.by.accessibilityLabel('test'))).not.toHaveValue('value');
+      await e.expect(e.element(e.by.accessibilityLabel('test'))).toHaveToggleValue(true);
+      await e.expect(e.element(e.by.accessibilityLabel('test'))).not.toHaveToggleValue(true);
+    });
+
+    it(`element by label (for backwards compat)`, async () => {
+      await e.expect(e.element(e.by.label('test'))).toBeVisible();
+      await e.expect(e.element(e.by.label('test'))).toBeNotVisible();
+      await e.expect(e.element(e.by.label('test'))).toExist();
+      await e.expect(e.element(e.by.label('test'))).toNotExist();
+      await e.expect(e.element(e.by.label('test'))).toHaveText('text');
+      await e.expect(e.element(e.by.label('test'))).toHaveLabel('label');
+      await e.expect(e.element(e.by.label('test'))).toHaveId('id');
+      await e.expect(e.element(e.by.label('test'))).toHaveValue('value');
+      await e.expect(e.element(e.by.label('test'))).toHaveToggleValue(false);
+      await e.expect(e.element(e.by.label('test'))).not.toHaveToggleValue(false);
+    });
+
+    it(`element by id`, async () => {
+      await e.expect(e.element(e.by.id('test'))).toBeVisible();
+    });
+
+    it(`element by type`, async () => {
+      await e.expect(e.element(e.by.type('test'))).toBeVisible();
+    });
+
+    it(`element by traits`, async () => {
+      await e.expect(e.element(e.by.traits(['button', 'link', 'header', 'search']))).toBeVisible();
+      await e.expect(e.element(e.by.traits(['image', 'selected', 'plays', 'key']))).toBeNotVisible();
+      await e.expect(e.element(e.by.traits(['text', 'summary', 'disabled', 'frequentUpdates']))).toBeNotVisible();
+      await e.expect(e.element(e.by.traits(['startsMedia', 'adjustable', 'allowsDirectInteraction', 'pageTurn']))).toBeNotVisible();
+    });
+
+    it(`matcher helpers`, async () => {
+      await e.expect(e.element(e.by.id('test').withAncestor(e.by.id('ancestor')))).toBeVisible();
+      await e.expect(e.element(e.by.id('test').withDescendant(e.by.id('descendant')))).toBeVisible();
+      await e.expect(e.element(e.by.id('test').and(e.by.type('type')))).toBeVisible();
+      await e.expect(e.element(e.by.id('test'))).not.toBeVisible();
+      await e.expect(e.element(e.by.id('test').or(e.by.id('test2')))).toBeVisible();
+    });
+
+    it(`expect with wrong parameters should throw`, async () => {
+      await expectToThrow(() => e.expect('notAnElement'));
+      await expectToThrow(() => e.expect(e.element('notAMatcher')));
+    });
+
+    it(`matchers with wrong parameters should throw`, async () => {
+      await expectToThrow(() => e.element(e.by.label(5)));
+      await expectToThrow(() => e.element(e.by.accessibilityLabel(5)));
+      await expectToThrow(() => e.element(e.by.id(5)));
+      await expectToThrow(() => e.element(e.by.type(0)));
+      await expectToThrow(() => e.by.traits(1));
+      await expectToThrow(() => e.by.traits(['nonExistentTrait']));
+      await expectToThrow(() => e.element(e.by.value(0)));
+      await expectToThrow(() => e.element(e.by.text(0)));
+      await expectToThrow(() => e.element(e.by.id('test').withAncestor('notAMatcher')));
+      await expectToThrow(() => e.element(e.by.id('test').withDescendant('notAMatcher')));
+      await expectToThrow(() => e.element(e.by.id('test').and('notAMatcher')));
+      await expectToThrow(() => e.element(e.by.id('test').or('notAMatcher')));
+    });
+
+    it(`waitFor (element)`, async () => {
+      await e.waitFor(e.element(e.by.id('id'))).toExist().withTimeout(0);
+      await e.waitFor(e.element(e.by.id('id'))).not.toExist().withTimeout(0);
+      await e.waitFor(e.element(e.by.id('id'))).toBeVisible();
+      await e.waitFor(e.element(e.by.id('id'))).toBeNotVisible();
+      await e.waitFor(e.element(e.by.id('id'))).toExist();
+      await e.waitFor(e.element(e.by.id('id'))).toExist().withTimeout(0);
+      await e.waitFor(e.element(e.by.id('id'))).toNotExist().withTimeout(0);
+
+      await e.waitFor(e.element(e.by.id('id'))).toHaveText('text');
+      await e.waitFor(e.element(e.by.id('id'))).toNotHaveText('value');
+      await e.waitFor(e.element(e.by.id('id'))).not.toHaveText('text');
+
+      await e.waitFor(e.element(e.by.id('id'))).toHaveLabel('text');
+      await e.waitFor(e.element(e.by.id('id'))).toNotHaveLabel('text');
+      await e.waitFor(e.element(e.by.id('id'))).not.toHaveLabel('text');
+
+      await e.waitFor(e.element(e.by.id('id'))).toHaveId('id');
+      await e.waitFor(e.element(e.by.id('id'))).toNotHaveId('id');
+      await e.waitFor(e.element(e.by.id('id'))).not.toHaveId('id');
+
+      await e.waitFor(e.element(e.by.id('id'))).toHaveValue('value');
+      await e.waitFor(e.element(e.by.id('id'))).toNotHaveValue('value');
+      await e.waitFor(e.element(e.by.id('id'))).not.toHaveValue('value');
+
+      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);
+
+    });
+
+    it(`waitFor (element) with wrong parameters should throw`, async () => {
+      await expectToThrow(() => e.waitFor('notAnElement'));
+      await expectToThrow(() => e.waitFor(e.element(e.by.id('id'))).toExist().withTimeout('notANumber'));
+      await expectToThrow(() => e.waitFor(e.element(e.by.id('id'))).toExist().withTimeout(-1));
+      await expectToThrow(() => e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement('notAnElement'));
+    });
+
+    it(`waitFor (element) with non-elements should throw`, async () => {
+      await expectToThrow(() => e.waitFor('notAnElement').toBeVisible());
+    });
+
+    describe('element interactions', () => {
+      it('should tap and long-press', async () => {
+        await e.element(e.by.label('Tap Me')).tap();
+        await e.element(e.by.label('Tap Me')).tap({ x: 10, y: 10 });
+        await e.element(e.by.label('Tap Me')).tapAtPoint({x: 100, y: 200});
+        await e.element(e.by.label('Tap Me')).longPress();
+        await e.element(e.by.id('UniqueId819')).multiTap(3);
+      });
+
+      it('should not tap and long-press given bad args', async () => {
+        await expectToThrow(() => e.element(e.by.id('UniqueId819')).multiTap('NaN'));
+      });
+
+      it('should press special keys', async () => {
+        await e.element(e.by.id('UniqueId937')).tapBackspaceKey();
+        await e.element(e.by.id('UniqueId937')).tapReturnKey();
+      });
+
+      it('should edit text', async () => {
+        await e.element(e.by.id('UniqueId937')).typeText('passcode');
+        await e.element(e.by.id('UniqueId005')).clearText();
+        await e.element(e.by.id('UniqueId005')).replaceText('replaceTo');
+      });
+
+      it('should not edit text given bad args', async () => {
+        await expectToThrow(() => e.element(e.by.id('UniqueId937')).typeText(0));
+        await expectToThrow(() => e.element(e.by.id('UniqueId005')).replaceText(3));
+      });
+
+      it('should scroll', async () => {
+        await e.element(e.by.id('ScrollView161')).scroll(100);
+        await e.element(e.by.id('ScrollView161')).scroll(100, 'down');
+        await e.element(e.by.id('ScrollView161')).scroll(100, 'up');
+        await e.element(e.by.id('ScrollView161')).scroll(100, 'right');
+        await e.element(e.by.id('ScrollView161')).scroll(100, 'left');
+        await e.element(e.by.id('ScrollView161')).scrollTo('bottom');
+        await e.element(e.by.id('ScrollView161')).scrollTo('top');
+        await e.element(e.by.id('ScrollView161')).scrollTo('left');
+        await e.element(e.by.id('ScrollView161')).scrollTo('right');
+      });
+
+      it('should not scroll given bad args', async () => {
+        await expectToThrow(() => e.element(e.by.id('ScrollView161')).scroll('NaN', 'down'));
+        await expectToThrow(() => e.element(e.by.id('ScrollView161')).scroll(100, 'noDirection'));
+        await expectToThrow(() => e.element(e.by.id('ScrollView161')).scroll(100, 0));
+        await expectToThrow(() => e.element(e.by.id('ScrollView161')).scrollTo(0));
+        await expectToThrow(() => e.element(e.by.id('ScrollView161')).scrollTo('noDirection'));
+      });
+
+      it('should swipe', async () => {
+        await e.element(e.by.id('ScrollView799')).swipe('down');
+        await e.element(e.by.id('ScrollView799')).swipe('down', 'fast');
+        await e.element(e.by.id('ScrollView799')).swipe('up', 'slow');
+        await e.element(e.by.id('ScrollView799')).swipe('left', 'fast');
+        await e.element(e.by.id('ScrollView799')).swipe('right', 'slow');
+        await e.element(e.by.id('ScrollView799')).swipe('down', 'fast', 0.9);
+        await e.element(e.by.id('ScrollView799')).swipe('up', 'slow', 0.9);
+        await e.element(e.by.id('ScrollView799')).swipe('left', 'fast', 0.9);
+        await e.element(e.by.id('ScrollView799')).swipe('right', 'slow', 0.9);
+        await e.element(e.by.id('ScrollView799')).swipe('down', 'fast', undefined, undefined, 0.25);
+        await e.element(e.by.id('ScrollView799')).swipe('up', 'slow', 0.9, 0.5, 0.5);
+      });
+
+      it('should not swipe given bad args', async () => {
+        await expectToThrow(() => e.element(e.by.id('ScrollView799')).swipe(4, 'fast'));
+        await expectToThrow(() => e.element(e.by.id('ScrollView799')).swipe('noDirection', 0));
+        await expectToThrow(() => e.element(e.by.id('ScrollView799')).swipe('noDirection', 'fast'));
+        await expectToThrow(() => e.element(e.by.id('ScrollView799')).swipe('down', 'NotFastNorSlow'));
+        await expectToThrow(() => e.element(e.by.id('ScrollView799')).swipe('down', 'NotFastNorSlow', 0.9));
+      });
+
+      it('should allow for index-based element discrepancy resolution', async () => {
+        await e.element(e.by.id('ScrollView799')).atIndex(1);
+      });
+
+      it('should fail to find index-based element given invalid args', async () => {
+        await expectToThrow(() => e.element(e.by.id('ScrollView799')).atIndex('NaN'));
+      });
+    });
+
+    describe('element screenshots', () => {
+      const tempFilePath = '/path/to/temp-file.png';
+      const invokeResultInBase64 = 'VGhlcmUgaXMgbm8gc3Bvb24h';
+
+      let tempfile;
+      let fs;
+      let _element;
+      beforeEach(() => {
+        mockExecutor.executeResult = Promise.resolve(invokeResultInBase64);
+
+        fs = require('fs-extra');
+        tempfile = require('tempfile');
+        tempfile.mockReturnValue(tempFilePath);
+
+        _element = e.element(e.by.id('FancyElement'));
+      });
+
+      it('should take and save the screenshot in a temp-file', async () => {
+        await _element.takeScreenshot();
+        expect(fs.writeFile).toHaveBeenCalledWith(tempFilePath, invokeResultInBase64, 'base64');
+        expect(tempfile).toHaveBeenCalledWith('detox.element-screenshot.png');
+      });
+
+      it('should return the path to the temp-file containing screenshot data', async () => {
+        const result = await _element.takeScreenshot();
+        expect(result).toEqual(tempFilePath);
+      });
+
+      it('should emit a named-artifact creation event', async () => {
+        const screenshotName = 'mock-screenshot-name';
+        await _element.takeScreenshot(screenshotName);
+        expect(emitter.emit).toHaveBeenCalledWith('createExternalArtifact', {
+          pluginId: 'screenshot',
+          artifactName: screenshotName,
+          artifactPath: tempFilePath,
+        });
+      });
+
+      it('should emit an artifact creation event with a default name', async () => {
+        await _element.takeScreenshot(undefined);
+        expect(emitter.emit).toHaveBeenCalledWith(
+          expect.any(String),
+          expect.objectContaining({
+              artifactName: 'temp-file',
+            },
+          ));
+      });
+    });
+  });
+
+  describe('web', () => {
+    describe('General', () => {
+      it('should return undefined', async () => {
+        mockExecutor.executeResult = Promise.resolve(undefined);
+        await e.web(e.by.id('webview_id')).element(e.by.htmlId('any')).tap();
+      });
+    })
+
+    describe('web', () => {
+
+      it('default', async () => {
+        await e.web.element(e.by.htmlId('id')).tap();
+      });
+
+      it('default explicit', async () => {
+        await e.web().element(e.by.htmlId('id')).tap();
+      });
+
+      it('with webview matcher', async () => {
+        await e.web(e.by.id('webview_id')).element(e.by.htmlId('id')).tap();
+      });
+
+      it(`with wrong matcher should throw`, async () => {
+        await expectToThrow(() => e.web(e.by.className('webMatcher')));
+        await expectToThrow(() => e.web(e.by.cssSelector('webMatcher')));
+        await expectToThrow(() => e.web(e.by.htmlId('webMatcher')));
+        await expectToThrow(() => e.web(e.by.linkText('webMatcher')));
+        await expectToThrow(() => e.web(e.by.name('webMatcher')));
+        await expectToThrow(() => e.web(e.by.partialLinkText('webMatcher')));
+        await expectToThrow(() => e.web(e.by.tag('webMatcher')));
+        await expectToThrow(() => e.web(e.by.xpath('webMatcher')));
+      });
+
+      it(`inner element with wrong matcher should throw`, async () => {
+        await expectToThrow(() => e.web.element(e.by.accessibilityLabel('nativeMatcher')));
+        await expectToThrow(() => e.web.element(e.by.id('nativeMatcher')));
+        await expectToThrow(() => e.web.element(e.by.label('nativeMatcher')));
+        await expectToThrow(() => e.web.element(e.by.text('nativeMatcher')));
+        await expectToThrow(() => e.web.element(e.by.traits('nativeMatcher')));
+        await expectToThrow(() => e.web.element(e.by.value('nativeMatcher')));
+      });
+    });
+
+    describe('WebElement Actions', () => {
+      it('tap', async () => {
+        await e.web.element(e.by.htmlId('id')).tap();
+        await e.web.element(e.by.className('className')).tap();
+        await e.web.element(e.by.cssSelector('cssSelector')).tap();
+        await e.web.element(e.by.name('name')).tap();
+        await e.web.element(e.by.xpath('xpath')).tap();
+        await e.web.element(e.by.linkText('linkText')).tap();
+        await e.web.element(e.by.partialLinkText('partialLinkText')).tap();
+        await e.web.element(e.by.tag('tag')).tap();
+      });
+
+      it('typeText', async () => {
+        await e.web.element(e.by.htmlId('id')).typeText('text');
+        await e.web.element(e.by.className('className')).typeText('text');
+        await e.web.element(e.by.cssSelector('cssSelector')).typeText('text');
+        await e.web.element(e.by.name('name')).typeText('text');
+        await e.web.element(e.by.xpath('xpath')).typeText('text');
+        await e.web.element(e.by.linkText('linkText')).typeText('text');
+        await e.web.element(e.by.partialLinkText('partialLinkText')).typeText('text');
+        await e.web.element(e.by.tag('tag')).typeText('text');
+      });
+
+      it('typeText with isContentEditable=false', async () => {
+        await e.web.element(e.by.htmlId('id')).typeText('text', false);
+        global.expect(device.typeText).not.toHaveBeenCalled();
+      });
+
+      it('typeText with isContentEditable=true', async () => {
+        await e.web.element(e.by.htmlId('id')).typeText('text', true);
+        global.expect(device.typeText).toHaveBeenCalled();
+      });
+
+      it('typeText default isContentEditable is false', async () => {
+        await e.web.element(e.by.htmlId('id')).typeText('text');
+        global.expect(device.typeText).not.toHaveBeenCalled();
+      });
+
+      it('replaceText', async () => {
+        await e.web.element(e.by.htmlId('id')).replaceText('text');
+        await e.web.element(e.by.className('className')).replaceText('text');
+        await e.web.element(e.by.cssSelector('cssSelector')).replaceText('text');
+        await e.web.element(e.by.name('name')).replaceText('text');
+        await e.web.element(e.by.xpath('xpath')).replaceText('text');
+        await e.web.element(e.by.linkText('linkText')).replaceText('text');
+        await e.web.element(e.by.partialLinkText('partialLinkText')).replaceText('text');
+        await e.web.element(e.by.tag('tag')).replaceText('text');
+      });
+
+      it('clearText', async () => {
+        await e.web.element(e.by.htmlId('id')).clearText();
+        await e.web.element(e.by.className('className')).clearText();
+        await e.web.element(e.by.cssSelector('cssSelector')).clearText();
+        await e.web.element(e.by.name('name')).clearText();
+        await e.web.element(e.by.xpath('xpath')).clearText();
+        await e.web.element(e.by.linkText('linkText')).clearText();
+        await e.web.element(e.by.partialLinkText('partialLinkText')).clearText();
+        await e.web.element(e.by.tag('tag')).clearText();
+      });
+
+      it('scrollToView', async () => {
+        await e.web.element(e.by.htmlId('id')).scrollToView();
+        await e.web.element(e.by.className('className')).scrollToView();
+        await e.web.element(e.by.cssSelector('cssSelector')).scrollToView();
+        await e.web.element(e.by.name('name')).scrollToView();
+        await e.web.element(e.by.xpath('xpath')).scrollToView();
+        await e.web.element(e.by.linkText('linkText')).scrollToView();
+        await e.web.element(e.by.partialLinkText('partialLinkText')).scrollToView();
+        await e.web.element(e.by.tag('tag')).scrollToView();
+      });
+
+      it('getText', async () => {
+        await e.web.element(e.by.htmlId('id')).getText();
+        await e.web.element(e.by.className('className')).getText();
+        await e.web.element(e.by.cssSelector('cssSelector')).getText();
+        await e.web.element(e.by.name('name')).getText();
+        await e.web.element(e.by.xpath('xpath')).getText();
+        await e.web.element(e.by.linkText('linkText')).getText();
+        await e.web.element(e.by.partialLinkText('partialLinkText')).getText();
+        await e.web.element(e.by.tag('tag')).getText();
+      });
+
+      it('focus', async () => {
+        await e.web.element(e.by.htmlId('id')).focus();
+        await e.web.element(e.by.className('className')).focus();
+        await e.web.element(e.by.cssSelector('cssSelector')).focus();
+        await e.web.element(e.by.name('name')).focus();
+        await e.web.element(e.by.xpath('xpath')).focus();
+        await e.web.element(e.by.linkText('linkText')).focus();
+        await e.web.element(e.by.partialLinkText('partialLinkText')).focus();
+        await e.web.element(e.by.tag('tag')).focus();
+      });
+
+      it('selectAllText', async () => {
+        await e.web.element(e.by.htmlId('id')).selectAllText();
+        await e.web.element(e.by.className('className')).selectAllText();
+        await e.web.element(e.by.cssSelector('cssSelector')).selectAllText();
+        await e.web.element(e.by.name('name')).selectAllText();
+        await e.web.element(e.by.xpath('xpath')).selectAllText();
+        await e.web.element(e.by.linkText('linkText')).selectAllText();
+        await e.web.element(e.by.partialLinkText('partialLinkText')).selectAllText();
+        await e.web.element(e.by.tag('tag')).selectAllText();
+      });
+
+      it('moveCursorToEnd', async () => {
+        await e.web.element(e.by.htmlId('id')).moveCursorToEnd();
+        await e.web.element(e.by.className('className')).moveCursorToEnd();
+        await e.web.element(e.by.cssSelector('cssSelector')).moveCursorToEnd();
+        await e.web.element(e.by.name('name')).moveCursorToEnd();
+        await e.web.element(e.by.xpath('xpath')).moveCursorToEnd();
+        await e.web.element(e.by.linkText('linkText')).moveCursorToEnd();
+        await e.web.element(e.by.partialLinkText('partialLinkText')).moveCursorToEnd();
+        await e.web.element(e.by.tag('tag')).moveCursorToEnd();
+      });
+
+      it('runScript', async () => {
+        const script = 'function foo(el) {}';
+        await e.web.element(e.by.htmlId('id')).runScript(script);
+        await e.web.element(e.by.className('className')).runScript(script);
+        await e.web.element(e.by.cssSelector('cssSelector')).runScript(script);
+        await e.web.element(e.by.name('name')).runScript(script);
+        await e.web.element(e.by.xpath('xpath')).runScript(script);
+        await e.web.element(e.by.linkText('linkText')).runScript(script);
+        await e.web.element(e.by.partialLinkText('partialLinkText')).runScript(script);
+        await e.web.element(e.by.tag('tag')).runScript(script);
+      });
+
+      it('runScriptWithArgs', async () => {
+        const script = 'function bar(a,b) {}';
+        const argsArr = ['fooA','barB'];
+        await e.web.element(e.by.htmlId('id')).runScriptWithArgs(script, argsArr);
+        await e.web.element(e.by.className('className')).runScriptWithArgs(script, argsArr);
+        await e.web.element(e.by.cssSelector('cssSelector')).runScriptWithArgs(script, argsArr);
+        await e.web.element(e.by.name('name')).runScriptWithArgs(script, argsArr);
+        await e.web.element(e.by.xpath('xpath')).runScriptWithArgs(script, argsArr);
+        await e.web.element(e.by.linkText('linkText')).runScriptWithArgs(script, argsArr);
+        await e.web.element(e.by.partialLinkText('partialLinkText')).runScriptWithArgs(script, argsArr);
+        await e.web.element(e.by.tag('tag')).runScriptWithArgs(script, argsArr);
+      });
+
+      it('getCurrentUrl', async () => {
+        await e.web.element(e.by.htmlId('id')).getCurrentUrl();
+        await e.web.element(e.by.className('className')).getCurrentUrl();
+        await e.web.element(e.by.cssSelector('cssSelector')).getCurrentUrl();
+        await e.web.element(e.by.name('name')).getCurrentUrl();
+        await e.web.element(e.by.xpath('xpath')).getCurrentUrl();
+        await e.web.element(e.by.linkText('linkText')).getCurrentUrl();
+        await e.web.element(e.by.partialLinkText('partialLinkText')).getCurrentUrl();
+        await e.web.element(e.by.tag('tag')).getCurrentUrl();
+      });
+
+      it('getTitle', async () => {
+        await e.web.element(e.by.htmlId('id')).getTitle();
+        await e.web.element(e.by.className('className')).getTitle();
+        await e.web.element(e.by.cssSelector('cssSelector')).getTitle();
+        await e.web.element(e.by.name('name')).getTitle();
+        await e.web.element(e.by.xpath('xpath')).getTitle();
+        await e.web.element(e.by.linkText('linkText')).getTitle();
+        await e.web.element(e.by.partialLinkText('partialLinkText')).getTitle();
+        await e.web.element(e.by.tag('tag')).getTitle();
+      });
+
+      it('should allow for access to by via element', async () => {
+        const webview = await e.web;
+        await webview.element(e.by.htmlId('id')).tap();
+      });
+    })
+
+    describe('Web Matchers',() => {
+      it('by.htmlId', async () => {
+        await e.expect(e.web.element(e.by.htmlId('id'))).toExist();
+        await e.expect(e.web.element(e.by.htmlId('id'))).not.toExist();
+        await e.expect(e.web.element(e.by.htmlId('id'))).toHaveText('text');
+        await e.expect(e.web.element(e.by.htmlId('id'))).not.toHaveText('text');
+      });
+
+      it('by.className', async () => {
+        await e.expect(e.web.element(e.by.className('className'))).toExist();
+        await e.expect(e.web.element(e.by.className('className'))).not.toExist();
+        await e.expect(e.web.element(e.by.className('className'))).toHaveText('text');
+        await e.expect(e.web.element(e.by.className('className'))).not.toHaveText('text');
+      });
+
+      it('by.cssSelector', async () => {
+        await e.expect(e.web.element(e.by.cssSelector('cssSelector'))).toExist();
+        await e.expect(e.web.element(e.by.cssSelector('cssSelector'))).not.toExist();
+        await e.expect(e.web.element(e.by.cssSelector('cssSelector'))).toHaveText('text');
+        await e.expect(e.web.element(e.by.cssSelector('cssSelector'))).not.toHaveText('text');
+      });
+
+      it('by.name', async () => {
+        await e.expect(e.web.element(e.by.name('name'))).toExist();
+        await e.expect(e.web.element(e.by.name('name'))).not.toExist();
+        await e.expect(e.web.element(e.by.name('name'))).toHaveText('text');
+        await e.expect(e.web.element(e.by.name('name'))).not.toHaveText('text');
+      });
+
+      it('by.xpath', async () => {
+        await e.expect(e.web.element(e.by.xpath('xpath'))).toExist();
+        await e.expect(e.web.element(e.by.xpath('xpath'))).not.toExist();
+        await e.expect(e.web.element(e.by.xpath('xpath'))).toHaveText('text');
+        await e.expect(e.web.element(e.by.xpath('xpath'))).not.toHaveText('text');
+      });
+
+      it('by.linkText', async () => {
+        await e.expect(e.web.element(e.by.linkText('link'))).toExist();
+        await e.expect(e.web.element(e.by.linkText('link'))).not.toExist();
+        await e.expect(e.web.element(e.by.linkText('link'))).toHaveText('text');
+        await e.expect(e.web.element(e.by.linkText('link'))).not.toHaveText('text');
+      });
+
+      it('by.partialLinkText', async () => {
+        await e.expect(e.web.element(e.by.partialLinkText('lin'))).toExist();
+        await e.expect(e.web.element(e.by.partialLinkText('lin'))).not.toExist();
+        await e.expect(e.web.element(e.by.partialLinkText('lin'))).toHaveText('text');
+        await e.expect(e.web.element(e.by.partialLinkText('lin'))).not.toHaveText('text');
+      });
+
+      it('by.tag', async () => {
+        await e.expect(e.web.element(e.by.tag('tag'))).toExist();
+        await e.expect(e.web.element(e.by.tag('tag'))).not.toExist();
+        await e.expect(e.web.element(e.by.tag('tag'))).toHaveText('text');
+        await e.expect(e.web.element(e.by.tag('tag'))).not.toHaveText('text');
+      });
+
+      it('should allow for access to except via element', async () => {
+        const webview = e.web();
+        await e.expect(webview.element(e.by.htmlId('id'))).toExist();
+      });
+    });
+  });
+});
+
+async function expectToThrow(func) {
+  try {
+    await func();
+  } catch (ex) {
+    expect(ex).toBeDefined();
+  }
+}
+
+class MockExecutor {
+  constructor() {
+    this.executeResult = undefined;
+  }
+
+  async execute(invocation) {
+    if (typeof invocation === 'function') {
+      invocation = invocation();
+    }
+    expect(invocation.target).toBeDefined();
+    expect(invocation.target.type).toBeDefined();
+    expect(invocation.target.value).toBeDefined();
+
+    this.recurse(invocation);
+    await this.timeout(1);
+    return this.executeResult ? {
+      result: this.executeResult,
+    } : undefined;
+  }
+
+  recurse(invocation) {
+    for (const key in invocation) {
+      if (invocation.hasOwnProperty(key)) {
+        if (invocation[key] instanceof Object) {
+          this.recurse(invocation[key]);
+        }
+        if (key === 'target' && invocation[key].type === 'Invocation') {
+          const innerValue = invocation.target.value;
+          expect(innerValue.target.type).toBeDefined();
+          expect(innerValue.target.value).toBeDefined();
+        }
+      }
+    }
+  }
+
+  async timeout(ms) {
+    return new Promise((resolve) => setTimeout(resolve, ms));
+  }
+}
diff --git a/detox/src/android/expect.test.js b/detox/src/android/expect.test.js
deleted file mode 100644
index 9bbd9d6c1..000000000
--- a/detox/src/android/expect.test.js
+++ /dev/null
@@ -1,318 +0,0 @@
-describe('expect', () => {
-  let e;
-
-  let mockExecutor;
-  let emitter;
-  beforeEach(() => {
-    jest.mock('tempfile');
-    jest.mock('fs-extra');
-
-    mockExecutor = new MockExecutor();
-
-    const Emitter = jest.genMockFromModule('../utils/AsyncEmitter');
-    emitter = new Emitter();
-
-    const AndroidExpect = require('./expect');
-    e = new AndroidExpect({
-      invocationManager: mockExecutor,
-      emitter,
-    });
-  });
-
-  it(`element by accessibilityLabel`, async () => {
-    await e.expect(e.element(e.by.accessibilityLabel('test'))).toBeVisible();
-    await e.expect(e.element(e.by.accessibilityLabel('test'))).toBeNotVisible();
-    await e.expect(e.element(e.by.accessibilityLabel('test'))).not.toBeVisible();
-    await e.expect(e.element(e.by.accessibilityLabel('test'))).toExist();
-    await e.expect(e.element(e.by.accessibilityLabel('test'))).toNotExist();
-    await e.expect(e.element(e.by.accessibilityLabel('test'))).not.toExist();
-    await e.expect(e.element(e.by.accessibilityLabel('test'))).toHaveText('text');
-    await e.expect(e.element(e.by.accessibilityLabel('test'))).toNotHaveText('text');
-    await e.expect(e.element(e.by.accessibilityLabel('test'))).not.toHaveText('text');
-    await e.expect(e.element(e.by.accessibilityLabel('test'))).toHaveLabel('label');
-    await e.expect(e.element(e.by.accessibilityLabel('test'))).toNotHaveLabel('label');
-    await e.expect(e.element(e.by.accessibilityLabel('test'))).not.toHaveLabel('label');
-    await e.expect(e.element(e.by.accessibilityLabel('test'))).toHaveId('id');
-    await e.expect(e.element(e.by.accessibilityLabel('test'))).toNotHaveId('id');
-    await e.expect(e.element(e.by.accessibilityLabel('test'))).not.toHaveId('id');
-    await e.expect(e.element(e.by.accessibilityLabel('test'))).toHaveValue('value');
-    await e.expect(e.element(e.by.accessibilityLabel('test'))).toNotHaveValue('value');
-    await e.expect(e.element(e.by.accessibilityLabel('test'))).not.toHaveValue('value');
-    await e.expect(e.element(e.by.accessibilityLabel('test'))).toHaveToggleValue(true);
-    await e.expect(e.element(e.by.accessibilityLabel('test'))).not.toHaveToggleValue(true);
-  });
-
-  it(`element by label (for backwards compat)`, async () => {
-    await e.expect(e.element(e.by.label('test'))).toBeVisible();
-    await e.expect(e.element(e.by.label('test'))).toBeNotVisible();
-    await e.expect(e.element(e.by.label('test'))).toExist();
-    await e.expect(e.element(e.by.label('test'))).toNotExist();
-    await e.expect(e.element(e.by.label('test'))).toHaveText('text');
-    await e.expect(e.element(e.by.label('test'))).toHaveLabel('label');
-    await e.expect(e.element(e.by.label('test'))).toHaveId('id');
-    await e.expect(e.element(e.by.label('test'))).toHaveValue('value');
-    await e.expect(e.element(e.by.label('test'))).toHaveToggleValue(false);
-    await e.expect(e.element(e.by.label('test'))).not.toHaveToggleValue(false);
-  });
-
-  it(`element by id`, async () => {
-    await e.expect(e.element(e.by.id('test'))).toBeVisible();
-  });
-
-  it(`element by type`, async () => {
-    await e.expect(e.element(e.by.type('test'))).toBeVisible();
-  });
-
-  it(`element by traits`, async () => {
-    await e.expect(e.element(e.by.traits(['button', 'link', 'header', 'search']))).toBeVisible();
-    await e.expect(e.element(e.by.traits(['image', 'selected', 'plays', 'key']))).toBeNotVisible();
-    await e.expect(e.element(e.by.traits(['text', 'summary', 'disabled', 'frequentUpdates']))).toBeNotVisible();
-    await e.expect(e.element(e.by.traits(['startsMedia', 'adjustable', 'allowsDirectInteraction', 'pageTurn']))).toBeNotVisible();
-  });
-
-  it(`matcher helpers`, async () => {
-    await e.expect(e.element(e.by.id('test').withAncestor(e.by.id('ancestor')))).toBeVisible();
-    await e.expect(e.element(e.by.id('test').withDescendant(e.by.id('descendant')))).toBeVisible();
-    await e.expect(e.element(e.by.id('test').and(e.by.type('type')))).toBeVisible();
-    await e.expect(e.element(e.by.id('test'))).not.toBeVisible();
-    await e.expect(e.element(e.by.id('test').or(e.by.id('test2')))).toBeVisible();
-  });
-
-  it(`expect with wrong parameters should throw`, async () => {
-    await expectToThrow(() => e.expect('notAnElement'));
-    await expectToThrow(() => e.expect(e.element('notAMatcher')));
-  });
-
-  it(`matchers with wrong parameters should throw`, async () => {
-    await expectToThrow(() => e.element(e.by.label(5)));
-    await expectToThrow(() => e.element(e.by.accessibilityLabel(5)));
-    await expectToThrow(() => e.element(e.by.id(5)));
-    await expectToThrow(() => e.element(e.by.type(0)));
-    await expectToThrow(() => e.by.traits(1));
-    await expectToThrow(() => e.by.traits(['nonExistentTrait']));
-    await expectToThrow(() => e.element(e.by.value(0)));
-    await expectToThrow(() => e.element(e.by.text(0)));
-    await expectToThrow(() => e.element(e.by.id('test').withAncestor('notAMatcher')));
-    await expectToThrow(() => e.element(e.by.id('test').withDescendant('notAMatcher')));
-    await expectToThrow(() => e.element(e.by.id('test').and('notAMatcher')));
-    await expectToThrow(() => e.element(e.by.id('test').or('notAMatcher')));
-  });
-
-  it(`waitFor (element)`, async () => {
-    await e.waitFor(e.element(e.by.id('id'))).toExist().withTimeout(0);
-    await e.waitFor(e.element(e.by.id('id'))).not.toExist().withTimeout(0);
-    await e.waitFor(e.element(e.by.id('id'))).toBeVisible();
-    await e.waitFor(e.element(e.by.id('id'))).toBeNotVisible();
-    await e.waitFor(e.element(e.by.id('id'))).toExist();
-    await e.waitFor(e.element(e.by.id('id'))).toExist().withTimeout(0);
-    await e.waitFor(e.element(e.by.id('id'))).toNotExist().withTimeout(0);
-
-    await e.waitFor(e.element(e.by.id('id'))).toHaveText('text');
-    await e.waitFor(e.element(e.by.id('id'))).toNotHaveText('value');
-    await e.waitFor(e.element(e.by.id('id'))).not.toHaveText('text');
-
-    await e.waitFor(e.element(e.by.id('id'))).toHaveLabel('text');
-    await e.waitFor(e.element(e.by.id('id'))).toNotHaveLabel('text');
-    await e.waitFor(e.element(e.by.id('id'))).not.toHaveLabel('text');
-
-    await e.waitFor(e.element(e.by.id('id'))).toHaveId('id');
-    await e.waitFor(e.element(e.by.id('id'))).toNotHaveId('id');
-    await e.waitFor(e.element(e.by.id('id'))).not.toHaveId('id');
-
-    await e.waitFor(e.element(e.by.id('id'))).toHaveValue('value');
-    await e.waitFor(e.element(e.by.id('id'))).toNotHaveValue('value');
-    await e.waitFor(e.element(e.by.id('id'))).not.toHaveValue('value');
-
-    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);
-
-  });
-
-  it(`waitFor (element) with wrong parameters should throw`, async () => {
-    await expectToThrow(() => e.waitFor('notAnElement'));
-    await expectToThrow(() => e.waitFor(e.element(e.by.id('id'))).toExist().withTimeout('notANumber'));
-    await expectToThrow(() => e.waitFor(e.element(e.by.id('id'))).toExist().withTimeout(-1));
-    await expectToThrow(() => e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement('notAnElement'));
-  });
-
-  it(`waitFor (element) with non-elements should throw`, async () => {
-    await expectToThrow(() => e.waitFor('notAnElement').toBeVisible());
-  });
-
-  describe('element interactions', () => {
-    it('should tap and long-press', async () => {
-      await e.element(e.by.label('Tap Me')).tap();
-      await e.element(e.by.label('Tap Me')).tap({ x: 10, y: 10 });
-      await e.element(e.by.label('Tap Me')).tapAtPoint({x: 100, y: 200});
-      await e.element(e.by.label('Tap Me')).longPress();
-      await e.element(e.by.id('UniqueId819')).multiTap(3);
-    });
-
-    it('should not tap and long-press given bad args', async () => {
-      await expectToThrow(() => e.element(e.by.id('UniqueId819')).multiTap('NaN'));
-    });
-
-    it('should press special keys', async () => {
-      await e.element(e.by.id('UniqueId937')).tapBackspaceKey();
-      await e.element(e.by.id('UniqueId937')).tapReturnKey();
-    });
-
-    it('should edit text', async () => {
-      await e.element(e.by.id('UniqueId937')).typeText('passcode');
-      await e.element(e.by.id('UniqueId005')).clearText();
-      await e.element(e.by.id('UniqueId005')).replaceText('replaceTo');
-    });
-
-    it('should not edit text given bad args', async () => {
-      await expectToThrow(() => e.element(e.by.id('UniqueId937')).typeText(0));
-      await expectToThrow(() => e.element(e.by.id('UniqueId005')).replaceText(3));
-    });
-
-    it('should scroll', async () => {
-      await e.element(e.by.id('ScrollView161')).scroll(100);
-      await e.element(e.by.id('ScrollView161')).scroll(100, 'down');
-      await e.element(e.by.id('ScrollView161')).scroll(100, 'up');
-      await e.element(e.by.id('ScrollView161')).scroll(100, 'right');
-      await e.element(e.by.id('ScrollView161')).scroll(100, 'left');
-      await e.element(e.by.id('ScrollView161')).scrollTo('bottom');
-      await e.element(e.by.id('ScrollView161')).scrollTo('top');
-      await e.element(e.by.id('ScrollView161')).scrollTo('left');
-      await e.element(e.by.id('ScrollView161')).scrollTo('right');
-    });
-
-    it('should not scroll given bad args', async () => {
-      await expectToThrow(() => e.element(e.by.id('ScrollView161')).scroll('NaN', 'down'));
-      await expectToThrow(() => e.element(e.by.id('ScrollView161')).scroll(100, 'noDirection'));
-      await expectToThrow(() => e.element(e.by.id('ScrollView161')).scroll(100, 0));
-      await expectToThrow(() => e.element(e.by.id('ScrollView161')).scrollTo(0));
-      await expectToThrow(() => e.element(e.by.id('ScrollView161')).scrollTo('noDirection'));
-    });
-
-    it('should swipe', async () => {
-      await e.element(e.by.id('ScrollView799')).swipe('down');
-      await e.element(e.by.id('ScrollView799')).swipe('down', 'fast');
-      await e.element(e.by.id('ScrollView799')).swipe('up', 'slow');
-      await e.element(e.by.id('ScrollView799')).swipe('left', 'fast');
-      await e.element(e.by.id('ScrollView799')).swipe('right', 'slow');
-      await e.element(e.by.id('ScrollView799')).swipe('down', 'fast', 0.9);
-      await e.element(e.by.id('ScrollView799')).swipe('up', 'slow', 0.9);
-      await e.element(e.by.id('ScrollView799')).swipe('left', 'fast', 0.9);
-      await e.element(e.by.id('ScrollView799')).swipe('right', 'slow', 0.9);
-      await e.element(e.by.id('ScrollView799')).swipe('down', 'fast', undefined, undefined, 0.25);
-      await e.element(e.by.id('ScrollView799')).swipe('up', 'slow', 0.9, 0.5, 0.5);
-    });
-
-    it('should not swipe given bad args', async () => {
-      await expectToThrow(() => e.element(e.by.id('ScrollView799')).swipe(4, 'fast'));
-      await expectToThrow(() => e.element(e.by.id('ScrollView799')).swipe('noDirection', 0));
-      await expectToThrow(() => e.element(e.by.id('ScrollView799')).swipe('noDirection', 'fast'));
-      await expectToThrow(() => e.element(e.by.id('ScrollView799')).swipe('down', 'NotFastNorSlow'));
-      await expectToThrow(() => e.element(e.by.id('ScrollView799')).swipe('down', 'NotFastNorSlow', 0.9));
-    });
-
-    it('should allow for index-based element discrepancy resolution', async () => {
-      await e.element(e.by.id('ScrollView799')).atIndex(1);
-    });
-
-    it('should fail to find index-based element given invalid args', async () => {
-      await expectToThrow(() => e.element(e.by.id('ScrollView799')).atIndex('NaN'));
-    });
-  });
-
-  describe('element screenshots', () => {
-    const tempFilePath = '/path/to/temp-file.png';
-    const invokeResultInBase64 = 'VGhlcmUgaXMgbm8gc3Bvb24h';
-
-    let tempfile;
-    let fs;
-    let _element;
-    beforeEach(() => {
-      mockExecutor.executeResult = Promise.resolve(invokeResultInBase64);
-
-      fs = require('fs-extra');
-      tempfile = require('tempfile');
-      tempfile.mockReturnValue(tempFilePath);
-
-      _element = e.element(e.by.id('FancyElement'));
-    });
-
-    it('should take and save the screenshot in a temp-file', async () => {
-      await _element.takeScreenshot();
-      expect(fs.writeFile).toHaveBeenCalledWith(tempFilePath, invokeResultInBase64, 'base64');
-      expect(tempfile).toHaveBeenCalledWith('detox.element-screenshot.png');
-    });
-
-    it('should return the path to the temp-file containing screenshot data', async () => {
-      const result = await _element.takeScreenshot();
-      expect(result).toEqual(tempFilePath);
-    });
-
-    it('should emit a named-artifact creation event', async () => {
-      const screenshotName = 'mock-screenshot-name';
-      await _element.takeScreenshot(screenshotName);
-      expect(emitter.emit).toHaveBeenCalledWith('createExternalArtifact', {
-        pluginId: 'screenshot',
-        artifactName: screenshotName,
-        artifactPath: tempFilePath,
-      });
-    });
-
-    it('should emit an artifact creation event with a default name', async () => {
-      await _element.takeScreenshot(undefined);
-      expect(emitter.emit).toHaveBeenCalledWith(
-        expect.any(String),
-        expect.objectContaining({
-          artifactName: 'temp-file',
-        },
-      ));
-    });
-  });
-});
-
-async function expectToThrow(func) {
-  try {
-    await func();
-  } catch (ex) {
-    expect(ex).toBeDefined();
-  }
-}
-
-class MockExecutor {
-  constructor() {
-    this.executeResult = undefined;
-  }
-
-  async execute(invocation) {
-    if (typeof invocation === 'function') {
-      invocation = invocation();
-    }
-    expect(invocation.target).toBeDefined();
-    expect(invocation.target.type).toBeDefined();
-    expect(invocation.target.value).toBeDefined();
-
-    this.recurse(invocation);
-    await this.timeout(1);
-    return this.executeResult ? {
-      result: this.executeResult,
-    } : undefined;
-  }
-
-  recurse(invocation) {
-    for (const key in invocation) {
-      if (invocation.hasOwnProperty(key)) {
-        if (invocation[key] instanceof Object) {
-          this.recurse(invocation[key]);
-        }
-        if (key === 'target' && invocation[key].type === 'Invocation') {
-          const innerValue = invocation.target.value;
-          expect(innerValue.target.type).toBeDefined();
-          expect(innerValue.target.value).toBeDefined();
-        }
-      }
-    }
-  }
-
-  async timeout(ms) {
-    return new Promise((resolve) => setTimeout(resolve, ms));
-  }
-}
diff --git a/detox/src/android/webExpect.test.js b/detox/src/android/webExpect.test.js
deleted file mode 100644
index 6a2837b53..000000000
--- a/detox/src/android/webExpect.test.js
+++ /dev/null
@@ -1,324 +0,0 @@
-describe('webExpect', () => {
-  let webExpect;
-  let expect;
-
-  let mockExecutor;
-  let emitter;
-  let deviceDriver;
-  beforeEach(() => {
-    jest.mock('tempfile');
-    jest.mock('fs-extra');
-
-    mockExecutor = new MockExecutor();
-
-    const Emitter = jest.genMockFromModule('../utils/AsyncEmitter');
-    emitter = new Emitter();
-
-    deviceDriver = {
-      typeText: jest.fn(),
-    };
-
-    const AndroidExpect = require('./expect');
-    expect = new AndroidExpect({
-      invocationManager: mockExecutor,
-      emitter,
-    });
-
-    const AndroidWebExpect = require('./webExpect');
-    webExpect = new AndroidWebExpect({
-      invocationManager: mockExecutor,
-      deviceDriver,
-      emitter,
-    });
-  });
-
-  describe('General', () => {
-    it('should return undefined', async () => {
-      mockExecutor.executeResult = Promise.resolve(undefined);
-      await webExpect.getWebView(expect.by.id('webview_id')).element(webExpect.by.id('any')).tap();
-    });
-  })
-
-  describe('getWebView', () => {
-
-    it('default', async () => {
-      await webExpect.getWebView().element(webExpect.by.id('id')).tap();
-    });
-
-    it('with webview matcher', async () => {
-      await webExpect.getWebView(expect.by.id('webview_id')).element(webExpect.by.id('id')).tap();
-    });
-  });
-
-  describe('WebElement Actions', () => {
-    it('tap', async () => {
-      await webExpect.getWebView().element(webExpect.by.id('id')).tap();
-      await webExpect.getWebView().element(webExpect.by.className('className')).tap();
-      await webExpect.getWebView().element(webExpect.by.cssSelector('cssSelector')).tap();
-      await webExpect.getWebView().element(webExpect.by.name('name')).tap();
-      await webExpect.getWebView().element(webExpect.by.xpath('xpath')).tap();
-      await webExpect.getWebView().element(webExpect.by.linkText('linkText')).tap();
-      await webExpect.getWebView().element(webExpect.by.partialLinkText('partialLinkText')).tap();
-      await webExpect.getWebView().element(webExpect.by.tag('tag')).tap();
-    });
-
-    it('typeText', async () => {
-      await webExpect.getWebView().element(webExpect.by.id('id')).typeText('text');
-      await webExpect.getWebView().element(webExpect.by.className('className')).typeText('text');
-      await webExpect.getWebView().element(webExpect.by.cssSelector('cssSelector')).typeText('text');
-      await webExpect.getWebView().element(webExpect.by.name('name')).typeText('text');
-      await webExpect.getWebView().element(webExpect.by.xpath('xpath')).typeText('text');
-      await webExpect.getWebView().element(webExpect.by.linkText('linkText')).typeText('text');
-      await webExpect.getWebView().element(webExpect.by.partialLinkText('partialLinkText')).typeText('text');
-      await webExpect.getWebView().element(webExpect.by.tag('tag')).typeText('text');
-    });
-
-    it('typeText with isContentEditable=false', async () => {
-      await webExpect.getWebView().element(webExpect.by.id('id')).typeText('text', false);
-      global.expect(deviceDriver.typeText).not.toHaveBeenCalled();
-    });
-
-    it('typeText with isContentEditable=true', async () => {
-      await webExpect.getWebView().element(webExpect.by.id('id')).typeText('text', true);
-      global.expect(deviceDriver.typeText).toHaveBeenCalled();
-    });
-
-    it('typeText default isContentEditable is false', async () => {
-      await webExpect.getWebView().element(webExpect.by.id('id')).typeText('text');
-      global.expect(deviceDriver.typeText).not.toHaveBeenCalled();
-    });
-
-    it('replaceText', async () => {
-      await webExpect.getWebView().element(webExpect.by.id('id')).replaceText('text');
-      await webExpect.getWebView().element(webExpect.by.className('className')).replaceText('text');
-      await webExpect.getWebView().element(webExpect.by.cssSelector('cssSelector')).replaceText('text');
-      await webExpect.getWebView().element(webExpect.by.name('name')).replaceText('text');
-      await webExpect.getWebView().element(webExpect.by.xpath('xpath')).replaceText('text');
-      await webExpect.getWebView().element(webExpect.by.linkText('linkText')).replaceText('text');
-      await webExpect.getWebView().element(webExpect.by.partialLinkText('partialLinkText')).replaceText('text');
-      await webExpect.getWebView().element(webExpect.by.tag('tag')).replaceText('text');
-    });
-
-    it('clearText', async () => {
-      await webExpect.getWebView().element(webExpect.by.id('id')).clearText();
-      await webExpect.getWebView().element(webExpect.by.className('className')).clearText();
-      await webExpect.getWebView().element(webExpect.by.cssSelector('cssSelector')).clearText();
-      await webExpect.getWebView().element(webExpect.by.name('name')).clearText();
-      await webExpect.getWebView().element(webExpect.by.xpath('xpath')).clearText();
-      await webExpect.getWebView().element(webExpect.by.linkText('linkText')).clearText();
-      await webExpect.getWebView().element(webExpect.by.partialLinkText('partialLinkText')).clearText();
-      await webExpect.getWebView().element(webExpect.by.tag('tag')).clearText();
-    });
-
-    it('scrollToView', async () => {
-      await webExpect.getWebView().element(webExpect.by.id('id')).scrollToView();
-      await webExpect.getWebView().element(webExpect.by.className('className')).scrollToView();
-      await webExpect.getWebView().element(webExpect.by.cssSelector('cssSelector')).scrollToView();
-      await webExpect.getWebView().element(webExpect.by.name('name')).scrollToView();
-      await webExpect.getWebView().element(webExpect.by.xpath('xpath')).scrollToView();
-      await webExpect.getWebView().element(webExpect.by.linkText('linkText')).scrollToView();
-      await webExpect.getWebView().element(webExpect.by.partialLinkText('partialLinkText')).scrollToView();
-      await webExpect.getWebView().element(webExpect.by.tag('tag')).scrollToView();
-    });
-
-    it('getText', async () => {
-      await webExpect.getWebView().element(webExpect.by.id('id')).getText();
-      await webExpect.getWebView().element(webExpect.by.className('className')).getText();
-      await webExpect.getWebView().element(webExpect.by.cssSelector('cssSelector')).getText();
-      await webExpect.getWebView().element(webExpect.by.name('name')).getText();
-      await webExpect.getWebView().element(webExpect.by.xpath('xpath')).getText();
-      await webExpect.getWebView().element(webExpect.by.linkText('linkText')).getText();
-      await webExpect.getWebView().element(webExpect.by.partialLinkText('partialLinkText')).getText();
-      await webExpect.getWebView().element(webExpect.by.tag('tag')).getText();
-    });
-
-    it('focus', async () => {
-      await webExpect.getWebView().element(webExpect.by.id('id')).focus();
-      await webExpect.getWebView().element(webExpect.by.className('className')).focus();
-      await webExpect.getWebView().element(webExpect.by.cssSelector('cssSelector')).focus();
-      await webExpect.getWebView().element(webExpect.by.name('name')).focus();
-      await webExpect.getWebView().element(webExpect.by.xpath('xpath')).focus();
-      await webExpect.getWebView().element(webExpect.by.linkText('linkText')).focus();
-      await webExpect.getWebView().element(webExpect.by.partialLinkText('partialLinkText')).focus();
-      await webExpect.getWebView().element(webExpect.by.tag('tag')).focus();
-    });
-
-    it('selectAllText', async () => {
-      await webExpect.getWebView().element(webExpect.by.id('id')).selectAllText();
-      await webExpect.getWebView().element(webExpect.by.className('className')).selectAllText();
-      await webExpect.getWebView().element(webExpect.by.cssSelector('cssSelector')).selectAllText();
-      await webExpect.getWebView().element(webExpect.by.name('name')).selectAllText();
-      await webExpect.getWebView().element(webExpect.by.xpath('xpath')).selectAllText();
-      await webExpect.getWebView().element(webExpect.by.linkText('linkText')).selectAllText();
-      await webExpect.getWebView().element(webExpect.by.partialLinkText('partialLinkText')).selectAllText();
-      await webExpect.getWebView().element(webExpect.by.tag('tag')).selectAllText();
-    });
-
-    it('moveCursorToEnd', async () => {
-      await webExpect.getWebView().element(webExpect.by.id('id')).moveCursorToEnd();
-      await webExpect.getWebView().element(webExpect.by.className('className')).moveCursorToEnd();
-      await webExpect.getWebView().element(webExpect.by.cssSelector('cssSelector')).moveCursorToEnd();
-      await webExpect.getWebView().element(webExpect.by.name('name')).moveCursorToEnd();
-      await webExpect.getWebView().element(webExpect.by.xpath('xpath')).moveCursorToEnd();
-      await webExpect.getWebView().element(webExpect.by.linkText('linkText')).moveCursorToEnd();
-      await webExpect.getWebView().element(webExpect.by.partialLinkText('partialLinkText')).moveCursorToEnd();
-      await webExpect.getWebView().element(webExpect.by.tag('tag')).moveCursorToEnd();
-    });
-
-    it('runScript', async () => {
-      const script = 'function foo(el) {}';
-      await webExpect.getWebView().element(webExpect.by.id('id')).runScript(script);
-      await webExpect.getWebView().element(webExpect.by.className('className')).runScript(script);
-      await webExpect.getWebView().element(webExpect.by.cssSelector('cssSelector')).runScript(script);
-      await webExpect.getWebView().element(webExpect.by.name('name')).runScript(script);
-      await webExpect.getWebView().element(webExpect.by.xpath('xpath')).runScript(script);
-      await webExpect.getWebView().element(webExpect.by.linkText('linkText')).runScript(script);
-      await webExpect.getWebView().element(webExpect.by.partialLinkText('partialLinkText')).runScript(script);
-      await webExpect.getWebView().element(webExpect.by.tag('tag')).runScript(script);
-    });
-
-    it('runScriptWithArgs', async () => {
-      const script = 'function bar(a,b) {}';
-      const argsArr = ['fooA','barB'];
-      await webExpect.getWebView().element(webExpect.by.id('id')).runScriptWithArgs(script, argsArr);
-      await webExpect.getWebView().element(webExpect.by.className('className')).runScriptWithArgs(script, argsArr);
-      await webExpect.getWebView().element(webExpect.by.cssSelector('cssSelector')).runScriptWithArgs(script, argsArr);
-      await webExpect.getWebView().element(webExpect.by.name('name')).runScriptWithArgs(script, argsArr);
-      await webExpect.getWebView().element(webExpect.by.xpath('xpath')).runScriptWithArgs(script, argsArr);
-      await webExpect.getWebView().element(webExpect.by.linkText('linkText')).runScriptWithArgs(script, argsArr);
-      await webExpect.getWebView().element(webExpect.by.partialLinkText('partialLinkText')).runScriptWithArgs(script, argsArr);
-      await webExpect.getWebView().element(webExpect.by.tag('tag')).runScriptWithArgs(script, argsArr);
-    });
-
-    it('getCurrentUrl', async () => {
-      await webExpect.getWebView().element(webExpect.by.id('id')).getCurrentUrl();
-      await webExpect.getWebView().element(webExpect.by.className('className')).getCurrentUrl();
-      await webExpect.getWebView().element(webExpect.by.cssSelector('cssSelector')).getCurrentUrl();
-      await webExpect.getWebView().element(webExpect.by.name('name')).getCurrentUrl();
-      await webExpect.getWebView().element(webExpect.by.xpath('xpath')).getCurrentUrl();
-      await webExpect.getWebView().element(webExpect.by.linkText('linkText')).getCurrentUrl();
-      await webExpect.getWebView().element(webExpect.by.partialLinkText('partialLinkText')).getCurrentUrl();
-      await webExpect.getWebView().element(webExpect.by.tag('tag')).getCurrentUrl();
-    });
-
-    it('getTitle', async () => {
-      await webExpect.getWebView().element(webExpect.by.id('id')).getTitle();
-      await webExpect.getWebView().element(webExpect.by.className('className')).getTitle();
-      await webExpect.getWebView().element(webExpect.by.cssSelector('cssSelector')).getTitle();
-      await webExpect.getWebView().element(webExpect.by.name('name')).getTitle();
-      await webExpect.getWebView().element(webExpect.by.xpath('xpath')).getTitle();
-      await webExpect.getWebView().element(webExpect.by.linkText('linkText')).getTitle();
-      await webExpect.getWebView().element(webExpect.by.partialLinkText('partialLinkText')).getTitle();
-      await webExpect.getWebView().element(webExpect.by.tag('tag')).getTitle();
-    });
-
-    it('should allow for access to by via element', async () => {
-      const webview = await webExpect.getWebView();
-      await webview.element(webview.by.id('id')).tap();
-    });
-  })
-
-  describe('Web Matchers',() => {
-      it('by.id', async () => {
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.id('id'))).toExist();
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.id('id'))).not.toExist();
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.id('id'))).toHaveText('text');
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.id('id'))).not.toHaveText('text');
-      });
-
-      it('by.className', async () => {
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.className('className'))).toExist();
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.className('className'))).not.toExist();
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.className('className'))).toHaveText('text');
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.className('className'))).not.toHaveText('text');
-      });
-
-      it('by.cssSelector', async () => {
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.cssSelector('cssSelector'))).toExist();
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.cssSelector('cssSelector'))).not.toExist();
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.cssSelector('cssSelector'))).toHaveText('text');
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.cssSelector('cssSelector'))).not.toHaveText('text');
-      });
-
-      it('by.name', async () => {
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.name('name'))).toExist();
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.name('name'))).not.toExist();
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.name('name'))).toHaveText('text');
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.name('name'))).not.toHaveText('text');
-      });
-
-      it('by.xpath', async () => {
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.xpath('xpath'))).toExist();
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.xpath('xpath'))).not.toExist();
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.xpath('xpath'))).toHaveText('text');
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.xpath('xpath'))).not.toHaveText('text');
-      });
-
-      it('by.linkText', async () => {
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.linkText('link'))).toExist();
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.linkText('link'))).not.toExist();
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.linkText('link'))).toHaveText('text');
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.linkText('link'))).not.toHaveText('text');
-      });
-
-      it('by.partialLinkText', async () => {
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.partialLinkText('lin'))).toExist();
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.partialLinkText('lin'))).not.toExist();
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.partialLinkText('lin'))).toHaveText('text');
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.partialLinkText('lin'))).not.toHaveText('text');
-      });
-
-      it('by.tag', async () => {
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.tag('tag'))).toExist();
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.tag('tag'))).not.toExist();
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.tag('tag'))).toHaveText('text');
-        await webExpect.expect(webExpect.getWebView().element(webExpect.by.tag('tag'))).not.toHaveText('text');
-      });
-
-      it('should allow for access to except via element', async () => {
-        const webview = await webExpect.getWebView();
-        await webview.expect(webview.element(webview.by.id('id'))).toExist();
-      });
-  });
-});
-
-
-class MockExecutor {
-  constructor() {
-    this.executeResult = undefined;
-  }
-
-  async execute(invocation) {
-    if (typeof invocation === 'function') {
-      invocation = invocation();
-    }
-    expect(invocation.target).toBeDefined();
-    expect(invocation.target.type).toBeDefined();
-    expect(invocation.target.value).toBeDefined();
-
-    this.recurse(invocation);
-    await this.timeout(1);
-    return this.executeResult ? {
-      result: this.executeResult,
-    } : undefined;
-  }
-
-  recurse(invocation) {
-    for (const key in invocation) {
-      if (invocation.hasOwnProperty(key)) {
-        if (invocation[key] instanceof Object) {
-          this.recurse(invocation[key]);
-        }
-        if (key === 'target' && invocation[key].type === 'Invocation') {
-          const innerValue = invocation.target.value;
-          expect(innerValue.target.type).toBeDefined();
-          expect(innerValue.target.value).toBeDefined();
-        }
-      }
-    }
-  }
-
-  async timeout(ms) {
-    return new Promise((resolve) => setTimeout(resolve, ms));
-  }
-}
diff --git a/detox/src/devices/Device.test.js b/detox/src/devices/Device.test.js
index 991e50b9f..1d15ad5c4 100644
--- a/detox/src/devices/Device.test.js
+++ b/detox/src/devices/Device.test.js
@@ -772,6 +772,14 @@ describe('Device', () => {
     expect(driverMock.driver.resetStatusBar).toHaveBeenCalledWith(device.id);
   });
 
+  it(`typeText() should pass to device driver`, async () => {
+    const device = await aValidDevice();
+    const params = {};
+    await device.typeText('Text');
+
+    expect(driverMock.driver.typeText).toHaveBeenCalledWith(device.id, 'Text');
+  });
+
   it(`shake() should pass to device driver`, async () => {
     const device = await aValidDevice();
     await device.shake();
diff --git a/detox/src/matchersRegistry.test.js b/detox/src/matchersRegistry.test.js
index 77587a0f9..41c59cdac 100644
--- a/detox/src/matchersRegistry.test.js
+++ b/detox/src/matchersRegistry.test.js
@@ -7,18 +7,13 @@ describe('Detox matchers registry', () => {
   };
 
   let AndroidExpect;
-  let AndroidWebExpect;
   let IosExpect;
   let device;
-  let deviceDriver;
   let resolveModuleFromPath;
   let uut;
   beforeEach(() => {
-    jest.mock('./android/expect');
-    AndroidExpect = require('./android/expect');
-
-    jest.mock('./android/webExpect');
-    AndroidWebExpect = require('./android/webExpect');
+    jest.mock('./android/AndroidExpect');
+    AndroidExpect = require('./android/AndroidExpect');
 
     jest.mock('./ios/expectTwo');
     IosExpect = require('./ios/expectTwo');
@@ -33,8 +28,6 @@ describe('Detox matchers registry', () => {
       getPlatform: jest.fn(),
     };
 
-    deviceDriver = {};
-
     uut = require('./matchersRegistry');
   });
 
@@ -45,15 +38,10 @@ describe('Detox matchers registry', () => {
     device.type = deviceType;
   }
 
-  it('should resolve Android matchers (native, web)', () => {
-    const opts = {
-      deviceDriver,
-    };
-
+  it('should resolve the Android matchers', () => {
     withAndroidDevice();
-    const { matchers, webMatchers } = uut.resolve(device, opts);
-    expect(matchers).toBeInstanceOf(AndroidExpect);
-    expect(webMatchers).toBeInstanceOf(AndroidWebExpect);
+    const result = uut.resolve(device);
+    expect(result).toBeInstanceOf(AndroidExpect);
   });
 
   it('should init the Android-matchers with opts', () => {
@@ -64,9 +52,8 @@ describe('Detox matchers registry', () => {
 
   it('should resolve the iOS matchers', () => {
     withIosDevice();
-    const {matchers, webMatchers} = uut.resolve(device);
-    expect(matchers).toBeInstanceOf(IosExpect);
-    expect(webMatchers).toBeUndefined();
+    const result = uut.resolve(device);
+    expect(result).toBeInstanceOf(IosExpect);
   });
 
   it('should init the ios-matchers with opts', () => {
@@ -79,9 +66,8 @@ describe('Detox matchers registry', () => {
     const deviceType = './path/to/external/module/index.js';
     withUnknownDevice(deviceType);
 
-    const {matchers, webMatchers} = uut.resolve(device, opts);
-    expect(matchers).toBeInstanceOf(MockExternalModuleExpect);
-    expect(webMatchers).toBeUndefined();
+    const result = uut.resolve(device, opts);
+    expect(result).toBeInstanceOf(MockExternalModuleExpect);
     expect(resolveModuleFromPath).toHaveBeenCalledWith(deviceType);
   });
 });
diff --git a/detox/test/e2e/28.webview.test.js b/detox/test/e2e/28.webview.test.js
index 8806cc574..67eef5683 100644
--- a/detox/test/e2e/28.webview.test.js
+++ b/detox/test/e2e/28.webview.test.js
@@ -1,73 +1,70 @@
 const MOCK_TEXT = 'Mock Text';
 
 describe(':android: WebView', () => {
+  let webview_1;
 
-  let webview;
   beforeEach(async () => {
     await device.reloadReactNative();
     await element(by.text('WebView')).tap();
-    webview = await getWebView();
+    webview_1 = web(by.id('webview_1'));
   });
 
-  const getWebView = async (matcher) =>
-    web.getWebView(matcher || by.id('webview_1'));
-
   describe('Expectations',() => {
     it('expect element to exists', async () => {
-      await web.expect(webview.element(web.by.id('testingPar'))).toExist();
+      await expect(webview_1.element(by.htmlId('testingPar'))).toExist();
     });
 
     it('expect element to NOT exists', async () => {
-      await web.expect(webview.element(web.by.id('not_found'))).not.toExist();
+      await expect(webview_1.element(by.htmlId('not_found'))).not.toExist();
     });
 
     it('expect element to have text', async () => {
-      await web.expect(webview.element(web.by.id('testingPar'))).toHaveText('Message');
+      await expect(webview_1.element(by.htmlId('testingPar'))).toHaveText('Message');
     });
 
     it('expect element to NOT have text', async () => {
-      await web.expect(webview.element(web.by.id('testingPar'))).not.toHaveText(MOCK_TEXT);
+      await expect(webview_1.element(by.htmlId('testingPar'))).not.toHaveText(MOCK_TEXT);
     });
   });
 
   describe('Element Matchers',() => {
     it('expect to find element by id', async () => {
-      await web.expect(webview.element(web.by.id('testingh1'))).toExist();
+      await expect(webview_1.element(by.htmlId('testingh1'))).toExist();
     });
 
     it('expect to find element by className', async () => {
-      await web.expect(webview.element(web.by.className('a'))).toExist();
+      await expect(webview_1.element(by.className('a'))).toExist();
     });
 
     it('expect to find element by cssSelector', async () => {
-      await web.expect(webview.element(web.by.cssSelector('#cssSelector'))).toExist();
+      await expect(webview_1.element(by.cssSelector('#cssSelector'))).toExist();
     });
 
     it('expect to find element by name', async () => {
-      await web.expect(webview.element(web.by.name('sec_input'))).toExist();
+      await expect(webview_1.element(by.name('sec_input'))).toExist();
     });
 
     it('expect to find element by xpath', async () => {
-      await web.expect(webview.element(web.by.xpath('//*[@id="testingh1-1"]'))).toExist();
+      await expect(webview_1.element(by.xpath('//*[@id="testingh1-1"]'))).toExist();
     });
 
     it('expect to find element by linkText', async () => {
-      await web.expect(webview.element(web.by.linkText('disney.com'))).toExist();
+      await expect(webview_1.element(by.linkText('disney.com'))).toExist();
     });
 
     it('expect to find element by partialLinkText', async () => {
-      await web.expect(webview.element(web.by.partialLinkText('disney'))).toExist();
+      await expect(webview_1.element(by.partialLinkText('disney'))).toExist();
     });
 
     it('expect to find element by tag', async () => {
-      await web.expect(webview.element(web.by.tag('mark'))).toExist();
+      await expect(webview_1.element(by.tag('mark'))).toExist();
     });
   });
 
   describe('ContentEditable', () => {
 
     it('should replace text by selecting all text', async () => {
-        const editable = await webview.element(web.by.className('public-DraftEditor-content'));
+        const editable = await webview_1.element(by.className('public-DraftEditor-content'));
         const text = await editable.getText();
 
         await editable.scrollToView();
@@ -79,12 +76,12 @@ describe(':android: WebView', () => {
 
         await editable.typeText(MOCK_TEXT, true);
 
-        await web.expect(editable).not.toHaveText(text);
-        await web.expect(editable).toHaveText(MOCK_TEXT);
+        await expect(editable).not.toHaveText(text);
+        await expect(editable).toHaveText(MOCK_TEXT);
     });
 
     it('move cursor to end and add text', async () => {
-      const editable = await webview.element(web.by.className('public-DraftEditor-content'));
+      const editable = await webview_1.element(by.className('public-DraftEditor-content'));
       await editable.scrollToView();
 
       //tapping, (at the moment not working on content-editable)
@@ -100,59 +97,59 @@ describe(':android: WebView', () => {
       await editable.moveCursorToEnd();
       await editable.typeText(ADDITION_TEXT, true);
 
-      await web.expect(editable).toHaveText(MOCK_TEXT + ADDITION_TEXT);
+      await expect(editable).toHaveText(MOCK_TEXT + ADDITION_TEXT);
     });
   });
 
   it('should set input and change text', async () => {
     // Verify initial value
-    const para = webview.element(web.by.id('testingPar'));
-    await web.expect(para).toHaveText('Message');
+    const para = webview_1.element(by.htmlId('testingPar'));
+    await expect(para).toHaveText('Message');
 
-    const textInput = await webview.element(web.by.id('textInput'));
+    const textInput = await webview_1.element(by.htmlId('textInput'));
     await textInput.scrollToView();
     await textInput.tap();
     await textInput.typeText(MOCK_TEXT);
 
-    await webview.element(web.by.id('changeTextBtn')).tap();
+    await webview_1.element(by.htmlId('changeTextBtn')).tap();
     // Verify text updated
-    await web.expect(para).toHaveText(MOCK_TEXT);
+    await expect(para).toHaveText(MOCK_TEXT);
   });
 
   it('should header get text and verify its value', async () => {
-    const text = await webview.element(web.by.id('testingh1')).getText();
+    const text = await webview_1.element(by.htmlId('testingh1')).getText();
 
-    const textInput = await webview.element(web.by.id('textInput'));
+    const textInput = await webview_1.element(by.htmlId('textInput'));
     await textInput.scrollToView();
     await textInput.tap();
     await textInput.typeText(text);
 
-    await webview.element(web.by.id('changeTextBtn')).tap();
+    await webview_1.element(by.htmlId('changeTextBtn')).tap();
 
     // Verify text is the title text
-    await web.expect(webview.element(web.by.id('testingPar'))).toHaveText(text);
+    await expect(webview_1.element(by.htmlId('testingPar'))).toHaveText(text);
 
   });
 
   it('should replace text', async () => {
-    const textInput = await webview.element(web.by.id('textInput'));
+    const textInput = await webview_1.element(by.htmlId('textInput'));
     await textInput.scrollToView();
     await textInput.tap();
     await textInput.typeText('first text');
 
-    await webview.element(web.by.id('changeTextBtn')).tap();
-    await web.expect(webview.element(web.by.id('testingPar'))).toHaveText('first text');
+    await webview_1.element(by.htmlId('changeTextBtn')).tap();
+    await expect(webview_1.element(by.htmlId('testingPar'))).toHaveText('first text');
 
     await textInput.replaceText(MOCK_TEXT);
-    await webview.element(web.by.id('changeTextBtn')).tap();
+    await webview_1.element(by.htmlId('changeTextBtn')).tap();
 
     // Verify param value is the latest changed text
-    await web.expect(webview.element(web.by.id('testingPar'))).toHaveText(MOCK_TEXT);
+    await expect(webview_1.element(by.htmlId('testingPar'))).toHaveText(MOCK_TEXT);
   });
 
   it('getWebView with matcher id', async () => {
-    const webview2 = await getWebView(by.id('webview_2'));
-    await web.expect(webview2.element(web.by.tag('p'))).toHaveText('Second Webview');
+    const webview_2 = await web(by.id('webview_2'));
+    await expect(webview_2.element(by.tag('p'))).toHaveText('Second Webview');
   });
 
 });
diff --git a/detox/test/types/detox-global-tests.ts b/detox/test/types/detox-global-tests.ts
index 881cd1c6a..ab10ee08c 100644
--- a/detox/test/types/detox-global-tests.ts
+++ b/detox/test/types/detox-global-tests.ts
@@ -38,8 +38,9 @@ describe("Test", () => {
             .whileElement(by.id("ScrollView630"))
             .scroll(50, "down");
 
-        await web.expect(
-            web.getWebView(by.id("webview")).element(web.by.cssSelector("#cssSel"), 2)
-            ).toExist();
+        await web.element(by.htmlId("btnSave")).tap();
+        await expect(
+            web(by.id("webview")).element(by.cssSelector("#cssSel"), 2)
+        ).toExist();
     });
 });