diff --git a/tests/browser/local-agent-activity.spec.mjs b/tests/browser/local-agent-activity.spec.mjs index 61819dd7e3..781c2b39d1 100644 --- a/tests/browser/local-agent-activity.spec.mjs +++ b/tests/browser/local-agent-activity.spec.mjs @@ -554,6 +554,11 @@ test("opens an active message's empty thread without losing its activity preview await expect( thread.getByRole("textbox", { name: "Reply to thread", exact: true }), ).toBeFocused(); + // The composer takes focus before the rows finish positioning, and the + // rows stay inert until then, so a press on one would land in the composer. + await expect( + thread.getByRole("region", { name: "Thread messages", exact: true }), + ).toHaveAttribute("aria-busy", "false"); await activity.press("Enter"); await expect( page.getByRole("tab", { name: "Activity", selected: true }), @@ -607,6 +612,11 @@ for (const focusOn of ["bubble", "stop", "composer"]) { name: /^View agent activity:/, }); await expect(bubble).toBeVisible(); + // Rows stay inert while the thread positions its first page, so focus() + // on a control inside one is a no-op until the region is no longer busy. + await expect( + thread.getByRole("region", { name: "Thread messages", exact: true }), + ).toHaveAttribute("aria-busy", "false"); // Finish entrance and hold only the exit so the intermediate state is testable. await animationsSettled(message.locator("section"), { subtree: true }); // Hold the actual exit animation before its first frame, rather than racing diff --git a/tests/browser/message-navigation.spec.mjs b/tests/browser/message-navigation.spec.mjs index 05b7db377a..92b35f1119 100644 --- a/tests/browser/message-navigation.spec.mjs +++ b/tests/browser/message-navigation.spec.mjs @@ -847,6 +847,11 @@ test.describe("fractional row geometry", () => { readTest( "exact reveal uses ordinary dwell rather than marking read at open", async ({ page, app }) => { + // The revealed target starts its 300 ms dwell as navigation completes, so + // only a paused clock keeps "nothing read at open" from racing that dwell. + // Pause at a fixed offset, not the page's already-ticking clock. + const base = Date.now(); + await page.clock.install({ time: base }); await open(page, app); await page.evaluate(() => window.fixtureRelay.snapshot().session.unread.ensure(), @@ -872,23 +877,42 @@ readTest( await route.continue().catch(() => {}); }); const before = app.report.readPublications.length; + const frontiers = async () => + (await readJournal(page))?.state.frontiers ?? {}; + await page.clock.pauseAt(base + 20_000); try { - expect(await openTarget(page, target(app))).toEqual({ status: "opened" }); + const opening = openTarget(page, target(app)); + // Exact reveal requires real focus for two animation frames. Advance one + // frame at a time until its owner completes, without earning reading dwell. + await expect + .poll(async () => { + await page.clock.runFor(16); + return status(page); + }) + .toBe("opened"); + expect(await opening).toEqual({ status: "opened" }); await seen; + // Opening marks nothing read locally, so nothing can have been published. + expect(await frontiers()).toEqual({}); expect(app.report.readPublications.length).toBe(before); await expect( thread(page).locator(`[data-message-id="${app.exact.target.id}"]`), ).toBeFocused(); + // Dwell then marks the target, the only row shown while its context is + // held, read locally before the publish debounce even starts; the + // debounced, signed publication itself is covered by unread.spec.mjs. + await page.clock.runFor(300); + await expect.poll(frontiers).toEqual({ + [`msg:${app.exact.target.id}`]: app.exact.target.created_at, + }); } finally { release(); } + await page.clock.resume(); await expect(thread(page).locator("[data-message-id]")).toHaveCount(81); await expect( thread(page).locator(`[data-message-id="${app.exact.target.id}"]`), ).toBeFocused(); - await expect - .poll(() => app.report.readPublications.length) - .toBeGreaterThan(before); }, );