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

Fix entering text into SecureTextField on iOS #446

Merged
merged 9 commits into from
Oct 14, 2022
53 changes: 41 additions & 12 deletions AutomatorServer/ios/AutomatorServerUITests/Automator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,18 @@ class Automator {
// MARK: General UI interaction

func tap(on text: String, inApp bundleId: String) async throws {
try await runAction("tapping on text \(text)") {
try await runAction("tapping on view with text \(format: text)") {
let app = try self.getApp(withBundleId: bundleId)
let element = app.descendants(matching: .any)[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("found view with text \(format: text), will tap on it")

element.firstMatch.tap()
element.firstMatch.forceTap()
}
}

Expand All @@ -78,7 +80,7 @@ class Automator {
throw PatrolError.viewNotExists("view with text \(format: text) in app \(format: bundleId)")
}

element.firstMatch.tap()
element.firstMatch.forceTap()
}
}

Expand All @@ -87,27 +89,42 @@ class Automator {
let app = try self.getApp(withBundleId: bundleId)
let element = app.textFields[text]

let exists = element.waitForExistence(timeout: self.timeout)
guard exists else {
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)")
}

element.firstMatch.typeText(data)
}
}

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

let exists = element.waitForExistence(timeout: self.timeout)
guard exists else {
throw PatrolError.viewNotExists("text field at index \(index) 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 textFields = app.descendants(matching: .any).matching(predicate)
let textFieldCount = textFields.allElementsBoundByIndex.count
Logger.shared.i("found \(textFields.count) text fields")
guard index < textFieldCount else {
throw PatrolError.viewNotExists("text field at index \(index)")
}

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

let textField = textFields.element(boundBy: index)
textField.forceTap()
textField.typeText(data)
}
}

Expand Down Expand Up @@ -508,3 +525,15 @@ extension String.StringInterpolation {
appendInterpolation("\"\(value)\"")
}
}

// Adapted from https://samwize.com/2016/02/28/everything-about-xcode-ui-testing-snapshot/
extension XCUIElement {
func forceTap() {
if self.isHittable {
self.tap()
} else {
let coordinate = self.coordinate(withNormalizedOffset: CGVectorMake(0.0, 0.0))
coordinate.tap()
}
}
}
23 changes: 23 additions & 0 deletions packages/patrol/example/integration_test/webview_a_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'package:example/main.dart';
import 'package:patrol/patrol.dart';

import 'config.dart';

Future<void> main() async {
patrolTest(
'interacts with the LeanCode website in a webview',
config: patrolConfig,
nativeAutomation: true,
($) async {
await $.pumpWidgetAndSettle(ExampleApp());

await $('Open webview screen A').scrollTo().tap();

await $.native.tap(Selector(text: 'Accept cookies'));
await $.native.tap(Selector(text: 'Select items'));
await $.native.tap(Selector(text: 'Developer'));
await $.native.tap(Selector(text: '1 item selected'));
await $.native.enterTextByIndex('test@leancode.pl', index: 0);
},
);
}
21 changes: 21 additions & 0 deletions packages/patrol/example/integration_test/webview_b_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'package:example/main.dart';
import 'package:patrol/patrol.dart';

import 'config.dart';

Future<void> main() async {
patrolTest(
'interacts with the orange website in a webview',
config: patrolConfig,
nativeAutomation: true,
($) async {
await $.pumpWidgetAndSettle(ExampleApp());

await $('Open webview screen B').scrollTo().tap();

await $.native.tap(Selector(text: 'login'));
await $.native.enterTextByIndex('test@leancode.pl', index: 0);
await $.native.enterTextByIndex('ny4ncat', index: 1);
},
);
}
58 changes: 0 additions & 58 deletions packages/patrol/example/integration_test/webview_test.dart

This file was deleted.

23 changes: 16 additions & 7 deletions packages/patrol/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import 'package:example/notifications_screen.dart';
import 'package:example/overlay_screen.dart';
import 'package:example/permissions_screen.dart';
import 'package:example/scrolling_screen.dart';
import 'package:example/webview_screen.dart';
import 'package:example/webview_screen_a.dart';
import 'package:example/webview_screen_b.dart';
import 'package:flutter/material.dart';

void main() {
Expand Down Expand Up @@ -164,26 +165,34 @@ class _ExampleHomePageState extends State<ExampleHomePage> {
TextButton(
onPressed: () async => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const PermissionsScreen(),
builder: (_) => const ScrollingScreen(),
),
),
child: const Text('Open permissions screen'),
child: const Text('Open scrolling screen'),
),
TextButton(
onPressed: () async => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const ScrollingScreen(),
builder: (_) => const WebViewScreenA(),
),
),
child: const Text('Open scrolling screen'),
child: const Text('Open webview screen A'),
),
TextButton(
onPressed: () async => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const WebViewScreenB(),
),
),
child: const Text('Open webview screen B'),
),
TextButton(
onPressed: () async => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const WebViewScreen(),
builder: (_) => const PermissionsScreen(),
),
),
child: const Text('Open webview screen'),
child: const Text('Open permissions screen'),
),
],
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class WebViewScreen extends StatelessWidget {
const WebViewScreen({super.key});
class WebViewScreenA extends StatelessWidget {
const WebViewScreenA({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('WebView'),
title: const Text('WebView A'),
),
body: WebView(
debuggingEnabled: true,
Expand Down
26 changes: 26 additions & 0 deletions packages/patrol/example/lib/webview_screen_b.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class WebViewScreenB extends StatelessWidget {
const WebViewScreenB({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('WebView B'),
),
body: WebView(
debuggingEnabled: true,
initialUrl: 'https://hackernews.com',
javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (controller) => print('WebView created'),
onPageStarted: (url) => print('Page started loading: $url'),
onProgress: (progress) {
print('WebView is loading (progress : $progress%)');
},
onPageFinished: (url) => print('Page finished loading: $url'),
),
);
}
}