diff --git a/Forji/ForjiUITests/NotificationsUITests.swift b/Forji/ForjiUITests/NotificationsUITests.swift index 3ef71a3..185f4c0 100644 --- a/Forji/ForjiUITests/NotificationsUITests.swift +++ b/Forji/ForjiUITests/NotificationsUITests.swift @@ -49,4 +49,67 @@ final class NotificationsUITests: ForgejoUITestBase { } } } + + // MARK: - Open-to-clear (#32) + + /// Opening an unread notification marks it read on the server, so it leaves + /// the Unread filter on the next refresh. + /// + /// Covers the in-app NotificationsOverviewView path: the tap gesture on the + /// row calls markReadOnOpen, which marks the thread read with + /// removeFromUnread: false so the row stays put during navigation and only + /// drops out of Unread on the next server fetch. The multi-instance and + /// system-notification open paths apply the same fix but aren't reachable + /// from this single-instance harness. + /// + /// Sorts after testNotifications and opens whichever unread row is first + /// rather than a fixed one, so it's unaffected by the two notifications that + /// test consumes (the seed leaves several more). + @MainActor + func testOpeningNotificationMarksItRead() throws { + loginAndWaitForHome() + app.tabBars.buttons["Notifications"].tap() + + let notificationsList = app.collectionViews.firstMatch + XCTAssertTrue(notificationsList.waitForExistence(timeout: 10)) + + // Make sure the Unread filter is active. + let unreadButton = app.buttons["Unread"] + XCTAssertTrue(unreadButton.waitForExistence(timeout: 5)) + unreadButton.tap() + XCTAssertTrue(notificationsList.waitForExistence(timeout: 10)) + + // Remember the first unread notification's title (the row's first text) + // so we can assert that specific one leaves the Unread list. + let firstCell = notificationsList.cells.firstMatch + XCTAssertTrue(firstCell.waitForExistence(timeout: 10), "Expected at least one unread notification") + let openedTitle = firstCell.staticTexts.firstMatch.label + XCTAssertFalse(openedTitle.isEmpty, "Could not read the unread notification's title") + + // Open it. Navigating into the detail proves the row was tappable; the + // nav bar title changes away from "Notifications" once it's on screen. + firstCell.tap() + let detailNavBar = app.navigationBars.element(boundBy: 0) + XCTAssertTrue(detailNavBar.waitForExistence(timeout: 10), "Detail view did not open") + XCTAssertNotEqual(detailNavBar.identifier, "Notifications", "Did not navigate into the notification detail") + + // Back to the list. + app.navigationBars.buttons.element(boundBy: 0).tap() + XCTAssertTrue(notificationsList.waitForExistence(timeout: 10)) + + // Force a server refresh of Unread by toggling the filter. Opening marks + // the thread read but keeps the row visible until the next fetch, so this + // round-trip is what surfaces the open-to-clear behaviour. + app.buttons["Read"].tap() + XCTAssertTrue(notificationsList.waitForExistence(timeout: 10)) + unreadButton.tap() + XCTAssertTrue(notificationsList.waitForExistence(timeout: 10)) + + // The opened notification should no longer be under Unread. + let openedRow = notificationsList.staticTexts[openedTitle] + XCTAssertFalse( + openedRow.waitForExistence(timeout: 5), + "Opened notification \"\(openedTitle)\" should have left the Unread filter after refresh", + ) + } }