-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4394 from wix/feat/ios-web-views
feat(ios): add web-view testing support.
- Loading branch information
Showing
58 changed files
with
2,518 additions
and
679 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
40 changes: 40 additions & 0 deletions
40
detox/ios/Detox/Invocation/WKWebView+evaluateJSAfterLoading.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
Oops, something went wrong.