Add open-to-clear UI regression test for notifications (#32)

Covers the in-app path: open an unread notification, then confirm it leaves the
Unread filter after a refresh. Validated against the seeded Docker Forgejo via
just mutating_a_classes=NotificationsUITests test-mutating-a; it fails on the
pre-fix code and passes with the fix. The merged-instance and system-notification
open paths apply the same fix but aren't reachable from this single-instance UI
harness.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
systemBlue 2026-06-01 12:24:28 -04:00
parent f42980c11c
commit ce32f4cae5

View file

@ -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",
)
}
}