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

[DO NOT MERGE] Add support for Selector.instance on iOS #1054

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/patrol-prepare.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@ jobs:
fail-fast: false
matrix:
include:
- version: '3.3.0'
- channel: stable

defaults:
Expand Down
10 changes: 8 additions & 2 deletions .github/workflows/test-ios-simulator.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,14 @@ jobs:
- run: patrol test -t integration_test/service_wifi_test.dart
if: ${{ false }} # Not on Simulator

- run: patrol test -t integration_test/webview_all_test.dart
if: ${{ false }} # Temporarily broken
- run: patrol test -t webview_hackernews_test.dart
if: ${{ false }} # https://github.com/leancodepl/patrol/issues/1139
- run: patrol test -t webview_leancode_test.dart
if: ${{ false }} # https://github.com/leancodepl/patrol/issues/1139
- run: patrol test -t webview_login_test.dart
if: ${{ false }} # https://github.com/leancodepl/patrol/issues/1139
- run: patrol test -t webview_stackoverflow_test.dart
if: ${{ false }} # https://github.com/leancodepl/patrol/issues/1139

- name: Set job status
id: set_status
Expand Down
10 changes: 6 additions & 4 deletions packages/patrol/example/integration_test/common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ export 'package:example/main.dart';
export 'package:flutter_test/flutter_test.dart';
export 'package:patrol/patrol.dart';

final _patrolTesterConfig = PatrolTesterConfig();
final _nativeAutomatorConfig = NativeAutomatorConfig();
final globalPatrolTesterConfig = PatrolTesterConfig();
final globalNativeAutomatorConfig = NativeAutomatorConfig();

Future<void> createApp(PatrolTester $) async {
await setUpTimezone();
Expand All @@ -17,12 +17,14 @@ Future<void> createApp(PatrolTester $) async {
void patrol(
String description,
Future<void> Function(PatrolTester) callback, {
PatrolTesterConfig? patrolTesterConfig,
NativeAutomatorConfig? nativeAutomatorConfig,
bool? skip,
}) {
patrolTest(
description,
config: _patrolTesterConfig,
nativeAutomatorConfig: _nativeAutomatorConfig,
config: patrolTesterConfig ?? globalPatrolTesterConfig,
nativeAutomatorConfig: nativeAutomatorConfig ?? globalNativeAutomatorConfig,
nativeAutomation: true,
skip: skip,
callback,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import '../common.dart';

final _nativeAutomatorConfig = globalNativeAutomatorConfig.copyWith(
findTimeout: Duration(seconds: 3), // shorter timeout for this test
);

void main() {
patrol(
'native tap() fails gracefully',
nativeAutomatorConfig: _nativeAutomatorConfig,
($) async {
await createApp($);

await expectLater(
() => $.native.tap(Selector(text: 'This does not exist, boom!')),
throwsA(
isA<PatrolActionException>().having(
(err) => err.message,
'message',
contains('This does not exist, boom!'),
),
),
);
},
);

patrol(
'native enterText() fails gracefully',
nativeAutomatorConfig: _nativeAutomatorConfig,
($) async {
await createApp($);

await expectLater(
() => $.native.enterText(
Selector(text: 'This does not exist, boom!'),
text: 'some text',
),
throwsA(
isA<PatrolActionException>().having(
(err) => err.message,
'message',
contains('This does not exist, boom!'),
),
),
);
},
);

patrol(
'native enterTextByIndex() fails gracefully',
nativeAutomatorConfig: _nativeAutomatorConfig,
($) async {
await createApp($);

await expectLater(
() => $.native.enterTextByIndex('some text', index: 100),
throwsA(isA<PatrolActionException>()),
);
},
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ void main() {
patrol('interacts with the orange website in a webview', ($) async {
await createApp($);

await $('Open webview (Hacker News').scrollTo().tap();
await $('Open webview (Hacker News)').scrollTo().tap();

await $.native.tap(Selector(text: 'login'));
await $.native.enterTextByIndex('[email protected]', index: 0);
await $.native.enterTextByIndex('[email protected]', index: 0);
await $.native.enterTextByIndex('ny4ncat', index: 1);
await $.native.tap(Selector(text: 'login'));
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@ import 'common.dart';
void main() async {
patrol('interacts with the LeanCode website in a webview', ($) async {
await createApp($);

await $('Open webview (LeanCode)').scrollTo().tap();

await $.native.tap(Selector(text: 'Accept cookies'));

// open dropdown
await $.native.tap(Selector(text: 'What do you do in IT?', instance: 1));

// select option in dropdown
await $.native.tap(Selector(text: 'Developer'));
await $.native.tap(Selector(text: '1 item selected'));

// close dropdown
await $.native.tap(Selector(text: 'What do you do in IT?', instance: 1));

await $.native.enterTextByIndex('[email protected]', index: 0);
});
}
138 changes: 94 additions & 44 deletions packages/patrol/ios/Classes/AutomatorServer/Automator.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import XCTest

let MICROSECONDS_IN_SECOND: UInt32 = 1_000_000

class Automator {
private lazy var device: XCUIDevice = {
return XCUIDevice.shared
Expand Down Expand Up @@ -54,57 +56,70 @@ class Automator {

// MARK: General UI interaction

func tap(on text: String, inApp bundleId: String) async throws {
try await runAction("tapping on view with text \(format: text) in app \(bundleId)") {
func tap(onText text: String, inApp bundleId: String, atIndex index: Int) async throws {
let view = "view with text \(format: text) in app \(bundleId) at index \(index)"

try await runAction("tapping on \(view)") {
let app = try self.getApp(withBundleId: bundleId)
let element = app.descendants(matching: .any)[text]
let elementQuery = app.descendants(matching: .any).matching(identifier: text)

Logger.shared.i("waiting for existence of view with text \(format: text)")
let exists = element.waitForExistence(timeout: self.timeout)
guard exists else {
throw PatrolError.viewNotExists("view with text \(format: text) in app \(format: bundleId)")
Logger.shared.i("waiting for existence of \(view)")
guard let element = self.waitForView(query: elementQuery, index: index) else {
throw PatrolError.viewNotExists(view)
}
Logger.shared.i("found view with text \(format: text), will tap on it")

element.firstMatch.forceTap()
Logger.shared.i("found \(view), will tap on it")
element.tap()
}
}

func doubleTap(on text: String, inApp bundleId: String) async throws {
try await runAction("double tapping on text \(format: text) in app \(bundleId)") {
func doubleTap(onText text: String, inApp bundleId: String, atIndex index: Int) async throws {
let view = "view with text \(format: text) in app \(bundleId) at index \(index)"

try await runAction("double tapping on \(view)") {
let app = try self.getApp(withBundleId: bundleId)
let element = app.descendants(matching: .any)[text]
let elementQuery = app.descendants(matching: .any).matching(identifier: text)

let exists = element.waitForExistence(timeout: self.timeout)
guard exists else {
throw PatrolError.viewNotExists("view with text \(format: text) in app \(format: bundleId)")
Logger.shared.i("waiting for existence of \(view)")
guard let element = self.waitForView(query: elementQuery, index: index) else {
throw PatrolError.viewNotExists(view)
}

element.firstMatch.forceTap()
Logger.shared.i("found \(view), will double tap on it")
element.doubleTap()
}
}

func enterText(_ data: String, by text: String, inApp bundleId: String) async throws {
try await runAction(
"entering text \(format: data) into text field with text \(text) in app \(bundleId)"
) {
func enterText(_ data: String, byText text: String, inApp bundleId: String, atIndex index: Int)
async throws
{
let view = "text field with ident/label \(format: text) in app \(bundleId) at index \(index)"

try await runAction("entering text \(format: data) into \(view)") {
let app = try self.getApp(withBundleId: bundleId)

guard
let element = self.waitForAnyElement(
elements: [app.textFields[text], app.secureTextFields[text]],
timeout: self.timeout
)
else {
throw PatrolError.viewNotExists(
"text field with text \(format: text) in app \(format: bundleId)")
// elementType must be specified as integer
// See:
// * https://developer.apple.com/documentation/xctest/xcuielementtype/xcuielementtypetextfield
// * https://developer.apple.com/documentation/xctest/xcuielementtype/xcuielementtypesecuretextfield
let textFieldPredicate = NSPredicate(format: "elementType == 49")
let secureTextFieldPredicate = NSPredicate(format: "elementType == 50")
let predicate = NSCompoundPredicate(
orPredicateWithSubpredicates: [textFieldPredicate, secureTextFieldPredicate]
)

let elementQuery = app.descendants(matching: .any).matching(predicate).matching(
identifier: text)
guard let element = self.waitForView(query: elementQuery, index: index) else {
throw PatrolError.viewNotExists(view)
}

element.firstMatch.typeText(data)
element.tap()
element.typeText(data)
}
}

func enterText(_ data: String, by index: Int, inApp bundleId: String) async throws {
func enterText(_ data: String, byIndex index: Int, inApp bundleId: String) async throws {
try await runAction("entering text \(format: data) by index \(index) in app \(bundleId)") {
let app = try self.getApp(withBundleId: bundleId)

Expand All @@ -118,18 +133,12 @@ class Automator {
orPredicateWithSubpredicates: [textFieldPredicate, secureTextFieldPredicate]
)

let textFieldsQuery = app.descendants(matching: .any).matching(predicate)
guard
let element = self.waitFor(
query: textFieldsQuery,
byIndex: index,
timeout: self.timeout
)
else {
let elementQuery = app.descendants(matching: .any).matching(predicate)
guard let element = self.waitForView(query: elementQuery, index: index) else {
throw PatrolError.viewNotExists("text field at index \(index) in app \(bundleId)")
}

element.forceTap()
element.tap()
element.typeText(data)
}
}
Expand Down Expand Up @@ -506,7 +515,8 @@ class Automator {
}
}

// MARK: Private stuff
// MARK: Private common methods

private func isSimulator() -> Bool {
#if targetEnvironment(simulator)
return true
Expand Down Expand Up @@ -570,9 +580,10 @@ class Automator {
start.press(forDuration: 0.1, thenDragTo: end)
}

private func runControlCenterAction(_ log: String, block: @escaping () throws -> Void)
async throws
{
private func runControlCenterAction(
_ log: String,
block: @escaping () throws -> Void
) async throws {
#if targetEnvironment(simulator)
throw PatrolError.internal("Control Center is not available on Simulator")
#endif
Expand Down Expand Up @@ -622,10 +633,49 @@ class Automator {
Logger.shared.i("\(log)...")
let result = try block()
Logger.shared.i("done \(log)")
Logger.shared.i("result: \(result)")
Logger.shared.i("result: \(result), type: \(type(of: result))")
return result
}
}

// MARK: Custom view utilities

/// Adapted from https://stackoverflow.com/q/47880395/7009800
@discardableResult
func waitForAnyElement(elements: [XCUIElement], timeout: TimeInterval? = nil) -> XCUIElement? {
var foundElement: XCUIElement?
let startTime = Date()

while Date().timeIntervalSince(startTime) < (timeout ?? self.timeout) {
if let elementFound = elements.first(where: { $0.exists }) {
foundElement = elementFound
break
}
usleep(MICROSECONDS_IN_SECOND * 1)
}

return foundElement
}

/// Adapted from https://stackoverflow.com/q/47880395/7009800
@discardableResult
func waitForView(query: XCUIElementQuery, index: Int, timeout: TimeInterval? = nil)
-> XCUIElement?
{
var foundElement: XCUIElement?
let startTime = Date()

while Date().timeIntervalSince(startTime) < (timeout ?? self.timeout) {
let elements = query.allElementsBoundByIndex
if index < elements.count && elements[index].exists {
foundElement = elements[index]
break
}
usleep(MICROSECONDS_IN_SECOND * 1)
}

return foundElement
}
}

// MARK: Utilities
Expand Down
Loading