Skip to content

Commit

Permalink
Implement native scrollTo for iOS and use it in webview_leancode_test…
Browse files Browse the repository at this point in the history
….dart
  • Loading branch information
piotruela committed Sep 20, 2024
1 parent 13c3927 commit 2a786cf
Show file tree
Hide file tree
Showing 20 changed files with 406 additions and 36 deletions.
49 changes: 23 additions & 26 deletions dev/e2e_app/integration_test/webview_leancode_test.dart
Original file line number Diff line number Diff line change
@@ -1,27 +1,8 @@
import 'dart:io' as io;

import 'common.dart';

void main() {
patrol('interacts with the LeanCode website in a webview', ($) async {
await createApp($);

await $('Open webview (LeanCode)').scrollTo().tap();
await $.pump(Duration(seconds: 8));

try {
await $.native.tap(Selector(text: 'Accept cookies'));
} on PatrolActionException catch (_) {
// ignore
}
await $.pumpAndSettle();

await $.native.enterTextByIndex(
'[email protected]',
index: 0,
keyboardBehavior: KeyboardBehavior.showAndDismiss,
);
await $.native.tap(Selector(text: 'Subscribe'));
});

patrol('interacts with the LeanCode website in a webview native2', ($) async {
await createApp($);

Expand All @@ -32,20 +13,36 @@ void main() {
try {
await $.native2.tap(
NativeSelector(
android: AndroidSelector(text: 'Accept cookies'),
ios: IOSSelector(label: 'Accept cookies'),
android: AndroidSelector(text: 'ACCEPT ALL COOKIES'),
ios: IOSSelector(label: 'ACCEPT ALL COOKIES'),
),
);
} on PatrolActionException catch (_) {
// ignore
}
await $.pumpAndSettle();

await $.native2.enterTextByIndex(
'[email protected]',
index: 0,
if (io.Platform.isIOS) {
await $.native2.scrollTo(
NativeSelector(
ios: IOSSelector(placeholderValue: 'Type your email'),
),
maxScrolls: 20,
);
}

await $.pump(Duration(seconds: 5));

await $.native2.enterText(
NativeSelector(
android: AndroidSelector(className: 'android.widget.EditText'),
ios: IOSSelector(placeholderValue: 'Type your email'),
),
text: '[email protected]',
keyboardBehavior: KeyboardBehavior.showAndDismiss,
tapLocation: Offset(0.5, 0.5),
);

await $.native2.tap(
NativeSelector(
android: AndroidSelector(text: 'Subscribe'),
Expand Down
2 changes: 1 addition & 1 deletion dev/e2e_app/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ packages:
path: "../../packages/patrol"
relative: true
source: path
version: "3.10.0"
version: "3.11.0"
patrol_finders:
dependency: transitive
description:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,10 @@ class Automator private constructor() {
delay()
}

fun scrollTo(uiSelector: UiSelector, bySelector: BySelector, index: Int, maxScrolls: Long) {
throw PatrolException("scrollTo() is not implemented")
}

fun waitUntilVisible(uiSelector: UiSelector, bySelector: BySelector, index: Int, timeout: Long? = null) {
Logger.d("waitUntilVisible(): $uiSelector, $bySelector")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,26 @@ class AutomatorServer(private val automation: Automator) : NativeAutomatorServer
)
}

override fun scrollTo(request: Contracts.ScrollToRequest) {
if (request.selector != null) {
automation.scrollTo(
uiSelector = request.selector.toUiSelector(),
bySelector = request.selector.toBySelector(),
index = request.selector.instance?.toInt() ?: 0,
maxScrolls = request.maxScrolls,
)
} else if (request.androidSelector != null) {
automation.scrollTo(
uiSelector = request.androidSelector.toUiSelector(),
bySelector = request.androidSelector.toBySelector(),
index = request.androidSelector.instance?.toInt() ?: 0,
maxScrolls = request.maxScrolls,
)
} else {
throw PatrolException("scrollTo(): neither selector nor androidSelector are set")
}
}

override fun waitUntilVisible(request: WaitUntilVisibleRequest) {
if (request.selector != null) {
automation.waitUntilVisible(
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,18 @@ extension Selector {
dy: CGFloat
) throws
func swipe(from start: CGVector, to end: CGVector, inApp bundleId: String) throws
func scrollTo(
on selector: Selector,
inApp bundleId: String,
atIndex index: Int,
maxScrolls scrolls: Int
) throws
func scrollTo(
on selector: IOSSelector,
inApp bundleId: String,
atIndex index: Int,
maxScrolls scrolls: Int
) throws
func waitUntilVisible(
on selector: Selector,
inApp bundleId: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,48 @@
}
}

func scrollTo(on selector: Selector, inApp bundleId: String, atIndex index: Int, maxScrolls scrolls: Int) throws {
var view = createLogMessage(element: "view", from: selector)
view += " in app \(bundleId)"

try runAction("scrolling to \(view)") {
let app = try self.getApp(withBundleId: bundleId)

let query = app.descendants(matching: .any).matching(selector.toNSPredicate())

Logger.shared.i("waiting for existence of \(view)")
guard
let element = self.waitFor(
query: query, index: selector.instance ?? 0, timeout: self.timeout)
else {
throw PatrolError.viewNotExists(view)
}

try self.scrollToElement(element: element, bundleId: bundleId, index: index, scrolls: scrolls)
}
}

func scrollTo(on selector: IOSSelector, inApp bundleId: String, atIndex index: Int, maxScrolls scrolls: Int) throws {
var view = createLogMessage(element: "view", from: selector)
view += " in app \(bundleId)"

try runAction("scrolling to \(view)") {
let app = try self.getApp(withBundleId: bundleId)

let query = app.descendants(matching: .any).matching(selector.toNSPredicate())

Logger.shared.i("waiting for existence of \(view)")
guard
let element = self.waitFor(
query: query, index: selector.instance ?? 0, timeout: self.timeout)
else {
throw PatrolError.viewNotExists(view)
}

try self.scrollToElement(element: element, bundleId: bundleId, index: index, scrolls: scrolls)
}
}

func waitUntilVisible(
on selector: Selector,
inApp bundleId: String,
Expand Down Expand Up @@ -889,6 +931,32 @@

element.typeText(delete + data)
}

private func scrollToElement(element: XCUIElement, bundleId: String, index: Int, scrolls: Int) throws {
var attempts = 0 // Track the number of scrolls
while try isVisible(element: element, bundleId: bundleId, index: index) == false
{
if attempts >= scrolls {
throw PatrolError.viewNotExists("Element not found after \(scrolls) scrolls.")
}
let app = try self.getApp(withBundleId: bundleId)

let startCoordinate = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.9))
let endCoordinate = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.1))
startCoordinate.press(forDuration: 0.1, thenDragTo: endCoordinate)

attempts += 1 // Increment the scroll count after each scroll
}
}

private func isVisible(element: XCUIElement, bundleId: String, index: Int) throws-> Bool
{
let app = try self.getApp(withBundleId: bundleId)

guard element.exists && element.isHittable && !CGRectIsEmpty(element.frame) else { return false }

return CGRectContainsRect(app.windows.element(boundBy: index).frame, element.frame)
}

private func isSimulator() -> Bool {
#if targetEnvironment(simulator)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,8 @@
inApp: request.appId,
dismissKeyboard: request.keyboardBehavior == .showAndDismiss,
withTimeout: request.timeoutMillis.map { TimeInterval($0 / 1000) },
dx: request.dx,
dy: request.dy
dx: request.dx ?? 0.9,
dy: request.dy ?? 0.9
)
} else if let selector = request.selector {
try automator.enterText(
Expand All @@ -165,8 +165,8 @@
inApp: request.appId,
dismissKeyboard: request.keyboardBehavior == .showAndDismiss,
withTimeout: request.timeoutMillis.map { TimeInterval($0 / 1000) },
dx: request.dx,
dy: request.dy
dx: request.dx ?? 0.9,
dy: request.dy ?? 0.9
)
} else if let iosSelector = request.iosSelector {
try automator.enterText(
Expand All @@ -175,8 +175,8 @@
inApp: request.appId,
dismissKeyboard: request.keyboardBehavior == .showAndDismiss,
withTimeout: request.timeoutMillis.map { TimeInterval($0 / 1000) },
dx: request.dx,
dy: request.dy
dx: request.dx ?? 0.9,
dy: request.dy ?? 0.9
)
} else {
throw PatrolError.internal("enterText(): neither index nor selector are set")
Expand All @@ -194,6 +194,28 @@
}
}

func scrollTo(request: ScrollToRequest) throws {
return try runCatching {
if let selector = request.selector {
return try automator.scrollTo(
on: selector,
inApp: request.appId,
atIndex: request.selector?.instance ?? 0,
maxScrolls: request.maxScrolls
)
} else if let iosSelector = request.iosSelector {
return try automator.scrollTo(
on: iosSelector,
inApp: request.appId,
atIndex: request.iosSelector?.instance ?? 0,
maxScrolls: request.maxScrolls
)
} else {
throw PatrolError.internal("scrollTo(): neither selector nor iosSelector are set")
}
}
}

func waitUntilVisible(request: WaitUntilVisibleRequest) throws {
return try runCatching {
if let selector = request.selector {
Expand Down
12 changes: 10 additions & 2 deletions packages/patrol/darwin/Classes/AutomatorServer/Contracts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -318,8 +318,8 @@ public struct EnterTextRequest: Codable {
public var iosSelector: IOSSelector?
public var keyboardBehavior: KeyboardBehavior
public var timeoutMillis: Int?
public var dx: Double
public var dy: Double
public var dx: Double?
public var dy: Double?
}

public struct SwipeRequest: Codable {
Expand All @@ -339,6 +339,14 @@ public struct WaitUntilVisibleRequest: Codable {
public var timeoutMillis: Int?
}

public struct ScrollToRequest: Codable {
public var selector: Selector?
public var androidSelector: AndroidSelector?
public var iosSelector: IOSSelector?
public var appId: String
public var maxScrolls: Int
}

public struct DarkModeRequest: Codable {
public var appId: String
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ protocol NativeAutomatorServer {
func tapAt(request: TapAtRequest) throws
func enterText(request: EnterTextRequest) throws
func swipe(request: SwipeRequest) throws
func scrollTo(request: ScrollToRequest) throws
func waitUntilVisible(request: WaitUntilVisibleRequest) throws
func pressVolumeUp() throws
func pressVolumeDown() throws
Expand Down Expand Up @@ -143,6 +144,12 @@ extension NativeAutomatorServer {
return HTTPResponse(.ok)
}

private func scrollToHandler(request: HTTPRequest) throws -> HTTPResponse {
let requestArg = try JSONDecoder().decode(ScrollToRequest.self, from: request.body)
try scrollTo(request: requestArg)
return HTTPResponse(.ok)
}

private func waitUntilVisibleHandler(request: HTTPRequest) throws -> HTTPResponse {
let requestArg = try JSONDecoder().decode(WaitUntilVisibleRequest.self, from: request.body)
try waitUntilVisible(request: requestArg)
Expand Down Expand Up @@ -361,6 +368,11 @@ extension NativeAutomatorServer {
request: request,
handler: swipeHandler)
}
server.route(.POST, "scrollTo") {
request in handleRequest(
request: request,
handler: scrollToHandler)
}
server.route(.POST, "waitUntilVisible") {
request in handleRequest(
request: request,
Expand Down
Loading

0 comments on commit 2a786cf

Please sign in to comment.