From b975b95519e5706e6f983002e267fc0d6cb18bd7 Mon Sep 17 00:00:00 2001 From: Joe Masilotti Date: Mon, 13 Nov 2023 12:51:16 -0800 Subject: [PATCH] Replace Quick/Nimble with XCTest (#139) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Replace Quick/Nimble with XCTest * Format file * Move last test helper to Test.swift with the rest * Specify latest stable version of Xcode on CI * Don't use async version of expectations in tests * Update CI to latest version of macOS and iOS * Address warnings from latest Xcode * Clean up GitHub action file * Perform all test visits async Increase timeout of failed page load to MORE than Turbo.js timeout so it triggers the invalid configuration error. * Increase XCTest timeout for slow GitHub Actions * Try Silicon on GitHub Actions * Try faster macOS image * Convert Swifter to Embassy * Remove xcpretty formatter - GitHub can't stream it * Move Embassy server config to helper file * Revert back to non-XL box on CI * Update Tests/ScriptMessageTests.swift Co-authored-by: Zoë Smith * Remove force try calls in favor of XCTUnwrap * Move Embassy server outside of class-level * Remove empty test file * Update Tests/SessionTests.swift Co-authored-by: Zoë Smith --------- Co-authored-by: Zoë Smith --- .github/workflows/main.yml | 10 +- Package.resolved | 44 +---- Package.swift | 8 +- Tests/ColdBootVisitSpec.swift | 121 ------------ Tests/ColdBootVisitTests.swift | 52 ++++++ Tests/JavaScriptExpressionSpec.swift | 37 ---- Tests/JavaScriptExpressionTests.swift | 30 +++ Tests/JavaScriptVisitSpec.swift | 8 - 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 | 53 ++++++ Tests/Server.swift | 42 +++++ Tests/SessionSpec.swift | 223 ----------------------- Tests/SessionTests.swift | 144 +++++++++++++++ Tests/Test.swift | 110 ++++++++--- Tests/VisitOptionsSpec.swift | 56 ------ Tests/VisitOptionsTests.swift | 35 ++++ 22 files changed, 635 insertions(+), 846 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 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 create mode 100644 Tests/Server.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/.github/workflows/main.yml b/.github/workflows/main.yml index 2bd097b..2ddbedc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,9 +4,13 @@ on: [push] jobs: test: - runs-on: macos-latest + 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 14' + run: xcodebuild test -scheme Turbo -destination "name=iPhone 15 Pro" | xcpretty && exit ${PIPESTATUS[0]} diff --git a/Package.resolved b/Package.resolved index 41b1182..4f66de1 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,30 +1,12 @@ { "pins" : [ { - "identity" : "cwlcatchexception", + "identity" : "embassy", "kind" : "remoteSourceControl", - "location" : "https://github.com/mattgallagher/CwlCatchException.git", + "location" : "https://github.com/envoy/Embassy.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" + "revision" : "8469f2c1b334a7c1c3566e2cb2f97826c7cca898", + "version" : "4.1.6" } }, { @@ -35,24 +17,6 @@ "revision" : "12f19662426d0434d6c330c6974d53e2eb10ecd9", "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", - "location" : "https://github.com/httpswift/swifter", - "state" : { - "revision" : "9483a5d459b45c3ffd059f7b55f9638e268632fd", - "version" : "1.5.0" - } } ], "version" : 2 diff --git a/Package.swift b/Package.swift index a05cf1d..a0e1b2c 100644 --- a/Package.swift +++ b/Package.swift @@ -14,10 +14,8 @@ 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")) + .package(url: "https://github.com/envoy/Embassy.git", .upToNextMajor(from: "4.1.4")) ], targets: [ .target( @@ -33,10 +31,8 @@ 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") + .product(name: "Embassy", package: "Embassy") ], path: "Tests", resources: [ 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..5eb7174 --- /dev/null +++ b/Tests/ColdBootVisitTests.swift @@ -0,0 +1,52 @@ +@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(_:)")) + } +} 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/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..8130fbe --- /dev/null +++ b/Tests/ScriptMessageTests.swift @@ -0,0 +1,53 @@ +@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") + + let options = try XCTUnwrap(message.options) + XCTAssertEqual(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/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/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..90085cf --- /dev/null +++ b/Tests/SessionTests.swift @@ -0,0 +1,144 @@ +import Embassy +@testable import Turbo +import WebKit +import XCTest + +private let defaultTimeout: TimeInterval = 10000 +private let turboTimeout: TimeInterval = 30 + +class SessionTests: XCTestCase { + private let sessionDelegate = TestSessionDelegate() + private var session: Session! + private var eventLoop: SelectorEventLoop! + private var server: DefaultHTTPServer! + + @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() { + 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) + } + + 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(try XCTUnwrap(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) + } + + @MainActor + func test_coldBootVisit_whenVisitFailsFromMissingLibrary_providesAnPageLoadError() async throws { + await visit("/missing-library", timeout: turboTimeout + defaultTimeout) + + XCTAssertTrue(sessionDelegate.sessionDidFailRequestCalled) + XCTAssertTrue(sessionDelegate.sessionDidFinishRequestCalled) + + XCTAssertNotNil(sessionDelegate.failedRequestError) + let error = try XCTUnwrap(sessionDelegate.failedRequestError) + XCTAssertEqual(error as? TurboError, TurboError.pageLoadFailure) + } + + 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(try XCTUnwrap(result as? Bool)) + } + + 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(try XCTUnwrap(result as? Bool)) + } + + // MARK: - Server + + @MainActor + private func visit(_ path: String, timeout: TimeInterval = defaultTimeout) 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], timeout: timeout) + } + + 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) + } +} diff --git a/Tests/Test.swift b/Tests/Test.swift index cffcb86..7fa2ce2 100644 --- a/Tests/Test.swift +++ b/Tests/Test.swift @@ -1,76 +1,140 @@ +@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 } } 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 } - + 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 } } + +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/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, "") + } +}