Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into backdoor
Browse files Browse the repository at this point in the history
  • Loading branch information
noomorph committed Mar 19, 2024
2 parents 1355117 + 275f00a commit 89f11f7
Show file tree
Hide file tree
Showing 80 changed files with 3,258 additions and 459 deletions.
35 changes: 27 additions & 8 deletions detox/detox.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1145,7 +1145,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 @@ -1515,8 +1529,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 @@ -1528,24 +1542,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 @@ -1560,12 +1577,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
184 changes: 184 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
40 changes: 40 additions & 0 deletions detox/ios/Detox/Invocation/WKWebView+evaluateJSAfterLoading.swift
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
}
}
}
14 changes: 14 additions & 0 deletions detox/ios/Detox/Invocation/WKWebViewConfiguration+Detox.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// WKWebViewConfiguration+Detox.h (Detox)
// Created by Asaf Korem (Wix.com) on 2024.
//

@import WebKit;

NS_ASSUME_NONNULL_BEGIN

@interface WKWebViewConfiguration (Detox)

@end

NS_ASSUME_NONNULL_END
70 changes: 70 additions & 0 deletions detox/ios/Detox/Invocation/WKWebViewConfiguration+Detox.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//
// WKWebViewConfiguration+Detox.m (Detox)
// Created by Asaf Korem (Wix.com) on 2024.
//

#import "WKWebViewConfiguration+Detox.h"

@import ObjectiveC;

void WKPreferencesSetWebSecurityEnabled(id, bool);

@interface DTXFakeWKPreferencesRef: NSObject
@property (nonatomic) void* _apiObject;
@end

@implementation DTXFakeWKPreferencesRef
@end

/// Set web-security policy for WebKit (e.g. CORS restriction).
///
/// @note Since we can't access the `WKPreferencesSetWebSecurityEnabled` directly with
/// a `WKPreferences*`, we wrap it in a `WKPreferencesRef`, which can be passed to this function.
/// This private API is not officially supported on iOS, and generally used for debugging / testing
/// purposes on MacOS only. So there's no guarantee that it will work in the future.
void DTXPreferencesSetWebSecurityEnabled(WKPreferences* prefs, bool enabled) {
DTXFakeWKPreferencesRef* fakeRef = [DTXFakeWKPreferencesRef new];

Ivar ivar = class_getInstanceVariable([WKPreferences class], "_preferences");
void* realPreferences = (void*)(((uintptr_t)prefs) + ivar_getOffset(ivar));
fakeRef._apiObject = realPreferences;

WKPreferencesSetWebSecurityEnabled(fakeRef, enabled);
}

@implementation WKWebViewConfiguration (Detox)

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];

SEL originalSelector = @selector(setPreferences:);
SEL swizzledSelector = @selector(dtx_setPreferences:);

Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));

if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}

- (void)dtx_setPreferences:(WKPreferences *)preferences {
DTXPreferencesSetWebSecurityEnabled(preferences, NO);

[self dtx_setPreferences:preferences];
}

@end
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)
}
}
}
}
Loading

0 comments on commit 89f11f7

Please sign in to comment.