eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1import XCTest
2
3// The notification-tap route, end to end in the simulator (2026-07-28): a
4// tapped banner must land IN the chat, cold launch included. No script can
5// press a real springboard banner, so the app's DEBUG tap door
6// (LCHAT_TEST_TAP_THREAD, PushManager) replays the tap's exact effect —
7// forceScrollThreadID + pendingThreadID — a beat after launch, and this test
8// drives the two launches around it. Runs against the local dev server like
9// FeedMenuUITests: pass TEST_RUNNER_DEV_LINK and TEST_RUNNER_THREAD_ID (both
10// from .lchat-dev.json) on the xcodebuild invocation; without them it skips.
11final class NotificationTapUITests: XCTestCase {
12
13 override func setUpWithError() throws {
14 continueAfterFailure = false
15 }
16
17 func testColdLaunchNotificationTapLandsInThread() throws {
18 let env = ProcessInfo.processInfo.environment
19 guard let link = env["DEV_LINK"], !link.isEmpty,
20 let threadID = env["THREAD_ID"], !threadID.isEmpty else {
21 throw XCTSkip("Set TEST_RUNNER_DEV_LINK + TEST_RUNNER_THREAD_ID from .lchat-dev.json.")
22 }
23
24 // ---- run 1: sign in and visit the chat once ------------------------
25 // A real phone that receives pushes has been in the space: the thread
26 // list refresh parks the SharedThreadKeys entries the tap route
27 // resolves against, and opening the space stamps lastInstanceID.
28 let app = XCUIApplication()
29 app.launchEnvironment["LCHAT_DEV_JOIN"] = link
30 app.launch()
31 allowNotificationsIfAsked(app)
32
33 // The dev-join claim lands on the spaces home screen — enter the space
34 // (its row wears the dev server's host as its name).
35 let devRow = app.staticTexts["Dev chat"].firstMatch
36 if !devRow.waitForExistence(timeout: 5) {
37 let spaceRow = app.staticTexts["127.0.0.1"].firstMatch
38 if spaceRow.waitForExistence(timeout: 25) { spaceRow.tap() }
39 }
40 XCTAssertTrue(devRow.waitForExistence(timeout: 25), "Dev chat not in thread list")
41 devRow.tap()
42 let bubble = app.staticTexts
43 .containing(NSPredicate(format: "label CONTAINS %@", "seeded by lchat-dev-up"))
44 .firstMatch
45 XCTAssertTrue(bubble.waitForExistence(timeout: 20), "seeded bubble never rendered")
46 shot(app, "tap-01-visited")
47 app.terminate()
48
49 // ---- run 2: cold launch with the tap waiting -----------------------
50 // The door fires 1s after launch — didReceive's timing on a cold
51 // launch (delivered right after the first frame).
52 app.launchEnvironment["LCHAT_TEST_TAP_THREAD"] = threadID
53 app.launchEnvironment["LCHAT_TEST_TAP_DELAY"] = "1.0"
54 app.launch()
55 shot(app, "tap-02-relaunched")
56
57 // The verdict: the tapped chat's feed is on screen — and stays there.
58 let landed = bubble.waitForExistence(timeout: 20)
59 shot(app, landed ? "tap-03-landed" : "tap-03-STRANDED")
60 XCTAssertTrue(landed, "notification tap never landed in the chat")
61
62 // A wedged NavigationStack still shows the feed it dropped into —
63 // prove the UI is alive by leaving the chat and coming back.
64 let back = app.navigationBars.buttons.firstMatch
65 XCTAssertTrue(back.waitForExistence(timeout: 5), "no back button on the chat screen")
66 back.tap()
67 let row = app.staticTexts["Dev chat"].firstMatch
68 let listAlive = row.waitForExistence(timeout: 10)
69 shot(app, listAlive ? "tap-04-back-alive" : "tap-04-back-DEAD")
70 XCTAssertTrue(listAlive, "back from the tapped chat never reached the list")
71 }
72
73 private func allowNotificationsIfAsked(_ app: XCUIApplication) {
74 let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
75 let allow = springboard.alerts.buttons["Allow"].firstMatch
76 if allow.waitForExistence(timeout: 5) { allow.tap() }
77 // Whether or not the alert showed, poke the app so the run continues.
78 app.activate()
79 }
80
81 private func shot(_ app: XCUIApplication, _ name: String) {
82 let a = XCTAttachment(screenshot: app.screenshot())
83 a.name = name
84 a.lifetime = .keepAlways
85 add(a)
86 }
87}
88