ReviewOS

also looking at this

craft-native/craft

test(ios): tap a real pushed notification that launches the app, and judge what the page gets

#257
Merged glennmichael123 wants to merge test/ios-e2e-notification-tap into main
5 files +308 -10
10 days ago

Follow-up to #255, and the first end-to-end push coverage on iOS.

#255 fixed two ways a tap on a notification that launched the app got lost. Each fix had a unit test, but nothing ran the sequence a person actually goes through: allow notifications, the app gets killed, a push arrives, and they tap the banner. This PR adds that sequence to the simulator harness, with a real push.

How it runs

NotificationTapColdStartTests is a new XCUITest in the probe app's UI test target:

  1. It opens crafte2eprobe://e2e/notify?run=…. The driver page sees that launch link and calls craft.permissions.request('notifications'), and the test taps Allow on SpringBoard's prompt. Without permission, iOS accepts the push and shows nothing.
  2. It kills the app and prints CRAFT-E2E-PUSH-READY.
  3. ios.ts spawns that xcodebuild itself instead of going through command, so it can read the log while the test runs. When the line appears, it runs xcrun simctl push with a payload that carries the run's nonce as the body, plus a custom route key.
  4. The test finds the banner by the nonce, so a notification left over from an earlier run can't be the one tapped. It taps the banner and reads the page's report back.

The page subscribes through notifications.onTap after its suite, seconds after the bridge appeared, the way a hydrating app subscribes. It reports what it was handed, plus how many craftNotificationResponse events native code dispatched, counted from its first line.

protocol.ts judges the report. Because of the dispatch count, a failure says which half of #255 broke:

  • Nothing dispatched: native code dropped the tap before any page existed.
  • Dispatched, but onTap got nothing: the page's replay flushed it before anyone subscribed.

It also fails if onTap is missing, if it returns no way to unsubscribe, if the tap is delivered twice or belongs to another run, or if the tap lost its route.

The existing deep-link run now uses -only-testing so the two UI test classes run separately.

Evidence (local, iOS 26.5 simulator, Xcode 26.6)

The full harness passed on both legs, including the existing 12 cases and both deep-link cold starts. The tap test took about 16 seconds per leg. This is the shim leg's report:

{"dispatched":1,"taps":[{"craftE2E":"craft-e2e-ios-shim-70910","aps":{"alert":{"title":"Craft E2E","body":"craft-e2e-ios-shim-70910"}},"route":"/e2e/notification-tap"}],"onTap":"function","unsubscribe":"function"}

I reintroduced each half of the bug in the template and reran the whole harness each time. Both runs failed on both legs, with only the matching message:

MutationResult
handleNotificationResponse drops the tap when the page isn't ready, as the old NotificationCenter route didthe app never dispatched the tap that launched it; native code dropped it before the page existed
the replay listener stops buffering tapsa page that subscribed to notifications.onTap after launch was handed [], expected the tap that launched the app once

protocol.test.ts has 7 new cases for the judge (62 in total), and they run in the harness job on every PR.

Choices

  • A stdout line to signal readiness, not a file. xcodebuild forwarded the runner's print live, so the host saw PUSH-READY while the test was still waiting. It needs no path shared between the simulator and the host.
  • Permission is asked only when a link asks for it. Nothing answers the prompt on the launches simctl makes for the main suite, and an unanswered prompt would sit over the app for the rest of the leg.
  • The probe config doesn't enable push. The capability gates registration, which needs a signed aps-environment entitlement that this unsigned simulator build can't have. Delivery and taps don't depend on it, and simctl push delivers regardless.
  • iOS only. The page skips the probe on Android, where there's no tap path to test yet.

Not covered

  • A push arriving while the app is open never reaches the page. Scoping this turned that up. It's filed as #256, and the harness can cover it the same way once it exists.
  • Real APNs delivery. simctl push goes around APNs, so it tests the app's side, not token registration or the server.

5 changed files on the files tab, with 0 review threads.

Sign in to comment on this pull request.