-
Notifications
You must be signed in to change notification settings - Fork 511
feat: add ability to switch themes on iOS and Android: setDarkMode & toggleDarkMode #2507
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4671e32
b1a7628
fd264a5
13410de
408fd6d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -159,6 +159,8 @@ env: | |
|
|
||
| # TODO: setAirplaneMode | ||
|
|
||
| # TODO: setDarkMode | ||
|
|
||
| # TODO: setLocation | ||
|
|
||
| # TODO: startRecording | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1026,6 +1026,39 @@ data class ToggleAirplaneModeCommand( | |
| } | ||
| } | ||
|
|
||
| enum class DarkModeValue { | ||
| Enable, | ||
| Disable, | ||
| } | ||
|
|
||
| data class SetDarkModeCommand( | ||
| val value: DarkModeValue, | ||
| override val label: String? = null, | ||
| override val optional: Boolean = false, | ||
| ) : Command { | ||
| override val originalDescription: String | ||
| get() = when (value) { | ||
| DarkModeValue.Enable -> "Enable dark mode" | ||
| DarkModeValue.Disable -> "Disable dark mode" | ||
| } | ||
|
|
||
| override fun evaluateScripts(jsEngine: JsEngine): Command { | ||
| return this | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we be allowing setting this variable with js? Not sure. Like runtime through JS. If yes then we might want to handle this. |
||
| } | ||
| } | ||
|
|
||
| data class ToggleDarkModeCommand( | ||
| override val label: String? = null, | ||
| override val optional: Boolean = false, | ||
| ) : Command { | ||
| override val originalDescription: String | ||
| get() = "Toggle dark mode" | ||
|
|
||
| override fun evaluateScripts(jsEngine: JsEngine): Command { | ||
| return this | ||
| } | ||
| } | ||
|
|
||
| internal fun tapOnDescription(isLongPress: Boolean?, repeat: TapRepeat?): String { | ||
| return if (isLongPress == true) "Long press" | ||
| else if (repeat != null) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,6 +56,7 @@ import maestro.orchestra.RunScriptCommand | |
| import maestro.orchestra.ScrollCommand | ||
| import maestro.orchestra.ScrollUntilVisibleCommand | ||
| import maestro.orchestra.SetAirplaneModeCommand | ||
| import maestro.orchestra.SetDarkModeCommand | ||
| import maestro.orchestra.SetLocationCommand | ||
| import maestro.orchestra.StartRecordingCommand | ||
| import maestro.orchestra.StopAppCommand | ||
|
|
@@ -65,6 +66,7 @@ import maestro.orchestra.TakeScreenshotCommand | |
| import maestro.orchestra.TapOnElementCommand | ||
| import maestro.orchestra.TapOnPointV2Command | ||
| import maestro.orchestra.ToggleAirplaneModeCommand | ||
| import maestro.orchestra.ToggleDarkModeCommand | ||
| import maestro.orchestra.TravelCommand | ||
| import maestro.orchestra.WaitForAnimationToEndCommand | ||
| import maestro.orchestra.error.InvalidFlowFile | ||
|
|
@@ -128,6 +130,8 @@ data class YamlFluentCommand( | |
| val addMedia: YamlAddMedia? = null, | ||
| val setAirplaneMode: YamlSetAirplaneMode? = null, | ||
| val toggleAirplaneMode: YamlToggleAirplaneMode? = null, | ||
| val setDarkMode: YamlSetDarkMode? = null, | ||
| val toggleDarkMode: YamlToggleDarkMode? = null, | ||
| val retry: YamlRetryCommand? = null, | ||
| @JsonIgnore val _location: JsonLocation, | ||
| ) { | ||
|
|
@@ -478,6 +482,25 @@ data class YamlFluentCommand( | |
| ) | ||
| ) | ||
|
|
||
| setDarkMode != null -> listOf( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder are there cases where it is useful to set appearance mid flow execution? Is this more useful as a command or flow based or upload level config?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can't think of a use-case for mid-flow. But given the JS comment later on down, I think I like the idea of being able to do it very early in execution, explicitly as part of your test setup, perhaps calling this before launchApp. |
||
| MaestroCommand( | ||
| SetDarkModeCommand( | ||
| setDarkMode.value, | ||
| setDarkMode.label, | ||
| setDarkMode.optional | ||
| ) | ||
| ) | ||
| ) | ||
|
|
||
| toggleDarkMode != null -> listOf( | ||
| MaestroCommand( | ||
| ToggleDarkModeCommand( | ||
| toggleDarkMode.label, | ||
| toggleDarkMode.optional | ||
| ) | ||
| ) | ||
| ) | ||
|
|
||
| else -> throw SyntaxError("Invalid command: No mapping provided for $this") | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| package maestro.orchestra.yaml | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonCreator | ||
| import com.fasterxml.jackson.core.JsonParser | ||
| import com.fasterxml.jackson.core.TreeNode | ||
| import com.fasterxml.jackson.databind.DeserializationContext | ||
| import com.fasterxml.jackson.databind.JsonDeserializer | ||
| import com.fasterxml.jackson.databind.ObjectMapper | ||
| import com.fasterxml.jackson.databind.annotation.JsonDeserialize | ||
| import maestro.orchestra.DarkModeValue | ||
|
|
||
| @JsonDeserialize(using = YamlSetDarkModeDeserializer::class) | ||
| data class YamlSetDarkMode( | ||
| val value: DarkModeValue, | ||
| val label: String? = null, | ||
| val optional: Boolean = false, | ||
| ) { | ||
| companion object { | ||
| @JvmStatic | ||
| @JsonCreator(mode = JsonCreator.Mode.DELEGATING) | ||
| fun parse(value: DarkModeValue): YamlSetDarkMode { | ||
| return YamlSetDarkMode(value) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| class YamlSetDarkModeDeserializer : JsonDeserializer<YamlSetDarkMode>() { | ||
|
|
||
| override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): YamlSetDarkMode { | ||
| val mapper = (parser.codec as ObjectMapper) | ||
| val root: TreeNode = mapper.readTree(parser) | ||
| val input = root.fieldNames().asSequence().toList() | ||
| val label = getLabel(root) | ||
| when { | ||
| input.contains("value") -> { | ||
| val parsedValue = root.get("value").toString().replace("\"", "") | ||
| val returnValue = when (parsedValue) { | ||
| "enabled" -> DarkModeValue.Enable | ||
| "disabled" -> DarkModeValue.Disable | ||
| else -> throwInvalidInputException(input) | ||
| } | ||
| return YamlSetDarkMode(returnValue, label) | ||
| } | ||
| (root.isValueNode && root.toString().contains("enabled")) -> { | ||
| return YamlSetDarkMode(DarkModeValue.Enable, label) | ||
| } | ||
| (root.isValueNode && root.toString().contains("disabled")) -> { | ||
| return YamlSetDarkMode(DarkModeValue.Disable, label) | ||
| } | ||
| else -> throwInvalidInputException(input) | ||
| } | ||
| } | ||
|
|
||
| private fun throwInvalidInputException(input: List<String>): Nothing { | ||
| throw IllegalArgumentException( | ||
| "setDarkMode command takes either: \n" + | ||
| "\t1. enabled: To enable dark mode\n" + | ||
| "\t2. disabled: To disable dark mode\n" + | ||
| "\t3. value: To set dark mode to a specific value (enabled or disabled) \n" + | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was a bit confused about this line
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was copying the airplane mode function as it allows this syntax: - setDarkMode enabledor - setDarkMode
value: enabled
label: "Something"I'm definitely open to suggestions on how to improve this!
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh yeah I think the syntax makes sense. The 3rd line of the error message is what was confusing me - seems redundant?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh ok I think I understand now. Maybe it would be clearer to show those two styles of syntax as examples in the error message |
||
| "It seems you provided invalid input with: $input" | ||
| ) | ||
| } | ||
|
|
||
| private fun getLabel(root: TreeNode): String? { | ||
| return if (root.path("label").isMissingNode) { | ||
| null | ||
| } else { | ||
| root.path("label").toString().replace("\"", "") | ||
| } | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package maestro.orchestra.yaml | ||
|
|
||
| data class YamlToggleDarkMode( | ||
| val label: String? = null, | ||
| val optional: Boolean = false, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a way to do this via xcuitest? We're trying to move away from simctl commands since they won't work for physical devices
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll look into this tomorrow to see if there's a way to do it. Not sure at the moment.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Leland-Takamine I found this reference already inside the repo's source code, but i have no idea how i would go about using it 🤷 https://github.com/mobile-dev-inc/Maestro/blob/main/maestro-ios-xctest-runner/maestro-driver-iosUITests/PrivateHeaders/XCTest/XCUIDevice.h#L18-L22
Edit: Also found this SO answer that indicates we might be able to just do:
But this is my first time ever looking at XCUIDevice so i have no idea how i'd implement that 😬
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @markrickert thanks for contribution, I think this is would be a new endpoint in our XCUITest driver here:
https://github.com/mobile-dev-inc/Maestro/tree/main/maestro-ios-xctest-runner/maestro-driver-iosUITests/Routes/Handlers
You can check any one handler there for example. On implementation we would receive the appearence and then as you shared:
And this endpoint would be reached from XCTest client here in maestro: https://github.com/mobile-dev-inc/Maestro/blob/main/maestro-ios/src/main/java/ios/xctest/XCTestIOSDevice.kt