ReviewOS

also looking at this

craft-native/craft

test(ios): verify app termination before local notification delivery

#308
Merged glennmichael123 wants to merge codex/ios-notification-deadline into main
5 files +96 -21
scripts/mobile-e2e/ios-uitests/NotificationTapColdStartTests.swiftmodified+17-6
Changes to scripts/mobile-e2e/ios-uitests/NotificationTapColdStartTests.swift
@@ -94,8 +94,9 @@ final class NotificationTapColdStartTests: XCTestCase {
9494 print("CRAFT-E2E-NOTIFICATION-RECEIVED-RESULT \(receipts.label)")
9595
9696 // #258: a notification the page scheduled itself, with data. The
97 // local link has the page schedule it a few seconds out, and the app
98 // is killed before it fires, so its tap is a cold start like the push.
97 // local link carries a bounded scheduling window. The page reports
98 // its earliest delivery deadline so slow UI calls cannot silently
99 // turn this into a foreground-delivery test (#307).
99100 app.terminate()
100101 XCTAssertTrue(app.wait(for: .notRunning, timeout: 15), "the app did not stop before the local link")
101102 XCUIDevice.shared.system.open(URL(string: localLink)!)
@@ -105,10 +106,20 @@ final class NotificationTapColdStartTests: XCTestCase {
105106 .containing(NSPredicate(format: "label BEGINSWITH 'CRAFT-E2E-LOCAL-SCHEDULED '"))
106107 .firstMatch
107108 XCTAssertTrue(scheduled.waitForExistence(timeout: 60), "the page never said whether it scheduled the notification")
108 XCTAssertEqual(scheduled.label, "CRAFT-E2E-LOCAL-SCHEDULED ok")
109 let schedulingReport = scheduled.label.split(separator: " ")
110 guard schedulingReport.count == 3, schedulingReport[1] == "ok",
111 let fireAtMs = Double(schedulingReport[2]), fireAtMs.isFinite, fireAtMs > 0 else {
112 XCTFail("invalid local scheduling report: \(scheduled.label)")
113 return
114 }
115 let fireAt = Date(timeIntervalSince1970: fireAtMs / 1000)
116 XCTAssertLessThanOrEqual(fireAt.timeIntervalSinceNow, 180, "local scheduling window is unbounded")
109117 app.terminate()
110118 XCTAssertTrue(app.wait(for: .notRunning, timeout: 15), "the app did not stop, so the local tap would not be a cold start")
111 tapBanner(springboard, app: app, containing: localBody)
119 let remaining = fireAt.timeIntervalSinceNow
120 print("CRAFT-E2E-LOCAL-TIMING deadlineMs=\(fireAtMs) remainingAfterStopSeconds=\(remaining)")
121 XCTAssertGreaterThan(remaining, 0, "local notification deadline elapsed before the app stopped; cold-start precondition was not met")
122 tapBanner(springboard, app: app, containing: localBody, timeout: remaining + 30)
112123
113124 let local = app.staticTexts
114125 .containing(NSPredicate(format: "label BEGINSWITH 'CRAFT-E2E-NOTIFICATION '"))
@@ -130,11 +141,11 @@ final class NotificationTapColdStartTests: XCTestCase {
130141 /// enough for the banner to go, and the test fails saying the tap could
131142 /// not find it. There is no retrying around that: a tap that misses ends
132143 /// the test method, even inside XCTExpectFailure.
133 private func tapBanner(_ springboard: XCUIApplication, app: XCUIApplication, containing text: String) {
144 private func tapBanner(_ springboard: XCUIApplication, app: XCUIApplication, containing text: String, timeout: TimeInterval = 120) {
134145 let banner = springboard.descendants(matching: .any)
135146 .matching(NSPredicate(format: "label CONTAINS %@", text))
136147 .firstMatch
137 XCTAssertTrue(banner.waitForExistence(timeout: 120), "no banner for \(text) ever appeared")
148 XCTAssertTrue(banner.waitForExistence(timeout: timeout), "no banner for \(text) ever appeared")
138149 banner.tap()
139150 XCTAssertTrue(app.wait(for: .runningForeground, timeout: 60), "tapping the banner did not start the app")
140151 }