import XCTest // The long-press message menu, driven with REAL synthesized touches in the // simulator (2026-07-21): every code-level harness passed while the device // press stayed dead, so the gesture itself must be exercised. Runs against // the local dev server — pass TEST_RUNNER_DEV_LINK (the dev sign-in link // from .lchat-dev.json) on the xcodebuild invocation; without it the test // skips. final class FeedMenuUITests: XCTestCase { override func setUpWithError() throws { continueAfterFailure = false } func testLongPressOpensMenuAndReacts() throws { let env = ProcessInfo.processInfo.environment guard let link = env["DEV_LINK"], !link.isEmpty else { throw XCTSkip("Set TEST_RUNNER_DEV_LINK to the dev server sign-in link.") } let app = XCUIApplication() // The DEBUG dev-join door (AppState.init): an empty install joins // the dev space natively from this link before first paint. app.launchEnvironment["LCHAT_DEV_JOIN"] = link app.launch() // Open the dev-seeded group ("Dev chat" — lchat-dev-up's fixture). let row = app.staticTexts["Dev chat"].firstMatch XCTAssertTrue(row.waitForExistence(timeout: 25), "Dev chat not in thread list") shot(app, "menu-01-threads") row.tap() // The dev seed plants a welcome message — press that bubble (no // composer/keyboard in the loop; sim keyboards are flaky). let bubble = app.staticTexts .containing(NSPredicate(format: "label CONTAINS %@", "seeded by lchat-dev-up")) .firstMatch XCTAssertTrue(bubble.waitForExistence(timeout: 20), "seeded bubble never rendered") shot(app, "menu-02-feed") // THE press. The owned menu's reaction palette is the verdict. bubble.press(forDuration: 0.8) let react = app.buttons["React 👍"].firstMatch let appeared = react.waitForExistence(timeout: 5) shot(app, appeared ? "menu-03-open" : "menu-03-DEAD") XCTAssertTrue(appeared, "long-press did not open the menu") // React, and the overlay must dismiss. react.tap() Thread.sleep(forTimeInterval: 0.6) shot(app, "menu-04-after-react") XCTAssertFalse(app.buttons["React ❤️"].firstMatch.exists, "menu overlay did not dismiss after reacting") // A second press on the same bubble reopens it (repeatability). bubble.press(forDuration: 0.8) XCTAssertTrue(app.buttons["React 😂"].firstMatch.waitForExistence(timeout: 5), "second long-press did not reopen the menu") // Tap outside (the dimmed area above) dismisses without acting. app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.88)).tap() Thread.sleep(forTimeInterval: 0.6) XCTAssertFalse(app.buttons["React 😂"].firstMatch.exists, "outside tap did not dismiss the menu") shot(app, "menu-05-dismissed") } private func shot(_ app: XCUIApplication, _ name: String) { let a = XCTAttachment(screenshot: app.screenshot()) a.name = name a.lifetime = .keepAlways add(a) } }