From cb8932e03474494e6575a5888dc034fab08d6eb6 Mon Sep 17 00:00:00 2001 From: OhKanghoon Date: Wed, 21 Jun 2023 10:27:44 +0900 Subject: [PATCH 1/3] refactor: use XCTest in URLMatcherTests --- .../URLMatcherTests/URLConvertibleSpec.swift | 121 -------- .../URLMatcherTests/URLConvertibleTests.swift | 125 +++++++++ .../URLMatcherInternalSpec.swift | 46 --- .../URLMatcherInternalTests.swift | 58 ++++ Tests/URLMatcherTests/URLMatcherSpec.swift | 175 ------------ Tests/URLMatcherTests/URLMatcherTests.swift | 261 ++++++++++++++++++ .../URLPathComponentSpec.swift | 52 ---- .../URLPathComponentTests.swift | 44 +++ 8 files changed, 488 insertions(+), 394 deletions(-) delete mode 100644 Tests/URLMatcherTests/URLConvertibleSpec.swift create mode 100644 Tests/URLMatcherTests/URLConvertibleTests.swift delete mode 100644 Tests/URLMatcherTests/URLMatcherInternalSpec.swift create mode 100644 Tests/URLMatcherTests/URLMatcherInternalTests.swift delete mode 100644 Tests/URLMatcherTests/URLMatcherSpec.swift create mode 100644 Tests/URLMatcherTests/URLMatcherTests.swift delete mode 100644 Tests/URLMatcherTests/URLPathComponentSpec.swift create mode 100644 Tests/URLMatcherTests/URLPathComponentTests.swift diff --git a/Tests/URLMatcherTests/URLConvertibleSpec.swift b/Tests/URLMatcherTests/URLConvertibleSpec.swift deleted file mode 100644 index 155c8a4..0000000 --- a/Tests/URLMatcherTests/URLConvertibleSpec.swift +++ /dev/null @@ -1,121 +0,0 @@ -import Foundation - -import Nimble -import Quick - -import URLMatcher - -final class URLConvertibleSpec: QuickSpec { - override func spec() { - describe("urlValue") { - it("returns an URL instance") { - let url = URL(string: "https://xoul.kr")! - expect(url.urlValue) == url - expect(url.absoluteString.urlValue) == url - } - - it("returns an URL instance from unicode string") { - let urlString = "https://xoul.kr/한글" - expect(urlString.urlValue) == URL(string: "https://xoul.kr/%ED%95%9C%EA%B8%80")! - } - } - - describe("urlStringValue") { - it("returns a URL string value") { - let url = URL(string: "https://xoul.kr")! - expect(url.urlStringValue) == url.absoluteString - expect(url.absoluteString.urlStringValue) == url.absoluteString - } - } - - describe("queryParameters") { - context("when there is no query string") { - it("returns empty dictionary") { - let url = "https://xoul.kr" - expect(url.urlValue?.queryParameters) == [:] - expect(url.urlStringValue.queryParameters) == [:] - } - } - - context("when there is an empty query string") { - it("returns empty dictionary") { - let url = "https://xoul.kr?" - expect(url.urlValue?.queryParameters) == [:] - expect(url.urlStringValue.queryParameters) == [:] - } - } - - context("when there is a query string") { - let url = "https://xoul.kr?key=this%20is%20a%20value&greeting=hello+world!&int=12&int=34&url=https://foo/bar?hello=world" - - it("has proper keys") { - expect(Set(url.urlValue!.queryParameters.keys)) == ["key", "greeting", "int", "url"] - expect(Set(url.urlStringValue.queryParameters.keys)) == ["key", "greeting", "int", "url"] - } - - it("decodes a percent encoding") { - expect(url.urlValue?.queryParameters["key"]) == "this is a value" - expect(url.urlStringValue.queryParameters["key"]) == "this is a value" - } - - it("doesn't convert + to whitespace") { - expect(url.urlValue?.queryParameters["greeting"]) == "hello+world!" - expect(url.urlStringValue.queryParameters["greeting"]) == "hello+world!" - } - - it("takes last value from duplicated keys") { - expect(url.urlValue?.queryParameters["int"]) == "34" - expect(url.urlStringValue.queryParameters["int"]) == "34" - } - - it("has an url") { - expect(url.urlValue?.queryParameters["url"]) == "https://foo/bar?hello=world" - } - } - } - - describe("queryItems") { - context("when there is no query string") { - it("returns nil") { - let url = "https://xoul.kr" - expect(url.urlValue?.queryItems).to(beNil()) - expect(url.urlStringValue.queryItems).to(beNil()) - } - } - - context("when there is an empty query string") { - it("returns an empty array") { - let url = "https://xoul.kr?" - expect(url.urlValue?.queryItems) == [] - expect(url.urlStringValue.queryItems) == [] - } - } - - context("when there is a query string") { - let url = "https://xoul.kr?key=this%20is%20a%20value&greeting=hello+world!&int=12&int=34" - - it("has exact number of items") { - expect(url.urlValue?.queryItems?.count) == 4 - expect(url.urlStringValue.queryItems?.count) == 4 - } - - it("decodes a percent encoding") { - expect(url.urlValue?.queryItems?[0]) == URLQueryItem(name: "key", value: "this is a value") - expect(url.urlStringValue.queryItems?[0]) == URLQueryItem(name: "key", value: "this is a value") - } - - it("doesn't convert + to whitespace") { - expect(url.urlValue?.queryItems?[1]) == URLQueryItem(name: "greeting", value: "hello+world!") - expect(url.urlStringValue.queryItems?[1]) == URLQueryItem(name: "greeting", value: "hello+world!") - } - - it("takes all duplicated keys") { - expect(url.urlValue?.queryItems?[2]) == URLQueryItem(name: "int", value: "12") - expect(url.urlValue?.queryItems?[3]) == URLQueryItem(name: "int", value: "34") - expect(url.urlStringValue.queryItems?[2]) == URLQueryItem(name: "int", value: "12") - expect(url.urlStringValue.queryItems?[3]) == URLQueryItem(name: "int", value: "34") - } - } - } - } -} diff --git a/Tests/URLMatcherTests/URLConvertibleTests.swift b/Tests/URLMatcherTests/URLConvertibleTests.swift new file mode 100644 index 0000000..bb43fbf --- /dev/null +++ b/Tests/URLMatcherTests/URLConvertibleTests.swift @@ -0,0 +1,125 @@ +import Foundation +import XCTest + +import URLMatcher + +final class URLConvertibleTests: XCTestCase { + + // MARK: URL + + func test_urlValue_returns_an_URL_instance() { + // given + let url = URL(string: "https://xoul.kr")! + + // then + XCTAssertEqual(url.urlValue, url) + XCTAssertEqual(url.absoluteString.urlValue, url) + } + + func test_urlValue_returns_an_URL_instance_from_unicode_string() { + // given + let urlString = "https://xoul.kr/한글" + + // then + XCTAssertEqual(urlString.urlValue, URL(string: "https://xoul.kr/%ED%95%9C%EA%B8%80")!) + } + + func test_urlStringValue_returns_a_URL_string_value() { + // given + let url = URL(string: "https://xoul.kr")! + + // then + XCTAssertEqual(url.urlStringValue, url.absoluteString) + XCTAssertEqual(url.absoluteString.urlStringValue, url.absoluteString) + } + + + // MARK: Query Parameters + + func test_queryParameters_when_there_is_no_query_string_return_empty_dict() { + // given + let url = "https://xoul.kr" + + // then + XCTAssertEqual(url.urlValue?.queryParameters, [:]) + XCTAssertEqual(url.urlStringValue.queryParameters, [:]) + } + + func test_queryParameters_when_there_is_an_empty_query_string_returns_empty_dict() { + // given + let url = "https://xoul.kr?" + + // then + XCTAssertEqual(url.urlValue?.queryParameters, [:]) + XCTAssertEqual(url.urlStringValue.queryParameters, [:]) + } + + func test_queryParameters_when_there_is_a_query_string() { + // given + let url = "https://xoul.kr?key=this%20is%20a%20value&greeting=hello+world!&int=12&int=34&url=https://foo/bar?hello=world" + + // then + /// has proper keys + XCTAssertEqual(Set(url.urlValue!.queryParameters.keys), ["key", "greeting", "int", "url"]) + XCTAssertEqual(Set(url.urlStringValue.queryParameters.keys), ["key", "greeting", "int", "url"]) + + /// decodes a percent encoding + XCTAssertEqual(url.urlValue?.queryParameters["key"], "this is a value") + XCTAssertEqual(url.urlStringValue.queryParameters["key"], "this is a value") + + /// doesn't convert + to whitespace + XCTAssertEqual(url.urlValue?.queryParameters["greeting"], "hello+world!") + XCTAssertEqual(url.urlStringValue.queryParameters["greeting"], "hello+world!") + + /// takes last value from duplicated keys + XCTAssertEqual(url.urlValue?.queryParameters["int"], "34") + XCTAssertEqual(url.urlStringValue.queryParameters["int"], "34") + + /// has an url + XCTAssertEqual(url.urlValue?.queryParameters["url"], "https://foo/bar?hello=world") + } + + // MARK: Query Items + + func test_queryItems_when_there_is_no_query_string_returns_nil() { + // given + let url = "https://xoul.kr" + + // then + XCTAssertNil(url.urlValue?.queryItems) + XCTAssertNil(url.urlStringValue.queryItems) + } + + func test_queryItems_when_there_is_an_empty_query_string_returns_an_empty_array() { + // given + let url = "https://xoul.kr?" + + // then + XCTAssertEqual(url.urlValue?.queryItems, []) + XCTAssertEqual(url.urlStringValue.queryItems, []) + } + + func test_queryItems_when_there_is_a_query_string() { + // given + let url = "https://xoul.kr?key=this%20is%20a%20value&greeting=hello+world!&int=12&int=34" + + // then + /// has exact number of items + XCTAssertEqual(url.urlValue?.queryItems?.count, 4) + XCTAssertEqual(url.urlStringValue.queryItems?.count, 4) + + /// decodes a percent encoding + XCTAssertEqual(url.urlValue?.queryItems?[0], URLQueryItem(name: "key", value: "this is a value")) + XCTAssertEqual(url.urlStringValue.queryItems?[0], URLQueryItem(name: "key", value: "this is a value")) + + /// doesn't convert + to whitespace + XCTAssertEqual(url.urlValue?.queryItems?[1], URLQueryItem(name: "greeting", value: "hello+world!")) + XCTAssertEqual(url.urlStringValue.queryItems?[1], URLQueryItem(name: "greeting", value: "hello+world!")) + + /// takes all duplicated keys + XCTAssertEqual(url.urlValue?.queryItems?[2], URLQueryItem(name: "int", value: "12")) + XCTAssertEqual(url.urlValue?.queryItems?[3], URLQueryItem(name: "int", value: "34")) + XCTAssertEqual(url.urlStringValue.queryItems?[2], URLQueryItem(name: "int", value: "12")) + XCTAssertEqual(url.urlStringValue.queryItems?[3], URLQueryItem(name: "int", value: "34")) + } +} diff --git a/Tests/URLMatcherTests/URLMatcherInternalSpec.swift b/Tests/URLMatcherTests/URLMatcherInternalSpec.swift deleted file mode 100644 index 2396b70..0000000 --- a/Tests/URLMatcherTests/URLMatcherInternalSpec.swift +++ /dev/null @@ -1,46 +0,0 @@ -import Nimble -import Quick - -@testable import URLMatcher - -final class URLMatcherInternalSpec: QuickSpec { - override func spec() { - var matcher: URLMatcher! - - beforeEach { - matcher = URLMatcher() - } - - describe("normalizeURL(_:)") { - it("doesn't change anything if there is nothing to normalize") { - expect(matcher.normalizeURL("myapp://user//hello").urlStringValue) == "myapp://user//hello" - expect(matcher.normalizeURL("https://").urlStringValue) == "https://" - expect(matcher.normalizeURL("https://").urlStringValue) == "https://" - } - - it("removes redundant slashes and query parameters and hashbangs") { - let dirtyURL = "myapp:///////user/////hello/??/#abc=/def" - expect(matcher.normalizeURL(dirtyURL).urlStringValue) == "myapp://user//hello" - } - } - - describe("pathComponents(from:)") { - it("returns proper path components") { - let components = matcher.pathComponents(from: "myapp://foo/bar//") - expect(components) == [ - .plain("foo"), - .plain("bar"), - .placeholder(type: nil, key: "name"), - .placeholder(type: "int", key: "id"), - ] - } - } - - describe("replaceRegex(_:_:_:)") { - it("replaces regular expression") { - expect(matcher.replaceRegex("a", "0", "abc")) == "0bc" - expect(matcher.replaceRegex("\\d", "A", "1234567abc098")) == "AAAAAAAabcAAA" - } - } - } -} diff --git a/Tests/URLMatcherTests/URLMatcherInternalTests.swift b/Tests/URLMatcherTests/URLMatcherInternalTests.swift new file mode 100644 index 0000000..f3674ef --- /dev/null +++ b/Tests/URLMatcherTests/URLMatcherInternalTests.swift @@ -0,0 +1,58 @@ +import XCTest + +@testable import URLMatcher + +final class URLMatcherInternalTests: XCTestCase { + + private var matcher: URLMatcher! + + override func setUp() { + super.setUp() + + matcher = URLMatcher() + } + + + // MARK: normalizeURL + + func test_normalizeURL_does_not_change_anything_if_there_is_nothing_to_normalize() { + XCTAssertEqual(matcher.normalizeURL("myapp://user//hello").urlStringValue, "myapp://user//hello") + XCTAssertEqual(matcher.normalizeURL("https://").urlStringValue, "https://") + XCTAssertEqual(matcher.normalizeURL("https://").urlStringValue, "https://") + } + + func test_normalizeURL_removes_redundant_slashes_and_query_parameters_and_hashbangs() { + // given + let dirtyURL = "myapp:///////user/////hello/??/#abc=/def" + + // then + XCTAssertEqual(matcher.normalizeURL(dirtyURL).urlStringValue, "myapp://user//hello") + } + + + // MARK: pathComponents + + func test_pathComponents_returns_proper_path_components() { + // given + let components = matcher.pathComponents(from: "myapp://foo/bar//") + + // then + XCTAssertEqual( + components, + [ + .plain("foo"), + .plain("bar"), + .placeholder(type: nil, key: "name"), + .placeholder(type: "int", key: "id"), + ] + ) + } + + + // MARK: replaceRegex + + func test_replaceRegex_replaces_regular_expression() { + XCTAssertEqual(matcher.replaceRegex("a", "0", "abc"), "0bc") + XCTAssertEqual(matcher.replaceRegex("\\d", "A", "1234567abc098"), "AAAAAAAabcAAA") + } +} diff --git a/Tests/URLMatcherTests/URLMatcherSpec.swift b/Tests/URLMatcherTests/URLMatcherSpec.swift deleted file mode 100644 index b13e517..0000000 --- a/Tests/URLMatcherTests/URLMatcherSpec.swift +++ /dev/null @@ -1,175 +0,0 @@ -import Foundation - -import Nimble -import Quick - -import URLMatcher - -final class URLMatcherSpec: QuickSpec { - override func spec() { - var matcher: URLMatcher! - - beforeEach { - matcher = URLMatcher() - } - - it("returns nil when there's no candidates") { - let result = matcher.match("myapp://user/1", from: []) - expect(result).to(beNil()) - } - - it("returns nil for unmatching scheme") { - let result = matcher.match("myapp://user/1", from: ["yourapp://user/"]) - expect(result).to(beNil()) - } - - it("returns nil for totally unmatching url") { - let result = matcher.match("myapp://user/1", from: ["myapp://comment/"]) - expect(result).to(beNil()) - } - - it("returns nil for partially unmatching url") { - let result = matcher.match("myapp://user/1", from: ["myapp://user//hello"]) - expect(result).to(beNil()) - } - - it("returns nil for an unmatching value type") { - let result = matcher.match("myapp://user/devxoul", from: ["myapp://user/"]) - expect(result).to(beNil()) - } - - it("returns a result for totally matching url") { - let candidates = ["myapp://hello/", "myapp://hello/world"] - let result = matcher.match("myapp://hello/world", from: candidates) - expect(result).notTo(beNil()) - expect(result?.pattern) == "myapp://hello/world" - expect(result?.values.count) == 0 - } - - it("returns a result for the longest matching url") { - let candidates = ["myapp://", "myapp://hello/"] - let result = matcher.match("myapp://hello/world", from: candidates) - expect(result).notTo(beNil()) - expect(result?.pattern) == "myapp://hello/" - expect(result?.values.count) == 1 - } - - it("returns a result with an url value for matching url") { - let candidates = ["myapp://user//hello", "myapp://user/"] - let result = matcher.match("myapp://user/1", from: candidates) - expect(result).notTo(beNil()) - expect(result?.pattern) == "myapp://user/" - expect(result?.values.count) == 1 - expect(result?.values["id"] as? String) == "1" - } - - it("returns a result with an string-type url value for matching url") { - let candidates = ["myapp://user/"] - let result = matcher.match("myapp://user/123", from: candidates) - expect(result).notTo(beNil()) - expect(result?.pattern) == "myapp://user/" - expect(result?.values.count) == 1 - expect(result?.values["id"] as? String) == "123" - } - - it("returns a result with an int-type url value for matching url") { - let candidates = ["myapp://user/"] - let result = matcher.match("myapp://user/123", from: candidates) - expect(result).notTo(beNil()) - expect(result?.pattern) == "myapp://user/" - expect(result?.values.count) == 1 - expect(result?.values["id"] as? Int) == 123 - } - - it("returns a result with a float-type url value for matching url") { - let candidates = ["myapp://user/"] - let result = matcher.match("myapp://user/123.456", from: candidates) - expect(result).notTo(beNil()) - expect(result?.pattern) == "myapp://user/" - expect(result?.values.count) == 1 - expect(result?.values["id"] as? Float) == 123.456 - } - - it("returns a result with an uuid-type url value for matching url") { - let candidates = ["myapp://user/"] - let uuidString = "621425B8-42D1-4AB4-9A58-1E69D708A84B" - let result = matcher.match("myapp://user/\(uuidString)" ,from: candidates) - expect(result).notTo(beNil()) - expect(result?.pattern) == "myapp://user/" - expect(result?.values.count) == 1 - expect(result?.values["id"] as? UUID) == UUID(uuidString: uuidString) - } - - it("returns a result with a custom-type url value for matching url") { - matcher.valueConverters["greeting"] = { pathComponents, index in - return "Hello, \(pathComponents[index])!" - } - let candidates = ["myapp://hello/"] - let result = matcher.match("myapp://hello/devxoul" ,from: candidates) - expect(result).notTo(beNil()) - expect(result?.pattern) == "myapp://hello/" - expect(result?.values.count) == 1 - expect(result?.values["name"] as? String) == "Hello, devxoul!" - } - - it("returns a result with multiple url values for matching url") { - let candidates = ["myapp://user/", "myapp://user//"] - let result = matcher.match("myapp://user/1/posts", from: candidates) - expect(result).notTo(beNil()) - expect(result?.pattern) == "myapp://user//" - expect(result?.values.count) == 2 - expect(result?.values["id"] as? String) == "1" - expect(result?.values["object"] as? String) == "posts" - } - - it("returns a result with ignoring a query string") { - let candidates = ["myapp://alert"] - let result = matcher.match("myapp://alert?title=hello&message=world", from: candidates) - expect(result).notTo(beNil()) - expect(result?.pattern) == "myapp://alert" - expect(result?.values.count) == 0 - } - - it("returns a result with a path-type url value") { - let candidates = ["https://"] - let result = matcher.match("https://google.com/search?q=URLNavigator", from: candidates) - expect(result).notTo(beNil()) - expect(result?.pattern) == "https://" - expect(result?.values["url"] as? String) == "google.com/search" - } - - it("returns a result with a path url value ending with trailing slash") { - let candidates = ["https://"] - let result = matcher.match("https://google.com/search/?q=URLNavigator", from: candidates) - expect(result).notTo(beNil()) - expect(result?.pattern) == "https://" - expect(result?.values["url"] as? String) == "google.com/search" - } - - it("returns nil when there's no candidates (issues-109)") { - let candidates = ["/anything/"] - let result = matcher.match("", from: candidates) - expect(result).to(beNil()) - } - - it("returns nil when there's no candidates (issues-109)") { - let candidates = ["http://host/anything/"] - let result = matcher.match("http://host/anything", from: candidates) - expect(result).to(beNil()) - } - - it("returns same candidate (issues-109)") { - let candidates1 = ["http://host/anything/", "http://host/anything"] - let result1 = matcher.match("http://host/anything", from: candidates1) - let candidates2 = ["http://host/anything", "http://host/anything/"] - let result2 = matcher.match("http://host/anything", from: candidates2) - expect(result1?.pattern).to(equal(result2?.pattern)) - } - - it("returns nil when there is anotehr url in the path (#123)") { - let candidates = ["myapp://browser/"] - let result = matcher.match("myapp://browser/http://google.fr", from: candidates) - expect(result).to(beNil()) - } - } -} diff --git a/Tests/URLMatcherTests/URLMatcherTests.swift b/Tests/URLMatcherTests/URLMatcherTests.swift new file mode 100644 index 0000000..9a39015 --- /dev/null +++ b/Tests/URLMatcherTests/URLMatcherTests.swift @@ -0,0 +1,261 @@ +import XCTest + +import URLMatcher + +final class URLMatcherTests: XCTestCase { + var matcher: URLMatcher! + + override func setUp() { + super.setUp() + matcher = URLMatcher() + } + + func test_returns_nil_when_there_is_no_candidates() { + // when + let result = matcher.match("myapp://user/1", from: []) + + // then + XCTAssertNil(result) + } + + func test_returns_nil_for_unmatched_scheme() { + // when + let result = matcher.match("myapp://user/1", from: ["yourapp://user/"]) + + // then + XCTAssertNil(result) + } + + func test_returns_nil_for_totally_unmatched_url() { + // when + let result = matcher.match("myapp://user/1", from: ["myapp://comment/"]) + + // then + XCTAssertNil(result) + } + + func test_returns_nil_for_partially_unmatched_url() { + // when + let result = matcher.match("myapp://user/1", from: ["myapp://user//hello"]) + + // then + XCTAssertNil(result) + } + + func test_returns_nil_for_an_unmatched_value_type() { + // when + let result = matcher.match("myapp://user/devxoul", from: ["myapp://user/"]) + + // then + XCTAssertNil(result) + } + + func test_returns_a_result_for_totally_matching_url() { + // given + let candidates = ["myapp://hello/", "myapp://hello/world"] + + // when + let result = matcher.match("myapp://hello/world", from: candidates) + + // then + XCTAssertNotNil(result) + XCTAssertEqual(result?.pattern, "myapp://hello/world") + XCTAssertEqual(result?.values.count, 0) + } + + func test_returns_a_result_for_the_longest_matching_url() { + // given + let candidates = ["myapp://", "myapp://hello/"] + + // when + let result = matcher.match("myapp://hello/world", from: candidates) + + // then + XCTAssertNotNil(result) + XCTAssertEqual(result?.pattern, "myapp://hello/") + XCTAssertEqual(result?.values.count, 1) + } + + func test_returns_a_result_with_an_url_value_for_matching_url() { + // given + let candidates = ["myapp://user//hello", "myapp://user/"] + + // when + let result = matcher.match("myapp://user/1", from: candidates) + + // then + XCTAssertNotNil(result) + XCTAssertEqual(result?.pattern, "myapp://user/") + XCTAssertEqual(result?.values.count, 1) + XCTAssertEqual(result?.values["id"] as? String, "1") + } + + func test_returns_a_result_with_an_string_type_url_value_for_matching_url() { + // given + let candidates = ["myapp://user/"] + + // when + let result = matcher.match("myapp://user/123", from: candidates) + + // then + XCTAssertNotNil(result) + XCTAssertEqual(result?.pattern, "myapp://user/") + XCTAssertEqual(result?.values.count, 1) + XCTAssertEqual(result?.values["id"] as? String, "123") + } + + func test_returns_a_result_with_an_int_type_url_value_for_matching_url() { + // given + let candidates = ["myapp://user/"] + + // when + let result = matcher.match("myapp://user/123", from: candidates) + + // then + XCTAssertNotNil(result) + XCTAssertEqual(result?.pattern, "myapp://user/") + XCTAssertEqual(result?.values.count, 1) + XCTAssertEqual(result?.values["id"] as? Int, 123) + } + + func test_returns_a_result_with_a_float_type_url_value_for_matching_url() { + // given + let candidates = ["myapp://user/"] + + // when + let result = matcher.match("myapp://user/123.456", from: candidates) + + // then + XCTAssertNotNil(result) + XCTAssertEqual(result?.pattern, "myapp://user/") + XCTAssertEqual(result?.values.count, 1) + XCTAssertEqual(result?.values["id"] as? Float, 123.456) + } + + func test_returns_a_result_with_a_uuid_type_url_value_for_matching_url() { + // given + let candidates = ["myapp://user/"] + let uuidString = "621425B8-42D1-4AB4-9A58-1E69D708A84B" + + // when + let result = matcher.match("myapp://user/\(uuidString)", from: candidates) + + // then + XCTAssertNotNil(result) + XCTAssertEqual(result?.pattern, "myapp://user/") + XCTAssertEqual(result?.values.count, 1) + XCTAssertEqual(result?.values["id"] as? UUID, UUID(uuidString: uuidString)) + } + + func test_returns_a_result_with_a_custom_type_url_value_for_matching_url() { + // given + matcher.valueConverters["greeting"] = { pathComponents, index in + return "Hello, \(pathComponents[index])!" + } + let candidates = ["myapp://hello/"] + + // when + let result = matcher.match("myapp://hello/devxoul" ,from: candidates) + + // then + XCTAssertNotNil(result) + XCTAssertEqual(result?.pattern, "myapp://hello/") + XCTAssertEqual(result?.values.count, 1) + XCTAssertEqual(result?.values["name"] as? String, "Hello, devxoul!") + } + + func test_returns_a_result_with_multiple_url_values_for_matching_url() { + // given + let candidates = ["myapp://user/", "myapp://user//"] + + // when + let result = matcher.match("myapp://user/1/posts", from: candidates) + + // then + XCTAssertNotNil(result) + XCTAssertEqual(result?.pattern, "myapp://user//") + XCTAssertEqual(result?.values.count, 2) + XCTAssertEqual(result?.values["id"] as? String, "1") + XCTAssertEqual(result?.values["object"] as? String, "posts") + } + + func test_returns_a_result_with_ignoring_a_query_string() { + // given + let candidates = ["myapp://alert"] + + // when + let result = matcher.match("myapp://alert?title=hello&message=world", from: candidates) + + // then + XCTAssertNotNil(result) + XCTAssertEqual(result?.pattern, "myapp://alert") + XCTAssertEqual(result?.values.count, 0) + } + + func test_returns_a_result_with_a_path_type_url_value() { + // given + let candidates = ["https://"] + + // when + let result = matcher.match("https://google.com/search?q=URLNavigator", from: candidates) + + // then + XCTAssertNotNil(result) + XCTAssertEqual(result?.pattern, "https://") + XCTAssertEqual(result?.values["url"] as? String, "google.com/search") + } + + func test_returns_a_result_with_a_path_url_value_ending_with_trailing_slash() { + // given + let candidates = ["https://"] + + // when + let result = matcher.match("https://google.com/search/?q=URLNavigator", from: candidates) + + // then + XCTAssertNotNil(result) + XCTAssertEqual(result?.pattern, "https://") + XCTAssertEqual(result?.values["url"] as? String, "google.com/search") + } + + // (issues-109) + func test_returns_nil_when_there_is_no_candidates_using_path() { + // given + let candidates = ["/anything/"] + let candidates2 = ["http://host/anything/"] + + // when + let result = matcher.match("", from: candidates) + let result2 = matcher.match("http://host/anything", from: candidates2) + + // then + XCTAssertNil(result) + XCTAssertNil(result2) + } + + // (issues-109) + func test_returns_same_candidate() { + // given + let candidates1 = ["http://host/anything/", "http://host/anything"] + let candidates2 = ["http://host/anything", "http://host/anything/"] + + // when + let result1 = matcher.match("http://host/anything", from: candidates1) + let result2 = matcher.match("http://host/anything", from: candidates2) + + // then + XCTAssertEqual(result1?.pattern, result2?.pattern) + } + + // #123 + func test_returns_nil_when_there_is_another_url_in_the_path() { + // given + let candidates = ["myapp://browser/"] + + // when + let result = matcher.match("myapp://browser/http://google.fr", from: candidates) + + // then + XCTAssertNil(result) + } +} diff --git a/Tests/URLMatcherTests/URLPathComponentSpec.swift b/Tests/URLMatcherTests/URLPathComponentSpec.swift deleted file mode 100644 index 8fc392f..0000000 --- a/Tests/URLMatcherTests/URLPathComponentSpec.swift +++ /dev/null @@ -1,52 +0,0 @@ -import Nimble -import Quick - -@testable import URLMatcher - -final class URLPathComponentSpec: QuickSpec { - override func spec() { - describe("init()") { - it("is .plain with plain string") { - expect(URLPathComponent("foo")) == URLPathComponent.plain("foo") - } - - it("is .placeholder with untyped placeholder string") { - expect(URLPathComponent("")) == URLPathComponent.placeholder(type: nil, key: "name") - } - - it("is .placeholder for typed placeholder string") { - expect(URLPathComponent("")) == URLPathComponent.placeholder(type: "int", key: "id") - } - } - - describe("==") { - it("is true for both .plain with same values") { - expect(URLPathComponent.plain("foo")) == URLPathComponent.plain("foo") - } - - it("is true for both .plain with different values") { - expect(URLPathComponent.plain("foo")) != URLPathComponent.plain("bar") - } - - it("is true for both .placeholder with same types and same keys") { - expect(URLPathComponent.placeholder(type: "int", key: "id")) == URLPathComponent.placeholder(type: "int", key: "id") - } - - it("is false for both .placeholder with different types and same keys") { - expect(URLPathComponent.placeholder(type: "int", key: "id")) != URLPathComponent.placeholder(type: "string", key: "id") - } - - it("is false for both .placeholder with same types and different keys") { - expect(URLPathComponent.placeholder(type: "int", key: "id")) != URLPathComponent.placeholder(type: "int", key: "name") - } - - it("is false for both .placeholder with different types and different keys") { - expect(URLPathComponent.placeholder(type: "int", key: "id")) != URLPathComponent.placeholder(type: "string", key: "name") - } - - it("is false for different cases") { - expect(URLPathComponent.plain("id")) != URLPathComponent.placeholder(type: "int", key: "id") - } - } - } -} diff --git a/Tests/URLMatcherTests/URLPathComponentTests.swift b/Tests/URLMatcherTests/URLPathComponentTests.swift new file mode 100644 index 0000000..1175204 --- /dev/null +++ b/Tests/URLMatcherTests/URLPathComponentTests.swift @@ -0,0 +1,44 @@ +import XCTest + +@testable import URLMatcher + +final class URLPathComponentTests: XCTestCase { + + func test_init_plain() { + XCTAssertEqual(URLPathComponent("foo"), URLPathComponent.plain("foo")) + } + + func test_init_placeholder_with_untyped_placeholder_string() { + XCTAssertEqual(URLPathComponent(""), URLPathComponent.placeholder(type: nil, key: "name")) + } + + func test_init_placeholder_for_typed_placeholder_string() { + XCTAssertEqual(URLPathComponent(""), URLPathComponent.placeholder(type: "int", key: "id")) + } + + func test_plain_equatable() { + XCTAssertEqual(URLPathComponent.plain("foo"), URLPathComponent.plain("foo")) + XCTAssertNotEqual(URLPathComponent.plain("foo"), URLPathComponent.plain("bar")) + } + + func test_placeholder_equatable() { + XCTAssertEqual( + URLPathComponent.placeholder(type: "int", key: "id"), + URLPathComponent.placeholder(type: "int", key: "id") + ) + XCTAssertNotEqual( + URLPathComponent.placeholder(type: "int", key: "id"), + URLPathComponent.placeholder(type: "string", key: "id") + ) + XCTAssertNotEqual( + URLPathComponent.placeholder(type: "int", key: "id"), + URLPathComponent.placeholder(type: "int", key: "name") + ) + XCTAssertNotEqual( + URLPathComponent.placeholder(type: "int", key: "id"), + URLPathComponent.placeholder(type: "string", key: "name") + ) + + XCTAssertNotEqual(URLPathComponent.plain("id"), URLPathComponent.placeholder(type: "int", key: "id")) + } +} From 21026375dd80da0045399bb3faf3a8dd7c8bee0d Mon Sep 17 00:00:00 2001 From: OhKanghoon Date: Mon, 10 Jul 2023 01:28:23 +0900 Subject: [PATCH 2/3] refactor: use XCTest in URLNavigatorTests --- .../NavigatorDelegateSpec.swift | 35 - .../NavigatorDelegateTests.swift | 35 + Tests/URLNavigatorTests/NavigatorSpec.swift | 339 ---------- Tests/URLNavigatorTests/NavigatorTests.swift | 629 ++++++++++++++++++ Tests/URLNavigatorTests/Stubs.swift | 32 +- .../TopMostViewControllerSpec.swift | 207 ------ .../TopMostViewControllerTests.swift | 227 +++++++ 7 files changed, 907 insertions(+), 597 deletions(-) delete mode 100644 Tests/URLNavigatorTests/NavigatorDelegateSpec.swift create mode 100644 Tests/URLNavigatorTests/NavigatorDelegateTests.swift delete mode 100644 Tests/URLNavigatorTests/NavigatorSpec.swift create mode 100644 Tests/URLNavigatorTests/NavigatorTests.swift delete mode 100644 Tests/URLNavigatorTests/TopMostViewControllerSpec.swift create mode 100644 Tests/URLNavigatorTests/TopMostViewControllerTests.swift diff --git a/Tests/URLNavigatorTests/NavigatorDelegateSpec.swift b/Tests/URLNavigatorTests/NavigatorDelegateSpec.swift deleted file mode 100644 index 913da80..0000000 --- a/Tests/URLNavigatorTests/NavigatorDelegateSpec.swift +++ /dev/null @@ -1,35 +0,0 @@ -#if os(iOS) || os(tvOS) -import UIKit - -import Nimble -import Quick - -import URLNavigator - -final class NavigatorDelegateSpec: QuickSpec { - override func spec() { - var delegate: NavigatorDelegateObject! - - beforeEach { - delegate = NavigatorDelegateObject() - } - - describe("shouldPush(viewController:from:)") { - it("returns true as default") { - let result = delegate.shouldPush(viewController: UIViewController(), from: UINavigationController()) - expect(result) == true - } - } - - describe("shouldPresent(viewController:from:)") { - it("returns true as default") { - let result = delegate.shouldPresent(viewController: UIViewController(), from: UIViewController()) - expect(result) == true - } - } - } -} - -private final class NavigatorDelegateObject: NavigatorDelegate { -} -#endif diff --git a/Tests/URLNavigatorTests/NavigatorDelegateTests.swift b/Tests/URLNavigatorTests/NavigatorDelegateTests.swift new file mode 100644 index 0000000..ada534f --- /dev/null +++ b/Tests/URLNavigatorTests/NavigatorDelegateTests.swift @@ -0,0 +1,35 @@ +#if os(iOS) || os(tvOS) +import UIKit +import XCTest + +import URLNavigator + +final class NavigatorDelegateTests: XCTestCase { + + private var delegate: NavigatorDelegateObject! + + override func setUp() { + super.setUp() + + delegate = .init() + } + + func test_shouldPush_returns_true_as_default() { + // given + let result = delegate.shouldPush(viewController: UIViewController(), from: UINavigationController()) + + // then + XCTAssertTrue(result) + } + + func test_shouldPresent_returns_true_as_default() { + // given + let result = delegate.shouldPresent(viewController: UIViewController(), from: UIViewController()) + + // then + XCTAssertTrue(result) + } +} + +fileprivate final class NavigatorDelegateObject: NavigatorDelegate {} +#endif diff --git a/Tests/URLNavigatorTests/NavigatorSpec.swift b/Tests/URLNavigatorTests/NavigatorSpec.swift deleted file mode 100644 index 5d0cbec..0000000 --- a/Tests/URLNavigatorTests/NavigatorSpec.swift +++ /dev/null @@ -1,339 +0,0 @@ -#if os(iOS) || os(tvOS) -import UIKit - -import Nimble -import Quick - -import URLNavigator - -final class NavigatorSpec: QuickSpec { - override func spec() { - var navigator: NavigatorProtocol! - - beforeEach { - navigator = Navigator() - } - - describe("viewController(for:context:)") { - context("when there is no registered view controller") { - it("returns nil") { - let viewController = navigator.viewController(for: "/article/123") - expect(viewController).to(beNil()) - } - } - - context("when there is a registered view controller") { - beforeEach { - navigator.register("myapp://article/") { url, values, context in - guard let articleID = values["id"] as? Int, articleID > 0 else { return nil } - return ArticleViewController(articleID: articleID, context: context) - } - } - - it("returns nil for not matching url") { - let viewController = navigator.viewController(for: "myapp://article") - expect(viewController).to(beNil()) - } - - it("returns nil for not matching value type") { - let viewController = navigator.viewController(for: "myapp://article/hello") - expect(viewController).to(beNil()) - } - - it("returns nil when the factory returns nil") { - let viewController = navigator.viewController(for: "myapp://article/-1") as? ArticleViewController - expect(viewController).to(beNil()) - } - - it("returns a matching view controller") { - let viewController = navigator.viewController(for: "myapp://article/123") as? ArticleViewController - expect(viewController).notTo(beNil()) - expect(viewController?.articleID) == 123 - expect(viewController?.context).to(beNil()) - } - - it("returns a matching view controller with a context") { - let viewController = navigator.viewController(for: "myapp://article/123", context: "Hello") as? ArticleViewController - expect(viewController).notTo(beNil()) - expect(viewController?.articleID) == 123 - expect(viewController?.context as? String) == "Hello" - } - } - } - - describe("push(url:context:from:animated:)") { - beforeEach { - navigator.register("myapp://article/") { url, values, context in - guard let articleID = values["id"] as? Int, articleID > 0 else { return nil } - return ArticleViewController(articleID: articleID, context: context) - } - } - - it("pushes a view controller to a navigation controller") { - let navigationController = StubNavigationController() - let viewController = navigator.push("myapp://article/123", from: navigationController) as? ArticleViewController - expect(viewController?.articleID) == 123 - expect(viewController?.context).to(beNil()) - } - - it("pushes a view controller to a navigation controller with a context") { - let navigationController = StubNavigationController() - let viewController = navigator.push("myapp://article/123", context: 456, from: navigationController) as? ArticleViewController - expect(viewController?.articleID) == 123 - expect(viewController?.context as? Int) == 456 - } - - it("executes pushViewController() with default arguments") { - let navigationController = StubNavigationController() - navigator.push("myapp://article/123", from: navigationController) - - expect(navigationController.pushViewControllerCallCount) == 1 - expect(navigationController.pushViewControllerParams?.viewController).to(beAKindOf(ArticleViewController.self)) - expect(navigationController.pushViewControllerParams?.animated) == true - } - - it("executes pushViewController() with given arguments") { - let navigationController = StubNavigationController() - navigator.push("myapp://article/123", from: navigationController, animated: false) - - expect(navigationController.pushViewControllerCallCount) == 1 - expect(navigationController.pushViewControllerParams?.viewController).to(beAKindOf(ArticleViewController.self)) - expect(navigationController.pushViewControllerParams?.animated) == false - } - } - - describe("present(url:context:wrap:from:animated:completion:)") { - beforeEach { - navigator.register("myapp://article/") { url, values, context in - guard let articleID = values["id"] as? Int, articleID > 0 else { return nil } - return ArticleViewController(articleID: articleID, context: context) - } - } - - it("presents a view controller") { - let rootViewController = StubViewController() - let viewController = navigator.present("myapp://article/123", from: rootViewController) as? ArticleViewController - expect(viewController?.articleID) == 123 - expect(viewController?.context).to(beNil()) - } - - it("presents a view controller with a context") { - let rootViewController = StubViewController() - let viewController = navigator.present("myapp://article/123", context: "Hello", from: rootViewController) as? ArticleViewController - expect(viewController?.articleID) == 123 - expect(viewController?.context as? String) == "Hello" - } - - it("executes present() with default arguments") { - let rootViewController = StubViewController() - navigator.present("myapp://article/123", from: rootViewController) - - expect(rootViewController.presentCallCount) == 1 - expect(rootViewController.presentParams?.viewControllerToPresent).to(beAKindOf(ArticleViewController.self)) - expect(rootViewController.presentParams?.animated) == true - expect(rootViewController.presentParams?.completion).to(beNil()) - } - - it("executes present() with given arguments") { - let rootViewController = StubViewController() - var completionExecutionCount = 0 - navigator.present("myapp://article/123", wrap: MyNavigationController.self, from: rootViewController, animated: false, completion: { - completionExecutionCount += 1 - }) - - expect(rootViewController.presentCallCount) == 1 - expect(rootViewController.presentParams?.viewControllerToPresent).to(beAKindOf(MyNavigationController.self)) - expect(rootViewController.presentParams?.animated) == false - expect(rootViewController.presentParams?.completion).notTo(beNil()) - } - } - - describe("handler(for:context:)") { - context("when there is no handler") { - it("returns nil") { - let handler = navigator.handler(for: "myapp://alert") - expect(handler).to(beNil()) - } - } - - context("when there is registered handlers") { - beforeEach { - navigator.handle("myapp://alert") { url, values, context in - return true - } - } - - it("returns false for not matching url") { - let handler = navigator.handler(for: "myapp://alerthello") - expect(handler).to(beNil()) - } - - it("returns a matching handler") { - let handler = navigator.handler(for: "myapp://alert?title=Hello%2C%20world!&message=It%27s%20me!") - expect(handler).notTo(beNil()) - } - - it("returns a matching handler with a context") { - let handler = navigator.handler(for: "myapp://alert?title=Hello%2C%20world!", context: "Hi") - expect(handler).notTo(beNil()) - } - } - } - - describe("open(url:context:)") { - var alerts: [(title: String, message: String?, context: Any?)]! - - beforeEach { - alerts = [] - } - - context("when there is no handler") { - it("returns false") { - let result = navigator.open("myapp://alert") - expect(result) == false - expect(alerts.count) == 0 - } - } - - context("when there is registered handlers") { - beforeEach { - navigator.handle("myapp://alert") { url, values, context in - guard let title = url.queryParameters["title"] else { return false } - let message = url.queryParameters["message"] - alerts.append((title: title, message: message, context: context)) - return true - } - } - - it("returns false for not matching url") { - let result = navigator.open("myapp://alerthello") - expect(result) == false - expect(alerts.count) == 0 - } - - it("executes a matching handler") { - let result = navigator.open("myapp://alert?title=Hello%2C%20world!&message=It%27s%20me!") - expect(result) == true - expect(alerts.count) == 1 - expect(alerts.first?.title) == "Hello, world!" - expect(alerts.first?.message) == "It's me!" - expect(alerts.first?.context).to(beNil()) - } - - it("executes a matching handler with a context") { - let result = navigator.open("myapp://alert?title=Hello%2C%20world!", context: "Hi") - expect(result) == true - expect(alerts.count) == 1 - expect(alerts.first?.title) == "Hello, world!" - expect(alerts.first?.message).to(beNil()) - expect(alerts.first?.context as? String) == "Hi" - } - } - } - - describe("delegate") { - var delegate: StubNavigatorDelegate! - var fromNavigationController: StubNavigationController! - var fromViewController: StubViewController! - var alerts: [(title: String, message: String?, context: Any?)]! - - beforeEach { - delegate = StubNavigatorDelegate() - fromNavigationController = StubNavigationController() - fromViewController = StubViewController() - alerts = [] - - navigator.delegate = delegate - navigator.register("myapp://article/") { url, values, context in - guard let articleID = values["id"] as? Int, articleID > 0 else { return nil } - return ArticleViewController(articleID: articleID, context: context) - } - navigator.handle("myapp://alert") { url, values, context in - guard let title = url.queryParameters["title"] else { return false } - let message = url.queryParameters["message"] - alerts.append((title: title, message: message, context: context)) - return true - } - } - - describe("shouldPush(viewController:from:)") { - context("on push()") { - it("doesn't get called for a not matching url") { - navigator.push("myapp://user/10", from: fromNavigationController) - expect(delegate.shouldPushCallCount) == 0 - } - - it("doesn't get called when the factory returns nil") { - navigator.push("myapp://article/-1", from: fromNavigationController) - expect(delegate.shouldPushCallCount) == 0 - } - - it("gets called for a valid url") { - navigator.push("myapp://article/123", from: fromNavigationController) - expect(delegate.shouldPushCallCount) == 1 - expect(delegate.shouldPushParams?.viewController).to(beAKindOf(ArticleViewController.self)) - expect(delegate.shouldPushParams?.from) === fromNavigationController - } - - it("doesn't prevent from pushing when returns true") { - delegate.shouldPushStub = true - navigator.push("myapp://article/123", from: fromNavigationController) - expect(fromNavigationController.pushViewControllerCallCount) == 1 - } - - it("prevents from pushing when returns false") { - delegate.shouldPushStub = false - navigator.push("myapp://article/123", from: fromNavigationController) - expect(fromNavigationController.pushViewControllerCallCount) == 0 - } - } - - context("on present()") { - it("doesn't get called") { - navigator.present("myapp://article/1", from: fromViewController) - } - } - } - - describe("shouldPresent(viewController:from:)") { - context("on push()") { - it("doesn't get called") { - navigator.push("myapp://article/1", from: fromNavigationController) - } - } - - context("on present()") { - it("doesn't get called for a not matching url") { - navigator.present("myapp://user/10", from: fromViewController) - expect(delegate.shouldPresentCallCount) == 0 - } - - it("doesn't get called when the factory returns nil") { - navigator.present("myapp://article/-1", from: fromViewController) - expect(delegate.shouldPresentCallCount) == 0 - } - - it("gets called for a valid url") { - navigator.present("myapp://article/123", from: fromViewController) - expect(delegate.shouldPresentCallCount) == 1 - expect(delegate.shouldPresentParams?.viewController).to(beAKindOf(ArticleViewController.self)) - expect(delegate.shouldPresentParams?.from) === fromViewController - } - - it("doesn't prevent from presenting when returns true") { - delegate.shouldPresentStub = true - navigator.present("myapp://article/123", from: fromViewController) - expect(fromViewController.presentCallCount) == 1 - } - - it("prevents from presenting when returns false") { - delegate.shouldPresentStub = false - navigator.present("myapp://article/123", from: fromViewController) - expect(fromViewController.presentCallCount) == 0 - } - } - } - } - } -} -#endif diff --git a/Tests/URLNavigatorTests/NavigatorTests.swift b/Tests/URLNavigatorTests/NavigatorTests.swift new file mode 100644 index 0000000..ca7eb8c --- /dev/null +++ b/Tests/URLNavigatorTests/NavigatorTests.swift @@ -0,0 +1,629 @@ +#if os(iOS) || os(tvOS) +import UIKit +import XCTest + +import URLNavigator + +final class NavigatorTests: XCTestCase { + + var navigator: NavigatorProtocol! + + override func setUp() { + super.setUp() + navigator = Navigator() + } +} + + +// MARK: - viewController(for:context:) + +extension NavigatorTests { + + func test_viewController_returns_nil_when_there_is_no_registered_view_controller() { + // given + let viewController = navigator.viewController(for: "/article/123") + + // then + XCTAssertNil(viewController) + } + + func test_viewController_returns_nil_for_not_matching_url_when_there_is_a_registered_view_controller() { + // given + navigator.register("myapp://article/") { url, values, context in + guard let articleID = values["id"] as? Int, articleID > 0 else { return nil } + return ArticleViewController(articleID: articleID, context: context) + } + + let viewController = navigator.viewController(for: "/article/123") + + // then + XCTAssertNil(viewController) + } + + func test_viewController_returns_nil_for_not_matching_value_type_when_there_is_a_registered_view_controller() { + // given + navigator.register("myapp://article/") { url, values, context in + guard let articleID = values["id"] as? Int, articleID > 0 else { return nil } + return ArticleViewController(articleID: articleID, context: context) + } + + let viewController = navigator.viewController(for: "myapp://article/hello") + + // then + XCTAssertNil(viewController) + } + + func test_viewController_returns_nil_for_not_matching_factory_when_there_is_a_registered_view_controller() { + // given + navigator.register("myapp://article/") { url, values, context in + guard let articleID = values["id"] as? Int, articleID > 0 else { return nil } + return ArticleViewController(articleID: articleID, context: context) + } + + let viewController = navigator.viewController(for: "myapp://article/-1") as? ArticleViewController + + // then + XCTAssertNil(viewController) + } + + func test_viewController_returns_matching_view_controller_when_there_is_a_registered_view_controller() { + // given + navigator.register("myapp://article/") { url, values, context in + guard let articleID = values["id"] as? Int, articleID > 0 else { return nil } + return ArticleViewController(articleID: articleID, context: context) + } + + let viewController = navigator.viewController(for: "myapp://article/123") as? ArticleViewController + + // then + XCTAssertNotNil(viewController) + XCTAssertEqual(viewController?.articleID, 123) + XCTAssertNil(viewController?.context) + } + + func test_viewController_returns_matching_view_controller_with_a_context_when_there_is_a_registered_view_controller() { + // given + navigator.register("myapp://article/") { url, values, context in + guard let articleID = values["id"] as? Int, articleID > 0 else { return nil } + return ArticleViewController(articleID: articleID, context: context) + } + + let viewController = navigator.viewController(for: "myapp://article/123", context: "Hello") as? ArticleViewController + + // then + XCTAssertNotNil(viewController) + XCTAssertEqual(viewController?.articleID, 123) + XCTAssertEqual(viewController?.context as? String, "Hello") + } +} + + +// MARK: - push(url:context:from:animated:) + +extension NavigatorTests { + + func test_pushes_a_view_controller_to_a_navigation_controller() { + // given + navigator.register("myapp://article/") { url, values, context in + guard let articleID = values["id"] as? Int, articleID > 0 else { return nil } + return ArticleViewController(articleID: articleID, context: context) + } + + let navigationController = StubNavigationController() + + // when + let viewController = navigator.push("myapp://article/123", from: navigationController) as? ArticleViewController + + // then + XCTAssertEqual(viewController?.articleID, 123) + XCTAssertNil(viewController?.context) + } + + func test_pushes_a_view_controller_to_a_navigation_controller_with_a_context() { + // given + navigator.register("myapp://article/") { url, values, context in + guard let articleID = values["id"] as? Int, articleID > 0 else { return nil } + return ArticleViewController(articleID: articleID, context: context) + } + + let navigationController = StubNavigationController() + + // when + let viewController = navigator.push("myapp://article/123", context: 456, from: navigationController) as? ArticleViewController + + // then + XCTAssertEqual(viewController?.articleID, 123) + XCTAssertEqual(viewController?.context as? Int, 456) + } + + func test_executes_pushViewController_with_default_arguments() { + // given + navigator.register("myapp://article/") { url, values, context in + guard let articleID = values["id"] as? Int, articleID > 0 else { return nil } + return ArticleViewController(articleID: articleID, context: context) + } + + let navigationController = StubNavigationController() + + // when + navigator.push("myapp://article/123", from: navigationController) + + // then + XCTAssertEqual(navigationController.pushViewControllerCallCount, 1) + XCTAssertTrue(navigationController.pushViewControllerParams?.viewController is ArticleViewController) + XCTAssertEqual(navigationController.pushViewControllerParams?.animated, true) + } + + func test_executes_pushViewController_with_given_arguments() { + // given + navigator.register("myapp://article/") { url, values, context in + guard let articleID = values["id"] as? Int, articleID > 0 else { return nil } + return ArticleViewController(articleID: articleID, context: context) + } + + let navigationController = StubNavigationController() + + // when + navigator.push("myapp://article/123", from: navigationController, animated: false) + + // then + XCTAssertEqual(navigationController.pushViewControllerCallCount, 1) + XCTAssertTrue(navigationController.pushViewControllerParams?.viewController is ArticleViewController) + XCTAssertEqual(navigationController.pushViewControllerParams?.animated, false) + } +} + + +// MARK: - present(url:context:wrap:from:animated:completion:) + +extension NavigatorTests { + + func test_presents_a_view_controller() { + // given + navigator.register("myapp://article/") { url, values, context in + guard let articleID = values["id"] as? Int, articleID > 0 else { return nil } + return ArticleViewController(articleID: articleID, context: context) + } + let rootViewController = StubViewController() + + // when + let viewController = navigator.present("myapp://article/123", from: rootViewController) as? ArticleViewController + + // then + XCTAssertEqual(viewController?.articleID, 123) + XCTAssertNil(viewController?.context) + } + + func test_presents_a_view_controller_with_a_context() { + // given + navigator.register("myapp://article/") { url, values, context in + guard let articleID = values["id"] as? Int, articleID > 0 else { return nil } + return ArticleViewController(articleID: articleID, context: context) + } + let rootViewController = StubViewController() + + // when + let viewController = navigator.present( + "myapp://article/123", + context: "Hello", + from: rootViewController + ) as? ArticleViewController + + // then + XCTAssertEqual(viewController?.articleID, 123) + XCTAssertEqual(viewController?.context as? String, "Hello") + } + + func test_executes_present_with_default_arguments() { + // given + navigator.register("myapp://article/") { url, values, context in + guard let articleID = values["id"] as? Int, articleID > 0 else { return nil } + return ArticleViewController(articleID: articleID, context: context) + } + let rootViewController = StubViewController() + + // when + navigator.present("myapp://article/123", from: rootViewController) + + // then + XCTAssertEqual(rootViewController.presentCallCount, 1) + XCTAssertTrue(rootViewController.presentParams?.viewControllerToPresent is ArticleViewController) + XCTAssertEqual(rootViewController.presentParams?.animated, true) + XCTAssertNil(rootViewController.presentParams?.completion) + } + + func test_executes_present_with_given_arguments() { + // given + navigator.register("myapp://article/") { url, values, context in + guard let articleID = values["id"] as? Int, articleID > 0 else { return nil } + return ArticleViewController(articleID: articleID, context: context) + } + let rootViewController = StubViewController() + var completionExecutionCount = 0 + + // when + navigator.present( + "myapp://article/123", + wrap: MyNavigationController.self, + from: rootViewController, + animated: false, + completion: { + completionExecutionCount += 1 + } + ) + + // then + XCTAssertEqual(rootViewController.presentCallCount, 1) + XCTAssertTrue(rootViewController.presentParams?.viewControllerToPresent is MyNavigationController) + XCTAssertEqual(rootViewController.presentParams?.animated, false) + XCTAssertNotNil(rootViewController.presentParams?.completion) + } +} + + +// MARK: - handler(for:context:) + +extension NavigatorTests { + + func test_handler_returns_nil_when_there_is_no_handler() { + // when + let handler = navigator.handler(for: "myapp://alert") + + // then + XCTAssertNil(handler) + } + + func test_handler_returns_nil_for_not_matching_url() { + // given + navigator.handle("myapp://alert") { url, values, context in + true + } + + // when + let handler = navigator.handler(for: "myapp://alerthello") + + // then + XCTAssertNil(handler) + } + + func test_handler_returns_a_matching_handler() { + // given + navigator.handle("myapp://alert") { url, values, context in + true + } + + // when + let handler = navigator.handler(for: "myapp://alert?title=Hello%2C%20world!&message=It%27s%20me!") + + // then + XCTAssertNotNil(handler) + } + + func test_handler_returns_a_matching_handler_with_a_context() { + // given + navigator.handle("myapp://alert") { url, values, context in + true + } + + // when + let handler = navigator.handler(for: "myapp://alert?title=Hello%2C%20world!", context: "Hi") + + // then + XCTAssertNotNil(handler) + } +} + + +// MARK: - open(url:context:) + +extension NavigatorTests { + + func test_open_returns_false_when_there_is_no_handler() { + // when + let result = navigator.open("myapp://alert") + + // then + XCTAssertFalse(result) + } + + func test_open_returns_false_for_not_matching_url() { + // given + var alerts: [(title: String, message: String?, context: Any?)]! = [] + navigator.handle("myapp://alert") { url, values, context in + guard let title = url.queryParameters["title"] else { return false } + let message = url.queryParameters["message"] + alerts.append((title: title, message: message, context: context)) + return true + } + + // when + let result = navigator.open("myapp://alerthello") + + // then + XCTAssertFalse(result) + XCTAssertTrue(alerts.isEmpty) + } + + func test_open_executes_a_matching_handler() { + // given + var alerts: [(title: String, message: String?, context: Any?)]! = [] + navigator.handle("myapp://alert") { url, values, context in + guard let title = url.queryParameters["title"] else { return false } + let message = url.queryParameters["message"] + alerts.append((title: title, message: message, context: context)) + return true + } + + // when + let result = navigator.open("myapp://alert?title=Hello%2C%20world!&message=It%27s%20me!") + + // then + XCTAssertTrue(result) + XCTAssertEqual(alerts.count, 1) + XCTAssertEqual(alerts.first?.title, "Hello, world!") + XCTAssertEqual(alerts.first?.message, "It's me!") + XCTAssertNil(alerts.first?.context) + } + + func test_open_executes_a_matching_handler_with_context() { + // given + var alerts: [(title: String, message: String?, context: Any?)]! = [] + navigator.handle("myapp://alert") { url, values, context in + guard let title = url.queryParameters["title"] else { return false } + let message = url.queryParameters["message"] + alerts.append((title: title, message: message, context: context)) + return true + } + + // when + let result = navigator.open("myapp://alert?title=Hello%2C%20world!", context: "Hi") + + // then + XCTAssertTrue(result) + XCTAssertEqual(alerts.count, 1) + XCTAssertEqual(alerts.first?.title, "Hello, world!") + XCTAssertNil(alerts.first?.message) + XCTAssertEqual(alerts.first?.context as? String, "Hi") + } +} + + +// MARK: - delegate > shouldPush + +extension NavigatorTests { + + func test_shouldPush_does_not_get_called_for_a_not_matching_url_on_push() { + // given + let delegate: StubNavigatorDelegate = .init() + + navigator.delegate = delegate + + let fromNavigationController = StubNavigationController() + + // when + navigator.push("myapp://user/10", from: fromNavigationController) + + // then + XCTAssertEqual(delegate.shouldPushCallCount, 0) + } + + func test_shouldPush_does_not_get_called_when_the_factory_returns_nil_on_push() { + // given + let delegate: StubNavigatorDelegate = .init() + + navigator.delegate = delegate + navigator.register("myapp://article/") { url, values, context in + guard let articleID = values["id"] as? Int, articleID > 0 else { return nil } + return ArticleViewController(articleID: articleID, context: context) + } + + let fromNavigationController = StubNavigationController() + + // when + navigator.push("myapp://article/-1", from: fromNavigationController) + + // then + XCTAssertEqual(delegate.shouldPushCallCount, 0) + } + + func test_shouldPush_gets_called_for_a_valid_url_on_push() { + // given + let delegate: StubNavigatorDelegate = .init() + + navigator.delegate = delegate + navigator.register("myapp://article/") { url, values, context in + guard let articleID = values["id"] as? Int, articleID > 0 else { return nil } + return ArticleViewController(articleID: articleID, context: context) + } + + let fromNavigationController = StubNavigationController() + + // when + navigator.push("myapp://article/123", from: fromNavigationController) + + // then + XCTAssertEqual(delegate.shouldPushCallCount, 1) + XCTAssertTrue(delegate.shouldPushParams?.viewController is ArticleViewController) + XCTAssertIdentical(delegate.shouldPushParams?.from, fromNavigationController) + } + + func test_shouldPush_does_not_prevent_from_pushing_when_returns_true_on_push() { + // given + let delegate: StubNavigatorDelegate = .init() + delegate.shouldPushStub = true + + navigator.delegate = delegate + navigator.register("myapp://article/") { url, values, context in + guard let articleID = values["id"] as? Int, articleID > 0 else { return nil } + return ArticleViewController(articleID: articleID, context: context) + } + + let fromNavigationController = StubNavigationController() + + // when + navigator.push("myapp://article/123", from: fromNavigationController) + + // then + XCTAssertEqual(fromNavigationController.pushViewControllerCallCount, 1) + } + + func test_shouldPush_prevents_from_pushing_when_returns_false_on_push() { + // given + let delegate: StubNavigatorDelegate = .init() + delegate.shouldPushStub = false + + navigator.delegate = delegate + navigator.register("myapp://article/") { url, values, context in + guard let articleID = values["id"] as? Int, articleID > 0 else { return nil } + return ArticleViewController(articleID: articleID, context: context) + } + + let fromNavigationController = StubNavigationController() + + // when + navigator.push("myapp://article/123", from: fromNavigationController) + + // then + XCTAssertEqual(fromNavigationController.pushViewControllerCallCount, 0) + } + + func test_shouldPush_does_not_get_called_on_present() { + // given + let delegate: StubNavigatorDelegate = .init() + + navigator.delegate = delegate + navigator.register("myapp://article/") { url, values, context in + guard let articleID = values["id"] as? Int, articleID > 0 else { return nil } + return ArticleViewController(articleID: articleID, context: context) + } + + let fromViewController = StubViewController() + + // when + navigator.present("myapp://article/1", from: fromViewController) + + // then + XCTAssertEqual(delegate.shouldPushCallCount, 0) + } +} + + +// MARK: - delegate > shouldPresent + +extension NavigatorTests { + + func test_shouldPresent_does_not_get_called_on_push() { + // given + let delegate: StubNavigatorDelegate = .init() + + navigator.delegate = delegate + navigator.register("myapp://article/") { url, values, context in + guard let articleID = values["id"] as? Int, articleID > 0 else { return nil } + return ArticleViewController(articleID: articleID, context: context) + } + + let fromNavigationController = StubNavigationController() + + // when + navigator.push("myapp://article/1", from: fromNavigationController) + + // then + XCTAssertEqual(delegate.shouldPresentCallCount, 0) + } + + func test_shouldPresent_does_not_get_called_for_a_not_matching_url_on_present() { + // given + let delegate: StubNavigatorDelegate = .init() + + navigator.delegate = delegate + + let fromViewController = StubViewController() + + // when + navigator.present("myapp://user/10", from: fromViewController) + + // then + XCTAssertEqual(delegate.shouldPresentCallCount, 0) + } + + func test_shouldPresent_does_not_get_called_when_the_factory_returns_nil_on_present() { + // given + let delegate: StubNavigatorDelegate = .init() + + navigator.delegate = delegate + navigator.register("myapp://article/") { url, values, context in + guard let articleID = values["id"] as? Int, articleID > 0 else { return nil } + return ArticleViewController(articleID: articleID, context: context) + } + + let fromViewController = StubViewController() + + // when + navigator.present("myapp://article/-1", from: fromViewController) + + // then + XCTAssertEqual(delegate.shouldPresentCallCount, 0) + } + + func test_shouldPresent_gets_called_for_valid_url_on_present() { + // given + let delegate: StubNavigatorDelegate = .init() + + navigator.delegate = delegate + navigator.register("myapp://article/") { url, values, context in + guard let articleID = values["id"] as? Int, articleID > 0 else { return nil } + return ArticleViewController(articleID: articleID, context: context) + } + + let fromViewController = StubViewController() + + // when + navigator.present("myapp://article/123", from: fromViewController) + + // then + XCTAssertEqual(delegate.shouldPresentCallCount, 1) + XCTAssertTrue(delegate.shouldPresentParams?.viewController is ArticleViewController) + XCTAssertIdentical(delegate.shouldPresentParams?.from, fromViewController) + } + + func test_shouldPresent_does_not_prevent_from_presenting_when_delegate_returns_true() { + // given + let delegate: StubNavigatorDelegate = .init() + delegate.shouldPresentStub = true + + navigator.delegate = delegate + navigator.register("myapp://article/") { url, values, context in + guard let articleID = values["id"] as? Int, articleID > 0 else { return nil } + return ArticleViewController(articleID: articleID, context: context) + } + + let fromViewController = StubViewController() + + // when + navigator.present("myapp://article/123", from: fromViewController) + + // then + XCTAssertEqual(fromViewController.presentCallCount, 1) + } + + func test_shouldPresent_prevents_from_presenting_when_delegate_returns_false() { + // given + let delegate: StubNavigatorDelegate = .init() + delegate.shouldPresentStub = false + + navigator.delegate = delegate + navigator.register("myapp://article/") { url, values, context in + guard let articleID = values["id"] as? Int, articleID > 0 else { return nil } + return ArticleViewController(articleID: articleID, context: context) + } + + let fromViewController = StubViewController() + + // when + navigator.present("myapp://article/123", from: fromViewController) + + // then + XCTAssertEqual(fromViewController.presentCallCount, 0) + } +} +#endif diff --git a/Tests/URLNavigatorTests/Stubs.swift b/Tests/URLNavigatorTests/Stubs.swift index 7e6b401..1236d13 100644 --- a/Tests/URLNavigatorTests/Stubs.swift +++ b/Tests/URLNavigatorTests/Stubs.swift @@ -4,47 +4,47 @@ import URLNavigator final class StubNavigationController: UINavigationControllerType { - var pushViewControllerCallCount: Int = 0 + var pushViewControllerCallCount = 0 var pushViewControllerParams: (viewController: UIViewController, animated: Bool)? func pushViewController(_ viewController: UIViewController, animated: Bool) { - self.pushViewControllerCallCount += 1 - self.pushViewControllerParams = (viewController, animated) + pushViewControllerCallCount += 1 + pushViewControllerParams = (viewController, animated) } } final class StubViewController: UIViewControllerType { - var presentCallCount: Int = 0 + var presentCallCount = 0 var presentParams: (viewControllerToPresent: UIViewController, animated: Bool, completion: (() -> Void)?)? func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)?) { completion?() - self.presentCallCount += 1 - self.presentParams = (viewControllerToPresent, flag, completion) + presentCallCount += 1 + presentParams = (viewControllerToPresent, flag, completion) } } final class StubNavigatorDelegate: NavigatorDelegate { - var shouldPushCallCount: Int = 0 + var shouldPushCallCount = 0 var shouldPushParams: (viewController: UIViewController, from: UINavigationControllerType)? - var shouldPushStub: Bool = true + var shouldPushStub = true func shouldPush(viewController: UIViewController, from: UINavigationControllerType) -> Bool { - self.shouldPushCallCount += 1 - self.shouldPushParams = (viewController, from) - return self.shouldPushStub + shouldPushCallCount += 1 + shouldPushParams = (viewController, from) + return shouldPushStub } - var shouldPresentCallCount: Int = 0 + var shouldPresentCallCount = 0 var shouldPresentParams: (viewController: UIViewController, from: UIViewControllerType)? - var shouldPresentStub: Bool = true + var shouldPresentStub = true func shouldPresent(viewController: UIViewController, from: UIViewControllerType) -> Bool { - self.shouldPresentCallCount += 1 - self.shouldPresentParams = (viewController, from) - return self.shouldPresentStub + shouldPresentCallCount += 1 + shouldPresentParams = (viewController, from) + return shouldPresentStub } } #endif diff --git a/Tests/URLNavigatorTests/TopMostViewControllerSpec.swift b/Tests/URLNavigatorTests/TopMostViewControllerSpec.swift deleted file mode 100644 index 0d85e88..0000000 --- a/Tests/URLNavigatorTests/TopMostViewControllerSpec.swift +++ /dev/null @@ -1,207 +0,0 @@ -#if os(iOS) || os(tvOS) -import UIKit - -import Nimble -import Quick - -import URLNavigator - -final class TopMostViewControllerSpec: QuickSpec { - fileprivate static var currentWindow: UIWindow? - - override func spec() { - var window: UIWindow! - var topMost: UIViewController? { - return UIViewController.topMost(of: window.rootViewController) - } - - beforeEach { - window = UIWindow(frame: UIScreen.main.bounds) - TopMostViewControllerSpec.currentWindow = window - } - - context("when the root view controller is a view controller") { - context("when there is only a root view controller") { - it("returns root view controller") { - let viewController = UIViewController().asRoot() - expect(topMost) == viewController - } - } - - context("when there is a presented view controller") { - it("returns a presented view controller") { - let A = UIViewController("A").asRoot() - let B = UIViewController("B") - A.present(B, animated: false, completion: nil) - expect(topMost) == B - } - } - - context("when there is a presented tab bar controller") { - it("returns the selected view controller of the presented tab bar controller") { - let A = UIViewController("A").asRoot() - let B = UIViewController("B") - let C = UIViewController("C") - let tabBarController = UITabBarController() - tabBarController.viewControllers = [B, C] - tabBarController.selectedIndex = 1 - A.present(tabBarController, animated: false, completion: nil) - expect(topMost) == C - } - } - - context("when there is a presented navigation controller") { - it("returns a top view controller of the presented navigation controller") { - let A = UIViewController("A").asRoot() - let B = UIViewController("B") - let C = UIViewController("C") - let D = UIViewController("D") - let navigationController = UINavigationController(rootViewController: B) - navigationController.pushViewController(C, animated: false) - navigationController.pushViewController(D, animated: false) - A.present(navigationController, animated: false, completion: nil) - expect(topMost) == D - } - } - - context("when there is a presented page view controller") { - it("returns the selected view controller of the presented page view controller") { - let A = UIViewController("A").asRoot() - let B = UIViewController("B") - let pageViewController = UIPageViewController() - pageViewController.setViewControllers([B], direction: .forward, animated: false, completion: nil) - A.present(pageViewController, animated: false, completion: nil) - expect(topMost) == B - } - } - } - - context("when the root view controller is a tab bar controller") { - context("when there is no view controller") { - it("returns the tab bar controller") { - let tabBarController = UITabBarController().asRoot() - expect(topMost) == tabBarController - } - } - - context("when there is a single view controller") { - it("returns the only view controller") { - let A = UIViewController("A") - let tabBarController = UITabBarController().asRoot() - tabBarController.viewControllers = [A] - expect(topMost) == A - } - } - - context("when there are multiple view controllers") { - it("returns the selected view controller of the tab bar controller") { - let A = UIViewController("A") - let B = UIViewController("B") - let C = UIViewController("C") - let tabBarController = UITabBarController().asRoot() - tabBarController.viewControllers = [A, B, C] - tabBarController.selectedIndex = 1 - expect(topMost) == B - } - } - - context("when a view controller is presented from the view controller in the tab bar controller") { - it("returns the presented view controller") { - let A = UIViewController("A") - let B = UIViewController("B") - let C = UIViewController("C") - let tabBarController = UITabBarController().asRoot() - tabBarController.viewControllers = [A, B, C] - tabBarController.selectedIndex = 2 - let D = UIViewController("D") - C.present(D, animated: false, completion: nil) - expect(topMost) == D - } - } - } - - context("when the root view controller is a navigation controller") { - context("when there is no view controller") { - it("returns the navigation controller") { - let navigationController = UINavigationController().asRoot() - expect(topMost) == navigationController - } - } - - context("when there is only a root view controller") { - it("returns the root view controller of the navigation controller") { - let A = UIViewController("A") - UINavigationController(rootViewController: A).asRoot() - expect(topMost) == A - } - } - - context("when there are multiple view controllers") { - it("returns the top view controller of the navigation controller") { - let A = UIViewController("A") - let B = UIViewController("B") - let navigationController = UINavigationController(rootViewController: A).asRoot() - navigationController.pushViewController(B, animated: false) - expect(topMost) == B - } - } - - context("when a view controller is presented from the view controller in the navigation controller") { - it("returns the presented view controller") { - let A = UIViewController("A") - let B = UIViewController("B") - let C = UIViewController("C") - let navigationController = UINavigationController(rootViewController: A).asRoot() - navigationController.pushViewController(B, animated: false) - navigationController.pushViewController(C, animated: false) - let D = UIViewController("D") - C.present(D, animated: false, completion: nil) - expect(topMost) == D - } - } - } - - context("when the root view controller is a page view controller") { - context("when there is a view controller") { - it("returns the visible view controller") { - let A = UIViewController("A") - let pageViewController = UIPageViewController().asRoot() - pageViewController.setViewControllers([A], direction: .forward, animated: false, completion: nil) - expect(topMost) == A - } - } - - context("when a view controller is presented from the view controller in the page view controller") { - it("returns the presented view controller") { - let A = UIViewController("A") - let B = UIViewController("B") - let pageViewController = UIPageViewController().asRoot() - pageViewController.setViewControllers([A], direction: .forward, animated: false, completion: nil) - A.present(B, animated: false, completion: nil) - expect(topMost) == B - } - } - } - } -} - -extension UIViewController { - convenience init(_ title: String) { - self.init() - self.title = title - } - - override open var description: String { - let title = self.title ?? String(format: "0x%x", self) - return "<\(type(of: self)): \(title)>" - } - - @discardableResult - func asRoot() -> Self { - TopMostViewControllerSpec.currentWindow?.rootViewController = self - TopMostViewControllerSpec.currentWindow?.addSubview(self.view) - return self - } -} -#endif - diff --git a/Tests/URLNavigatorTests/TopMostViewControllerTests.swift b/Tests/URLNavigatorTests/TopMostViewControllerTests.swift new file mode 100644 index 0000000..cf1944d --- /dev/null +++ b/Tests/URLNavigatorTests/TopMostViewControllerTests.swift @@ -0,0 +1,227 @@ +#if os(iOS) || os(tvOS) +import XCTest + +import URLNavigator + +final class TopMostViewControllerTests: XCTestCase { + fileprivate static var currentWindow: UIWindow? + + var window: UIWindow! + var topMost: UIViewController? { + UIViewController.topMost(of: window.rootViewController) + } + + override func setUp() { + super.setUp() + + window = UIWindow(frame: UIScreen.main.bounds) + Self.currentWindow = window + } + + func test_returns_root_view_controller_when_there_is_only_a_root_view_controller() { + // given + let viewController = UIViewController().asRoot() + + // then + XCTAssertIdentical(topMost, viewController) + } + + func test_returns_a_presented_view_controller_when_there_is_a_presented_view_controller() { + // given + let A = UIViewController("A").asRoot() + let B = UIViewController("B") + + A.present(B, animated: false, completion: nil) + + // then + XCTAssertIdentical(topMost, B) + } + + func test_returns_the_selected_view_controller_of_the_presented_tab_bar_controller() { + // given + let A = UIViewController("A").asRoot() + let B = UIViewController("B") + let C = UIViewController("C") + + let tabBarController = UITabBarController() + tabBarController.viewControllers = [B, C] + tabBarController.selectedIndex = 1 + + A.present(tabBarController, animated: false, completion: nil) + + // then + XCTAssertIdentical(topMost, C) + } + + func test_returns_a_top_view_controller_of_the_presented_navigation_controller() { + // given + let A = UIViewController("A").asRoot() + let B = UIViewController("B") + let C = UIViewController("C") + let D = UIViewController("D") + + let navigationController = UINavigationController(rootViewController: B) + navigationController.pushViewController(C, animated: false) + navigationController.pushViewController(D, animated: false) + + A.present(navigationController, animated: false, completion: nil) + + // then + XCTAssertIdentical(topMost, D) + } + + func test_returns_the_selected_view_controller_of_the_presented_page_view_controller() { + // given + let A = UIViewController("A").asRoot() + let B = UIViewController("B") + + let pageViewController = UIPageViewController() + pageViewController.setViewControllers([B], direction: .forward, animated: false, completion: nil) + + A.present(pageViewController, animated: false, completion: nil) + + // then + XCTAssertIdentical(topMost, B) + } + + func test_returns_the_tab_bar_controller_when_there_is_no_view_controller() { + // given + let tabBarController = UITabBarController().asRoot() + + // then + XCTAssertIdentical(topMost, tabBarController) + } + + func test_returns_the_only_view_controller_when_there_single_view_controller_in_tab_bar_controller() { + // given + + let A = UIViewController("A") + + let tabBarController = UITabBarController().asRoot() + tabBarController.viewControllers = [A] + + // then + XCTAssertIdentical(topMost, A) + } + + func test_returns_the_selected_view_controller_of_the_tab_bar_controller() { + // given + let A = UIViewController("A") + let B = UIViewController("B") + let C = UIViewController("C") + + let tabBarController = UITabBarController().asRoot() + tabBarController.viewControllers = [A, B, C] + tabBarController.selectedIndex = 1 + + // then + XCTAssertIdentical(topMost, B) + } + + func test_returns_the_presented_view_controller_when_it__presented_from_tab_bar_controller() { + // given + let A = UIViewController("A") + let B = UIViewController("B") + let C = UIViewController("C") + + let tabBarController = UITabBarController().asRoot() + tabBarController.viewControllers = [A, B, C] + tabBarController.selectedIndex = 2 + + let D = UIViewController("D") + C.present(D, animated: false, completion: nil) + + // then + XCTAssertIdentical(topMost, D) + } + + func test_returns_the_navigation_controller_when_ther_is_no_view_controller_in_navigation_controller() { + // given + let navigationController = UINavigationController().asRoot() + + // then + XCTAssertIdentical(topMost, navigationController) + } + + func test_returns_the_root_view_controller_of_the_navigation_controller() { + // given + let A = UIViewController("A") + + UINavigationController(rootViewController: A).asRoot() + + // then + XCTAssertIdentical(topMost, A) + } + + func test_returns_the_top_view_controller_of_the_navigation_controller() { + // given + let A = UIViewController("A") + let B = UIViewController("B") + + let navigationController = UINavigationController(rootViewController: A).asRoot() + navigationController.pushViewController(B, animated: false) + + // then + XCTAssertIdentical(topMost, B) + } + + func test_returns_the_presented_view_controller_when_it_presented_from_navigation_controller() { + // given + let A = UIViewController("A") + let B = UIViewController("B") + let C = UIViewController("C") + + let navigationController = UINavigationController(rootViewController: A).asRoot() + navigationController.pushViewController(B, animated: false) + navigationController.pushViewController(C, animated: false) + + let D = UIViewController("D") + C.present(D, animated: false, completion: nil) + + // then + XCTAssertIdentical(topMost, D) + } + + func test_returns_the_visible_view_controller_when_there_is_a_view_controller_in_page_view_controller() { + // given + let A = UIViewController("A") + let pageViewController = UIPageViewController().asRoot() + + pageViewController.setViewControllers([A], direction: .forward, animated: false, completion: nil) + + // then + XCTAssertIdentical(topMost, A) + } + + func test_returns_the_presented_view_controller_from_page_view_controller() { + // given + let A = UIViewController("A") + let B = UIViewController("B") + + let pageViewController = UIPageViewController().asRoot() + pageViewController.setViewControllers([A], direction: .forward, animated: false, completion: nil) + + A.present(B, animated: false, completion: nil) + + // then + XCTAssertIdentical(topMost, B) + } +} + + +// MARK: - Test + +extension UIViewController { + fileprivate convenience init(_ title: String) { + self.init() + self.title = title + } + + @discardableResult + fileprivate func asRoot() -> Self { + TopMostViewControllerTests.currentWindow?.rootViewController = self + TopMostViewControllerTests.currentWindow?.addSubview(view) + return self + } +} +#endif From 30305463f12c93843a9f498336f2471a4e4c928a Mon Sep 17 00:00:00 2001 From: OhKanghoon Date: Mon, 10 Jul 2023 01:28:39 +0900 Subject: [PATCH 3/3] chore: remove `Quick`, `Nimble` dependencies --- Package.resolved | 43 ------------------------------------------- Package.swift | 8 ++------ 2 files changed, 2 insertions(+), 49 deletions(-) delete mode 100644 Package.resolved diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index da83f9a..0000000 --- a/Package.resolved +++ /dev/null @@ -1,43 +0,0 @@ -{ - "object": { - "pins": [ - { - "package": "CwlCatchException", - "repositoryURL": "https://github.com/mattgallagher/CwlCatchException.git", - "state": { - "branch": null, - "revision": "35f9e770f54ce62dd8526470f14c6e137cef3eea", - "version": "2.1.1" - } - }, - { - "package": "CwlPreconditionTesting", - "repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git", - "state": { - "branch": null, - "revision": "c21f7bab5ca8eee0a9998bbd17ca1d0eb45d4688", - "version": "2.1.0" - } - }, - { - "package": "Nimble", - "repositoryURL": "https://github.com/Quick/Nimble.git", - "state": { - "branch": null, - "revision": "1f3bde57bde12f5e7b07909848c071e9b73d6edc", - "version": "10.0.0" - } - }, - { - "package": "Quick", - "repositoryURL": "https://github.com/Quick/Quick.git", - "state": { - "branch": null, - "revision": "f9d519828bb03dfc8125467d8f7b93131951124c", - "version": "5.0.1" - } - } - ] - }, - "version": 1 -} diff --git a/Package.swift b/Package.swift index 2e74124..3a5f2b4 100644 --- a/Package.swift +++ b/Package.swift @@ -11,14 +11,10 @@ let package = Package( .library(name: "URLMatcher", targets: ["URLMatcher"]), .library(name: "URLNavigator", targets: ["URLNavigator"]), ], - dependencies: [ - .package(url: "https://github.com/Quick/Quick.git", .upToNextMajor(from: "5.0.0")), - .package(url: "https://github.com/Quick/Nimble.git", .upToNextMajor(from: "10.0.0")), - ], targets: [ .target(name: "URLMatcher"), .target(name: "URLNavigator", dependencies: ["URLMatcher"]), - .testTarget(name: "URLMatcherTests", dependencies: ["URLMatcher", "Quick", "Nimble"]), - .testTarget(name: "URLNavigatorTests", dependencies: ["URLNavigator", "Quick", "Nimble"]), + .testTarget(name: "URLMatcherTests", dependencies: ["URLMatcher"]), + .testTarget(name: "URLNavigatorTests", dependencies: ["URLNavigator"]), ] )