From 95f04ca1d9d23a49d97f973a71e788cae09578a9 Mon Sep 17 00:00:00 2001 From: Joe Masilotti Date: Tue, 12 Sep 2023 21:22:25 -0700 Subject: [PATCH 01/21] Replace Quick/Nimble with XCTest --- Package.resolved | 36 ---- Package.swift | 4 - Tests/ColdBootVisitSpec.swift | 121 ------------ Tests/ColdBootVisitTests.swift | 114 ++++++++++++ Tests/JavaScriptExpressionSpec.swift | 37 ---- Tests/JavaScriptExpressionTests.swift | 30 +++ Tests/JavaScriptVisitSpec.swift | 8 - Tests/JavaScriptVisitTests.swift | 3 + Tests/PathConfigurationLoaderSpec.swift | 110 ----------- Tests/PathConfigurationLoaderTests.swift | 77 ++++++++ Tests/PathConfigurationSpec.swift | 106 ----------- Tests/PathConfigurationTests.swift | 72 ++++++++ Tests/PathRuleSpec.swift | 44 ----- Tests/PathRuleTests.swift | 30 +++ Tests/ScriptMessageSpec.swift | 69 ------- Tests/ScriptMessageTests.swift | 51 ++++++ Tests/SessionSpec.swift | 223 ----------------------- Tests/SessionTests.swift | 199 ++++++++++++++++++++ Tests/Test.swift | 6 +- Tests/VisitOptionsSpec.swift | 56 ------ Tests/VisitOptionsTests.swift | 35 ++++ 21 files changed, 615 insertions(+), 816 deletions(-) delete mode 100644 Tests/ColdBootVisitSpec.swift create mode 100644 Tests/ColdBootVisitTests.swift delete mode 100644 Tests/JavaScriptExpressionSpec.swift create mode 100644 Tests/JavaScriptExpressionTests.swift delete mode 100644 Tests/JavaScriptVisitSpec.swift create mode 100644 Tests/JavaScriptVisitTests.swift delete mode 100644 Tests/PathConfigurationLoaderSpec.swift create mode 100644 Tests/PathConfigurationLoaderTests.swift delete mode 100644 Tests/PathConfigurationSpec.swift create mode 100644 Tests/PathConfigurationTests.swift delete mode 100644 Tests/PathRuleSpec.swift create mode 100644 Tests/PathRuleTests.swift delete mode 100644 Tests/ScriptMessageSpec.swift create mode 100644 Tests/ScriptMessageTests.swift delete mode 100644 Tests/SessionSpec.swift create mode 100644 Tests/SessionTests.swift delete mode 100644 Tests/VisitOptionsSpec.swift create mode 100644 Tests/VisitOptionsTests.swift diff --git a/Package.resolved b/Package.resolved index 41b1182..b47b31d 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,32 +1,5 @@ { "pins" : [ - { - "identity" : "cwlcatchexception", - "kind" : "remoteSourceControl", - "location" : "https://github.com/mattgallagher/CwlCatchException.git", - "state" : { - "revision" : "3b123999de19bf04905bc1dfdb76f817b0f2cc00", - "version" : "2.1.2" - } - }, - { - "identity" : "cwlpreconditiontesting", - "kind" : "remoteSourceControl", - "location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git", - "state" : { - "revision" : "a23ded2c91df9156628a6996ab4f347526f17b6b", - "version" : "2.1.2" - } - }, - { - "identity" : "nimble", - "kind" : "remoteSourceControl", - "location" : "https://github.com/quick/nimble", - "state" : { - "revision" : "1f3bde57bde12f5e7b07909848c071e9b73d6edc", - "version" : "10.0.0" - } - }, { "identity" : "ohhttpstubs", "kind" : "remoteSourceControl", @@ -36,15 +9,6 @@ "version" : "9.1.0" } }, - { - "identity" : "quick", - "kind" : "remoteSourceControl", - "location" : "https://github.com/quick/quick", - "state" : { - "revision" : "f9d519828bb03dfc8125467d8f7b93131951124c", - "version" : "5.0.1" - } - }, { "identity" : "swifter", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index a05cf1d..921486e 100644 --- a/Package.swift +++ b/Package.swift @@ -14,8 +14,6 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/quick/quick", .upToNextMajor(from: "5.0.0")), - .package(url: "https://github.com/quick/nimble", .upToNextMajor(from: "10.0.0")), .package(url: "https://github.com/AliSoftware/OHHTTPStubs", .upToNextMajor(from: "9.0.0")), .package(url: "https://github.com/httpswift/swifter.git", .upToNextMajor(from: "1.5.0")) ], @@ -33,8 +31,6 @@ let package = Package( name: "TurboTests", dependencies: [ "Turbo", - .product(name: "Quick", package: "quick"), - .product(name: "Nimble", package: "nimble"), .product(name: "OHHTTPStubsSwift", package: "OHHTTPStubs"), .product(name: "Swifter", package: "Swifter") ], diff --git a/Tests/ColdBootVisitSpec.swift b/Tests/ColdBootVisitSpec.swift deleted file mode 100644 index 96666a3..0000000 --- a/Tests/ColdBootVisitSpec.swift +++ /dev/null @@ -1,121 +0,0 @@ -import Quick -import Nimble -import WebKit -@testable import Turbo - -class ColdBootVisitSpec: QuickSpec { - override func spec() { - var webView: WKWebView! - var bridge: WebViewBridge! - var visit: ColdBootVisit! - var visitDelegate: TestVisitDelegate! - let url = URL(string: "http://localhost/")! - - beforeEach { - webView = WKWebView(frame: .zero, configuration: WKWebViewConfiguration()) - bridge = WebViewBridge(webView: webView) - visitDelegate = TestVisitDelegate() - visit = ColdBootVisit(visitable: TestVisitable(url: url), options: VisitOptions(), bridge: bridge) - visit.delegate = visitDelegate - } - - describe(".start()") { - beforeEach { - expect(visit.state) == .initialized - visit.start() - } - - it("transitions to a started state") { - expect(visit.state) == .started - } - - it("notifies the delegate the visit will start") { - expect(visitDelegate.didCall("visitWillStart(_:)")).toEventually(beTrue()) - } - - it("kicks off the web view load") { - expect(visit.navigation).toNot(beNil()) - } - - it("becomes the navigation delegate") { - expect(webView.navigationDelegate) === visit - } - - it("notifies the delegate the visit did start") { - visit.start() - expect(visitDelegate.didCall("visitDidStart(_:)")).toEventually(beTrue()) - } - - it("ignores the call if already started") { - visit.start() - expect(visitDelegate.methodsCalled.contains("visitDidStart(_:)")).toEventually(beTrue()) - - visitDelegate.methodsCalled.remove("visitDidStart(_:)") - visit.start() - expect(visitDelegate.didCall("visitDidStart(_:)")).toEventually(beFalse()) - } - } - } -} - -private class TestVisitDelegate { - var methodsCalled: Set = [] - - func didCall(_ method: String) -> Bool { - methodsCalled.contains(method) - } - - private func record(_ string: String = #function) { - methodsCalled.insert(string) - } -} - -extension TestVisitDelegate: VisitDelegate { - func visitDidInitializeWebView(_ visit: Visit) { - record() - } - - func visitWillStart(_ visit: Visit) { - record() - } - - func visitDidStart(_ visit: Visit) { - record() - } - - func visitDidComplete(_ visit: Visit) { - record() - } - - func visitDidFail(_ visit: Visit) { - record() - } - - func visitDidFinish(_ visit: Visit) { - record() - } - - func visitWillLoadResponse(_ visit: Visit) { - record() - } - - func visitDidRender(_ visit: Visit) { - record() - } - - func visitRequestDidStart(_ visit: Visit) { - record() - } - - func visit(_ visit: Visit, requestDidFailWithError error: Error) { - record() - } - - func visitRequestDidFinish(_ visit: Visit) { - record() - } - - func visit(_ visit: Visit, didReceiveAuthenticationChallenge challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { - record() - } -} diff --git a/Tests/ColdBootVisitTests.swift b/Tests/ColdBootVisitTests.swift new file mode 100644 index 0000000..9eace24 --- /dev/null +++ b/Tests/ColdBootVisitTests.swift @@ -0,0 +1,114 @@ +@testable import Turbo +import WebKit +import XCTest + +class ColdBootVisitTests: XCTestCase { + private let webView = WKWebView() + private let visitDelegate = TestVisitDelegate() + private var visit: ColdBootVisit! + + override func setUp() { + let url = URL(string: "http://localhost/")! + let bridge = WebViewBridge(webView: webView) + + visit = ColdBootVisit(visitable: TestVisitable(url: url), options: VisitOptions(), bridge: bridge) + visit.delegate = visitDelegate + } + + func test_start_transitionsToStartState() { + XCTAssertEqual(visit.state, .initialized) + visit.start() + XCTAssertEqual(visit.state, .started) + } + + func test_start_notifiesTheDelegateTheVisitWillStart() { + visit.start() + XCTAssertTrue(visitDelegate.didCall("visitWillStart(_:)")) + } + + func test_start_kicksOffTheWebViewLoad() { + visit.start() + XCTAssertNotNil(visit.navigation) + } + + func test_visit_becomesTheNavigationDelegate() { + visit.start() + XCTAssertIdentical(webView.navigationDelegate, visit) + } + + func test_visit_notifiesTheDelegateTheVisitDidStart() { + visit.start() + XCTAssertTrue(visitDelegate.didCall("visitDidStart(_:)")) + } + + func test_visit_ignoresTheCallIfAlreadyStarted() { + visit.start() + XCTAssertTrue(visitDelegate.methodsCalled.contains("visitDidStart(_:)")) + + visitDelegate.methodsCalled.remove("visitDidStart(_:)") + visit.start() + XCTAssertFalse(visitDelegate.didCall("visitDidStart(_:)")) + } +} + +private class TestVisitDelegate { + var methodsCalled: Set = [] + + func didCall(_ method: String) -> Bool { + methodsCalled.contains(method) + } + + private func record(_ string: String = #function) { + methodsCalled.insert(string) + } +} + +extension TestVisitDelegate: VisitDelegate { + func visitDidInitializeWebView(_ visit: Visit) { + record() + } + + func visitWillStart(_ visit: Visit) { + record() + } + + func visitDidStart(_ visit: Visit) { + record() + } + + func visitDidComplete(_ visit: Visit) { + record() + } + + func visitDidFail(_ visit: Visit) { + record() + } + + func visitDidFinish(_ visit: Visit) { + record() + } + + func visitWillLoadResponse(_ visit: Visit) { + record() + } + + func visitDidRender(_ visit: Visit) { + record() + } + + func visitRequestDidStart(_ visit: Visit) { + record() + } + + func visit(_ visit: Visit, requestDidFailWithError error: Error) { + record() + } + + func visitRequestDidFinish(_ visit: Visit) { + record() + } + + func visit(_ visit: Visit, didReceiveAuthenticationChallenge challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + record() + } +} diff --git a/Tests/JavaScriptExpressionSpec.swift b/Tests/JavaScriptExpressionSpec.swift deleted file mode 100644 index dd42c39..0000000 --- a/Tests/JavaScriptExpressionSpec.swift +++ /dev/null @@ -1,37 +0,0 @@ -import Quick -import Nimble -@testable import Turbo - -class JavaScriptExpressionSpec: QuickSpec { - override func spec() { - describe(".string") { - it("converts function and arguments into a valid expression") { - let expression = JavaScriptExpression(function: "console.log", arguments: []) - expect(expression.string) == "console.log()" - - let expression2 = JavaScriptExpression(function: "console.log", arguments: ["one", nil, 2]) - expect(expression2.string) == "console.log(\"one\",null,2)" - } - } - - describe(".wrapped") { - it("wraps expression in IIFE and try/catch") { - let expression = JavaScriptExpression(function: "console.log", arguments: []) - let expected = """ - (function(result) { - try { - result.value = console.log() - } catch (error) { - result.error = error.toString() - result.stack = error.stack - } - - return result - })({}) - """ - - expect(expression.wrappedString) == expected - } - } - } -} diff --git a/Tests/JavaScriptExpressionTests.swift b/Tests/JavaScriptExpressionTests.swift new file mode 100644 index 0000000..7cfbbca --- /dev/null +++ b/Tests/JavaScriptExpressionTests.swift @@ -0,0 +1,30 @@ +@testable import Turbo +import XCTest + +class JavaScriptExpressionTests: XCTestCase { + func test_string_convertsFunctionAndArgumentsIntoAValidExpression() { + let expression = JavaScriptExpression(function: "console.log", arguments: []) + XCTAssertEqual(expression.string, "console.log()") + + let expression2 = JavaScriptExpression(function: "console.log", arguments: ["one", nil, 2]) + XCTAssertEqual(expression2.string, "console.log(\"one\",null,2)") + } + + func test_wrapped_wrapsExpressionIn_IIFE_AndTryCatch() { + let expression = JavaScriptExpression(function: "console.log", arguments: []) + let expected = """ + (function(result) { + try { + result.value = console.log() + } catch (error) { + result.error = error.toString() + result.stack = error.stack + } + + return result + })({}) + """ + + XCTAssertEqual(expression.wrappedString, expected) + } +} diff --git a/Tests/JavaScriptVisitSpec.swift b/Tests/JavaScriptVisitSpec.swift deleted file mode 100644 index c25e64e..0000000 --- a/Tests/JavaScriptVisitSpec.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Quick -import Nimble - -class JavaScriptVisitSpec: QuickSpec { - override func spec() { - - } -} diff --git a/Tests/JavaScriptVisitTests.swift b/Tests/JavaScriptVisitTests.swift new file mode 100644 index 0000000..581f9e8 --- /dev/null +++ b/Tests/JavaScriptVisitTests.swift @@ -0,0 +1,3 @@ +import XCTest + +class JavaScriptVisitTests: XCTestCase {} diff --git a/Tests/PathConfigurationLoaderSpec.swift b/Tests/PathConfigurationLoaderSpec.swift deleted file mode 100644 index e0476a3..0000000 --- a/Tests/PathConfigurationLoaderSpec.swift +++ /dev/null @@ -1,110 +0,0 @@ -import XCTest -import Quick -import Nimble -import OHHTTPStubs -import OHHTTPStubsSwift -@testable import Turbo - -class PathConfigurationLoaderSpec: QuickSpec { - override func spec() { - let serverURL = URL(string: "http://turbo.test/configuration.json")! - let fileURL = Bundle.module.url(forResource: "test-configuration", withExtension: "json", subdirectory: "Fixtures")! - - describe("load") { - context("data") { - it("automatically loads from passed in data and calls the handler") { - let data = try! Data(contentsOf: fileURL) - let loader = PathConfigurationLoader(sources: [.data(data)]) - - var config: PathConfigurationDecoder? = nil - loader.load { conf in - config = conf - } - - expect(config).toEventuallyNot(beNil()) - expect(config!.rules.count) == 4 - } - } - - context("file") { - it("automatically loads from the local file and calls the handler") { - let loader = PathConfigurationLoader(sources: [.file(fileURL)]) - - var config: PathConfigurationDecoder? = nil - loader.load { conf in - config = conf - } - - expect(config).toNot(beNil()) - expect(config!.rules.count) == 4 - } - } - - context("server") { - var loader: PathConfigurationLoader! - - beforeEach { - loader = PathConfigurationLoader(sources: [.server(serverURL)]) - stub(condition: { _ in true }) { _ in - let json = ["rules": [["patterns": ["/new"], "properties": ["presentation": "test"]] as [String : Any]]] - return HTTPStubsResponse(jsonObject: json, statusCode: 200, headers: [:]) - } - - clearCache(loader.configurationCacheURL) - } - - it("automatically downloads the file and calls the handler") { - var config: PathConfigurationDecoder? = nil - loader.load { conf in - config = conf - } - - expect(config).toEventuallyNot(beNil()) - expect(config!.rules.count) == 1 - } - - it("caches the file") { - var handlerCalled = false - loader.load { rs in - handlerCalled = true - } - - expect(handlerCalled).toEventually(beTrue()) - expect(FileManager.default.fileExists(atPath: loader!.configurationCacheURL.path)) == true - } - } - - context("when file and remote") { - it("loads the file url and the remote url") { - let loader = PathConfigurationLoader(sources: [.file(fileURL), .server(serverURL)]) - clearCache(loader.configurationCacheURL) - - stub(condition: { _ in true }) { _ in - let json = ["rules": [["patterns": ["/new"], "properties": ["presentation": "test"]] as [String : Any]]] - return HTTPStubsResponse(jsonObject: json, statusCode: 200, headers: [:]) - } - - var handlerCalledTimes = 0 - - loader.load { config in - if handlerCalledTimes == 0 { - expect(config.rules.count) == 4 - } else { - expect(config.rules.count) == 1 - } - - handlerCalledTimes += 1 - } - - expect(handlerCalledTimes).toEventually(equal(2)) - } - } - } - } -} - -private func clearCache(_ url: URL) { - do { - try FileManager.default.removeItem(at: url) - } catch {} -} diff --git a/Tests/PathConfigurationLoaderTests.swift b/Tests/PathConfigurationLoaderTests.swift new file mode 100644 index 0000000..e04956d --- /dev/null +++ b/Tests/PathConfigurationLoaderTests.swift @@ -0,0 +1,77 @@ +import OHHTTPStubs +import OHHTTPStubsSwift +@testable import Turbo +import XCTest + +class PathConfigurationLoaderTests: XCTestCase { + private let serverURL = URL(string: "http://turbo.test/configuration.json")! + private let fileURL = Bundle.module.url(forResource: "test-configuration", withExtension: "json", subdirectory: "Fixtures")! + + func test_load_data_automaticallyLoadsFromPassedInDataAndCallsHandler() throws { + let data = try! Data(contentsOf: fileURL) + let loader = PathConfigurationLoader(sources: [.data(data)]) + + var loadedConfig: PathConfigurationDecoder? = nil + loader.load { loadedConfig = $0 } + + let config = try XCTUnwrap(loadedConfig) + XCTAssertEqual(config.rules.count, 4) + } + + func test_file_automaticallyLoadsFromTheLocalFileAndCallsTheHandler() throws { + let loader = PathConfigurationLoader(sources: [.file(fileURL)]) + + var loadedConfig: PathConfigurationDecoder? = nil + loader.load { loadedConfig = $0 } + + let config = try XCTUnwrap(loadedConfig) + XCTAssertEqual(config.rules.count, 4) + } + + func test_server_automaticallyDownloadsTheFileAndCallsTheHandler() throws { + let loader = PathConfigurationLoader(sources: [.server(serverURL)]) + let expectation = stubRequest(for: loader) + + var loadedConfig: PathConfigurationDecoder? = nil + loader.load { config in + loadedConfig = config + expectation.fulfill() + } + wait(for: [expectation]) + + let config = try XCTUnwrap(loadedConfig) + XCTAssertEqual(config.rules.count, 1) + } + + func test_server_cachesTheFile() { + let loader = PathConfigurationLoader(sources: [.server(serverURL)]) + let expectation = stubRequest(for: loader) + + var handlerCalled = false + loader.load { _ in + handlerCalled = true + expectation.fulfill() + } + wait(for: [expectation]) + + XCTAssertTrue(handlerCalled) + XCTAssertTrue(FileManager.default.fileExists(atPath: loader.configurationCacheURL.path)) + } + + private func stubRequest(for loader: PathConfigurationLoader) -> XCTestExpectation { + stub(condition: { _ in true }) { _ in + let json = ["rules": [["patterns": ["/new"], "properties": ["presentation": "test"]] as [String: Any]]] + return HTTPStubsResponse(jsonObject: json, statusCode: 200, headers: [:]) + } + + clearCache(loader.configurationCacheURL) + + return expectation(description: "Wait for configuration to load.") + } + + private func clearCache(_ url: URL) { + do { + try FileManager.default.removeItem(at: url) + } catch {} + } +} diff --git a/Tests/PathConfigurationSpec.swift b/Tests/PathConfigurationSpec.swift deleted file mode 100644 index eb6486f..0000000 --- a/Tests/PathConfigurationSpec.swift +++ /dev/null @@ -1,106 +0,0 @@ -import Quick -import Nimble -import Foundation -@testable import Turbo - -class PathConfigurationSpec: QuickSpec { - override func spec() { - let fileURL = Bundle.module.url(forResource: "test-configuration", withExtension: "json", subdirectory: "Fixtures")! - var configuration: PathConfiguration! - - beforeEach { - configuration = PathConfiguration(sources: [.file(fileURL)]) - expect(configuration.rules.count).toEventually(beGreaterThan(0)) - } - - describe("init") { - it("automatically loads the configuration from the specified location") { - expect(configuration.settings.count) == 2 - expect(configuration.rules.count) == 4 - } - } - - describe("settings") { - it("returns current settings") { - expect(configuration.settings) == [ - "some-feature-enabled": true, - "server": "beta" - ] - } - } - - describe("properties(for: path)") { - context("when path matches") { - it("returns properties") { - expect(configuration.properties(for: "/")) == [ - "page": "root" - ] - } - } - - context("when path matches multiple rules") { - it("merges properties") { - expect(configuration.properties(for: "/new")) == [ - "context": "modal", - "background_color": "black" - ] - - expect(configuration.properties(for: "/edit")) == [ - "context": "modal", - "background_color": "white" - ] - } - } - - context("when no match") { - it("returns empty properties") { - expect(configuration.properties(for: "/missing")) == [:] - } - } - } - - describe("subscript") { - it("is a convenience method for properties(for path)") { - expect(configuration.properties(for: "/new")) == configuration["/new"] - expect(configuration.properties(for: "/edit")) == configuration["/edit"] - expect(configuration.properties(for: "/")) == configuration["/"] - expect(configuration.properties(for: "/missing")) == configuration["/missing"] - } - } - } -} - -class PathConfigSpec: QuickSpec { - override func spec() { - describe("json") { - context("with valid json") { - it("decodes successfully") { - let fileURL = Bundle.module.url(forResource: "test-configuration", withExtension: "json", subdirectory: "Fixtures")! - - do { - let data = try Data(contentsOf: fileURL) - let json = try JSONSerialization.jsonObject(with: data) as! [String: Any] - let config = try PathConfigurationDecoder(json: json) - - expect(config.settings.count) == 2 - expect(config.rules.count) == 4 - } catch { - fail("Error decoding from JSON: \(error)") - } - } - } - - context("with missing rules key") { - it("fails to decode") { - do { - _ = try PathConfigurationDecoder(json: [:]) - fail("Path config should not have decoded invalid json") - } catch { - expect(error).to(matchError(JSONDecodingError.invalidJSON)) - } - } - } - } - } -} - diff --git a/Tests/PathConfigurationTests.swift b/Tests/PathConfigurationTests.swift new file mode 100644 index 0000000..5a45e83 --- /dev/null +++ b/Tests/PathConfigurationTests.swift @@ -0,0 +1,72 @@ +@testable import Turbo +import XCTest + +class PathConfigurationTests: XCTestCase { + private let fileURL = Bundle.module.url(forResource: "test-configuration", withExtension: "json", subdirectory: "Fixtures")! + var configuration: PathConfiguration! + + override func setUp() { + configuration = PathConfiguration(sources: [.file(fileURL)]) + XCTAssertGreaterThan(configuration.rules.count, 0) + } + + func test_init_automaticallyLoadsTheConfigurationFromTheSpecifiedLocation() { + XCTAssertEqual(configuration.settings.count, 2) + XCTAssertEqual(configuration.rules.count, 4) + } + + func test_settings_returnsCurrentSettings() { + XCTAssertEqual(configuration.settings, [ + "some-feature-enabled": true, + "server": "beta" + ]) + } + + func test_propertiesForPath_whenPathMatches_returnsProperties() { + XCTAssertEqual(configuration.properties(for: "/"), [ + "page": "root" + ]) + } + + func test_propertiesForPath_whenPathMatchesMultipleRules_mergesProperties() { + XCTAssertEqual(configuration.properties(for: "/new"), [ + "context": "modal", + "background_color": "black" + ]) + + XCTAssertEqual(configuration.properties(for: "/edit"), [ + "context": "modal", + "background_color": "white" + ]) + } + + func test_propertiesForPath_whenNoMatch_returnsEmptyProperties() { + XCTAssertEqual(configuration.properties(for: "/missing"), [:]) + } + + func test_subscript_isAConvenienceMethodForPropertiesForPath() { + XCTAssertEqual(configuration.properties(for: "/new"), configuration["/new"]) + XCTAssertEqual(configuration.properties(for: "/edit"), configuration["/edit"]) + XCTAssertEqual(configuration.properties(for: "/"), configuration["/"]) + XCTAssertEqual(configuration.properties(for: "/missing"), configuration["/missing"]) + } +} + +class PathConfigTests: XCTestCase { + func test_json_withValidJSON_decodesSuccessfully() throws { + let fileURL = Bundle.module.url(forResource: "test-configuration", withExtension: "json", subdirectory: "Fixtures")! + + let data = try Data(contentsOf: fileURL) + let json = try JSONSerialization.jsonObject(with: data) as! [String: Any] + let config = try PathConfigurationDecoder(json: json) + + XCTAssertEqual(config.settings.count, 2) + XCTAssertEqual(config.rules.count, 4) + } + + func test_json_withMissingRulesKey_failsToDecode() throws { + XCTAssertThrowsError(try PathConfigurationDecoder(json: [:])) { error in + XCTAssertEqual(error as? JSONDecodingError, JSONDecodingError.invalidJSON) + } + } +} diff --git a/Tests/PathRuleSpec.swift b/Tests/PathRuleSpec.swift deleted file mode 100644 index 30482fc..0000000 --- a/Tests/PathRuleSpec.swift +++ /dev/null @@ -1,44 +0,0 @@ -import XCTest -import Quick -import Nimble -@testable import Turbo - -class PathRuleSpec: QuickSpec { - override func spec() { - describe("subscript") { - it("returns a String value for key") { - let rule = PathRule(patterns: ["^/new$"], properties: ["color": "blue", "modal": false]) - - expect(rule["color"]) == "blue" - expect(rule["modal"]).to(beNil()) - } - } - - describe(".match") { - context("when path matches single pattern") { - it("returns true") { - let rule = PathRule(patterns: ["^/new$"], properties: [:]) - - expect(rule.match(path: "/new")) == true - } - } - - context("when path matches any pattern in array") { - it("returns true") { - let rule = PathRule(patterns: ["^/new$", "^/edit"], properties: [:]) - - expect(rule.match(path: "/edit/1")) == true - } - } - - context("when path doesn't match any patterns") { - it("returns false") { - let rule = PathRule(patterns: ["^/new/bar"], properties: [:]) - - expect(rule.match(path: "/new")) == false - expect(rule.match(path: "foo")) == false - } - } - } - } -} diff --git a/Tests/PathRuleTests.swift b/Tests/PathRuleTests.swift new file mode 100644 index 0000000..c575224 --- /dev/null +++ b/Tests/PathRuleTests.swift @@ -0,0 +1,30 @@ +@testable import Turbo +import XCTest + +class PathRuleTests: XCTestCase { + func test_subscript_returnsAStringValueForKey() { + let rule = PathRule(patterns: ["^/new$"], properties: ["color": "blue", "modal": false]) + + XCTAssertEqual(rule["color"], "blue") + XCTAssertNil(rule["modal"]) + } + + func test_match_whenPathMatchesSinglePattern_returnsTrue() { + let rule = PathRule(patterns: ["^/new$"], properties: [:]) + + XCTAssertTrue(rule.match(path: "/new")) + } + + func test_match_whenPathMatchesAnyPatternInArray_returnsTrue() { + let rule = PathRule(patterns: ["^/new$", "^/edit"], properties: [:]) + + XCTAssertTrue(rule.match(path: "/edit/1")) + } + + func test_match_whenPathDoesntMatchAnyPatterns_returnsFalse() { + let rule = PathRule(patterns: ["^/new/bar"], properties: [:]) + + XCTAssertFalse(rule.match(path: "/new")) + XCTAssertFalse(rule.match(path: "foo")) + } +} diff --git a/Tests/ScriptMessageSpec.swift b/Tests/ScriptMessageSpec.swift deleted file mode 100644 index b8e1da0..0000000 --- a/Tests/ScriptMessageSpec.swift +++ /dev/null @@ -1,69 +0,0 @@ -import WebKit -import XCTest -import Quick -import Nimble -@testable import Turbo - -class ScriptMessageSpec: QuickSpec { - override func spec() { - describe(".parse") { - context("with valid data") { - it("returns message") { - let data = ["identifier": "123", "restorationIdentifier": "abc", "options": ["action": "advance"], "location": "http://turbo.test"] as [String : Any] - let script = FakeScriptMessage(body: ["name": "pageLoaded", "data": data] as [String : Any]) - - guard let message = ScriptMessage(message: script) else { - fail("Error parsing script message") - return - } - - expect(message.name) == .pageLoaded - expect(message.identifier) == "123" - expect(message.restorationIdentifier) == "abc" - expect(message.options!.action) == .advance - expect(message.location) == URL(string: "http://turbo.test")! - } - } - - context("with invalid body") { - it("returns nil") { - let script = FakeScriptMessage(body: "foo") - - let message = ScriptMessage(message: script) - expect(message).to(beNil()) - } - } - - context("with invalid name") { - it("returns nil") { - let script = FakeScriptMessage(body: ["name": "foobar"]) - - let message = ScriptMessage(message: script) - expect(message).to(beNil()) - } - } - - context("with missing data") { - it("returns nil") { - let script = FakeScriptMessage(body: ["name": "pageLoaded"]) - - let message = ScriptMessage(message: script) - expect(message).to(beNil()) - } - } - } - } -} - -// Can't instantiate a WKScriptMessage directly -private class FakeScriptMessage: WKScriptMessage { - override var body: Any { - return actualBody - } - - var actualBody: Any - - init(body: Any) { - self.actualBody = body - } -} diff --git a/Tests/ScriptMessageTests.swift b/Tests/ScriptMessageTests.swift new file mode 100644 index 0000000..3d44af3 --- /dev/null +++ b/Tests/ScriptMessageTests.swift @@ -0,0 +1,51 @@ +@testable import Turbo +import WebKit +import XCTest + +class ScriptMessageTests: XCTestCase { + func test_parse_withValidData_returnsMessage() throws { + let data = ["identifier": "123", "restorationIdentifier": "abc", "options": ["action": "advance"], "location": "http://turbo.test"] as [String: Any] + let script = FakeScriptMessage(body: ["name": "pageLoaded", "data": data] as [String: Any]) + + let message = try XCTUnwrap(ScriptMessage(message: script)) + XCTAssertEqual(message.name, .pageLoaded) + XCTAssertEqual(message.identifier, "123") + XCTAssertEqual(message.restorationIdentifier, "abc") + XCTAssertEqual(message.options!.action, .advance) + XCTAssertEqual(message.location, URL(string: "http://turbo.test")!) + } + + func test_parse_withInvalidBody_returnsNil() { + let script = FakeScriptMessage(body: "foo") + + let message = ScriptMessage(message: script) + XCTAssertNil(message) + } + + func test_parse_withInvalidName_returnsNil() { + let script = FakeScriptMessage(body: ["name": "foobar"]) + + let message = ScriptMessage(message: script) + XCTAssertNil(message) + } + + func test_parse_withMissingData_returnsNil() { + let script = FakeScriptMessage(body: ["name": "pageLoaded"]) + + let message = ScriptMessage(message: script) + XCTAssertNil(message) + } +} + +// Can't instantiate a WKScriptMessage directly +private class FakeScriptMessage: WKScriptMessage { + override var body: Any { + return actualBody + } + + var actualBody: Any + + init(body: Any) { + self.actualBody = body + } +} diff --git a/Tests/SessionSpec.swift b/Tests/SessionSpec.swift deleted file mode 100644 index 28f3f31..0000000 --- a/Tests/SessionSpec.swift +++ /dev/null @@ -1,223 +0,0 @@ -import WebKit -import XCTest -import Quick -import Nimble -import Swifter -@testable import Turbo - -private let timeout = DispatchTimeInterval.seconds(35) - -class SessionSpec: QuickSpec { - let server = HttpServer() - - override func spec() { - var session: Session! - var sessionDelegate: TestSessionDelegate! - - beforeSuite { - self.startServer() - } - - beforeEach { - sessionDelegate = TestSessionDelegate() - - let configuration = WKWebViewConfiguration() - configuration.applicationNameForUserAgent = "Turbo iOS Test/1.0" - session = Session(webViewConfiguration: configuration) - session.delegate = sessionDelegate - } - - afterEach { - session.webView.configuration.userContentController.removeScriptMessageHandler(forName: "turbo") - } - - describe("init") { - it("initializes web view with configuration") { - expect(session.webView.configuration.applicationNameForUserAgent) == "Turbo iOS Test/1.0" - } - } - - describe("cold boot visit") { - it("makes the session the visitable delegate") { - let visitable = TestVisitable(url: self.url("/")) - expect(visitable.visitableDelegate).to(beNil()) - - session.visit(visitable) - expect(visitable.visitableDelegate) === session - } - - it("calls start request") { - let visitable = TestVisitable(url: self.url("/")) - session.visit(visitable) - - expect(sessionDelegate.sessionDidStartRequestCalled).toEventually(beTrue(), timeout: timeout) - } - - context("when visit succeeds") { - beforeEach { - let visitable = TestVisitable(url: self.url("/")) - session.visit(visitable) - } - - it("calls sessionDidLoadWebView delegate method") { - expect(sessionDelegate.sessionDidLoadWebViewCalled).toEventually(beTrue(), timeout: timeout) - expect(sessionDelegate.sessionDidFailRequestCalled) == false - } - - it("calls sessionDidFinishRequest delegate method") { - expect(sessionDelegate.sessionDidFinishRequestCalled).toEventually(beTrue(), timeout: timeout) - expect(sessionDelegate.sessionDidFailRequestCalled) == false - } - - it("configures JavaScript bridge") { - expect(sessionDelegate.sessionDidLoadWebViewCalled).toEventually(beTrue(), timeout: timeout) - - waitUntil { done in - session.webView.evaluateJavaScript("Turbo.navigator.adapter == window.turboNative") { result, _ in - XCTAssertEqual(result as? Bool, true) - done() - } - } - } - } - - context("when visit fails from http error") { - beforeEach { - let visitable = TestVisitable(url: self.url("/invalid")) - session.visit(visitable) - } - - it("calls sessionDidFailRequest delegate method") { - expect(sessionDelegate.sessionDidFailRequestCalled).toEventually(beTrue(), timeout: timeout) - } - - it("provides an error") { - expect(sessionDelegate.failedRequestError).toEventuallyNot(beNil(), timeout: timeout) - guard let error = sessionDelegate.failedRequestError else { - fail("Should have gotten an error") - return - } - - expect(error).to(matchError(TurboError.http(statusCode: 404))) - } - - it("calls sessionDidFinishRequest delegate method") { - expect(sessionDelegate.sessionDidFinishRequestCalled).toEventually(beTrue(), timeout: timeout) - } - } - - context("when visit fails from missing library") { - beforeEach { - let visitable = TestVisitable(url: self.url("/missing-library")) - session.visit(visitable) - } - - it("calls sessionDidFailRequest delegate method") { - expect(sessionDelegate.sessionDidFailRequestCalled).toEventually(beTrue(), timeout: timeout) - } - - it("provides an page load error") { - expect(sessionDelegate.failedRequestError).toEventuallyNot(beNil(), timeout: timeout) - guard let error = sessionDelegate.failedRequestError else { - fail("Should have gotten an error") - return - } - - expect(error).to(matchError(TurboError.pageLoadFailure)) - } - - it("calls sessionDidFinishRequest delegate method") { - expect(sessionDelegate.sessionDidFinishRequestCalled).toEventually(beTrue(), timeout: timeout) - } - } - - describe("Turbolinks 5 compatibility") { - it("loads the page and sets the adapter") { - let visitable = TestVisitable(url: self.url("/turbolinks")) - session.visit(visitable) - - expect(sessionDelegate.sessionDidLoadWebViewCalled).toEventually(beTrue(), timeout: timeout) - - waitUntil { done in - session.webView.evaluateJavaScript("Turbolinks.controller.adapter === window.turboNative") { result, _ in - XCTAssertEqual(result as? Bool, true) - done() - } - } - } - } - - describe("Turbolinks 5.3 compatibility") { - it("loads the page and sets the adapter") { - let visitable = TestVisitable(url: self.url("/turbolinks-5.3")) - session.visit(visitable) - - expect(sessionDelegate.sessionDidLoadWebViewCalled).toEventually(beTrue(), timeout: timeout) - - waitUntil { done in - session.webView.evaluateJavaScript("Turbolinks.controller.adapter === window.turboNative") { result, _ in - XCTAssertEqual(result as? Bool, true) - done() - } - } - } - } - } - } - - // MARK: - Server - - private func url(_ path: String) -> URL { - let baseURL = URL(string: "http://localhost:8080")! - let relativePath = path.hasPrefix("/") ? String(path.dropFirst()) : path - return baseURL.appendingPathComponent(relativePath) - } - - private func startServer() { - server["/turbo-7.0.0-beta.1.js"] = { _ in - let fileURL = Bundle.module.url(forResource: "turbo-7.0.0-beta.1", withExtension: "js", subdirectory: "Server")! - let data = try! Data(contentsOf: fileURL) - return .ok(.data(data)) - } - - server["/turbolinks-5.2.0.js"] = { _ in - let fileURL = Bundle.module.url(forResource: "turbolinks-5.2.0", withExtension: "js", subdirectory: "Server")! - let data = try! Data(contentsOf: fileURL) - return .ok(.data(data)) - } - - server["/turbolinks-5.3.0-dev.js"] = { _ in - let fileURL = Bundle.module.url(forResource: "turbolinks-5.3.0-dev", withExtension: "js", subdirectory: "Server")! - let data = try! Data(contentsOf: fileURL) - return .ok(.data(data)) - } - - server["/"] = { _ in - let fileURL = Bundle.module.url(forResource: "turbo", withExtension: "html", subdirectory: "Server")! - let data = try! Data(contentsOf: fileURL) - return .ok(.data(data)) - } - - server["/turbolinks"] = { _ in - let fileURL = Bundle.module.url(forResource: "turbolinks", withExtension: "html", subdirectory: "Server")! - let data = try! Data(contentsOf: fileURL) - return .ok(.data(data)) - } - - server["/turbolinks-5.3"] = { _ in - let fileURL = Bundle.module.url(forResource: "turbolinks-5.3", withExtension: "html", subdirectory: "Server")! - let data = try! Data(contentsOf: fileURL) - return .ok(.data(data)) - } - - server["/missing-library"] = { _ in - .ok(.html("")) - } - - server["/invalid"] = { _ in - .notFound - } - - try! server.start() - } -} diff --git a/Tests/SessionTests.swift b/Tests/SessionTests.swift new file mode 100644 index 0000000..1fcbfc6 --- /dev/null +++ b/Tests/SessionTests.swift @@ -0,0 +1,199 @@ +import Swifter +@testable import Turbo +import WebKit +import XCTest + +class SessionTests: XCTestCase { + private static let server = HttpServer() + + private let sessionDelegate = TestSessionDelegate() + private var session: Session! + + override class func setUp() { + startServer() + } + + override class func tearDown() { + server.stop() + } + + override func setUp() { + let configuration = WKWebViewConfiguration() + configuration.applicationNameForUserAgent = "Turbo iOS Test/1.0" + + session = Session(webViewConfiguration: configuration) + session.delegate = sessionDelegate + } + + override func tearDown() { + session.webView.configuration.userContentController.removeScriptMessageHandler(forName: "turbo") + } + + func test_init_initializesWebViewWithConfiguration() { + XCTAssertEqual(session.webView.configuration.applicationNameForUserAgent, "Turbo iOS Test/1.0") + } + + func test_coldBootVisit_makesTheSessionTheVisitableDelegate() { + let visitable = TestVisitable(url: url("/")) + XCTAssertNil(visitable.visitableDelegate) + + session.visit(visitable) + XCTAssertIdentical(visitable.visitableDelegate, session) + } + + func test_coldBootVisit_callsStartRequest() { + let visitable = TestVisitable(url: url("/")) + session.visit(visitable) + + XCTAssertTrue(sessionDelegate.sessionDidStartRequestCalled) + } + + func test_coldBootVisit_whenVisitSucceeds_callsSessionDidLoadWebViewDelegateMethod() async { + await visit("/") + + XCTAssertTrue(sessionDelegate.sessionDidLoadWebViewCalled) + XCTAssertFalse(sessionDelegate.sessionDidFailRequestCalled) + } + + func test_coldBootVisit_whenVisitSucceeds_callsSessionDidFinishRequestDelegateMethod() async { + await visit("/") + + XCTAssertTrue(sessionDelegate.sessionDidFinishRequestCalled) + XCTAssertFalse(sessionDelegate.sessionDidFailRequestCalled) + } + + @MainActor + func test_coldBootVisit_whenVisitSucceeds_configuresJavaScriptBridge() async throws { + await visit("/") + + XCTAssertTrue(sessionDelegate.sessionDidLoadWebViewCalled) + let result = try await session.webView.evaluateJavaScript("Turbo.navigator.adapter == window.turboNative") + XCTAssertTrue(result as! Bool) + } + + func test_coldBootVisit_whenVisitFailsFromHTTPError_callsSessionDidFailRequestDelegateMethod() async { + await visit("/invalid") + + XCTAssertTrue(sessionDelegate.sessionDidFailRequestCalled) + } + + func test_coldBootVisit_whenVisitFailsFromHTTPError_providesAnError() async throws { + await visit("/invalid") + + XCTAssertNotNil(sessionDelegate.failedRequestError) + let error = try XCTUnwrap(sessionDelegate.failedRequestError) + XCTAssertEqual(error as? TurboError, TurboError.http(statusCode: 404)) + } + + func test_coldBootVisit_whenVisitFailsFromHTTPError_callsSessionDidFinishRequestDelegateMethod() async { + await visit("/invalid") + + XCTAssertTrue(sessionDelegate.sessionDidFinishRequestCalled) + } + + func test_coldBootVisit_whenVisitFailsFromMissingLibrary_callsSessionDidFailRequestDelegateMethod() async { + await visit("/missing-library") + + XCTAssertTrue(sessionDelegate.sessionDidFailRequestCalled) + } + + func test_coldBootVisit_whenVisitFailsFromMissingLibrary_providesAnPageLoadError() async throws { + await visit("/missing-library") + + XCTAssertNotNil(sessionDelegate.failedRequestError) + let error = try XCTUnwrap(sessionDelegate.failedRequestError) + XCTAssertEqual(error as? TurboError, TurboError.pageLoadFailure) + } + + func test_coldBootVisit_whenVisitFailsFromMissingLibrary_callsSessionDidFinishRequestDelegateMethod() async { + await visit("/missing-library") + + XCTAssertTrue(sessionDelegate.sessionDidFinishRequestCalled) + } + + @MainActor + func test_coldBootVisit_Turbolinks5Compatibility_loadsThePageAndSetsTheAdapter() async throws { + await visit("/turbolinks") + + XCTAssertTrue(sessionDelegate.sessionDidLoadWebViewCalled) + + let result = try await session.webView.evaluateJavaScript("Turbolinks.controller.adapter === window.turboNative") + XCTAssertTrue(result as! Bool) + } + + @MainActor + func test_coldBootVisit_Turbolinks5_3Compatibility_loadsThePageAndSetsTheAdapter() async throws { + await visit("/turbolinks-5.3") + + XCTAssertTrue(sessionDelegate.sessionDidLoadWebViewCalled) + + let result = try await session.webView.evaluateJavaScript("Turbolinks.controller.adapter === window.turboNative") + XCTAssertTrue(result as! Bool) + } + + // MARK: - Server + + @MainActor + private func visit(_ path: String) async { + let expectation = self.expectation(description: "Wait for request to load.") + sessionDelegate.didChange = { expectation.fulfill() } + + let visitable = TestVisitable(url: url(path)) + session.visit(visitable) + await fulfillment(of: [expectation]) + } + + private func url(_ path: String) -> URL { + let baseURL = URL(string: "http://localhost:8080")! + let relativePath = path.hasPrefix("/") ? String(path.dropFirst()) : path + return baseURL.appendingPathComponent(relativePath) + } + + private static func startServer() { + server["/turbo-7.0.0-beta.1.js"] = { _ in + let fileURL = Bundle.module.url(forResource: "turbo-7.0.0-beta.1", withExtension: "js", subdirectory: "Server")! + let data = try! Data(contentsOf: fileURL) + return .ok(.data(data)) + } + + server["/turbolinks-5.2.0.js"] = { _ in + let fileURL = Bundle.module.url(forResource: "turbolinks-5.2.0", withExtension: "js", subdirectory: "Server")! + let data = try! Data(contentsOf: fileURL) + return .ok(.data(data)) + } + + server["/turbolinks-5.3.0-dev.js"] = { _ in + let fileURL = Bundle.module.url(forResource: "turbolinks-5.3.0-dev", withExtension: "js", subdirectory: "Server")! + let data = try! Data(contentsOf: fileURL) + return .ok(.data(data)) + } + + server["/"] = { _ in + let fileURL = Bundle.module.url(forResource: "turbo", withExtension: "html", subdirectory: "Server")! + let data = try! Data(contentsOf: fileURL) + return .ok(.data(data)) + } + + server["/turbolinks"] = { _ in + let fileURL = Bundle.module.url(forResource: "turbolinks", withExtension: "html", subdirectory: "Server")! + let data = try! Data(contentsOf: fileURL) + return .ok(.data(data)) + } + + server["/turbolinks-5.3"] = { _ in + let fileURL = Bundle.module.url(forResource: "turbolinks-5.3", withExtension: "html", subdirectory: "Server")! + let data = try! Data(contentsOf: fileURL) + return .ok(.data(data)) + } + + server["/missing-library"] = { _ in + .ok(.html("")) + } + + server["/invalid"] = { _ in + .notFound + } + + try! server.start() + } +} diff --git a/Tests/Test.swift b/Tests/Test.swift index cffcb86..33e2b54 100644 --- a/Tests/Test.swift +++ b/Tests/Test.swift @@ -37,12 +37,14 @@ class TestVisitable: UIViewController, Visitable { } class TestSessionDelegate: NSObject, SessionDelegate { - var sessionDidLoadWebViewCalled = false + var sessionDidLoadWebViewCalled = false { didSet { didChange?() }} var sessionDidStartRequestCalled = false var sessionDidFinishRequestCalled = false var failedRequestError: Error? = nil - var sessionDidFailRequestCalled = false + var sessionDidFailRequestCalled = false { didSet { didChange?() }} var sessionDidProposeVisitCalled = false + + var didChange: (() -> Void)? func sessionDidLoadWebView(_ session: Session) { sessionDidLoadWebViewCalled = true diff --git a/Tests/VisitOptionsSpec.swift b/Tests/VisitOptionsSpec.swift deleted file mode 100644 index 577214d..0000000 --- a/Tests/VisitOptionsSpec.swift +++ /dev/null @@ -1,56 +0,0 @@ -import Quick -import Nimble -import Foundation -@testable import Turbo - -class VisitOptionsSpec: QuickSpec { - override func spec() { - describe("Decodable") { - it("defaults to advance action when not provided") { - let json = "{}".data(using: .utf8)! - - do { - let decoder = JSONDecoder() - let options = try decoder.decode(VisitOptions.self, from: json) - expect(options.action) == .advance - expect(options.response).to(beNil()) - } catch { - fail(error.localizedDescription) - } - } - - it("uses provided action when not nil") { - let json = """ - {"action": "restore"} - """.data(using: .utf8)! - - do { - let decoder = JSONDecoder() - let options = try decoder.decode(VisitOptions.self, from: json) - expect(options.action) == .restore - expect(options.response).to(beNil()) - } catch { - fail(error.localizedDescription) - } - } - - it("can be initialized with response") { - let json = """ - {"response": {"statusCode": 200, "responseHTML": ""}} - """.data(using: .utf8)! - - do { - let decoder = JSONDecoder() - let options = try decoder.decode(VisitOptions.self, from: json) - expect(options.action) == .advance - expect(options.response).toNot(beNil()) - expect(options.response!.statusCode) == 200 - expect(options.response!.responseHTML) == "" - - } catch { - fail(error.localizedDescription) - } - } - } - } -} diff --git a/Tests/VisitOptionsTests.swift b/Tests/VisitOptionsTests.swift new file mode 100644 index 0000000..f98c1e3 --- /dev/null +++ b/Tests/VisitOptionsTests.swift @@ -0,0 +1,35 @@ +@testable import Turbo +import XCTest + +class VisitOptionsTests: XCTestCase { + func test_Decodable_defaultsToAdvanceActionWhenNotProvided() throws { + let json = "{}".data(using: .utf8)! + + let options = try JSONDecoder().decode(VisitOptions.self, from: json) + XCTAssertEqual(options.action, .advance) + XCTAssertNil(options.response) + } + + func test_Decodable_usesProvidedActionWhenNotNil() throws { + let json = """ + {"action": "restore"} + """.data(using: .utf8)! + + let options = try JSONDecoder().decode(VisitOptions.self, from: json) + XCTAssertEqual(options.action, .restore) + XCTAssertNil(options.response) + } + + func test_Decodable_canBeInitializedWithResponse() throws { + let json = """ + {"response": {"statusCode": 200, "responseHTML": ""}} + """.data(using: .utf8)! + + let options = try JSONDecoder().decode(VisitOptions.self, from: json) + XCTAssertEqual(options.action, .advance) + + let response = try XCTUnwrap(options.response) + XCTAssertEqual(response.statusCode, 200) + XCTAssertEqual(response.responseHTML, "") + } +} From 0205a22c5fc1af565c669b7c22a99c2654b1da36 Mon Sep 17 00:00:00 2001 From: Joe Masilotti Date: Tue, 12 Sep 2023 21:23:19 -0700 Subject: [PATCH 02/21] Format file --- Tests/Test.swift | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/Tests/Test.swift b/Tests/Test.swift index 33e2b54..295cbd9 100644 --- a/Tests/Test.swift +++ b/Tests/Test.swift @@ -1,36 +1,39 @@ +@testable import Turbo import UIKit import WebKit -@testable import Turbo class TestVisitable: UIViewController, Visitable { // MARK: - Tests + var visitableDidRenderCalled = false var visitableDidActivateWebViewWasCalled = false var visitableDidDeactivateWebViewWasCalled = false - + // MARK: - Visitable + var visitableDelegate: VisitableDelegate? var visitableView: VisitableView! var visitableURL: URL! - + init(url: URL) { self.visitableURL = url self.visitableView = VisitableView(frame: .zero) super.init(nibName: nil, bundle: nil) } - + + @available(*, unavailable) required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + func visitableDidRender() { visitableDidRenderCalled = true } - + func visitableDidActivateWebView(_ webView: WKWebView) { visitableDidActivateWebViewWasCalled = true } - + func visitableDidDeactivateWebView() { visitableDidDeactivateWebViewWasCalled = true } @@ -45,33 +48,30 @@ class TestSessionDelegate: NSObject, SessionDelegate { var sessionDidProposeVisitCalled = false var didChange: (() -> Void)? - + func sessionDidLoadWebView(_ session: Session) { sessionDidLoadWebViewCalled = true } - + func sessionDidStartRequest(_ session: Session) { sessionDidStartRequestCalled = true } - + func sessionDidFinishRequest(_ session: Session) { sessionDidFinishRequestCalled = true } - - func sesssionDidStartFormSubmission(_ session: Session) { - } - - func sessionDidFinishFormSubmission(_ session: Session) { - } - - func sessionWebViewProcessDidTerminate(_ session: Session) { - } - + + func sesssionDidStartFormSubmission(_ session: Session) {} + + func sessionDidFinishFormSubmission(_ session: Session) {} + + func sessionWebViewProcessDidTerminate(_ session: Session) {} + func session(_ session: Session, didFailRequestForVisitable visitable: Visitable, error: Error) { sessionDidFailRequestCalled = true failedRequestError = error } - + func session(_ session: Session, didProposeVisit proposal: VisitProposal) { sessionDidProposeVisitCalled = true } From 7dddd0442f473581918f3b1dde60dd5de710df37 Mon Sep 17 00:00:00 2001 From: Joe Masilotti Date: Tue, 12 Sep 2023 21:45:56 -0700 Subject: [PATCH 03/21] Move last test helper to Test.swift with the rest --- Tests/ColdBootVisitTests.swift | 62 ---------------------------------- Tests/Test.swift | 62 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 62 deletions(-) diff --git a/Tests/ColdBootVisitTests.swift b/Tests/ColdBootVisitTests.swift index 9eace24..5eb7174 100644 --- a/Tests/ColdBootVisitTests.swift +++ b/Tests/ColdBootVisitTests.swift @@ -50,65 +50,3 @@ class ColdBootVisitTests: XCTestCase { XCTAssertFalse(visitDelegate.didCall("visitDidStart(_:)")) } } - -private class TestVisitDelegate { - var methodsCalled: Set = [] - - func didCall(_ method: String) -> Bool { - methodsCalled.contains(method) - } - - private func record(_ string: String = #function) { - methodsCalled.insert(string) - } -} - -extension TestVisitDelegate: VisitDelegate { - func visitDidInitializeWebView(_ visit: Visit) { - record() - } - - func visitWillStart(_ visit: Visit) { - record() - } - - func visitDidStart(_ visit: Visit) { - record() - } - - func visitDidComplete(_ visit: Visit) { - record() - } - - func visitDidFail(_ visit: Visit) { - record() - } - - func visitDidFinish(_ visit: Visit) { - record() - } - - func visitWillLoadResponse(_ visit: Visit) { - record() - } - - func visitDidRender(_ visit: Visit) { - record() - } - - func visitRequestDidStart(_ visit: Visit) { - record() - } - - func visit(_ visit: Visit, requestDidFailWithError error: Error) { - record() - } - - func visitRequestDidFinish(_ visit: Visit) { - record() - } - - func visit(_ visit: Visit, didReceiveAuthenticationChallenge challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { - record() - } -} diff --git a/Tests/Test.swift b/Tests/Test.swift index 295cbd9..7fa2ce2 100644 --- a/Tests/Test.swift +++ b/Tests/Test.swift @@ -76,3 +76,65 @@ class TestSessionDelegate: NSObject, SessionDelegate { sessionDidProposeVisitCalled = true } } + +class TestVisitDelegate { + var methodsCalled: Set = [] + + func didCall(_ method: String) -> Bool { + methodsCalled.contains(method) + } + + private func record(_ string: String = #function) { + methodsCalled.insert(string) + } +} + +extension TestVisitDelegate: VisitDelegate { + func visitDidInitializeWebView(_ visit: Visit) { + record() + } + + func visitWillStart(_ visit: Visit) { + record() + } + + func visitDidStart(_ visit: Visit) { + record() + } + + func visitDidComplete(_ visit: Visit) { + record() + } + + func visitDidFail(_ visit: Visit) { + record() + } + + func visitDidFinish(_ visit: Visit) { + record() + } + + func visitWillLoadResponse(_ visit: Visit) { + record() + } + + func visitDidRender(_ visit: Visit) { + record() + } + + func visitRequestDidStart(_ visit: Visit) { + record() + } + + func visit(_ visit: Visit, requestDidFailWithError error: Error) { + record() + } + + func visitRequestDidFinish(_ visit: Visit) { + record() + } + + func visit(_ visit: Visit, didReceiveAuthenticationChallenge challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + record() + } +} From a86e6e9dd3fa8edfdf89b693036d3cfbf341b87e Mon Sep 17 00:00:00 2001 From: Joe Masilotti Date: Tue, 12 Sep 2023 21:58:16 -0700 Subject: [PATCH 04/21] Specify latest stable version of Xcode on CI --- .github/workflows/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2bd097b..c05c6aa 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,5 +8,9 @@ jobs: steps: - uses: actions/checkout@master + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + - name: Run Tests run: xcodebuild -scheme Turbo test -quiet -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14' From d247d36858ea2a2bdf85c9ef36f9e2c89d02fab4 Mon Sep 17 00:00:00 2001 From: Joe Masilotti Date: Wed, 13 Sep 2023 06:15:42 -0700 Subject: [PATCH 05/21] Don't use async version of expectations in tests --- Tests/SessionTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/SessionTests.swift b/Tests/SessionTests.swift index 1fcbfc6..5a3730f 100644 --- a/Tests/SessionTests.swift +++ b/Tests/SessionTests.swift @@ -134,13 +134,13 @@ class SessionTests: XCTestCase { // MARK: - Server @MainActor - private func visit(_ path: String) async { + private func visit(_ path: String) { let expectation = self.expectation(description: "Wait for request to load.") sessionDelegate.didChange = { expectation.fulfill() } let visitable = TestVisitable(url: url(path)) session.visit(visitable) - await fulfillment(of: [expectation]) + wait(for: [expectation]) } private func url(_ path: String) -> URL { From 491f206d6712822c47f175f36c4866cf90268f3d Mon Sep 17 00:00:00 2001 From: Joe Masilotti Date: Thu, 9 Nov 2023 05:56:48 -0800 Subject: [PATCH 06/21] Update CI to latest version of macOS and iOS --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c05c6aa..aae956d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,7 +4,7 @@ on: [push] jobs: test: - runs-on: macos-latest + runs-on: macos-13 steps: - uses: actions/checkout@master @@ -13,4 +13,4 @@ jobs: xcode-version: latest-stable - name: Run Tests - run: xcodebuild -scheme Turbo test -quiet -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14' + run: xcodebuild -scheme Turbo test -quiet -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15 Pro' From eabaa1c51b857c01e260b3eb8d51d2acbb7c82b5 Mon Sep 17 00:00:00 2001 From: Joe Masilotti Date: Thu, 9 Nov 2023 06:02:30 -0800 Subject: [PATCH 07/21] Address warnings from latest Xcode --- Tests/SessionTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/SessionTests.swift b/Tests/SessionTests.swift index 5a3730f..e2580fc 100644 --- a/Tests/SessionTests.swift +++ b/Tests/SessionTests.swift @@ -64,7 +64,7 @@ class SessionTests: XCTestCase { @MainActor func test_coldBootVisit_whenVisitSucceeds_configuresJavaScriptBridge() async throws { - await visit("/") + visit("/") XCTAssertTrue(sessionDelegate.sessionDidLoadWebViewCalled) let result = try await session.webView.evaluateJavaScript("Turbo.navigator.adapter == window.turboNative") @@ -113,7 +113,7 @@ class SessionTests: XCTestCase { @MainActor func test_coldBootVisit_Turbolinks5Compatibility_loadsThePageAndSetsTheAdapter() async throws { - await visit("/turbolinks") + visit("/turbolinks") XCTAssertTrue(sessionDelegate.sessionDidLoadWebViewCalled) @@ -123,7 +123,7 @@ class SessionTests: XCTestCase { @MainActor func test_coldBootVisit_Turbolinks5_3Compatibility_loadsThePageAndSetsTheAdapter() async throws { - await visit("/turbolinks-5.3") + visit("/turbolinks-5.3") XCTAssertTrue(sessionDelegate.sessionDidLoadWebViewCalled) From 9a34fb7478eebdfbffdd2548a169d108bd3e3714 Mon Sep 17 00:00:00 2001 From: Joe Masilotti Date: Thu, 9 Nov 2023 08:34:11 -0800 Subject: [PATCH 08/21] Clean up GitHub action file --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index aae956d..aedc7eb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,11 +6,11 @@ jobs: test: runs-on: macos-13 steps: - - uses: actions/checkout@master + - uses: actions/checkout@v4 - uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: latest-stable - name: Run Tests - run: xcodebuild -scheme Turbo test -quiet -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15 Pro' + run: xcodebuild test -scheme Turbo -destination "name=iPhone 15 Pro" | xcpretty -t && exit ${PIPESTATUS[0]} From 8db3da2a949e91a7f155f51f3222260deac49f00 Mon Sep 17 00:00:00 2001 From: Joe Masilotti Date: Thu, 9 Nov 2023 09:54:30 -0800 Subject: [PATCH 09/21] Perform all test visits async Increase timeout of failed page load to MORE than Turbo.js timeout so it triggers the invalid configuration error. --- Tests/SessionTests.swift | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/Tests/SessionTests.swift b/Tests/SessionTests.swift index e2580fc..6e2fbc2 100644 --- a/Tests/SessionTests.swift +++ b/Tests/SessionTests.swift @@ -62,9 +62,8 @@ class SessionTests: XCTestCase { XCTAssertFalse(sessionDelegate.sessionDidFailRequestCalled) } - @MainActor func test_coldBootVisit_whenVisitSucceeds_configuresJavaScriptBridge() async throws { - visit("/") + await visit("/") XCTAssertTrue(sessionDelegate.sessionDidLoadWebViewCalled) let result = try await session.webView.evaluateJavaScript("Turbo.navigator.adapter == window.turboNative") @@ -91,29 +90,21 @@ class SessionTests: XCTestCase { XCTAssertTrue(sessionDelegate.sessionDidFinishRequestCalled) } - func test_coldBootVisit_whenVisitFailsFromMissingLibrary_callsSessionDidFailRequestDelegateMethod() async { - await visit("/missing-library") + @MainActor + func test_coldBootVisit_whenVisitFailsFromMissingLibrary_providesAnPageLoadError() async throws { + // 5 seconds more than Turbo.js timeout. + await visit("/missing-library", timeout: 35) XCTAssertTrue(sessionDelegate.sessionDidFailRequestCalled) - } - - func test_coldBootVisit_whenVisitFailsFromMissingLibrary_providesAnPageLoadError() async throws { - await visit("/missing-library") + XCTAssertTrue(sessionDelegate.sessionDidFinishRequestCalled) XCTAssertNotNil(sessionDelegate.failedRequestError) let error = try XCTUnwrap(sessionDelegate.failedRequestError) XCTAssertEqual(error as? TurboError, TurboError.pageLoadFailure) } - func test_coldBootVisit_whenVisitFailsFromMissingLibrary_callsSessionDidFinishRequestDelegateMethod() async { - await visit("/missing-library") - - XCTAssertTrue(sessionDelegate.sessionDidFinishRequestCalled) - } - - @MainActor func test_coldBootVisit_Turbolinks5Compatibility_loadsThePageAndSetsTheAdapter() async throws { - visit("/turbolinks") + await visit("/turbolinks") XCTAssertTrue(sessionDelegate.sessionDidLoadWebViewCalled) @@ -121,9 +112,8 @@ class SessionTests: XCTestCase { XCTAssertTrue(result as! Bool) } - @MainActor func test_coldBootVisit_Turbolinks5_3Compatibility_loadsThePageAndSetsTheAdapter() async throws { - visit("/turbolinks-5.3") + await visit("/turbolinks-5.3") XCTAssertTrue(sessionDelegate.sessionDidLoadWebViewCalled) @@ -134,13 +124,13 @@ class SessionTests: XCTestCase { // MARK: - Server @MainActor - private func visit(_ path: String) { + private func visit(_ path: String, timeout: TimeInterval = 5) async { let expectation = self.expectation(description: "Wait for request to load.") sessionDelegate.didChange = { expectation.fulfill() } let visitable = TestVisitable(url: url(path)) session.visit(visitable) - wait(for: [expectation]) + await fulfillment(of: [expectation], timeout: timeout) } private func url(_ path: String) -> URL { From 0b64ccdc95ef41206e546b5fd7c9daa7b97af814 Mon Sep 17 00:00:00 2001 From: Joe Masilotti Date: Thu, 9 Nov 2023 10:08:36 -0800 Subject: [PATCH 10/21] Increase XCTest timeout for slow GitHub Actions --- Tests/SessionTests.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Tests/SessionTests.swift b/Tests/SessionTests.swift index 6e2fbc2..2fc5d2a 100644 --- a/Tests/SessionTests.swift +++ b/Tests/SessionTests.swift @@ -3,6 +3,9 @@ import Swifter import WebKit import XCTest +private let defaultTimeout: TimeInterval = 10 +private let turboTimeout: TimeInterval = 30 + class SessionTests: XCTestCase { private static let server = HttpServer() @@ -92,8 +95,7 @@ class SessionTests: XCTestCase { @MainActor func test_coldBootVisit_whenVisitFailsFromMissingLibrary_providesAnPageLoadError() async throws { - // 5 seconds more than Turbo.js timeout. - await visit("/missing-library", timeout: 35) + await visit("/missing-library", timeout: turboTimeout + defaultTimeout) XCTAssertTrue(sessionDelegate.sessionDidFailRequestCalled) XCTAssertTrue(sessionDelegate.sessionDidFinishRequestCalled) @@ -124,7 +126,7 @@ class SessionTests: XCTestCase { // MARK: - Server @MainActor - private func visit(_ path: String, timeout: TimeInterval = 5) async { + private func visit(_ path: String, timeout: TimeInterval = defaultTimeout) async { let expectation = self.expectation(description: "Wait for request to load.") sessionDelegate.didChange = { expectation.fulfill() } From 43680a0606463eb2ff93209c236bda6bf63fbb9f Mon Sep 17 00:00:00 2001 From: Joe Masilotti Date: Thu, 9 Nov 2023 10:08:54 -0800 Subject: [PATCH 11/21] Try Silicon on GitHub Actions --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index aedc7eb..646833d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,7 +4,7 @@ on: [push] jobs: test: - runs-on: macos-13 + runs-on: macos-13-arm64 steps: - uses: actions/checkout@v4 From 465e784fec24a970905cd16ff4cfb25f335202fd Mon Sep 17 00:00:00 2001 From: Joe Masilotti Date: Thu, 9 Nov 2023 10:43:36 -0800 Subject: [PATCH 12/21] Try faster macOS image --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 646833d..fe19664 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,7 +4,7 @@ on: [push] jobs: test: - runs-on: macos-13-arm64 + runs-on: macos-13-xl steps: - uses: actions/checkout@v4 From 03045f84c9ba06b84c0269e4b95beeea2527baec Mon Sep 17 00:00:00 2001 From: Joe Masilotti Date: Thu, 9 Nov 2023 13:52:33 -0800 Subject: [PATCH 13/21] Convert Swifter to Embassy --- Package.resolved | 16 +++---- Package.swift | 4 +- Tests/SessionTests.swift | 94 +++++++++++++++++++++------------------- 3 files changed, 59 insertions(+), 55 deletions(-) diff --git a/Package.resolved b/Package.resolved index b47b31d..4f66de1 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,21 +1,21 @@ { "pins" : [ { - "identity" : "ohhttpstubs", + "identity" : "embassy", "kind" : "remoteSourceControl", - "location" : "https://github.com/AliSoftware/OHHTTPStubs", + "location" : "https://github.com/envoy/Embassy.git", "state" : { - "revision" : "12f19662426d0434d6c330c6974d53e2eb10ecd9", - "version" : "9.1.0" + "revision" : "8469f2c1b334a7c1c3566e2cb2f97826c7cca898", + "version" : "4.1.6" } }, { - "identity" : "swifter", + "identity" : "ohhttpstubs", "kind" : "remoteSourceControl", - "location" : "https://github.com/httpswift/swifter", + "location" : "https://github.com/AliSoftware/OHHTTPStubs", "state" : { - "revision" : "9483a5d459b45c3ffd059f7b55f9638e268632fd", - "version" : "1.5.0" + "revision" : "12f19662426d0434d6c330c6974d53e2eb10ecd9", + "version" : "9.1.0" } } ], diff --git a/Package.swift b/Package.swift index 921486e..a0e1b2c 100644 --- a/Package.swift +++ b/Package.swift @@ -15,7 +15,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/AliSoftware/OHHTTPStubs", .upToNextMajor(from: "9.0.0")), - .package(url: "https://github.com/httpswift/swifter.git", .upToNextMajor(from: "1.5.0")) + .package(url: "https://github.com/envoy/Embassy.git", .upToNextMajor(from: "4.1.4")) ], targets: [ .target( @@ -32,7 +32,7 @@ let package = Package( dependencies: [ "Turbo", .product(name: "OHHTTPStubsSwift", package: "OHHTTPStubs"), - .product(name: "Swifter", package: "Swifter") + .product(name: "Embassy", package: "Embassy") ], path: "Tests", resources: [ diff --git a/Tests/SessionTests.swift b/Tests/SessionTests.swift index 2fc5d2a..b3add00 100644 --- a/Tests/SessionTests.swift +++ b/Tests/SessionTests.swift @@ -1,23 +1,27 @@ -import Swifter +import Embassy @testable import Turbo import WebKit import XCTest -private let defaultTimeout: TimeInterval = 10 +private let defaultTimeout: TimeInterval = 10000 private let turboTimeout: TimeInterval = 30 class SessionTests: XCTestCase { - private static let server = HttpServer() + private static var eventLoop: EventLoop! + private static var server: HTTPServer! private let sessionDelegate = TestSessionDelegate() private var session: Session! override class func setUp() { + super.setUp() startServer() } override class func tearDown() { - server.stop() + super.tearDown() + server.stopAndWait() + eventLoop.stop() } override func setUp() { @@ -142,50 +146,50 @@ class SessionTests: XCTestCase { } private static func startServer() { - server["/turbo-7.0.0-beta.1.js"] = { _ in - let fileURL = Bundle.module.url(forResource: "turbo-7.0.0-beta.1", withExtension: "js", subdirectory: "Server")! - let data = try! Data(contentsOf: fileURL) - return .ok(.data(data)) + let loop = try! SelectorEventLoop(selector: try! KqueueSelector()) + eventLoop = loop + + let server = DefaultHTTPServer(eventLoop: loop, port: 8080) { environ, startResponse, sendBody in + let path = environ["PATH_INFO"] as! String + + func respondWithFile(resourceName: String, resourceType: String) { + let fileURL = Bundle.module.url(forResource: resourceName, withExtension: resourceType, subdirectory: "Server")! + let data = try! Data(contentsOf: fileURL) + + let contentType = (resourceType == "js") ? "application/javascript" : "text/html" + startResponse("200 OK", [("Content-Type", contentType)]) + sendBody(data) + sendBody(Data()) + } + + switch path { + case "/turbo-7.0.0-beta.1.js": + respondWithFile(resourceName: "turbo-7.0.0-beta.1", resourceType: "js") + case "/turbolinks-5.2.0.js": + respondWithFile(resourceName: "turbolinks-5.2.0", resourceType: "js") + case "/turbolinks-5.3.0-dev.js": + respondWithFile(resourceName: "turbolinks-5.3.0-dev", resourceType: "js") + case "/": + respondWithFile(resourceName: "turbo", resourceType: "html") + case "/turbolinks": + respondWithFile(resourceName: "turbolinks", resourceType: "html") + case "/turbolinks-5.3": + respondWithFile(resourceName: "turbolinks-5.3", resourceType: "html") + case "/missing-library": + startResponse("200 OK", [("Content-Type", "text/html")]) + sendBody("".data(using: .utf8)!) + sendBody(Data()) + default: + startResponse("404 Not Found", [("Content-Type", "text/plain")]) + sendBody(Data()) + } } - server["/turbolinks-5.2.0.js"] = { _ in - let fileURL = Bundle.module.url(forResource: "turbolinks-5.2.0", withExtension: "js", subdirectory: "Server")! - let data = try! Data(contentsOf: fileURL) - return .ok(.data(data)) - } - - server["/turbolinks-5.3.0-dev.js"] = { _ in - let fileURL = Bundle.module.url(forResource: "turbolinks-5.3.0-dev", withExtension: "js", subdirectory: "Server")! - let data = try! Data(contentsOf: fileURL) - return .ok(.data(data)) - } - - server["/"] = { _ in - let fileURL = Bundle.module.url(forResource: "turbo", withExtension: "html", subdirectory: "Server")! - let data = try! Data(contentsOf: fileURL) - return .ok(.data(data)) - } - - server["/turbolinks"] = { _ in - let fileURL = Bundle.module.url(forResource: "turbolinks", withExtension: "html", subdirectory: "Server")! - let data = try! Data(contentsOf: fileURL) - return .ok(.data(data)) - } - - server["/turbolinks-5.3"] = { _ in - let fileURL = Bundle.module.url(forResource: "turbolinks-5.3", withExtension: "html", subdirectory: "Server")! - let data = try! Data(contentsOf: fileURL) - return .ok(.data(data)) - } - - server["/missing-library"] = { _ in - .ok(.html("")) - } + self.server = server + try! server.start() - server["/invalid"] = { _ in - .notFound + DispatchQueue.global().async { + loop.runForever() } - - try! server.start() } } From e8ee29ef0c9ad64522b6a451f9b7756b79e95e13 Mon Sep 17 00:00:00 2001 From: Joe Masilotti Date: Thu, 9 Nov 2023 13:58:07 -0800 Subject: [PATCH 14/21] Remove xcpretty formatter - GitHub can't stream it --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fe19664..275e1a6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,4 +13,4 @@ jobs: xcode-version: latest-stable - name: Run Tests - run: xcodebuild test -scheme Turbo -destination "name=iPhone 15 Pro" | xcpretty -t && exit ${PIPESTATUS[0]} + run: xcodebuild test -scheme Turbo -destination "name=iPhone 15 Pro" | xcpretty && exit ${PIPESTATUS[0]} From eb5e73317c3b560fe9ec51bdbc7b7c6b72977521 Mon Sep 17 00:00:00 2001 From: Joe Masilotti Date: Thu, 9 Nov 2023 14:11:44 -0800 Subject: [PATCH 15/21] Move Embassy server config to helper file --- Tests/Server.swift | 42 ++++++++++++++++++++++++++++++ Tests/SessionTests.swift | 55 +++------------------------------------- 2 files changed, 46 insertions(+), 51 deletions(-) create mode 100644 Tests/Server.swift diff --git a/Tests/Server.swift b/Tests/Server.swift new file mode 100644 index 0000000..45ca519 --- /dev/null +++ b/Tests/Server.swift @@ -0,0 +1,42 @@ +import Embassy +import Foundation + +extension DefaultHTTPServer { + static func turboServer(eventLoop: EventLoop, port: Int = 8080) -> DefaultHTTPServer { + return DefaultHTTPServer(eventLoop: eventLoop, port: port) { environ, startResponse, sendBody in + let path = environ["PATH_INFO"] as! String + + func respondWithFile(resourceName: String, resourceType: String) { + let fileURL = Bundle.module.url(forResource: resourceName, withExtension: resourceType, subdirectory: "Server")! + let data = try! Data(contentsOf: fileURL) + let contentType = (resourceType == "js") ? "application/javascript" : "text/html" + + startResponse("200 OK", [("Content-Type", contentType)]) + sendBody(data) + sendBody(Data()) + } + + switch path { + case "/turbo-7.0.0-beta.1.js": + respondWithFile(resourceName: "turbo-7.0.0-beta.1", resourceType: "js") + case "/turbolinks-5.2.0.js": + respondWithFile(resourceName: "turbolinks-5.2.0", resourceType: "js") + case "/turbolinks-5.3.0-dev.js": + respondWithFile(resourceName: "turbolinks-5.3.0-dev", resourceType: "js") + case "/": + respondWithFile(resourceName: "turbo", resourceType: "html") + case "/turbolinks": + respondWithFile(resourceName: "turbolinks", resourceType: "html") + case "/turbolinks-5.3": + respondWithFile(resourceName: "turbolinks-5.3", resourceType: "html") + case "/missing-library": + startResponse("200 OK", [("Content-Type", "text/html")]) + sendBody("".data(using: .utf8)!) + sendBody(Data()) + default: + startResponse("404 Not Found", [("Content-Type", "text/plain")]) + sendBody(Data()) + } + } + } +} diff --git a/Tests/SessionTests.swift b/Tests/SessionTests.swift index b3add00..c163a99 100644 --- a/Tests/SessionTests.swift +++ b/Tests/SessionTests.swift @@ -7,15 +7,16 @@ private let defaultTimeout: TimeInterval = 10000 private let turboTimeout: TimeInterval = 30 class SessionTests: XCTestCase { - private static var eventLoop: EventLoop! - private static var server: HTTPServer! + private static let eventLoop = try! SelectorEventLoop(selector: try! KqueueSelector()) + private static let server = DefaultHTTPServer.turboServer(eventLoop: eventLoop) private let sessionDelegate = TestSessionDelegate() private var session: Session! override class func setUp() { super.setUp() - startServer() + try! server.start() + DispatchQueue.global().async { eventLoop.runForever() } } override class func tearDown() { @@ -144,52 +145,4 @@ class SessionTests: XCTestCase { let relativePath = path.hasPrefix("/") ? String(path.dropFirst()) : path return baseURL.appendingPathComponent(relativePath) } - - private static func startServer() { - let loop = try! SelectorEventLoop(selector: try! KqueueSelector()) - eventLoop = loop - - let server = DefaultHTTPServer(eventLoop: loop, port: 8080) { environ, startResponse, sendBody in - let path = environ["PATH_INFO"] as! String - - func respondWithFile(resourceName: String, resourceType: String) { - let fileURL = Bundle.module.url(forResource: resourceName, withExtension: resourceType, subdirectory: "Server")! - let data = try! Data(contentsOf: fileURL) - - let contentType = (resourceType == "js") ? "application/javascript" : "text/html" - startResponse("200 OK", [("Content-Type", contentType)]) - sendBody(data) - sendBody(Data()) - } - - switch path { - case "/turbo-7.0.0-beta.1.js": - respondWithFile(resourceName: "turbo-7.0.0-beta.1", resourceType: "js") - case "/turbolinks-5.2.0.js": - respondWithFile(resourceName: "turbolinks-5.2.0", resourceType: "js") - case "/turbolinks-5.3.0-dev.js": - respondWithFile(resourceName: "turbolinks-5.3.0-dev", resourceType: "js") - case "/": - respondWithFile(resourceName: "turbo", resourceType: "html") - case "/turbolinks": - respondWithFile(resourceName: "turbolinks", resourceType: "html") - case "/turbolinks-5.3": - respondWithFile(resourceName: "turbolinks-5.3", resourceType: "html") - case "/missing-library": - startResponse("200 OK", [("Content-Type", "text/html")]) - sendBody("".data(using: .utf8)!) - sendBody(Data()) - default: - startResponse("404 Not Found", [("Content-Type", "text/plain")]) - sendBody(Data()) - } - } - - self.server = server - try! server.start() - - DispatchQueue.global().async { - loop.runForever() - } - } } From 0d8bd6d3fce82ea39076d3b3a6c5761c6cbcebdb Mon Sep 17 00:00:00 2001 From: Joe Masilotti Date: Sat, 11 Nov 2023 06:11:56 -0800 Subject: [PATCH 16/21] Revert back to non-XL box on CI --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 275e1a6..2ddbedc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,7 +4,7 @@ on: [push] jobs: test: - runs-on: macos-13-xl + runs-on: macos-13 steps: - uses: actions/checkout@v4 From 06cf8179141a931313de3faab357d868bdf0c67d Mon Sep 17 00:00:00 2001 From: Joe Masilotti Date: Sat, 11 Nov 2023 06:20:50 -0800 Subject: [PATCH 17/21] Update Tests/ScriptMessageTests.swift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Zoë Smith --- Tests/ScriptMessageTests.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/ScriptMessageTests.swift b/Tests/ScriptMessageTests.swift index 3d44af3..fd5966a 100644 --- a/Tests/ScriptMessageTests.swift +++ b/Tests/ScriptMessageTests.swift @@ -11,7 +11,8 @@ class ScriptMessageTests: XCTestCase { XCTAssertEqual(message.name, .pageLoaded) XCTAssertEqual(message.identifier, "123") XCTAssertEqual(message.restorationIdentifier, "abc") - XCTAssertEqual(message.options!.action, .advance) + let options = try XCTUnwrap(message.options) + XCTAssertEqual(options.action, .advance) XCTAssertEqual(message.location, URL(string: "http://turbo.test")!) } From e74a7801b83a4e23d27170301f1f03c6a9a0d1d8 Mon Sep 17 00:00:00 2001 From: Joe Masilotti Date: Sat, 11 Nov 2023 06:22:25 -0800 Subject: [PATCH 18/21] Remove force try calls in favor of XCTUnwrap --- Tests/ScriptMessageTests.swift | 1 + Tests/SessionTests.swift | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Tests/ScriptMessageTests.swift b/Tests/ScriptMessageTests.swift index fd5966a..8130fbe 100644 --- a/Tests/ScriptMessageTests.swift +++ b/Tests/ScriptMessageTests.swift @@ -11,6 +11,7 @@ class ScriptMessageTests: XCTestCase { XCTAssertEqual(message.name, .pageLoaded) XCTAssertEqual(message.identifier, "123") XCTAssertEqual(message.restorationIdentifier, "abc") + let options = try XCTUnwrap(message.options) XCTAssertEqual(options.action, .advance) XCTAssertEqual(message.location, URL(string: "http://turbo.test")!) diff --git a/Tests/SessionTests.swift b/Tests/SessionTests.swift index c163a99..bd516f0 100644 --- a/Tests/SessionTests.swift +++ b/Tests/SessionTests.swift @@ -75,7 +75,7 @@ class SessionTests: XCTestCase { XCTAssertTrue(sessionDelegate.sessionDidLoadWebViewCalled) let result = try await session.webView.evaluateJavaScript("Turbo.navigator.adapter == window.turboNative") - XCTAssertTrue(result as! Bool) + XCTAssertTrue(try XCTUnwrap(result as? Bool)) } func test_coldBootVisit_whenVisitFailsFromHTTPError_callsSessionDidFailRequestDelegateMethod() async { @@ -116,7 +116,7 @@ class SessionTests: XCTestCase { XCTAssertTrue(sessionDelegate.sessionDidLoadWebViewCalled) let result = try await session.webView.evaluateJavaScript("Turbolinks.controller.adapter === window.turboNative") - XCTAssertTrue(result as! Bool) + XCTAssertTrue(try XCTUnwrap(result as? Bool)) } func test_coldBootVisit_Turbolinks5_3Compatibility_loadsThePageAndSetsTheAdapter() async throws { @@ -125,7 +125,7 @@ class SessionTests: XCTestCase { XCTAssertTrue(sessionDelegate.sessionDidLoadWebViewCalled) let result = try await session.webView.evaluateJavaScript("Turbolinks.controller.adapter === window.turboNative") - XCTAssertTrue(result as! Bool) + XCTAssertTrue(try XCTUnwrap(result as? Bool)) } // MARK: - Server From 8e21ce67edb238d76836c47c971733f602c5596b Mon Sep 17 00:00:00 2001 From: Joe Masilotti Date: Sat, 11 Nov 2023 06:28:44 -0800 Subject: [PATCH 19/21] Move Embassy server outside of class-level --- Tests/SessionTests.swift | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/Tests/SessionTests.swift b/Tests/SessionTests.swift index bd516f0..4c42767 100644 --- a/Tests/SessionTests.swift +++ b/Tests/SessionTests.swift @@ -7,34 +7,30 @@ private let defaultTimeout: TimeInterval = 10000 private let turboTimeout: TimeInterval = 30 class SessionTests: XCTestCase { - private static let eventLoop = try! SelectorEventLoop(selector: try! KqueueSelector()) - private static let server = DefaultHTTPServer.turboServer(eventLoop: eventLoop) - private let sessionDelegate = TestSessionDelegate() private var session: Session! + private var eventLoop: SelectorEventLoop! + private var server: DefaultHTTPServer! - override class func setUp() { - super.setUp() - try! server.start() - DispatchQueue.global().async { eventLoop.runForever() } - } - - override class func tearDown() { - super.tearDown() - server.stopAndWait() - eventLoop.stop() - } - - override func setUp() { + @MainActor + override func setUp() async throws { let configuration = WKWebViewConfiguration() configuration.applicationNameForUserAgent = "Turbo iOS Test/1.0" session = Session(webViewConfiguration: configuration) session.delegate = sessionDelegate + + eventLoop = try SelectorEventLoop(selector: KqueueSelector()) + server = DefaultHTTPServer.turboServer(eventLoop: eventLoop) + try! server.start() + DispatchQueue.global().async { self.eventLoop.runForever() } } override func tearDown() { session.webView.configuration.userContentController.removeScriptMessageHandler(forName: "turbo") + + server.stopAndWait() + eventLoop.stop() } func test_init_initializesWebViewWithConfiguration() { From 0ccafeaac89be7dcb9ce1a7019acb76c8b3eccb6 Mon Sep 17 00:00:00 2001 From: Joe Masilotti Date: Sat, 11 Nov 2023 06:36:37 -0800 Subject: [PATCH 20/21] Remove empty test file --- Tests/JavaScriptVisitTests.swift | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 Tests/JavaScriptVisitTests.swift diff --git a/Tests/JavaScriptVisitTests.swift b/Tests/JavaScriptVisitTests.swift deleted file mode 100644 index 581f9e8..0000000 --- a/Tests/JavaScriptVisitTests.swift +++ /dev/null @@ -1,3 +0,0 @@ -import XCTest - -class JavaScriptVisitTests: XCTestCase {} From f3db394cfb3a6d409747544afbffc0364b42732e Mon Sep 17 00:00:00 2001 From: Joe Masilotti Date: Mon, 13 Nov 2023 12:43:18 -0800 Subject: [PATCH 21/21] Update Tests/SessionTests.swift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Zoë Smith --- Tests/SessionTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/SessionTests.swift b/Tests/SessionTests.swift index 4c42767..90085cf 100644 --- a/Tests/SessionTests.swift +++ b/Tests/SessionTests.swift @@ -22,7 +22,7 @@ class SessionTests: XCTestCase { eventLoop = try SelectorEventLoop(selector: KqueueSelector()) server = DefaultHTTPServer.turboServer(eventLoop: eventLoop) - try! server.start() + try server.start() DispatchQueue.global().async { self.eventLoop.runForever() } }