-
Notifications
You must be signed in to change notification settings - Fork 21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Include default server route rules by default #76
base: main
Are you sure you want to change the base?
Changes from all commits
9f7c19a
d6d1aa3
d4c6bc9
5308857
84983ff
c973461
8ea8818
c4aa282
42a4e8d
98e6c3b
ba3cb06
0a9f18c
09e0b13
1c78973
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import Foundation | ||
|
||
extension PathRule { | ||
static let defaultServerRoutes: [PathRule] = [ | ||
.recedeHistoricalLocation, | ||
.resumeHistoricalLocation, | ||
.refreshHistoricalLocation | ||
] | ||
|
||
static let recedeHistoricalLocation = PathRule( | ||
patterns: ["/recede_historical_location"], | ||
properties: [ | ||
"presentation": "pop", | ||
"context": "default", | ||
"historical_location": true | ||
] | ||
) | ||
|
||
static let resumeHistoricalLocation = PathRule( | ||
patterns: ["/resume_historical_location"], | ||
properties: [ | ||
"presentation": "none", | ||
"context": "default", | ||
"historical_location": true | ||
] | ||
) | ||
|
||
static let refreshHistoricalLocation = PathRule( | ||
patterns: ["/refresh_historical_location"], | ||
properties: [ | ||
"presentation": "refresh", | ||
"context": "default", | ||
"historical_location": true | ||
] | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
{ | ||
"rules":[ | ||
{ | ||
"patterns":[ | ||
"/recede_historical_location" | ||
], | ||
"properties":{ | ||
"presentation":"pop", | ||
"context":"modal" | ||
} | ||
}, | ||
{ | ||
"patterns":[ | ||
"/resume_historical_location" | ||
], | ||
"properties":{ | ||
"presentation":"refresh", | ||
"context":"modal" | ||
} | ||
}, | ||
{ | ||
"patterns":[ | ||
"/refresh_historical_location" | ||
], | ||
"properties":{ | ||
"presentation":"pop", | ||
"context":"modal" | ||
} | ||
} | ||
] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
@testable import HotwireNative | ||
import XCTest | ||
|
||
@MainActor | ||
final class NavigationHierarchyControllerHistoricalLocationTests: XCTestCase { | ||
override func setUp() { | ||
navigationController = TestableNavigationController() | ||
modalNavigationController = TestableNavigationController() | ||
|
||
navigator = Navigator(session: session, modalSession: modalSession) | ||
hierarchyController = NavigationHierarchyController( | ||
delegate: navigator, | ||
navigationController: navigationController, | ||
modalNavigationController: modalNavigationController | ||
) | ||
navigator.hierarchyController = hierarchyController | ||
|
||
loadNavigationControllerInWindow() | ||
} | ||
|
||
// Resume behaviour: | ||
// 1. Dismiss the modal view controller. | ||
// 2. Arrive back at the view controller on the "default" stack. | ||
func test_resumeHistoricalLocation() async throws { | ||
let defaultOne = VisitProposal(path: "/default_one", context: .default) | ||
navigator.route(defaultOne) | ||
try await delay() | ||
|
||
let defaultTwo = VisitProposal(path: "/default_two", context: .default) | ||
navigator.route(defaultTwo) | ||
try await delay() | ||
|
||
XCTAssertEqual(navigationController.viewControllers.count, 2) | ||
|
||
let modalOne = VisitProposal(path: "/modal_one", context: .modal) | ||
navigator.route(modalOne) | ||
|
||
try await delay() | ||
XCTAssertEqual(modalNavigationController.viewControllers.count, 1) | ||
|
||
// Reset spy's properties. | ||
session.visitWasCalled = false | ||
session.visitAction = nil | ||
|
||
let resumeHistoricalLocationProposal = VisitProposal( | ||
path: PathRule.resumeHistoricalLocation.patterns.first!, | ||
additionalProperties: PathRule.resumeHistoricalLocation.properties | ||
) | ||
navigator.route(resumeHistoricalLocationProposal) | ||
try await delay() | ||
|
||
XCTAssertNil(navigationController.presentedViewController) | ||
XCTAssertEqual(navigationController.viewControllers.count, 2) | ||
XCTAssertEqual(navigator.session.activeVisitable?.visitableURL, defaultTwo.url) | ||
XCTAssertFalse(session.visitWasCalled) | ||
} | ||
|
||
// Refresh behaviour: | ||
// 1. Dismiss the modal view controller. | ||
// 2. Arrive back at the view controller on the "default" stack. | ||
// 3. Refresh the view controller on the "default" stack by revisiting the location. | ||
func test_refreshHistoricalLocation() async throws { | ||
let defaultOne = VisitProposal(path: "/default_one", context: .default) | ||
navigator.route(defaultOne) | ||
try await delay() | ||
|
||
XCTAssertEqual(navigationController.viewControllers.count, 1) | ||
|
||
let modalOne = VisitProposal(path: "/modal_one", context: .modal) | ||
navigator.route(modalOne) | ||
try await delay() | ||
|
||
XCTAssertEqual(modalNavigationController.viewControllers.count, 1) | ||
|
||
// Reset spy's properties. | ||
session.visitWasCalled = false | ||
session.visitAction = nil | ||
|
||
let refreshHistoricalLocationProposal = VisitProposal( | ||
path: PathRule.refreshHistoricalLocation.patterns.first!, | ||
additionalProperties: PathRule.refreshHistoricalLocation.properties | ||
) | ||
navigator.route(refreshHistoricalLocationProposal) | ||
try await delay() | ||
|
||
XCTAssertNil(navigationController.presentedViewController) | ||
XCTAssertEqual(navigationController.viewControllers.count, 1) | ||
XCTAssertEqual(navigator.session.activeVisitable?.visitableURL, defaultOne.url) | ||
XCTAssertTrue(session.visitWasCalled) | ||
XCTAssertEqual(session.visitAction, .restore) | ||
} | ||
|
||
// Recede behaviour: | ||
// 1. Dismiss the modal view controller. | ||
// 2. Arrive back at the view controller on the "default" stack. | ||
// 3. Pop the view controller on the "default" stack (unless it's already at the beginning of the backstack). | ||
// 4. This will trigger a refresh on the appeared view controller if the snapshot cache has been cleared by a form submission. | ||
@MainActor | ||
func test_recedeHistoricalLocation() async throws { | ||
let defaultOne = VisitProposal(path: "/default_one", context: .default) | ||
navigator.route(defaultOne) | ||
try await delay() | ||
|
||
let defaultTwo = VisitProposal(path: "/default_two", context: .default) | ||
navigator.route(defaultTwo) | ||
try await delay() | ||
|
||
XCTAssertEqual(navigationController.viewControllers.count, 2) | ||
|
||
let modalOne = VisitProposal(path: "/modal_one", context: .modal) | ||
navigator.route(modalOne) | ||
try await delay() | ||
|
||
XCTAssertEqual(modalNavigationController.viewControllers.count, 1) | ||
|
||
let recedeHistoricalLocationProposal = VisitProposal( | ||
path: PathRule.recedeHistoricalLocation.patterns.first!, | ||
additionalProperties: PathRule.recedeHistoricalLocation.properties | ||
) | ||
navigator.route(recedeHistoricalLocationProposal) | ||
try await delay() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The delays are far from ideal, but they are necessary to ensure the view lifecycle methods execute properly and the tests pass. I’ll explore architectural improvements in the future. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm surprised these are needed even with the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Without the delay the |
||
|
||
XCTAssertNil(navigationController.presentedViewController) | ||
XCTAssertEqual(navigationController.viewControllers.count, 1) | ||
XCTAssertEqual(navigator.session.activeVisitable?.visitableURL, defaultOne.url) | ||
} | ||
|
||
func delay() async throws { | ||
try await Task.sleep(nanoseconds: 1_000_000_000) // 1 second | ||
} | ||
|
||
private let session = SessionSpy(webView: Hotwire.config.makeWebView()) | ||
private let modalSession = Session(webView: Hotwire.config.makeWebView()) | ||
|
||
private var navigator: Navigator! | ||
private var hierarchyController: NavigationHierarchyController! | ||
private var navigationController: TestableNavigationController! | ||
private var modalNavigationController: TestableNavigationController! | ||
|
||
private let window = UIWindow() | ||
|
||
// Simulate a "real" app so presenting view controllers works under test. | ||
private func loadNavigationControllerInWindow() { | ||
window.rootViewController = navigationController | ||
window.makeKeyAndVisible() | ||
navigationController.loadViewIfNeeded() | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The guard with the
!= nil
feels like a lot of inverse logic. What if we used anif
statement here instead?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I personally don't have issues with this and use
guard
extensively for early returns. If you feel strongly about this we can switch toif
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't feel strongly, no. It just took me a few reads to understand when
dismiss(animated:)
would be called.