From 743ee9f0d5678293cf99868d78268843cde628d3 Mon Sep 17 00:00:00 2001 From: iMoe Date: Tue, 23 Apr 2024 13:23:22 +0800 Subject: [PATCH 1/3] include real-world tests --- Tests/DSBridgeTests/DSBridgeTests.swift | 251 ++++++++++++++++++++++++ 1 file changed, 251 insertions(+) diff --git a/Tests/DSBridgeTests/DSBridgeTests.swift b/Tests/DSBridgeTests/DSBridgeTests.swift index 7477a99..32bf5ca 100644 --- a/Tests/DSBridgeTests/DSBridgeTests.swift +++ b/Tests/DSBridgeTests/DSBridgeTests.swift @@ -1,6 +1,257 @@ import XCTest @testable import DSBridge +import WebKit final class DSBridgeTests: XCTestCase { + @Exposed + struct Interface { + static var testInvoked = false + func test() { + Self.testInvoked = true + } + func willReturn1() -> Int { + 1 + } + + static var input: Int? + func input(_ value: Int) { + Self.input = value + } + static var asyncFuncReturnValue = 800 + func asyncFunc(block: (Int) -> Void) { + block(Self.asyncFuncReturnValue) + } + } + var webView: DSBridge.WebView! + + func testCallingFromJavaScript() { + let expectations = [ + XCTestExpectation(), + XCTestExpectation(), + ] + runInJS("bridge.call('test')") { _ in + XCTAssertTrue(Interface.testInvoked) + expectations[0].fulfill() + } + runInJS("bridge.call('willReturn1')") { returned in + XCTAssert(returned as! Int == 1) + expectations[1].fulfill() + } + wait(for: expectations) + } + + func testCallingAsyncFromJavaScript() { + let expectation = XCTestExpectation() + runInJS(""" + bridge.call('asyncFunc', function(returnValue){ + bridge.call('input', returnValue) + }) + """) { _ in + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + XCTAssert(Interface.input == Interface.asyncFuncReturnValue) + expectation.fulfill() + } + } + wait(for: [expectation]) + } + + func testCallingFromNative() { + let expectations = [XCTestExpectation(), XCTestExpectation()] + webView.call("addValue", with: [1, 1], thatReturns: Int.self) { + let returned = try! $0.get() + XCTAssert(returned == 2) + expectations[0].fulfill() + } + webView.call("append", with: ["1", "2", "3"], thatReturns: String.self) { + let returned = try! $0.get() + XCTAssert(returned == "1 2 3") + expectations[1].fulfill() + } + wait(for: expectations) + } + + override func setUp() { + Interface.testInvoked = false + Interface.input = nil + } + + override func invokeTest() { + let exp = XCTestExpectation() + webView = DSBridge.WebView() + injectBridgeScript(into: webView) + let url = URL(string: "about:blank")! + webView.load(URLRequest(url: url)) + + let interface = Interface() + webView.addInterface(interface, by: nil) + webView.evaluateJavaScript( + """ + bridge.register('addValue', function(l, r){ + return l + r; + }) + bridge.registerAsyn('append', function(arg1, arg2, arg3, responseCallback){ + responseCallback(arg1 + " " + arg2 + " " + arg3); + }) + """ + ) { _, _ in + exp.fulfill() + } + wait(for: [exp]) + super.invokeTest() + } + + func runInJS(_ script: String, completion: @escaping (Any?) -> Void) { + webView.evaluateJavaScript(script) { result, _ in + completion(result) + } + } + + func injectBridgeScript(into webView: DSBridge.WebView) { + webView.configuration.userContentController.addUserScript( + WKUserScript( + source: bridgeScript, + injectionTime: .atDocumentStart, + forMainFrameOnly: true + ) + ) + } + + var bridgeScript: String { + """ + var bridge = { + default:this,// for typescript + call: function (method, args, cb) { + var ret = ''; + if (typeof args == 'function') { + cb = args; + args = {}; + } + var arg={data:args===undefined?null:args} + if (typeof cb == 'function') { + var cbName = 'dscb' + window.dscb++; + window[cbName] = cb; + arg['_dscbstub'] = cbName; + } + arg = JSON.stringify(arg) + + //if in webview that dsBridge provided, call! + if(window._dsbridge){ + ret= _dsbridge.call(method, arg) + }else if(window._dswk||navigator.userAgent.indexOf("_dsbridge")!=-1){ + ret = prompt("_dsbridge=" + method, arg); + } + + return JSON.parse(ret||'{}').data + }, + register: function (name, fun, asyn) { + var q = asyn ? window._dsaf : window._dsf + if (!window._dsInit) { + window._dsInit = true; + //notify native that js apis register successfully on next event loop + setTimeout(function () { + bridge.call("_dsb.dsinit"); + }, 0) + } + if (typeof fun == "object") { + q._obs[name] = fun; + } else { + q[name] = fun + } + }, + registerAsyn: function (name, fun) { + this.register(name, fun, true); + }, + hasNativeMethod: function (name, type) { + return this.call("_dsb.hasNativeMethod", {name: name, type:type||"all"}); + }, + disableJavascriptDialogBlock: function (disable) { + this.call("_dsb.disableJavascriptDialogBlock", { + disable: disable !== false + }) + } + }; + + !function () { + if (window._dsf) return; + var ob = { + _dsf: { + _obs: {} + }, + _dsaf: { + _obs: {} + }, + dscb: 0, + dsBridge: bridge, + close: function () { + bridge.call("_dsb.closePage") + }, + _handleMessageFromNative: function (info) { + var arg = JSON.parse(info.data); + var ret = { + id: info.callbackId, + complete: true + } + var f = this._dsf[info.method]; + var af = this._dsaf[info.method] + var callSyn = function (f, ob) { + ret.data = f.apply(ob, arg) + bridge.call("_dsb.returnValue", ret) + } + var callAsyn = function (f, ob) { + arg.push(function (data, complete) { + ret.data = data; + ret.complete = complete!==false; + bridge.call("_dsb.returnValue", ret) + }) + f.apply(ob, arg) + } + if (f) { + callSyn(f, this._dsf); + } else if (af) { + callAsyn(af, this._dsaf); + } else { + //with namespace + var name = info.method.split('.'); + if (name.length<2) return; + var method=name.pop(); + var namespace=name.join('.') + var obs = this._dsf._obs; + var ob = obs[namespace] || {}; + var m = ob[method]; + if (m && typeof m == "function") { + callSyn(m, ob); + return; + } + obs = this._dsaf._obs; + ob = obs[namespace] || {}; + m = ob[method]; + if (m && typeof m == "function") { + callAsyn(m, ob); + return; + } + } + } + } + for (var attr in ob) { + window[attr] = ob[attr] + } + bridge.register("_hasJavascriptMethod", function (method, tag) { + var name = method.split('.') + if(name.length<2) { + return !!(_dsf[name]||_dsaf[name]) + }else{ + // with namespace + var method=name.pop() + var namespace=name.join('.') + var ob=_dsf._obs[namespace]||_dsaf._obs[namespace] + return ob&&!!ob[method] + } + }) + }(); + + module.exports = bridge; + """ + } + } From 10ad5645c94240a98b0b1ce86a28c9bcb8692507 Mon Sep 17 00:00:00 2001 From: iMoe Date: Tue, 23 Apr 2024 23:33:42 +0800 Subject: [PATCH 2/3] Adding test cases --- Tests/DSBridgeTests/DSBridgeTests.swift | 36 ++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/Tests/DSBridgeTests/DSBridgeTests.swift b/Tests/DSBridgeTests/DSBridgeTests.swift index 32bf5ca..807b432 100644 --- a/Tests/DSBridgeTests/DSBridgeTests.swift +++ b/Tests/DSBridgeTests/DSBridgeTests.swift @@ -71,6 +71,21 @@ final class DSBridgeTests: XCTestCase { wait(for: expectations) } + func testCallingNamespaceFromNative() { + let expectations = [XCTestExpectation(), XCTestExpectation()] + webView.call("syncFunctions.returnAsIs", with: [99], thatReturns: Int.self) { + let returned = try! $0.get() + XCTAssert(returned == 99) + expectations[0].fulfill() + } + webView.call("asyncFunctions.adding100", with: [7], thatReturns: Int.self) { + let returned = try! $0.get() + XCTAssert(returned == 107) + expectations[1].fulfill() + } + wait(for: expectations) + } + override func setUp() { Interface.testInvoked = false Interface.input = nil @@ -87,11 +102,24 @@ final class DSBridgeTests: XCTestCase { webView.addInterface(interface, by: nil) webView.evaluateJavaScript( """ - bridge.register('addValue', function(l, r){ - return l + r; + bridge.register('addValue', function(l, r) { + return l + r; + }) + + bridge.registerAsyn('append', function(arg1, arg2, arg3, respond) { + respond(arg1 + " " + arg2 + " " + arg3); + }) + + bridge.register("syncFunctions",{ + returnAsIs: function(v) { + return v + } }) - bridge.registerAsyn('append', function(arg1, arg2, arg3, responseCallback){ - responseCallback(arg1 + " " + arg2 + " " + arg3); + + bridge.registerAsyn("asyncFunctions",{ + adding100: function(input, respond) { + respond(input + 100) + } }) """ ) { _, _ in From 38ad6398bd0ad097a2339e40fb6efdb4e000269a Mon Sep 17 00:00:00 2001 From: iMoe Date: Tue, 23 Apr 2024 23:38:19 +0800 Subject: [PATCH 3/3] Update README --- README.md | 72 ++++----------------------------------------- README.zh-Hans.md | 74 ++++------------------------------------------- 2 files changed, 12 insertions(+), 134 deletions(-) diff --git a/README.md b/README.md index b135887..3f79112 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,11 @@ DSBridge-Swift is a [DSBridge-iOS](https://github.com/wendux/DSBridge-IOS) fork in Swift. It allows developers to send method calls back and forth between Swift and JavaScript. -# Installation +# Usage + +Check out [wiki](https://github.com/EdgarDegas/DSBridge-Swift/wiki) for docs. + +## Installation DSBridge is available on both iOS and Android. @@ -34,7 +38,7 @@ Or install with npm: npm install dsbridge@3.1.4 ``` -# Usage +# ## Brief @@ -165,70 +169,6 @@ bridge.call('asyncFunction', 1, function(v) { console.log(v) }); // 4 ``` -# Declaration Rules -## Allowed Interface Types -You can declare your interface as these types: -- class -- enum -- struct - - > actors are not supported yet. Please file up your ideas about it. -## Allowed Data Types -You can receive or send the following types: -- String -- Int, Double (types toll-free bridged to NSNumber) -- Bool -- Standard JSON top-level objects: - - - Dictionary that's encodable - - - Array that's encodable - - -## Allowed Function Declarations -DSBridge-Swift ignores argument labels and parameter names of your functions. Thus you can name your parameters whatever you want. - -#### Synchronous Functions - -About parameters, synchronous functions can have: - -- 1 parameter, which is one of the above-mentioned *Allowed Data Types* -- no parameter - -About return value, synchronous functions can have: - -- return value that's one of the above-mentioned *Allowed Data Types* -- no return value - -For simplicity, we use `Allowed` to represent the before-mentioned Allowed Data Types. - -```swift -func name() -func name(Allowed) -func name(Allowed) -> Allowed -``` -#### Asynchronous Functions - -Asynchronous functions are allowed to have 1 or 2 parameters and no return value. - -If there are 2 parameters, the first one must be one of the above-mentioned *Allowed Data Types*. - -The last parameter has to be a closure that returns nothing (i.e., `Void`). For parameters, the closure can have: - -- 1 parameter, one of the above-mentioned *Allowed Data Types* -- 2 parameters, the first one is one of the above-mentioned *Allowed Data Types* and the second one is a `Bool` - -```swift -typealias Completion = (Allowed) -> Void -typealias RepeatableCompletion = (Allowed, Bool) -> Void - -func name(Completion) -func name(RepeatableCompletion) -func name(Allowed, Completion) -func name(Allowed, RepeatableCompletion) -``` -Attribute your closure with `@ecaping` if needed. Otherwise, keep in mind that your functions run on the main thread and try not to block it. - # Differences with DSBridge-iOS ## Seamless `WKWebView` Experience diff --git a/README.zh-Hans.md b/README.zh-Hans.md index 120464f..0029a78 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -9,7 +9,11 @@ DSBridge-Swift 是 [DSBridge-iOS](https://github.com/wendux/DSBridge-IOS) 的一个 Swift 版 fork。它允许开发者在原生和 JavaScript 之间调用彼此的方法。 -# 集成方式 +# 使用 + +在 [wiki](https://github.com/EdgarDegas/DSBridge-Swift/wiki) 中查看详细文档。 + +## 集成 DSBridge 是一个三端可用的 JavaScript Bridge。 @@ -20,6 +24,7 @@ DSBridge 是一个三端可用的 JavaScript Bridge。 Android 端集成方式见 [DSBridge-Android](https://github.com/wendux/DSBridge-Android)。 你可以通过 CDN 引入 JavaScript 代码(或下载 JS 文件并添加到工程中以避免网络问题): + ```html ``` @@ -30,8 +35,6 @@ Android 端集成方式见 [DSBridge-Android](https://github.com/wendux/DSBridge npm install dsbridge@3.1.4 ``` -# 使用 - ## 简介 首先,在你的视图中使用 `DSBridge.WebView` 而非 `WKWebView`: @@ -168,71 +171,6 @@ bridge.call('asyncFunction', 1, function(v) { console.log(v) }); // 4 ``` -## `Interface` 声明规则 - -### 支持的 `Interface` 类型 - -你可以将 `Interface` 声明为 `class`、`struct` 或 `enum`。暂未支持 `actor`,欢迎大家的想法。 - -### 支持的数据类型 - -你可以发送或接收这些类型的数据: - -- String -- Int, Double 等(与 NSNumber 无缝转换的类型) -- Bool -- 标准的 JSON 顶层对象: - - Dictionary,必须可编码为 JSON - - Array,必须可编码为 JSON - -### 支持的方法声明 - -DSBridge-Swift 无视 `Interface` 中的方法的参数名,无论调用名还是内部名,因此你可以使用任意的参数名。 - -#### 同步方法 - -关于参数,同步方法只能: - -- 有 1 个参数,类型符合上述”支持的数据类型“ - -- 没有参数 - -关于返回值,同步方法可以: - -- 有返回值,类型符合上述”支持的数据类型“ -- 没有返回值 - -为了简便,使用 `Allowed` 代指上面说的”支持的数据类型“: - -```swift -func name() -func name(Allowed) -func name(Allowed) -> Allowed -``` - -#### 异步方法 - -异步方法可以有 1 个或 2 个参数,不允许有返回值。 - -如果有 2 个参数,第 1 个参数类型必须符合上述”支持的数据类型“。 - -方法的最后一个参数必须是闭包,返回 `Void`。关于参数,闭包只能: - -- 有 1 个参数,类型符合上述”支持的数据类型“ -- 有 2 个参数,第 1 个类型符合上述”支持的数据类型“,第 2 个必须是 `Bool` 类型 - -```swift -typealias Completion = (Allowed) -> Void -typealias RepeatableCompletion = (Allowed, Bool) -> Void - -func name(Completion) -func name(RepeatableCompletion) -func name(Allowed, Completion) -func name(Allowed, RepeatableCompletion) -``` - -闭包可以是 `@escaping` 的;如果不是的话,请注意,你的方法应当快速执行、立即返回,否则将会阻塞主线程。 - # 与 DSBridge-iOS 的不同 ## 无感的 `WKWebView` 体验