ReviewOS

also looking at this

craft-native/craft

fix(ios): schedule a notification with the data the page gave it

#262
Merged glennmichael123 wants to merge fix/ios-notification-data into main
8 files +242 -22
scripts/mobile-e2e/ios-uitests/NotificationTapColdStartTests.swiftmodified+40-10
Changes to scripts/mobile-e2e/ios-uitests/NotificationTapColdStartTests.swift
@@ -1,8 +1,9 @@
11import XCTest
22
3/// Taps a pushed notification that launches the probe app, then has a second
4/// push arrive while it is open, and reports what its page was handed each
5/// time (#255, #256).
3/// Taps a pushed notification that launches the probe app, has a second push
4/// arrive while it is open, then taps a local notification the page scheduled
5/// with data, and reports what its page was handed each time (#255, #256,
6/// #258).
67///
78/// A person's sequence, end to end: the app asks for permission and is
89/// allowed, the app is killed, a real push arrives through `simctl push`, and
@@ -10,6 +11,8 @@ import XCTest
1011/// `simctl` runs on the host, so the test prints `CRAFT-E2E-PUSH-READY cold`
1112/// once the app is gone and `CRAFT-E2E-PUSH-READY foreground` once the page
1213/// is listening, and the harness sends each push when it reads the line.
14/// Last, a link has the page schedule a notification of its own, the app is
15/// killed before it fires, and its banner is tapped.
1316///
1417/// As with the deep-link test, no assertions about the answer live here. The
1518/// page's report is printed and `scripts/mobile-e2e/protocol.ts` judges it.
@@ -18,6 +21,8 @@ final class NotificationTapColdStartTests: XCTestCase {
1821 private var bundleId: String { environment["PROBE_BUNDLE_ID"] ?? "" }
1922 private var permissionLink: String { environment["PROBE_NOTIFY_LINK"] ?? "" }
2023 private var pushBody: String { environment["PROBE_PUSH_BODY"] ?? "" }
24 private var localLink: String { environment["PROBE_LOCAL_LINK"] ?? "" }
25 private var localBody: String { environment["PROBE_LOCAL_BODY"] ?? "" }
2126
2227 override func setUp() {
2328 // Each step stands on the one before it; carrying on past a failure
@@ -25,10 +30,12 @@ final class NotificationTapColdStartTests: XCTestCase {
2530 continueAfterFailure = false
2631 }
2732
28 func testPushesReachThePage() throws {
33 func testNotificationsReachThePage() throws {
2934 XCTAssertFalse(bundleId.isEmpty, "PROBE_BUNDLE_ID is not set")
3035 XCTAssertFalse(permissionLink.isEmpty, "PROBE_NOTIFY_LINK is not set")
3136 XCTAssertFalse(pushBody.isEmpty, "PROBE_PUSH_BODY is not set")
37 XCTAssertFalse(localLink.isEmpty, "PROBE_LOCAL_LINK is not set")
38 XCTAssertFalse(localBody.isEmpty, "PROBE_LOCAL_BODY is not set")
3239
3340 let app = XCUIApplication(bundleIdentifier: bundleId)
3441 let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
@@ -60,7 +67,8 @@ final class NotificationTapColdStartTests: XCTestCase {
6067 // case that always worked.
6168 app.terminate()
6269 XCTAssertTrue(app.wait(for: .notRunning, timeout: 15), "the app did not stop, so this would not be a cold start")
63 tapColdPush(springboard, app: app)
70 print("CRAFT-E2E-PUSH-READY cold")
71 tapBanner(springboard, app: app, containing: pushBody)
6472
6573 let report = app.staticTexts
6674 .containing(NSPredicate(format: "label BEGINSWITH 'CRAFT-E2E-NOTIFICATION '"))
@@ -84,9 +92,32 @@ final class NotificationTapColdStartTests: XCTestCase {
8492 .firstMatch
8593 _ = arrived.waitForExistence(timeout: 30)
8694 print("CRAFT-E2E-NOTIFICATION-RECEIVED-RESULT \(receipts.label)")
95
96 // #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.
99 app.terminate()
100 XCTAssertTrue(app.wait(for: .notRunning, timeout: 15), "the app did not stop before the local link")
101 XCUIDevice.shared.system.open(URL(string: localLink)!)
102 if open.waitForExistence(timeout: 5) { open.tap() }
103 XCTAssertTrue(app.wait(for: .runningForeground, timeout: 60), "the local link did not start the app")
104 let scheduled = app.staticTexts
105 .containing(NSPredicate(format: "label BEGINSWITH 'CRAFT-E2E-LOCAL-SCHEDULED '"))
106 .firstMatch
107 XCTAssertTrue(scheduled.waitForExistence(timeout: 60), "the page never said whether it scheduled the notification")
108 XCTAssertEqual(scheduled.label, "CRAFT-E2E-LOCAL-SCHEDULED ok")
109 app.terminate()
110 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)
112
113 let local = app.staticTexts
114 .containing(NSPredicate(format: "label BEGINSWITH 'CRAFT-E2E-NOTIFICATION '"))
115 .firstMatch
116 XCTAssertTrue(local.waitForExistence(timeout: 120), "the page never reported what the local notification's tap handed it")
117 print("CRAFT-E2E-LOCAL-RESULT \(local.label)")
87118 }
88119
89 /// Asks the host for the cold push and taps its banner.
120 /// Taps the banner of a notification that has arrived or is about to.
90121 ///
91122 /// The banner, not Notification Center: on iOS 26 Notification Center is
92123 /// pulled down as the Lock Screen, and a tap there only offers Open. The
@@ -99,12 +130,11 @@ final class NotificationTapColdStartTests: XCTestCase {
99130 /// enough for the banner to go, and the test fails saying the tap could
100131 /// not find it. There is no retrying around that: a tap that misses ends
101132 /// the test method, even inside XCTExpectFailure.
102 private func tapColdPush(_ springboard: XCUIApplication, app: XCUIApplication) {
133 private func tapBanner(_ springboard: XCUIApplication, app: XCUIApplication, containing text: String) {
103134 let banner = springboard.descendants(matching: .any)
104 .matching(NSPredicate(format: "label CONTAINS %@", pushBody))
135 .matching(NSPredicate(format: "label CONTAINS %@", text))
105136 .firstMatch
106 print("CRAFT-E2E-PUSH-READY cold")
107 XCTAssertTrue(banner.waitForExistence(timeout: 120), "no banner for the push ever appeared")
137 XCTAssertTrue(banner.waitForExistence(timeout: 120), "no banner for \(text) ever appeared")
108138 banner.tap()
109139 XCTAssertTrue(app.wait(for: .runningForeground, timeout: 60), "tapping the banner did not start the app")
110140 }