Repository: wix/Detox (js)

Issue:
toHaveValue() for Switch does not match the view on Android
**Describe the bug**
`await expect(element(by.id('passcode-switch'))).toHaveValue('0');`

causes the following error

```
androidx.test.espresso.base.DefaultFailureHandler$AssertionFailedWithCauseError: '(with content description text: is "0" and view has effective visibility=VISIBLE)' doesn't match the selected view.
    Expected: (with content description text: is "0" and view has effective visibility=VISIBLE)
         Got: "ReactSwitch{id=3503, visibility=VISIBLE, width=122, height=71, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=true, is-enabled=true, is-focused=false, is-focusable=true, is-layout-requested=false, is-selected=false, layout-params=android.view.ViewGroup$LayoutParams@36b9888, tag=passcode-switch, root-is-layout-requested=false, has-input-connection=false, x=874.0, y=21.0, text=, input-type=0, ime-target=false, has-links=false, is-checked=false}"

    	at dalvik.system.VMStack.getThreadStackTrace(Native Method)
    	at java.lang.Thread.getStackTrace(Thread.java:1536)
    	at androidx.test.espresso.base.DefaultFailureHandler.getUserFriendlyError(DefaultFailureHandler.java:96)
    	at androidx.test.espresso.base.DefaultFailureHandler.handle(DefaultFailureHandler.java:59)
    	at androidx.test.espresso.ViewInteraction.waitForAndHandleInteractionResults(ViewInteraction.java:324)
    	at androidx.test.espresso.ViewInteraction.check(ViewInteraction.java:306)
    	at com.wix.detox.espresso.DetoxAssertion.assertMatcher(DetoxAssertion.java:32)
    	at java.lang.reflect.Method.invoke(Native Method)
    	at org.apache.commons.lang3.reflect.MethodUtils.invokeStaticMethod(MethodUtils.java:443)
    	at org.apache.commons.lang3.reflect.MethodUtils.invokeStaticMethod(MethodUtils.java:405)
    	at com.wix.invoke.types.ClassTarget.execute(ClassTarget.java:23)
    	at com.wix.invoke.types.Target.invoke(Target.java:59)
    	at com.wix.invoke.MethodInvocation.invoke(MethodInvocation.java:35)
    	at com.wix.invoke.MethodInvocation.invoke(MethodInvocation.java:26)
    	at com.wix.invoke.MethodInvocation.invoke(MethodInvocation.java:20)
    	at com.wix.detox.InvokeActionHandler.handle(DetoxActionHandlers.kt:52)
    	at com.wix.detox.DetoxManager$4.run(DetoxManager.java:121)
    	at android.os.Handler.handleCallback(Handler.java:789)
    	at android.os.Handler.dispatchMessage(Handler.java:98)
    	at android.os.Looper.loop(Looper.java:164)
    	at com.wix.detox.Detox$1.run(Detox.java:135)
    	at java.lang.Thread.run(Thread.java:764)
    Caused by: junit.framework.AssertionFailedError: '(with content description text: is "0" and view has effective visibility=VISIBLE)' doesn't match the selected view.
    Expected: (with content description text: is "0" and view has effective visibility=VISIBLE)
         Got: "ReactSwitch{id=3503, visibility=VISIBLE, width=122, height=71, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=true, is-enabled=true, is-focused=false, is-focusable=true, is-layout-requested=false, is-selected=false, layout-params=android.view.ViewGroup$LayoutParams@36b9888, tag=passcode-switch, root-is-layout-requested=false, has-input-connection=false, x=874.0, y=21.0, text=, input-type=0, ime-target=false, has-links=false, is-checked=false}"

    	at androidx.test.espresso.matcher.ViewMatchers.assertThat(ViewMatchers.java:540)
    	at com.wix.detox.espresso.assertion.ViewAssertions$MatchesViewAssertion.check(ViewAssertions.java:52)
    	at androidx.test.espresso.ViewInteraction$SingleExecutionViewAssertion.check(ViewInteraction.java:425)
    	at androidx.test.espresso.ViewInteraction$2.call(ViewInteraction.java:288)
    	at androidx.test.espresso.ViewInteraction$2.call(ViewInteraction.java:272)
    	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    	at android.os.Handler.handleCallback(Handler.java:789)
    	at android.os.Handler.dispatchMessage(Handler.java:98)
    	at android.os.Looper.loop(Looper.java:164)
    	at android.app.ActivityThread.main(ActivityThread.java:6541)
    	at java.lang.reflect.Method.invoke(Native Method)
    	at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
    	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)
```

**To Reproduce**

- [x] I have tested this issue on the latest Detox release and it still reproduces

<!--
Provide the steps necessary to reproduce the issue. If you are seeing a regression, try to provide the last known version where the issue did not reproduce.
-->

Steps to reproduce:
1. Create simple component
```
import React from 'react';
import { Switch as ReactSwitch } from 'react-native';
class PasscodeSwitch extends React.Component {
  render() {
    return (
       <ReactSwitch
         testID="passcode-switch"
         value={false}
       />
    )
  }
}
```
2. Create a test `switch.spec.js`
```
describe('Switch', () => {
  it('should check if the Switch is OFF', async () => {
    await expect(element(by.id('passcode-switch'))).toHaveValue('0');
  })
})
```
3. Run the test
`npx detox test -l verbose --configuration android.emu.debug  --findRelatedTests 'e2e/switch.spec.js'`

<!--
If possible, please provide a small demo project that reproduces the issue, or attach a video with the reproduction - this would be very appreciated.
-->

**Expected behavior**
The test should pass

**Screenshots**
N/A

**Device and Verbose Detox Logs**
<!--
Provide the device and verbose Detox logs so we can understand what happened.
Detox logs can be obtained by passing the loglevel param: `detox test --loglevel trace`.
Device logs can be retrieved from the device using `adb logcat`, or if recorded, Detox' artifacts.
-->

logcat log
```
03-25 16:41:14.579 19875 19952 I DetoxManager: onAction: type: invoke params: {"target":{"type":"Class","value":"com.wix.detox.espresso.DetoxAssertion"},"method":"assertMatcher","args":[{"type":"Invocation","value":{"target":{"type":"Class","value":"androidx.test.espresso.Espresso"},"method":"onView","args":[{"type":"Invocation","value":{"target":{"type":"Class","value":"com.wix.detox.espresso.DetoxMatcher"},"method":"matcherForTestId","args":["passcode-switch"]}}]}},{"type":"Invocation","value":{"target":{"type":"Class","value":"com.wix.detox.espresso.DetoxMatcher"},"method":"matcherForContentDescription","args":["0"]}}]}
03-25 16:41:14.579 19875 19930 D Detox   : class com.wix.detox.espresso.DetoxMatcher, matcherForTestId, [passcode-switch]
03-25 16:41:14.579 19875 19930 D Detox   : class androidx.test.espresso.Espresso, onView, [(with tag value: is "passcode-switch" and view has effective visibility=VISIBLE)]
03-25 16:41:14.580 19875 19930 D Detox   : class com.wix.detox.espresso.DetoxMatcher, matcherForContentDescription, [0]
03-25 16:41:14.580 19875 19930 D Detox   : class com.wix.detox.espresso.DetoxAssertion, assertMatcher, [androidx.test.espresso.ViewInteraction@537ac2b, (with content description text: is "0" and view has effective visibility=VISIBLE)]
03-25 16:41:14.582 19875 19875 I ViewInteraction: Checking 'MatchesViewAssertion(Detox){viewMatcher=(with content description text: is "0" and view has effective visibility=VISIBLE)}' assertion on view (with tag value: is "passcode-switch" and view has effective visibility=VISIBLE)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: Exception
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: java.lang.reflect.InvocationTargetException
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at java.lang.reflect.Method.invoke(Native Method)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at org.apache.commons.lang3.reflect.MethodUtils.invokeStaticMethod(MethodUtils.java:443)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at org.apache.commons.lang3.reflect.MethodUtils.invokeStaticMethod(MethodUtils.java:405)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at com.wix.invoke.types.ClassTarget.execute(ClassTarget.java:23)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at com.wix.invoke.types.Target.invoke(Target.java:59)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at com.wix.invoke.MethodInvocation.invoke(MethodInvocation.java:35)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at com.wix.invoke.MethodInvocation.invoke(MethodInvocation.java:26)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at com.wix.invoke.MethodInvocation.invoke(MethodInvocation.java:20)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at com.wix.detox.InvokeActionHandler.handle(DetoxActionHandlers.kt:52)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at com.wix.detox.DetoxManager$4.run(DetoxManager.java:121)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at android.os.Handler.handleCallback(Handler.java:789)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at android.os.Handler.dispatchMessage(Handler.java:98)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at android.os.Looper.loop(Looper.java:164)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at com.wix.detox.Detox$1.run(Detox.java:135)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at java.lang.Thread.run(Thread.java:764)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: Caused by: androidx.test.espresso.base.DefaultFailureHandler$AssertionFailedWithCauseError: '(with content description text: is "0" and view has effective visibility=VISIBLE)' doesn't match the selected view.
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: Expected: (with content description text: is "0" and view has effective visibility=VISIBLE)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers:      Got: "ReactSwitch{id=3503, visibility=VISIBLE, width=122, height=71, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=true, is-enabled=true, is-focused=false, is-focusable=true, is-layout-requested=false, is-selected=false, layout-params=android.view.ViewGroup$LayoutParams@36b9888, tag=passcode-switch, root-is-layout-requested=false, has-input-connection=false, x=874.0, y=21.0, text=, input-type=0, ime-target=false, has-links=false, is-checked=false}"
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at dalvik.system.VMStack.getThreadStackTrace(Native Method)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at java.lang.Thread.getStackTrace(Thread.java:1536)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at androidx.test.espresso.base.DefaultFailureHandler.getUserFriendlyError(DefaultFailureHandler.java:96)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at androidx.test.espresso.base.DefaultFailureHandler.handle(DefaultFailureHandler.java:59)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at androidx.test.espresso.ViewInteraction.waitForAndHandleInteractionResults(ViewInteraction.java:324)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at androidx.test.espresso.ViewInteraction.check(ViewInteraction.java:306)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at com.wix.detox.espresso.DetoxAssertion.assertMatcher(DetoxAssertion.java:32)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	... 15 more
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: Caused by: junit.framework.AssertionFailedError: '(with content description text: is "0" and view has effective visibility=VISIBLE)' doesn't match the selected view.
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: Expected: (with content description text: is "0" and view has effective visibility=VISIBLE)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers:      Got: "ReactSwitch{id=3503, visibility=VISIBLE, width=122, height=71, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=true, is-enabled=true, is-focused=false, is-focusable=true, is-layout-requested=false, is-selected=false, layout-params=android.view.ViewGroup$LayoutParams@36b9888, tag=passcode-switch, root-is-layout-requested=false, has-input-connection=false, x=874.0, y=21.0, text=, input-type=0, ime-target=false, has-links=false, is-checked=false}"
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at androidx.test.espresso.matcher.ViewMatchers.assertThat(ViewMatchers.java:540)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at com.wix.detox.espresso.assertion.ViewAssertions$MatchesViewAssertion.check(ViewAssertions.java:52)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at androidx.test.espresso.ViewInteraction$SingleExecutionViewAssertion.check(ViewInteraction.java:425)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at androidx.test.espresso.ViewInteraction$2.call(ViewInteraction.java:288)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at androidx.test.espresso.ViewInteraction$2.call(ViewInteraction.java:272)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at android.os.Handler.handleCallback(Handler.java:789)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at android.os.Handler.dispatchMessage(Handler.java:98)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at android.os.Looper.loop(Looper.java:164)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at android.app.ActivityThread.main(ActivityThread.java:6541)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at java.lang.reflect.Method.invoke(Native Method)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
03-25 16:41:14.584 19875 19930 E DetoxActionHandlers: 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)
```

**Environment (please complete the following information):**
 - Detox: `16.0.1`
 - React Native: `0.61.5`
 - Node: `12.8.0`
 - Device: `Pixel_2_API_28 emulator`
 - OS: `Android`
 - Test-runner `jest`

<!-- Note: Test-runner is set in Detox.test-runner in your package.json -->


Pull request description:
Introduce expectation for matching toggleable widgets
- [ ] This is a small change 
- [ ] This change has been discussed in issue #<?> and the solution has been agreed upon with maintainers.

---

**Description:**
Fixes #1972 by introducing an explicit expect-API for supporting anything toggle-able, such as option-switches and check-boxes.

Patch:
diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/DetoxMatcher.java b/detox/android/detox/src/main/java/com/wix/detox/espresso/DetoxMatcher.java
index 63d9fdf7d..84d0b5f27 100644
--- a/detox/android/detox/src/main/java/com/wix/detox/espresso/DetoxMatcher.java
+++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/DetoxMatcher.java
@@ -8,9 +8,11 @@ import androidx.test.espresso.matcher.ViewMatchers.Visibility;
 
 import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
 import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom;
+import static androidx.test.espresso.matcher.ViewMatchers.isChecked;
 import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
 import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
 import static androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast;
+import static androidx.test.espresso.matcher.ViewMatchers.isNotChecked;
 import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
 import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
 import static androidx.test.espresso.matcher.ViewMatchers.withTagValue;
@@ -47,6 +49,10 @@ public class DetoxMatcher {
         return allOf(withTagValue(is((Object) testId)), withEffectiveVisibility(Visibility.VISIBLE));
     }
 
+    public static Matcher<View> matcherForToggleable(boolean value) {
+        return (value ? isChecked() : isNotChecked());
+    }
+
     public static Matcher<View> matcherForAnd(Matcher<View> m1, Matcher<View> m2) {
         return allOf(m1, m2);
     }
diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/matcher/ViewMatchers.kt b/detox/android/detox/src/main/java/com/wix/detox/espresso/matcher/ViewMatchers.kt
index 81bfd005c..26527d8ff 100644
--- a/detox/android/detox/src/main/java/com/wix/detox/espresso/matcher/ViewMatchers.kt
+++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/matcher/ViewMatchers.kt
@@ -23,7 +23,7 @@ fun isOfClassName(className: String): Matcher<View> {
         // empty
     }
 
-    return object : BaseMatcher<View>() {
+    return object: BaseMatcher<View>() {
         override fun matches(item: Any) = false
         override fun describeTo(description: Description) {
             description.appendText("Class $className not found on classpath. Are you using full class name?")
diff --git a/detox/src/android/espressoapi/DetoxMatcher.js b/detox/src/android/espressoapi/DetoxMatcher.js
index 6dc617741..a5b35af3e 100644
--- a/detox/src/android/espressoapi/DetoxMatcher.js
+++ b/detox/src/android/espressoapi/DetoxMatcher.js
@@ -50,6 +50,21 @@ class DetoxMatcher {
     };
   }
 
+  static matcherForToggleable(value) {
+    if (typeof value !== "boolean") throw new Error("value should be a boolean, but got " + (value + (" (" + (typeof value + ")"))));
+    return {
+      target: {
+        type: "Class",
+        value: "com.wix.detox.espresso.DetoxMatcher"
+      },
+      method: "matcherForToggleable",
+      args: [{
+        type: "boolean",
+        value: value
+      }]
+    };
+  }
+
   static matcherForAnd(m1, m2) {
     return {
       target: {
diff --git a/detox/src/android/expect.js b/detox/src/android/expect.js
index dbfdad1b9..82f02eb9a 100644
--- a/detox/src/android/expect.js
+++ b/detox/src/android/expect.js
@@ -2,22 +2,24 @@ const fs = require('fs-extra');
 const path = require('path');
 const tempfile = require('tempfile');
 const invoke = require('../invoke');
-const matchers = require('./matcher');
 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 = matchers.Matcher;
-const LabelMatcher = matchers.LabelMatcher;
-const IdMatcher = matchers.IdMatcher;
-const TypeMatcher = matchers.TypeMatcher;
-const TraitsMatcher = matchers.TraitsMatcher;
-const VisibleMatcher = matchers.VisibleMatcher;
-const ExistsMatcher = matchers.ExistsMatcher;
-const TextMatcher = matchers.TextMatcher;
-const ValueMatcher = matchers.ValueMatcher;
+const {
+  Matcher,
+  LabelMatcher,
+  IdMatcher,
+  TypeMatcher,
+  TraitsMatcher,
+  VisibleMatcher,
+  ExistsMatcher,
+  TextMatcher,
+  ValueMatcher,
+  ToggleMatcher,
+} = require('./matcher');
 
 function call(maybeAFunction) {
   return maybeAFunction instanceof Function ? maybeAFunction() : maybeAFunction;
@@ -360,6 +362,10 @@ class ExpectElement extends Expect {
   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 {
diff --git a/detox/src/android/matcher.js b/detox/src/android/matcher.js
index 49e544666..b12ef44e5 100644
--- a/detox/src/android/matcher.js
+++ b/detox/src/android/matcher.js
@@ -92,6 +92,13 @@ class ValueMatcher extends Matcher {
   }
 }
 
+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 {
@@ -112,5 +119,6 @@ module.exports = {
   VisibleMatcher,
   ExistsMatcher,
   TextMatcher,
-  ValueMatcher
+  ValueMatcher,
+  ToggleMatcher,
 };
diff --git a/detox/src/ios/expectTwo.js b/detox/src/ios/expectTwo.js
index 70c668192..d253915c3 100644
--- a/detox/src/ios/expectTwo.js
+++ b/detox/src/ios/expectTwo.js
@@ -60,6 +60,10 @@ class Expect {
     return this.expect('toHaveSliderPosition', position, tolerance);
   }
 
+  toHaveToggleValue(value) {
+    return this.toHaveValue(`${Number(value)}`);
+  }
+
   get not() {
     this.modifiers.push('not');
     return this;
diff --git a/docs/APIRef.Expect.md b/docs/APIRef.Expect.md
index 78f1d2f37..75cbb06b3 100644
--- a/docs/APIRef.Expect.md
+++ b/docs/APIRef.Expect.md
@@ -13,6 +13,7 @@ Use [actions](APIRef.ActionsOnElement.md) to simulate use interaction with eleme
 - [`.toHaveId()`](#tohaveidid)
 - [`.toHaveValue()`](#tohavevaluevalue)
 - [`.toHaveSliderPosition()`](#tohavesliderpositionnormalizedposition-tolerance--ios-only) **iOS only**
+- [`.toHaveToggleValue()`](#tohavetogglevaluevalue)
 - [`.not`](#not)
 - [`.withTimeout()`](#withtimeouttimeout)
 
@@ -73,6 +74,15 @@ await expect(element(by.id('slider'))).toHaveSliderPosition(0.75);
 await expect(element(by.id('slider'))).toHaveSliderPosition(0.3113, 0.00001);
 ```
 
+### `toHaveToggleValue(value)`
+
+Expects a toggle-able element (e.g. a Switch or a Check-Box) to be on/checked or off/unchecked. As a reference, in react-native, this is the [equivalent switch component](https://reactnative.dev/docs/switch).
+
+```js
+await expect(element(by.id('switch'))).toHaveToggleValue(true);
+await expect(element(by.id('checkbox'))).toHaveToggleValue(false);
+```
+
 ### `not`
 
 Negates the expectation.


Test patch:
diff --git a/detox/src/android/expect.test.js b/detox/src/android/expect.test.js
index 7cfbecfa7..a3f5369a6 100644
--- a/detox/src/android/expect.test.js
+++ b/detox/src/android/expect.test.js
@@ -38,6 +38,8 @@ describe('expect', () => {
     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 () => {
@@ -49,6 +51,8 @@ describe('expect', () => {
     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 () => {
@@ -119,10 +123,9 @@ describe('expect', () => {
     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 () => {
diff --git a/detox/src/ios/expectTwo.test.js b/detox/src/ios/expectTwo.test.js
index a7fd6e0e7..c1e882f5b 100644
--- a/detox/src/ios/expectTwo.test.js
+++ b/detox/src/ios/expectTwo.test.js
@@ -300,6 +300,23 @@ describe('expectTwo', () => {
     expect(testCall).deepEquals(jsonOutput);
   });
 
+  it(`expect(element(by.id('switch'))).toHaveToggleValue(value)`, () => {
+    const testCall = e.expect(e.element(e.by.id('switch'))).toHaveToggleValue(true);
+    const jsonOutput = {
+      'invocation': {
+        'type': 'expectation',
+        'predicate': {
+          'type': 'id',
+          'value': 'switch'
+        },
+        'expectation': 'toHaveValue',
+        'params': ['1']
+      }
+    };
+
+    expect(testCall).deepEquals(jsonOutput);
+  });
+
   it(`waitFor(element(by.text('Text5'))).toBeNotVisible().whileElement(by.id('ScrollView630')).scroll(50, 'down')`, () => {
     const testCall = e.waitFor(e.element(e.by.text('Text5'))).toBeNotVisible().whileElement(e.by.id('ScrollView630')).scroll(50, 'down');
     const jsonOutput = {
diff --git a/detox/test/e2e/04.assertions.test.js b/detox/test/e2e/04.assertions.test.js
index f1c63c92f..7264bb737 100644
--- a/detox/test/e2e/04.assertions.test.js
+++ b/detox/test/e2e/04.assertions.test.js
@@ -41,4 +41,11 @@ describe('Assertions', () => {
     await element(by.id('UniqueId146')).tap();
     await expect(element(by.id('UniqueId146'))).toHaveValue('1');
   });
+
+  it('assert toggle-switch widget', async () => {
+    await expect(element(by.id('UniqueId146'))).toHaveToggleValue(false);
+    await element(by.id('UniqueId146')).tap();
+    await expect(element(by.id('UniqueId146'))).toHaveToggleValue(true);
+    await expect(element(by.id('UniqueId146'))).not.toHaveToggleValue(false);
+  });
 });
diff --git a/detox/test/src/Screens/AssertionsScreen.js b/detox/test/src/Screens/AssertionsScreen.js
index 3ba907a51..baeb83f23 100644
--- a/detox/test/src/Screens/AssertionsScreen.js
+++ b/detox/test/src/Screens/AssertionsScreen.js
@@ -2,9 +2,23 @@ import React, { Component } from 'react';
 import {
   Text,
   View,
-  Switch
+  Switch,
 } from 'react-native';
 
+class TestSwitchWidget extends Switch {
+  constructor(props) {
+    super(props);
+    this.state = {
+      value: false
+    }
+    this.onChange = () => this.setState({ value: !this.state.value });
+  }
+
+  render() {
+    return <Switch testID={this.props.testID} onChange={this.onChange} value={this.state.value}/>
+  }
+}
+
 export default class AssertionsScreen extends Component {
 
   constructor(props) {
@@ -17,17 +31,10 @@ export default class AssertionsScreen extends Component {
   render() {
     return (
       <View style={{flex: 1, paddingTop: 20, justifyContent: 'center', alignItems: 'center'}}>
-
         <Text testID='UniqueId204' style={{marginBottom: 20}} accessibilityLabel={'I contain some text'}>I contain some text</Text>
-
         <Text testID='UniqueId205' style={{marginBottom: 20, position: 'absolute', left: -200}}>I am not visible</Text>
-
-        <Switch testID='UniqueId146'
-          onValueChange={(value) => this.setState({switchValue: value})}
-          value={this.state.switchValue} />
-
+        <TestSwitchWidget testID='UniqueId146'/>
       </View>
     );
   }
-
 }