fix: restore iOS navigation with unsaved forms
This commit is contained in:
@@ -135,6 +135,94 @@ final class WorkspaceTests: XCTestCase {
|
||||
"Changing workspaces must retain native gesture navigation")
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testDirtyFormNavigationPresentsNativeConfirmation() async throws {
|
||||
let animationsEnabled = UIView.areAnimationsEnabled
|
||||
UIView.setAnimationsEnabled(false)
|
||||
defer { UIView.setAnimationsEnabled(animationsEnabled) }
|
||||
let name = "ios-confirmation-\(UUID())"
|
||||
let defaults = try XCTUnwrap(UserDefaults(suiteName: name))
|
||||
defer { defaults.removePersistentDomain(forName: name) }
|
||||
let controller = WorkspaceViewController(defaults: defaults)
|
||||
let window = UIWindow(frame: UIScreen.main.bounds)
|
||||
window.rootViewController = controller
|
||||
window.makeKeyAndVisible()
|
||||
defer { window.isHidden = true; window.rootViewController = nil }
|
||||
let url = URL(string: "https://example.invalid/")!
|
||||
controller.open(url)
|
||||
let webview = try XCTUnwrap(controller.workspace)
|
||||
webview.stopLoading()
|
||||
webview.loadSimulatedRequest(URLRequest(url: url.appendingPathComponent("projects/create")), responseHTML: """
|
||||
<html><body><input id="duplicate" type="radio"><input id="draft" value="Unsaved project">
|
||||
<a id="back" href="#back">Back</a><script>
|
||||
window.visits = 0;
|
||||
document.querySelector('#back').onclick = event => {
|
||||
event.preventDefault();
|
||||
if (!document.querySelector('#duplicate').checked || confirm('Discard unsaved changes?')) window.visits++;
|
||||
};
|
||||
</script></body></html>
|
||||
""")
|
||||
try await waitFor(webview, "document.readyState === 'complete' && typeof window.visits === 'number'")
|
||||
_ = try await webview.evaluateJavaScript("document.querySelector('#back').click(); true")
|
||||
let pristineVisits = try await webview.evaluateJavaScript("window.visits")
|
||||
XCTAssertEqual(pristineVisits as? Int, 1)
|
||||
XCTAssertNil(controller.presentedViewController)
|
||||
_ = try await webview.evaluateJavaScript("document.querySelector('#duplicate').click(); true")
|
||||
var expectedVisits = 1
|
||||
for (index, accepted) in [false, true, false, true].enumerated() {
|
||||
let click = Task { try await webview.evaluateJavaScript("document.querySelector('#back').click(); true") }
|
||||
let alert = try await waitForConfirmation(controller)
|
||||
XCTAssertEqual(alert.title, "example.invalid")
|
||||
XCTAssertEqual(alert.message, "Discard unsaved changes?")
|
||||
XCTAssertEqual(alert.actions.map(\.style), [.cancel, .default])
|
||||
XCTAssertTrue(alert.preferredAction === alert.actions.first, "The default action must preserve the draft")
|
||||
if index == 3 {
|
||||
await withCheckedContinuation { continuation in
|
||||
alert.dismiss(animated: false) { continuation.resume() }
|
||||
}
|
||||
}
|
||||
controller.completeJavaScriptConfirmation(accepted)
|
||||
controller.completeJavaScriptConfirmation(!accepted)
|
||||
_ = try await click.value
|
||||
if accepted { expectedVisits += 1 }
|
||||
let visits = try await webview.evaluateJavaScript("window.visits")
|
||||
XCTAssertEqual(visits as? Int, expectedVisits, "Only accepted visits may run, exactly once each")
|
||||
let draft = try await webview.evaluateJavaScript("document.querySelector('#draft').value")
|
||||
XCTAssertEqual(draft as? String, "Unsaved project")
|
||||
XCTAssertNil(controller.presentedViewController)
|
||||
}
|
||||
|
||||
let otherAlert = UIAlertController(title: "Already open", message: nil, preferredStyle: .alert)
|
||||
controller.present(otherAlert, animated: false)
|
||||
let blocked = try await webview.evaluateJavaScript("confirm('Another dialog')")
|
||||
XCTAssertEqual(blocked as? Bool, false, "A blocked presentation must release JavaScript")
|
||||
XCTAssertTrue(controller.presentedViewController === otherAlert)
|
||||
await withCheckedContinuation { continuation in
|
||||
otherAlert.dismiss(animated: false) { continuation.resume() }
|
||||
}
|
||||
|
||||
let pendingClick = Task { try await webview.evaluateJavaScript("document.querySelector('#back').click(); true") }
|
||||
_ = try await waitForConfirmation(controller)
|
||||
controller.open(URL(string: "https://other.invalid/")!)
|
||||
_ = try await pendingClick.value
|
||||
XCTAssertNil(controller.presentedViewController, "Replacing a workspace must dismiss its confirmation")
|
||||
let oldVisits = try await webview.evaluateJavaScript("window.visits")
|
||||
XCTAssertEqual(oldVisits as? Int, expectedVisits)
|
||||
let stale = try await webview.evaluateJavaScript("confirm('Previous workspace')")
|
||||
XCTAssertEqual(stale as? Bool, false, "A previous workspace must not present new confirmations")
|
||||
XCTAssertNil(controller.presentedViewController)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func waitForConfirmation(_ controller: WorkspaceViewController) async throws -> UIAlertController {
|
||||
for _ in 0..<100 {
|
||||
if let alert = controller.presentedViewController as? UIAlertController, !alert.isBeingPresented { return alert }
|
||||
try await Task.sleep(for: .milliseconds(50))
|
||||
}
|
||||
return try XCTUnwrap(controller.presentedViewController as? UIAlertController,
|
||||
"Dirty forms must offer a visible confirmation instead of silently cancelling navigation")
|
||||
}
|
||||
|
||||
func testSettingsProtocolRejectsUnsupportedActionsAndInvalidPayloads() throws {
|
||||
for body in [
|
||||
#"{"version":2,"id":1,"action":"getState","payload":{}}"#,
|
||||
|
||||
Reference in New Issue
Block a user