Skip to content
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

feat(iOS): add system-dialogs interaction support. #4457

Open
wants to merge 42 commits into
base: master
Choose a base branch
from

Conversation

asafkorem
Copy link
Contributor

@asafkorem asafkorem commented Apr 21, 2024

Resolves: #4433, #4434, #4435, #4275, #3227, #3017

This PR add support for system-dialogs interactions (system alerts and permission requests).

There are several changes in this PR:

  • Initial integration with XCUITest: add new scripts for detox-cli (xcuitest-cache-build/rebuild/clean), and refactored existing scripts.
  • Add initial system-matchers: by.system.label, by.system.type.
  • Add initial system-action (tap()).
  • Add initial system-expectations (toExist(), atIndex(..), not modifier).

Check the system test suite for example.

Check the rationale behind the new API design decision here.

@asafkorem asafkorem self-assigned this Apr 21, 2024
@asafkorem asafkorem force-pushed the feat/initial-xcuitest-integration branch from 193c43a to d1c9e1e Compare April 21, 2024 12:36
@asafkorem asafkorem removed the request for review from d4vidi April 24, 2024 06:42
@asafkorem asafkorem force-pushed the feat/initial-xcuitest-integration branch from 70bc7b5 to 7bfba22 Compare April 24, 2024 07:05
docs/api/system.md Outdated Show resolved Hide resolved
@noomorph
Copy link
Collaborator

noomorph commented Apr 25, 2024

The elephant in the room (i.e. the longest answer to write) is:

This pull request current lacks elaboration on:

  1. why by.system and system.element are the best possible solution in today’s context,
  2. what were the other solutions considered,
  3. why the latter are less suitable or subpar.

It’s unlikely you’ll be happy to write an essay on this, but this kind of engineering notes really helps colleagues and fellow contributors, so please 🙏

Copy link
Collaborator

@noomorph noomorph left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless there was a really good reason to do so, I find it pointless to inflate the CLI toolkit with typical parameterless commands like:

  1. build-framework-cache
  2. build-xcuitest-cache
  3. rebuild-framework-cache
  4. rebuild-xcuitest-cache

I see that it created a cascade of changes in documentation, and quite many copy&pastes between files.

First of all, I don't see significant value in exposing these implementation details to user on top level. Our users may be more than happy to live in blissful ignorance about what exactly the magical build-framework-cache command does in terms of troubleshooting.

Secondly, I recognize the importance of isolating the build/cleanup for these different projects, and this is why I'd suggest introducing a couple of useful CLI arguments:

detox build-framework-cache --xcuitest # build only XCUITest
detox build-framework-cache --detox # build only Detox.framework
detox build-framework-cache --clean # clean before rebuilding this or that

In that case:

  • build-xcuitest-cache, clean-xcuitest-cache, rebuild-xcuitest-cache can be omitted
  • rebuild-framework-cache can serve as a mere alias tobuild-framework-cache with an extra --clean option
  • maybe even clean-framework-cache can reuse build-framework-cache with (clean=true, xcuitest=false, detox=false), if this doesn't seem "too clever" to maintain.
  • we won't be adding any new CLI commands
  • just small additions to the docs will be required

I will be adding a few more notes but overall I must admit that this a very clean and nice PR. I decided not to wait with other notes, because this one implies some bit of rework, and the earlier you see it the better for us all it is.

Copy link
Collaborator

@noomorph noomorph left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I've finished the review for now. Thanks a lot for keeping it compact. Great job overall. See my comments. 🙏

@asafkorem
Copy link
Contributor Author

asafkorem commented Apr 30, 2024

The elephant in the room (i.e. the longest answer to write) is:

This pull request current lacks elaboration on:

  1. why by.system and system.element are the best possible solution in today’s context,
  2. what were the other solutions considered,
  3. why the latter are less suitable or subpar.

It’s unlikely you’ll be happy to write an essay on this, but this kind of engineering notes really helps colleagues and fellow contributors, so please 🙏

Gladly, let's start by addressing the elephant in the room:

Why by.system and system.element?

These APIs specifically cater to system-level interactions, distinguishing them from other types of element interactions. This clear separation enables targeted, system-level access for elements outside the app, such as system alerts, where our basic app-level methods are insufficient. Additionally, by.system and system.element introduce an implicit natural priority for API calls. This prioritization ensures that system-level methods are called only when necessary, making the interaction pattern more declarative and streamlined for users.

API design decision

By aligning by.system with our by.web approach, we maintain a consistent and modular design philosophy. Each API is tailored for specific interaction domains, simplifying understanding, usage, and maintenance while avoiding unnecessary performance overhead by utilizing XCUITest only when absolutely necessary.

Considered alternatives (Integration with existing APIs)

The main alternative I considered was using a fallback mechanism - where interactions would automatically escalate to system-level (using XCUITest) if no elements were found. This approach introduced significant complexity and performance degradation, requiring a first check using the injected framework, then XCUITest.

In my opinion, any other "seamless" integration with our current APIs without explicitly specifying the system level would have blurred the operational clarity of our APIs.

Another approach considered was introducing another matcher withSystem() / onSystemContext(), which is somewhat similar to withAncestor(..) & withDescendant(..) which positions the matcher within some specific hierarchy. However, I decided it's better to align with by.web since withAncestor allows integration with all other matchers, and our current system matchers are not even a subset (i.e., by.type uses the accessibility type accessible through XCUITest, not the element class as we do within the app interactions context). Also, withAncestor only makes the matching more specific and does not change the entire context as onSystemContext() (or similar) would do.

Additionally, it's important to note that similar considerations apply to the future integration of UIAutomator (UIDevice) with this API on Android (i.e. considerations that are related to XCUITest).

To summarize, this system-focused approach ensures we do not introduce any performance degradation and making this solution more maintainable and specific for system-interaction needs and abilities. I hope this clarifies the rationale behind my design decisions.

@noomorph
Copy link
Collaborator

noomorph commented May 2, 2024

@asafkorem, I'm particularly concerned about the next things:

  1. How well does it translate to Android? Will there be 1:1 system.element and by.system mapping, and will it be needed at all? For example, the way you're adding the XCUI test driver interface doesn't align with the Android counterpart, UIAutomator:

    https://wix.github.io/Detox/docs/api/device/#devicegetuidevice-android-only

    We should understand how we align device.getUiDevice() and system.element – either UIAutomator gets a rewrite to *.system.*, or XCUITest gets implemented for iOS in device.getUiDevice(), or something like that.

  2. How sure are we that the XCUI test driver won't be needed elsewhere except for interacting with system dialogs? As we found out recently, cross-iframe WebView interactions can be done only via XCUI. Do you intend to mix web and system APIs in that case, e.g.:?

await web.element(by.system.type('button')).tap()
  1. Are blind (x,y) taps and swipes on the device screen also an example of system elements and actions, in your opinion?

@asafkorem
Copy link
Contributor Author

Unless there was a really good reason to do so, I find it pointless to inflate the CLI toolkit with typical parameterless commands like:

  1. build-framework-cache
  2. build-xcuitest-cache
  3. rebuild-framework-cache
  4. rebuild-xcuitest-cache

I see that it created a cascade of changes in documentation, and quite many copy&pastes between files.

First of all, I don't see significant value in exposing these implementation details to user on top level. Our users may be more than happy to live in blissful ignorance about what exactly the magical build-framework-cache command does in terms of troubleshooting.

Secondly, I recognize the importance of isolating the build/cleanup for these different projects, and this is why I'd suggest introducing a couple of useful CLI arguments:

detox build-framework-cache --xcuitest # build only XCUITest
detox build-framework-cache --detox # build only Detox.framework
detox build-framework-cache --clean # clean before rebuilding this or that

In that case:

  • build-xcuitest-cache, clean-xcuitest-cache, rebuild-xcuitest-cache can be omitted
  • rebuild-framework-cache can serve as a mere alias tobuild-framework-cache with an extra --clean option
  • maybe even clean-framework-cache can reuse build-framework-cache with (clean=true, xcuitest=false, detox=false), if this doesn't seem "too clever" to maintain.
  • we won't be adding any new CLI commands
  • just small additions to the docs will be required

I will be adding a few more notes but overall I must admit that this a very clean and nice PR. I decided not to wait with other notes, because this one implies some bit of rework, and the earlier you see it the better for us all it is.

xcuitest-cache commands were removed, and replaced with args for build-framework-cache and extended behaviour.

New options (args for detox-build-framework):

  • --build: Specifies binaries to build (all / detox / xcuitest / none). Defaults to all.
  • --clean: Specifies binaries to clean (all / detox / xcuitest / none). Defaults to none.

detox clean-framework-cache and detox rebuild-framework-cache are now aliases for detox build-framework-cache with specific flags.

@asafkorem
Copy link
Contributor Author

asafkorem commented May 2, 2024

@asafkorem, I'm particularly concerned about the next things:

  1. How well does it translate to Android? Will there be 1:1 system.element and by.system mapping, and will it be needed at all? For example, the way you're adding the XCUI test driver interface doesn't align with the Android counterpart, UIAutomator:
    https://wix.github.io/Detox/docs/api/device/#devicegetuidevice-android-only
    We should understand how we align device.getUiDevice() and system.element – either UIAutomator gets a rewrite to *.system.*, or XCUITest gets implemented for iOS in device.getUiDevice(), or something like that.

The current getUIDevice API is specific to Android and not aligned with our goal for a cohesive API across platforms. It essentially wraps Android's UIAutomator, which doesn't fit well (at all) with Detox's design. Instead of seeking a direct alignment with this, I plan to treat UIDevice as an underlying detail for the System APIs on Android. Both feature parity and clean API design are goals here.

  1. How sure are we that the XCUI test driver won't be needed elsewhere except for interacting with system dialogs? As we found out recently, cross-iframe WebView interactions can be done only via XCUI. Do you intend to mix web and system APIs in that case, e.g.:?
await web.element(by.system.type('button')).tap()

System APIs should definitely provide more than interactions with system dialogs.

My approach is to expand system APIs to include functionalities like push notifications, photo library interactions, system browsers (Safari, Chrome), and interactions with complex WebViews within apps.
Regarding special webview cases, you raised a nice idea (to mix between system matcher and web-view apis), another solution will be to extend the current (and future) system API abilities to match and interact with elements within the app. IMO that's a discussion for itself. Anyway we will definitely leverage the new system abilities to interact with web-views in special scenarios as we are already familiar with one within the company.

  1. Are blind (x,y) taps and swipes on the device screen also an example of system elements and actions, in your opinion?

First, we need to evaluate if there are valid use cases that require blind interactions on the device screen.

Although these interactions are performed on system level, integrating them into the System API could complicate its design. The Device API, already provides a good abstraction for these types of gestures (special kind of system element with unique actions). Therefore, implementing those blind actions within the device object will help to maintain a clear distinction in functionalities although the overlapping aspects of device and system interactions.

@asafkorem asafkorem force-pushed the feat/initial-xcuitest-integration branch from 7f7b2e7 to 37a0ee4 Compare May 2, 2024 17:26
@d4vidi d4vidi self-assigned this May 5, 2024
const frameworkPath = path.join(os.homedir(), '/Library/Detox/ios/framework');
const xcuitestPath = path.join(os.homedir(), '/Library/Detox/ios/xcuitest-runner');

await handleCleaning(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uh-oh... 😢

Actually, what I meant was:

  • to call handleBuilding in build-framework-cache
  • to call handleCleaning in clean-framework-cache
  • to call both handleCleaning and handleBuilding in rebuild-framework-cache

I envisioned that those utility functions will reside in separate files, and be imported by each CLI tool independently, instead of dirty tricks with using yargs while you are using yargs:

  const argv = hideBin(process.argv);
  yargs(argv)
  .command(require('./build-framework-cache'))
  .parse(['build-framework-cache', '--clean="all"', '--build="all"']);

In such case, the implementation should have been dead simple:

handleCleaning({ xcui: boolean; framework: boolean; }) => Promise<void>
handleBuilding({ xcui: boolean; framework: boolean; }) => Promise<void>

Sorry for nitpicking, but wouldn't detox (re|build|clean)-framework-cache --this --that --those look and feel more natural and simple?

Copy link
Collaborator

@noomorph noomorph May 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean that this API doesn't make sense to me:

--build: Specifies binaries to build (all / detox / xcuitest / none). Defaults to all.
--clean: Specifies binaries to clean (all / detox / xcuitest / none). Defaults to none.

What practical sense does it bear to call detox ... --build xcuitest --clean detox ?

To my understanding, --clean is a boolean flag which is attributed to every target in the question, be it all, detox, or xcuitest. There's no sense to attach "clean logic" to something that is not being built or rebuilt.

I think that

detox build-framework-cache --clean --xcuitest --detox

or

detox ... build-framework-cache --clean --target=xcuitest,detox would make more sense.

For implementing the latter, you can check coerce function in yargs, where you can post-process the value.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For detox build-framework-cache --clean --xcuitest --detox, the default setting should enable both xcuitest and detox enabled. However, if only one of these options is specified, the other should be disabled. This setup may not be very intuitive, but I'm okay with it. I'd prefer not to spend too much time on discussing this internal utility to avoid bike-shedding. I'm totally fine with that, will change

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, something bothers me. What's the point of adding the --clean flag here if we have the rebuild-... command? It's like using --build from clean-...
I'll just add the --xcuitest and --detox flags for all commands, and the clean / rebuild command will call the clean method.

@@ -1,76 +0,0 @@
#!/bin/bash -e
Copy link
Collaborator

@noomorph noomorph May 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@asafkorem I wonder, won't removing legacy scripts break someone? Who's our vulnerable audience?

Copy link
Contributor Author

@asafkorem asafkorem May 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Who's our vulnerable audience?

Allegedly, Xcode < 12 if such thing still exist (current Xcode version is 15), though they can update their xcodebuild (CLI) version and they'll be fine.

Xcode 11 is quite outdated (supports only iOS <= 13). Xcode 12 was released in 2020, and I have no confidence that Detox will compile on such an old version (it also comes with the respective old simctl and xcodebuild command).
IMO there is no good reason to continue maintaining or supporting this script

];

try {
return await exec(`TEST_RUNNER_PARAMS="${base64InvocationParams}" xcodebuild ${flags.join(' ')}`);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we pass TEST_RUNNER_PARAMS to { env } parameter to exec?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uh, I saw your comment about this. This is not critical, but I'll check later why it fails. I just really don't want to pollute detox.trace.log, so I'm rather persistent and returning to this every time. 😊

}

expect(expectation, traceDescription) {
assert(traceDescription, `must provide trace description for expectation: \n ${JSON.stringify(expectation)}`);
Copy link
Collaborator

@noomorph noomorph May 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel uneasy about eager object stringifying each time we try to send something. To my embarrassment, I would even be fine with stupid if (!traceDescription) { assert(...) }, just not to spend CPU time for things that won't be needed in 99.99% cases.

I see two good options:

  1. check if stack traces are usable even without this stringification – this really can be so
  2. if not, use homebrewed assertion utils from ./src/utils (if needed, add some), or add a helper function somewhere below maybe... 🤷‍♂️

Sorry for nitpicking. 🙏

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if some workaround like

const lazyMessage = {
    toString: function() {
        return `... ${JSON.stringify(...)}`;
    }
};

will do the trick when passing it to the assert function. however that's a bit dirty.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're right about 1, the JSON is redundant here.. there's enough information without it

@@ -171,24 +172,36 @@ function throwMissingGmsaasError() {
throw new DetoxRuntimeError(`Failed to locate Genymotion's gmsaas executable. Please add it to your $PATH variable!\nPATH is currently set to: ${process.env.PATH}`);
}

function getDetoxVersion() {
const getDetoxVersion = _.memoize(() => {
Copy link
Collaborator

@noomorph noomorph May 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather a nitpicking, but you don't need to use memoize here and all the way down the file. You'll be absolutely fine with _.once since your functions don't require any arguments. It is a more lightweight implementation, and its mental model is a bit easier/lighter.

Copy link
Contributor Author

@asafkorem asafkorem May 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, great. replaced

Copy link
Collaborator

@noomorph noomorph left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The biggest concern is that CLI redirection logic between those scripts looks too heavy-weight and clumsy. Reusing and calling helper functions would have been much cleaner and simpler to comprehend. I seriously recommend reconsider that implementation. 🙏

Since title is being hidden by the "Active" / "Busy" mark.
@asafkorem asafkorem force-pushed the feat/initial-xcuitest-integration branch from a5e3348 to 7cea0f4 Compare May 19, 2024 15:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[iOS Dialogs] Initial integration with XCUITest.
4 participants