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 web-view testing support. #4394

Merged
merged 72 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
367b42a
docs(web-views): improve and update (iOS support).
asafkorem Feb 28, 2024
d74f7bf
docs(webviews): fix redundant empty line.
asafkorem Feb 28, 2024
4e3d3ed
test(e2e, webviews): enable on ios.
asafkorem Feb 28, 2024
309d981
feat(webviews): add trace descriptions.
asafkorem Mar 1, 2024
8516b17
test(e2e): enable webview tests on iOS.
asafkorem Mar 1, 2024
a8316bd
feat(ios): add web-expect and web-action APIs.
asafkorem Mar 1, 2024
d49b053
test(unit, ios): test web-view apis.
asafkorem Mar 1, 2024
0721463
iOS(WebJSCodeBuilder): initial commit.
asafkorem Mar 2, 2024
6e6eba5
feat(ios): add web-view utils for `WKWebView`.
asafkorem Mar 2, 2024
eea60a9
feat(ios): add `WebExpectation`, route from `InvocationManager`.
asafkorem Mar 2, 2024
7e5c5f2
feat(iOS): make web-view interaction work.
asafkorem Mar 4, 2024
2543268
fix(ios): handle async actions on web-view expectation JS evaluation.
asafkorem Mar 4, 2024
db60bb2
feat(ios): introduce web actions.
asafkorem Mar 4, 2024
18c20fc
feat(ios, webviews): make runscript work.
asafkorem Mar 5, 2024
68e94c6
fix(ios, webview): fix web-view matching with element matcher.
asafkorem Mar 6, 2024
a4acd73
fix(ios, webview): improve element-not-found error.
asafkorem Mar 6, 2024
c18fe6e
refactor(ios, webview): extract WebInteraction.
asafkorem Mar 7, 2024
d7f3d9d
refactor(ios, webview): extract WKWebView utils.
asafkorem Mar 7, 2024
1e6ca9b
refactor(ios, webview): extract findView method.
asafkorem Mar 7, 2024
9c48f33
refactor(ios, webview): extract methods.
asafkorem Mar 7, 2024
8f51173
fix(ios, webview): fix type text.
asafkorem Mar 7, 2024
dc3482f
fix(test): export error.
asafkorem Mar 7, 2024
e49dc05
feat(test): duplicate `WebViewScreen` to `WebViewScreenV2`.
asafkorem Mar 7, 2024
49ed906
feat(ios, webview): add tests.
asafkorem Mar 7, 2024
81d96b3
test(e2e): extract common code.
asafkorem Mar 8, 2024
03d5c28
test(e2e): extract snapshot util.
asafkorem Mar 8, 2024
74a3470
docs(webviews): clarify android-only limitations.
asafkorem Mar 9, 2024
f47d0c5
text(e2e): fix snapshot util.
asafkorem Mar 9, 2024
36b6361
refactor(ios): introduce web action creation methods.
asafkorem Mar 9, 2024
fbb9b58
refactor(ios): improve web-view action error message.
asafkorem Mar 9, 2024
0f3758f
feat(ios, WKWebView): add OS logs.
asafkorem Mar 9, 2024
39a2a83
refactor(ios, WebCodeBuilder): use new create methods.
asafkorem Mar 9, 2024
6ff44b8
test(fix e2e): update snapshot.
asafkorem Mar 9, 2024
8b8e9a8
fix(ios): change logging category name.
asafkorem Mar 9, 2024
d1be0d6
test(ios, webview): update snapshot.
asafkorem Mar 9, 2024
f8a8afb
fix(ios, webview): improve error message, fix typing.
asafkorem Mar 9, 2024
4a50b46
fix(ios, webview): fix return value of action.
asafkorem Mar 9, 2024
606255c
test(e2e): enable all webview tests.
asafkorem Mar 9, 2024
646a1a1
test(e2e): change webview screen.
asafkorem Mar 9, 2024
1b4a2d6
test(e2e, webview): update snapshot.
asafkorem Mar 9, 2024
036303f
test(e2e, webview): fix tests.
asafkorem Mar 9, 2024
d76313f
test(e2e, webview): fix tests.
asafkorem Mar 9, 2024
b81b838
test(e2e): remove legacy tests with new version.
asafkorem Mar 9, 2024
099c0ad
docs: make doclint happy.
asafkorem Mar 9, 2024
2f77665
refactor(ios): add cosmetic ELs.
asafkorem Mar 9, 2024
cc08275
types(webviews): update docs.
asafkorem Mar 9, 2024
a9d7882
test(e2e, webviews): add content-editable tests.
asafkorem Mar 9, 2024
8bad7f5
feat(webview, ios): support at-index for web-predicates.
asafkorem Mar 9, 2024
840a150
feat(webviews, ios): support `atIndex` for all matchers.
asafkorem Mar 10, 2024
594fe15
test(unit): cover new web-view apis.
asafkorem Mar 10, 2024
baf24ed
test(unit): add missing unit test for coverage.
asafkorem Mar 10, 2024
20b0cd5
feat(webviews, ios): support at-index for web-view matching.
asafkorem Mar 10, 2024
329659a
docs(webviews): update with at-index docs.
asafkorem Mar 10, 2024
15f642a
fix(webviews, ios): fix at-index support.
asafkorem Mar 10, 2024
739b6b8
test(webviews, unit): add tests for full coverage.
asafkorem Mar 10, 2024
8312134
test(e2e): improve web-view screen.
asafkorem Mar 10, 2024
603c787
feat(android, webview): throw atIndex not supported.
asafkorem Mar 10, 2024
7910f38
feat(ios, webview): improve support for at-index.
asafkorem Mar 10, 2024
5fd93ed
test(e2e, webview): update tests and snapshots.
asafkorem Mar 10, 2024
b65d658
test(e2e, webview): fix tests.
asafkorem Mar 10, 2024
eda491c
test(e2e, webviews): update snapshots.
asafkorem Mar 10, 2024
6ac6d23
refactor(ios): remove redundant el.
asafkorem Mar 10, 2024
8e30e25
test: add ssim.js package.
asafkorem Mar 10, 2024
7da57a5
test(e2e): use SSIM score to compare snapshots.
asafkorem Mar 11, 2024
b60aa62
test: remove redundant `;`.
asafkorem Mar 11, 2024
1703d71
test(e2e): extract `waitForCondition` method.
asafkorem Mar 11, 2024
8d791ec
feat(ios, webview): allow multiple matchings for id.
asafkorem Mar 11, 2024
72434f2
test(e2e, webview): disable android failing tests.
asafkorem Mar 11, 2024
e344028
test(e2e, android): upload snapshots for web-view matchers.
asafkorem Mar 11, 2024
7fe2b4f
test(android, webviews): update snapshots.
asafkorem Mar 11, 2024
fe27f77
fix(android): add workaround delay before return from web action.
asafkorem Mar 11, 2024
1aaebce
refactor: remove / change comments.
asafkorem Mar 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
35 changes: 27 additions & 8 deletions detox/detox.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1137,7 +1137,21 @@ declare global {
}

interface WebViewElement {
/**
* Find a web element by a matcher.
* @param webMatcher a web matcher for the web element.
*/
element(webMatcher: WebMatcher): IndexableWebElement;

/**
* Returns the index-th web-view in the UI hierarchy that is matched by the given matcher.
* @param index the index of the web-view.
*
* @note Currently, supported only for iOS.
*
* @example await web(by.id('webview')).atIndex(1);
*/
atIndex(index: number): WebViewElement;
}

interface WebFacade extends WebViewElement {
Expand Down Expand Up @@ -1507,8 +1521,8 @@ declare global {

interface IndexableWebElement extends WebElement {
/**
* Choose from multiple elements matching the same matcher using index
* @example await web.element(by.web.hrefContains('Details')).atIndex(2).tap();
* Choose from multiple elements matching the same matcher using index.
* @example await web.element(by.web.tag('p')).atIndex(2).tap();
*/
atIndex(index: number): WebElement;
}
Expand All @@ -1520,24 +1534,27 @@ declare global {
tap(): Promise<void>;

/**
* Type text into a web element.
* @param text to type
* @param isContentEditable whether its a ContentEditable element, default is false.
* @param isContentEditable whether the element is content-editable, default is false. Ignored on iOS.
*/
typeText(text: string, isContentEditable: boolean): Promise<void>;

/**
* At the moment not working on content-editable
* Replaces the input content with the new text.
* @note On Android, not working for content-editable elements.
* @param text to replace with the old content.
*/
replaceText(text: string): Promise<void>;

/**
* At the moment not working on content-editable
* Clears the input content.
* @note On Android, not working for content-editable elements.
*/
clearText(): Promise<void>;

/**
* scrolling to the view, the element top position will be at the top of the screen.
* Scrolling to the view, the element top position will be at the top of the screen.
*/
scrollToView(): Promise<void>;

Expand All @@ -1552,12 +1569,14 @@ declare global {
focus(): Promise<void>;

/**
* Selects all the input content, works on ContentEditable at the moment.
* Selects all the input content.
* @note On Android, it works only for content-editable elements.
*/
selectAllText(): Promise<void>;

/**
* Moves the input cursor / caret to the end of the content, works on ContentEditable at the moment.
* Moves the input cursor to the end of the content.
* @note On Android, it works only for content-editable elements.
*/
moveCursorToEnd(): Promise<void>;

Expand Down
168 changes: 168 additions & 0 deletions detox/ios/Detox.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion detox/ios/Detox/Invocation/Element.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class Element : NSObject {
return array
}

private var view : NSObject {
var view : NSObject {
let array = self.views

let element : NSObject
Expand Down
25 changes: 23 additions & 2 deletions detox/ios/Detox/Invocation/InvocationManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ final class InvocationManager {
internal struct Types {
static let action = "action"
static let expectation = "expectation"

static let webAction = "webAction"
static let webExpectation = "webExpectation"
}

class func invoke(dictionaryRepresentation: [String: Any], completionHandler: @escaping ([String: Any]?, Error?) -> Void) {
Expand All @@ -33,14 +36,32 @@ final class InvocationManager {
switch kind {
case Types.action:
let action = try Action.with(dictionaryRepresentation: dictionaryRepresentation)
os_signpost(.begin, log: log.osLog, name: "Action Invocation", signpostID: signpostID, "%{public}s", action.description)
os_signpost(.begin, log: log.osLog, name: "Action Invocation",
signpostID: signpostID, "%{public}s", action.description)
action.perform(completionHandler: signpostCompletionHandler)

case Types.expectation:
let expectation = try Expectation.with(dictionaryRepresentation: dictionaryRepresentation)
os_signpost(.begin, log: log.osLog, name: "Expectation Invocation", signpostID: signpostID, "%{public}s", expectation.description)
os_signpost(.begin, log: log.osLog, name: "Expectation Invocation",
signpostID: signpostID, "%{public}s", expectation.description)
expectation.evaluate { error in
signpostCompletionHandler(nil, error)
}

case Types.webAction:
let action = try WebAction.init(json: dictionaryRepresentation)
os_signpost(.begin, log: log.osLog, name: "Web action Invocation",
signpostID: signpostID, "%{public}s", action.description)
action.perform(completionHandler: signpostCompletionHandler)

case Types.webExpectation:
let expectation = try WebExpectation.init(json: dictionaryRepresentation)
os_signpost(.begin, log: log.osLog, name: "Web expectation Invocation",
signpostID: signpostID, "%{public}s", expectation.description)
expectation.evaluate { error in
signpostCompletionHandler(nil, error)
}

default:
fatalError("Unknown invocation type “\(kind)”")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// WKWebView+evaluateJSAfterLoading.swift (Detox)
// Created by Asaf Korem (Wix.com) on 2024.
//

import WebKit

fileprivate let log = DetoxLog(category: "WebView")

/// Extends WKWebView with the ability to evaluate JavaScript after the web view has
/// finished loading.
extension WKWebView {
func evaluateJSAfterLoading(
_ javaScriptString: String,
completionHandler: ((Any?, Error?) -> Void)? = nil
) {
let cleanJavaScriptString = replaceConsecutiveSpacesAndTabs(in: javaScriptString)
log.debug("Evaluating JavaScript after loading: `\(cleanJavaScriptString)`")

var observation: NSKeyValueObservation?
observation = self.observe(
\.isLoading, options: [.new, .old, .initial]
) { (webView, change) in
guard change.newValue == false else { return }

observation?.invalidate()

log.debug("Evaluating JavaScript on web-view: `\(cleanJavaScriptString)`")
webView.evaluateJavaScript(cleanJavaScriptString, completionHandler: completionHandler)
}
}

private func replaceConsecutiveSpacesAndTabs(in input: String) -> String {
let pattern = "[ \\t\\r\\n]+"
let regex = try! NSRegularExpression(pattern: pattern, options: [])
let range = NSRange(location: 0, length: input.utf16.count)
let modifiedString = regex.stringByReplacingMatches(in: input, options: [], range: range, withTemplate: " ")
return modifiedString
}
}
65 changes: 65 additions & 0 deletions detox/ios/Detox/Invocation/WKWebView+findView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// WKWebView+findView.swift (Detox)
// Created by Asaf Korem (Wix.com) on 2024.
//

import WebKit

/// Extends WKWebView with the ability to find a web view element.
extension WKWebView {
/// Finds a web view element by the given `predicate` at the given `index`.
class func findView(
by predicate: Predicate?,
atIndex index: Int?
) throws -> WKWebView {
let webView: WKWebView?

if let predicate = predicate {
guard let ancestor = Element(predicate: predicate, index: index).view as? UIView else {
throw dtx_errorForFatalError(
"Failed to find web view with predicate: \(predicate.description)")
}

webView = try findWebViewDescendant(in: ancestor)
} else {
webView = try findWebViewDescendant()
}

guard let webView = webView else {
throw dtx_errorForFatalError(
"Failed to find web view with predicate: `\(predicate?.description ?? "")` " +
"at index: `\(index ?? 0)`")
}

return webView
}

fileprivate class func findWebViewDescendant(
in ancestor: UIView? = nil
) throws -> WKWebView? {
let predicate = NSPredicate.init { (view, _) -> Bool in
return view is WKWebView
}

var webViews: [WKWebView]
if let ancestor = ancestor {
webViews = UIView.dtx_findViews(inHierarchy: ancestor, passing: predicate).compactMap {
$0 as? WKWebView
}
} else {
webViews = UIView.dtx_findViewsInAllWindows(passing: predicate).compactMap {
$0 as? WKWebView
}
}

if webViews.count == 0 {
return nil
} else if webViews.count > 1 {
throw dtx_errorForFatalError(
"Found more than one matching web view in the hierarchy. " +
"Please specify a predicate to find the correct web view.")
} else {
return webViews.first
}
}
}
61 changes: 61 additions & 0 deletions detox/ios/Detox/Invocation/WebAction.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// WebAction.swift (Detox)
// Created by Asaf Korem (Wix.com) on 2024.
//

import WebKit

/// Represents a web action to be performed on a web view.
class WebAction: WebInteraction {
var webAction: WebActionType
var params: [Any]?

override init(json: [String: Any]) throws {
self.webAction = WebActionType(rawValue: json["webAction"] as! String)!
self.params = json["params"] as? [Any]
try super.init(json: json)
}

override var description: String {
return "WebAction: \(webAction.rawValue)"
}

func perform(completionHandler: @escaping ([String: Any]?, Error?) -> Void) {
var jsString: String
var webView: WKWebView

do {
jsString = try WebCodeBuilder()
.with(predicate: webPredicate, atIndex: webAtIndex)
.with(action: webAction, params: params)
.build()

webView = try WKWebView.findView(by: predicate, atIndex: atIndex)
} catch {
completionHandler(nil, error)
return
}

webView.evaluateJSAfterLoading(jsString) { (result, error) in
if let error = error {
completionHandler(
["result": false, "error": error.localizedDescription],
dtx_errorForFatalError(
"Failed to evaluate JavaScript on web view: \(webView.debugDescription). " +
"Error: \(error.localizedDescription)")
)
} else if let jsError = (result as? [String: Any])?["error"] as? String {
completionHandler(
["result": false, "error": jsError],
dtx_errorForFatalError(
"Failed to evaluate JavaScript on web view: \(webView.debugDescription). " +
"JS exception: \(jsError)")
)
} else if let result = (result as? [String: Any])?["result"] as? String {
completionHandler(["result": result], nil)
} else {
completionHandler(nil, nil)
}
}
}
}
20 changes: 20 additions & 0 deletions detox/ios/Detox/Invocation/WebActionType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// WebActionType.swift (Detox)
// Created by Asaf Korem (Wix.com) on 2024.
//

enum WebActionType: String, Codable {
case tap = "tap"
case typeText = "typeText"
case replaceText = "replaceText"
case clearText = "clearText"
case selectAllText = "selectAllText"
case getText = "getText"
case scrollToView = "scrollToView"
case focus = "focus"
case moveCursorToEnd = "moveCursorToEnd"
case runScript = "runScript"
case runScriptWithArgs = "runScriptWithArgs"
case getCurrentUrl = "getCurrentUrl"
case getTitle = "getTitle"
}