diff --git a/e2e/coach-open-at-end.spec.ts b/e2e/coach-open-at-end.spec.ts index ef3ef4fd1..a86f0426c 100644 --- a/e2e/coach-open-at-end.spec.ts +++ b/e2e/coach-open-at-end.spec.ts @@ -185,12 +185,26 @@ test.describe("Coach: a saved conversation opens at its end", () => { waitUntil: "domcontentloaded", }); - const thread = page.locator('[data-slot="coach-message-thread"]'); - await expect(thread).toBeVisible({ timeout: 15_000 }); + // The empty state carries the same `coach-message-thread` slot, so the + // thread being visible says nothing about the conversation having + // arrived; the height used to be read from the empty state on a slow + // run. Wait for the frame only a loaded thread renders, and for every + // stored answer in it. + await expect( + page.locator('[data-slot="coach-message-thread-frame"]'), + ).toBeVisible({ timeout: 15_000 }); + await expect( + page.locator('[data-slot="coach-assistant-turn"]'), + ).toHaveCount(8, { timeout: 15_000 }); + const thread = page.locator( + '[data-slot="coach-message-thread-frame"] [data-slot="coach-message-thread"]', + ); // The thread scrolls at all: otherwise "at the end" proves nothing. - expect( - await thread.evaluate((el) => el.scrollHeight > el.clientHeight + 200), - ).toBe(true); + await expect + .poll(() => + thread.evaluate((el) => el.scrollHeight > el.clientHeight + 200), + ) + .toBe(true); const chart = page.locator( '[data-slot="coach-result-chart"][data-ref="r1"]', diff --git a/e2e/day-view.spec.ts b/e2e/day-view.spec.ts index 3b99bacd5..0aae88187 100644 --- a/e2e/day-view.spec.ts +++ b/e2e/day-view.spec.ts @@ -275,6 +275,25 @@ test.describe("the day view", () => { ), ) .toBe(true); + // On a phone the calendar is a sheet over the day's own sheet. Focus + // arrives before Radix has handed it the Escape key: in between, the day + // underneath still holds it. The handover shows as the day's layer + // turning inert (`pointer-events: none`) once the calendar is the top + // layer; wait for that, not for a fixed time. The docked column is no + // dialog and has no such handover. + if (!desktop) + await expect + .poll(() => + panel(page).evaluate( + (el) => + new Promise((resolve) => + requestAnimationFrame(() => + setTimeout(() => resolve(getComputedStyle(el).pointerEvents)), + ), + ), + ), + ) + .toBe("none"); await page.keyboard.press("Escape"); await expect(picker).toHaveCount(0); await expect(panel(page)).toBeVisible(); diff --git a/src/components/day/day-layer.tsx b/src/components/day/day-layer.tsx index d0cfaec05..8c75f2ea3 100644 --- a/src/components/day/day-layer.tsx +++ b/src/components/day/day-layer.tsx @@ -717,6 +717,18 @@ function DaySheet({ event.preventDefault(); returnFocus(); }} + onEscapeKeyDown={(event) => { + // A dialog opened from inside the day (the date's calendar) is + // portaled out of the panel. For a moment after it opens, until + // Radix has re-rendered both layers, the panel still holds the + // Escape listener: an Escape aimed at the calendar then closed the + // whole day. Escape from inside another dialog is never the day's. + const owner = + event.target instanceof Element + ? event.target.closest('[role="dialog"]') + : null; + if (owner && owner !== panelRef.current) event.preventDefault(); + }} className={cn( // Quicker than the primitive's 500 ms, which read as lag on a // layer opened from a tap; no slide at all under reduced motion.