Merge the day picker, Coach new-chat button, day panel on /coach and text cleanup into release/v1.42.0

This commit is contained in:
Marc-André Bombeck committed 2026-10-09 15:11:24 +02:00
commit 1d080ee166
69 files changed
+2293 -2241

No files matched your search

+3 -3
View File
@@ -33,7 +33,7 @@ import {
* 3. Nothing opens by itself: the row stays closed (`aria-expanded`
* false) in every state, after the answer and after a reload; a row
* opened during the run closes when the answer is done.
* 4. After the answer the row reads "Thought process · 4 steps" and the
* 4. After the answer the row reads "Thought process (4 steps)" and the
* table the answer points at renders as its chart.
* 5. A tap opens the steps; a reasoning round shows its summary under
* its title.
@@ -435,7 +435,7 @@ test.describe("Coach live trail", () => {
await expect(settled).toHaveAttribute("data-state", "done");
await expect(
settled.locator('[data-slot="coach-turn-steps-done"]'),
).toHaveText("Thought process · 4 steps");
).toHaveText("Thought process (4 steps)");
await expect(settled.locator(".text-shimmer")).toHaveCount(0);
const settledToggle = settled.locator(
'[data-slot="coach-turn-steps-toggle"]',
@@ -506,7 +506,7 @@ test.describe("Coach live trail", () => {
await expect(reloaded).toHaveAttribute("data-state", "done");
await expect(
reloaded.locator('[data-slot="coach-turn-steps-done"]'),
).toHaveText("Thought process · 4 steps");
).toHaveText("Thought process (4 steps)");
const reloadedToggle = reloaded.locator(
'[data-slot="coach-turn-steps-toggle"]',
);
+196 -125
View File
@@ -4,26 +4,27 @@ import type { Locator, Page, Request, Route, TestInfo } from "@playwright/test";
import { expect, test } from "./setup/test";
import { STORAGE_STATE_PATH } from "./setup/global-setup";
import { aiBlockAvailable, serveAiBlock } from "./setup/ai-capabilities";
import { mockDay } from "./setup/day-mock";
/**
* The Coach page frame: the conversations panel on the right, its toggle in
* the top bar, the settings gear in the panel header, and New chat opening
* that header row.
* The Coach page frame: the conversation, the conversations panel right of
* it, and the day right of that; New conversation floating in the
* conversation itself.
*
* Contracts under test, at 1440, 1280 and 390 px:
* Contracts under test, at 1920, 1440, 1280 and 390 px:
*
* 1. From 1280 px the panel docks beside the thread, open on a first visit,
* and the reading column (thread and composer) is exactly as wide with
* the panel open as with it closed. The choice survives a reload. The
* panel is flush with the viewport's right edge (no scrollbar gutter
* beside it) and runs from the top of the window to the bottom; the
* top bar ends at its left edge and the panel's header border is the
* top bar's. The toggle is the bar's last item, within 16 px of its
* right edge, open and shut; shut, no pixel of the panel shows and the
* bar runs to the window's edge. New chat is the header row's first
* control, left of the title, and never overlaps the list.
* 2. Escape inside the docked panel closes it and returns focus to the
* toggle.
* the panel open as folded. Folded, the panel keeps a 48 px edge whose
* button (in the place of the open panel's fold control) opens it
* again; the choice survives a reload. The panel is flush with the
* viewport's right edge and runs from the top of the window to the
* bottom; the top bar ends at its left edge and the panel's header
* border is the top bar's. Focus moves between the fold control and
* the edge's button, and Escape inside the open panel folds it.
* 2. New conversation is a round 56 px button in the conversation column,
* never over the panel, never over the composer's field, and absent
* from the panel's header.
* 3. A row's menu renames by keyboard (Escape cancels without closing the
* panel, Enter saves once) and deletes with an Undo that keeps the
* DELETE from ever leaving; a delete left alone commits once.
@@ -32,10 +33,13 @@ import { aiBlockAvailable, serveAiBlock } from "./setup/ai-capabilities";
* can see" and drops the param from the URL.
* 5. axe finds nothing on the open panel or the open settings, in the
* light and the dark theme.
* 6. At 390 px the panel is a dialog sheet with New chat first in its
* header and the title beside it; Escape closes it and focus returns to the toggle. The gear
* opens a bottom sheet that closes on Escape back to the gear. Nothing
* scrolls sideways.
* 6. The day docks right of the panel. At 1440 only one of the two is
* open: a day folds the panel, opening the panel folds the day. At 1920
* both stay open and the conversation keeps at least 560 px.
* 7. At 390 px the panel is a dialog sheet opened from the top bar's
* toggle; Escape closes it and focus returns to the toggle. The gear
* opens a bottom sheet that closes on Escape back to the gear. New
* conversation sits above the composer. Nothing scrolls sideways.
*
* Every Coach route is stubbed; the provider chain and provider config are
* stubbed too, so the model section renders the same on every machine.
@@ -174,9 +178,6 @@ async function mockCoach(page: Page): Promise<CoachMock> {
await page.route("**/api/coach/about-me/questions*", (route) =>
fulfilJson(route, { questions: [] }),
);
await page.route("**/api/insights/coach/seeded-question*", (route) =>
fulfilJson(route, { signal: null }),
);
await page.route("**/api/insights/provider-chain", (route) =>
route.request().method() === "GET"
? fulfilJson(route, {
@@ -213,6 +214,11 @@ async function mockCoach(page: Page): Promise<CoachMock> {
}
const toggle = (page: Page) => page.locator('[data-slot="coach-panel-toggle"]');
const collapse = (page: Page) =>
page.locator('[data-slot="coach-panel-collapse"]');
const expand = (page: Page) => page.locator('[data-slot="coach-panel-expand"]');
const fab = (page: Page) => page.locator('[data-slot="coach-new-chat-fab"]');
const dayPanel = (page: Page) => page.locator('[data-slot="day-panel"]');
const panel = (page: Page) =>
page.locator('[data-slot="coach-conversations-panel"]');
@@ -252,9 +258,10 @@ async function expectPanelFlush(page: Page, width: number, height: number) {
'[data-slot="coach-conversations-panel"]',
)!;
const inner = aside.firstElementChild as HTMLElement;
const header = aside.querySelector(
'[data-slot="coach-conversations-panel-header"]',
)!;
// Open, the header row; folded, the edge's band.
const header =
aside.querySelector('[data-slot="coach-conversations-panel-header"]') ??
aside.querySelector('[data-slot="coach-panel-expand"]')!.parentElement!;
const bar = document.querySelector('[data-slot="top-bar"]')!;
const main = document.getElementById("main-content")!;
const style = getComputedStyle(inner);
@@ -287,68 +294,37 @@ async function expectPanelFlush(page: Page, width: number, height: number) {
}
/**
* The toggle is the last item of the top bar (the account menu that follows
* it on phones is not rendered from `md`), and its right edge is within
* 16 px of the bar's right edge.
* New conversation floats in the conversation column: a round 56 px button
* with a plus and its name, inside the column (never over a docked panel
* right of it) and clear of the composer's field. The panel's header has no
* such control.
*/
async function expectToggleAtBarEnd(page: Page) {
const at = await page.evaluate(() => {
const bar = document.querySelector('[data-slot="top-bar"]')!;
const shown = [...bar.querySelectorAll("*")].filter(
(el) => el.getClientRects().length > 0,
);
const toggle = bar.querySelector('[data-slot="coach-panel-toggle"]')!;
const rightmost = Math.max(
...shown.map((el) => el.getBoundingClientRect().right),
);
return {
gap:
bar.getBoundingClientRect().right -
toggle.getBoundingClientRect().right,
last: toggle.getBoundingClientRect().right >= rightmost,
lastChild:
toggle.closest('[data-slot="top-bar-actions"]')?.lastElementChild ===
toggle,
};
});
expect(at.last, "nothing in the bar stands right of the toggle").toBe(true);
expect(at.lastChild).toBe(true);
expect(at.gap).toBeGreaterThanOrEqual(0);
expect(at.gap).toBeLessThanOrEqual(16);
}
/**
* New chat opens the panel header: the row's first control, left of the
* title, a plus glyph with an accessible name. It sits above the list, so no
* row ever slides under it.
*/
async function expectNewChatInHeader(scope: Locator) {
const header = scope.locator(
'[data-slot="coach-conversations-panel-header"]',
);
const button = header.locator('[data-slot="coach-panel-new-chat"]');
await expect(button).toHaveCount(1);
await expect(button.locator("svg.lucide-plus")).toHaveCount(1);
await expect(button).toHaveAttribute("aria-label", "New chat");
const order = await header.evaluate((el) => {
const first = el.firstElementChild as HTMLElement;
const heading = el.querySelector("h2") as HTMLElement;
return {
first: first.dataset.slot,
buttonRight: first.getBoundingClientRect().right,
headingLeft: heading.getBoundingClientRect().left,
};
});
expect(order.first).toBe("coach-panel-new-chat");
expect(order.buttonRight).toBeLessThanOrEqual(order.headingLeft);
const buttonBox = (await button.boundingBox())!;
const listBox = (await scope
.locator('[data-slot="coach-history-list"]')
async function expectNewChatFab(page: Page) {
await expect(fab(page)).toHaveCount(1);
await expect(fab(page)).toHaveAttribute("aria-label", "New conversation");
await expect(fab(page).locator("svg.lucide-plus")).toHaveCount(1);
await expect(
panel(page).locator('[data-slot="coach-panel-new-chat"]'),
).toHaveCount(0);
const box = (await fab(page).boundingBox())!;
expect(Math.round(box.width)).toBe(56);
expect(Math.round(box.height)).toBe(56);
const main = (await page
.locator('[data-slot="coach-page-main"]')
.boundingBox())!;
expect(
buttonBox.y + buttonBox.height,
"the button ends above the list",
).toBeLessThanOrEqual(listBox.y);
expect(box.x).toBeGreaterThanOrEqual(main.x);
expect(box.x + box.width).toBeLessThanOrEqual(main.x + main.width);
const field = (await page
.locator(
'[data-slot="coach-page-composer"] [data-slot="coach-input-textarea"]',
)
.boundingBox())!;
const overlaps =
box.x < field.x + field.width &&
box.x + box.width > field.x &&
box.y < field.y + field.height &&
box.y + box.height > field.y;
expect(overlaps, "the button leaves the composer's field free").toBe(false);
}
/** Rendered heights of the docked panel's header buttons and first row. */
@@ -361,7 +337,7 @@ async function panelControlHeights(page: Page) {
.boundingBox())!.height,
);
return {
newChat: await height("coach-panel-new-chat"),
collapse: await height("coach-panel-collapse"),
plans: await height("coach-panel-plans"),
gear: await height("coach-settings"),
row: await height("coach-history-select"),
@@ -446,8 +422,10 @@ test.describe("Coach page frame", () => {
// First visit: open.
await expect(panel(page)).toHaveAttribute("data-state", "open");
await waitForWidth(panel(page), 288);
await expect(toggle(page)).toHaveAttribute("aria-expanded", "true");
await expect(toggle(page)).toHaveAttribute(
// Docked, the fold control lives in the panel; the top bar has none.
await expect(toggle(page)).toHaveCount(0);
await expect(collapse(page)).toHaveAttribute("aria-expanded", "true");
await expect(collapse(page)).toHaveAttribute(
"aria-controls",
"coach-conversations-panel",
);
@@ -458,9 +436,7 @@ test.describe("Coach page frame", () => {
const aside = (await panel(page).boundingBox())!;
expect(aside.x).toBeGreaterThanOrEqual(open.threadX + open.thread);
await expectPanelFlush(page, width, 900);
// Open, the toggle is the top bar's last item, against the panel.
await expectToggleAtBarEnd(page);
await expectNewChatInHeader(panel(page));
await expectNewChatFab(page);
// The bar says where the reader is: the page, then the conversation.
await expect(
page.locator('[data-slot="coach-top-bar-trail"]'),
@@ -468,37 +444,25 @@ test.describe("Coach page frame", () => {
await expectNoSidewaysScroll(page);
await shot(page, testInfo, `coach-frame-${width}-open`);
await toggle(page).click();
// Folded: a 48 px edge stays, its button takes the focus, and the
// top bar ends at the edge.
await collapse(page).click();
await expect(panel(page)).toHaveAttribute("data-state", "closed");
await waitForWidth(panel(page), 0);
await expect(toggle(page)).toHaveAttribute("aria-expanded", "false");
await waitForWidth(panel(page), 48);
await expect(expand(page)).toBeFocused();
await expect(expand(page)).toHaveAttribute("aria-expanded", "false");
await expect(expand(page)).toHaveAttribute(
"aria-label",
"Show conversations",
);
await expect(
panel(page).locator('[data-slot="coach-history-select"]'),
).toHaveCount(0);
await expectPanelFlush(page, width, 900);
const closed = await columnWidths(page);
// Closed, the panel is entirely off-screen, the top bar runs to the
// window's edge and the toggle is still its last item.
await expect
.poll(() =>
page.evaluate(
() =>
document
.querySelector('[data-slot="top-bar"]')!
.getBoundingClientRect().right,
),
)
.toBe(width);
expect(
await panel(page).evaluate((el) => {
const box = el.getBoundingClientRect();
return box.width === 0 || box.left >= window.innerWidth;
}),
"no pixel of the shut panel shows",
).toBe(true);
await expectToggleAtBarEnd(page);
expect(Math.abs(closed.thread - open.thread)).toBeLessThanOrEqual(1);
expect(Math.abs(closed.composer - open.composer)).toBeLessThanOrEqual(1);
// Closed, nothing inside the panel is reachable.
await expect(
panel(page).locator('[data-slot="coach-history-select"]').first(),
).not.toBeInViewport();
await expectNewChatFab(page);
await expectNoSidewaysScroll(page);
await shot(page, testInfo, `coach-frame-${width}-closed`);
@@ -506,19 +470,118 @@ test.describe("Coach page frame", () => {
await openCoach(page);
await expect(panel(page)).toHaveAttribute("data-state", "closed");
// Open again, then Escape from inside closes it, focus to the toggle.
await toggle(page).click();
// Opened from the keyboard, focus lands on the fold control; Escape
// from inside folds it again and focus goes back to the edge.
await expand(page).focus();
await page.keyboard.press("Enter");
await expect(panel(page)).toHaveAttribute("data-state", "open");
await expect(collapse(page)).toBeFocused();
await panel(page)
.locator('[data-slot="coach-history-select"]')
.first()
.focus();
await page.keyboard.press("Escape");
await expect(panel(page)).toHaveAttribute("data-state", "closed");
await expect(toggle(page)).toBeFocused();
await expect(expand(page)).toBeFocused();
});
}
test("1440: the day and the conversations take turns, focus included", async ({
page,
}, testInfo) => {
await page.setViewportSize({ width: 1440, height: 900 });
await mockCoach(page);
await mockDay(page);
const day = "2026-06-01";
await page.goto(`/coach?c=frame-bp&day=${day}`, {
waitUntil: "domcontentloaded",
});
await expect(
page.locator('[data-slot="coach-message-thread"]').getByText(ANSWER),
).toBeVisible({ timeout: 15_000 });
// The day opens docked, right of the conversations, which fold to
// their edge for it.
await expect(dayPanel(page)).toHaveAttribute("data-shell", "docked");
await expect(panel(page)).toHaveAttribute("data-state", "closed");
await waitForWidth(panel(page), 48);
const rail = (await panel(page).boundingBox())!;
const dayBox = (await dayPanel(page).boundingBox())!;
expect(rail.x + rail.width).toBeLessThanOrEqual(dayBox.x + 1);
expect(Math.round(dayBox.x + dayBox.width)).toBe(1440);
await expectNewChatFab(page);
expect(
(await page.locator('[data-slot="coach-page-main"]').boundingBox())!
.width,
).toBeGreaterThanOrEqual(540);
await expectNoSidewaysScroll(page);
await shot(page, testInfo, "coach-frame-1440-day");
// Opening the conversations folds the day to its edge; focus goes to
// the conversations' fold control, the day is out of the URL, and the
// conversation stays the same.
await expand(page).click();
await expect(panel(page)).toHaveAttribute("data-state", "open");
await expect(dayPanel(page)).toHaveCount(0);
await expect(page.locator('[data-slot="day-rail"]')).toHaveAttribute(
"data-day",
day,
);
await expect(collapse(page)).toBeFocused();
await expect(page).toHaveURL(/\/coach\?c=frame-bp$/);
await expectNewChatFab(page);
await shot(page, testInfo, "coach-frame-1440-list");
// And back: the day's edge opens the day, the conversations fold.
await page.locator('[data-slot="day-expand"]').click();
await expect(dayPanel(page)).toHaveAttribute("data-shell", "docked");
await expect(panel(page)).toHaveAttribute("data-state", "closed");
await expect(page).toHaveURL(new RegExp(`c=frame-bp&day=${day}$`));
// Switching conversations keeps the day open.
await expand(page).click();
await page.locator('[data-slot="day-expand"]').click();
await expect(dayPanel(page)).toBeVisible();
await fab(page).click();
await expect(page).toHaveURL(new RegExp(`/coach\\?day=${day}$`));
await expect(dayPanel(page)).toBeVisible();
});
test("1920: the conversations and the day stay open side by side", async ({
page,
}, testInfo) => {
await page.setViewportSize({ width: 1920, height: 1000 });
await mockCoach(page);
await mockDay(page);
await page.goto("/coach?c=frame-bp&day=2026-06-01", {
waitUntil: "domcontentloaded",
});
await expect(
page.locator('[data-slot="coach-message-thread"]').getByText(ANSWER),
).toBeVisible({ timeout: 15_000 });
await expect(dayPanel(page)).toHaveAttribute("data-shell", "docked");
await expect(panel(page)).toHaveAttribute("data-state", "open");
await waitForWidth(panel(page), 288);
const list = (await panel(page).boundingBox())!;
const dayBox = (await dayPanel(page).boundingBox())!;
expect(list.x + list.width).toBeLessThanOrEqual(dayBox.x + 1);
expect(
(await page.locator('[data-slot="coach-page-main"]').boundingBox())!
.width,
).toBeGreaterThanOrEqual(560);
await expectNewChatFab(page);
await expectNoSidewaysScroll(page);
await shot(page, testInfo, "coach-frame-1920-both");
// Folding one leaves the other alone.
await collapse(page).click();
await expect(panel(page)).toHaveAttribute("data-state", "closed");
await expect(dayPanel(page)).toBeVisible();
await expand(page).click();
await expect(panel(page)).toHaveAttribute("data-state", "open");
await expect(dayPanel(page)).toBeVisible();
});
test("1280 fine pointer: compact header buttons and rows", async ({
page,
}) => {
@@ -526,7 +589,7 @@ test.describe("Coach page frame", () => {
await mockCoach(page);
await openCoach(page);
expect(await panelControlHeights(page)).toEqual({
newChat: 28,
collapse: 28,
plans: 28,
gear: 28,
row: 36,
@@ -546,7 +609,7 @@ test.describe("Coach page frame", () => {
await openCoach(page);
await expect(panel(page)).toHaveAttribute("data-state", "open");
expect(await panelControlHeights(page)).toEqual({
newChat: 44,
collapse: 44,
plans: 44,
gear: 44,
row: 44,
@@ -575,8 +638,7 @@ test.describe("Coach page frame", () => {
await expect(page).toHaveURL(/\/coach\?c=frame-sleep$/);
await expect(rows.nth(1)).toHaveAttribute("aria-current", "true");
const newChat = panel(page).locator('[data-slot="coach-panel-new-chat"]');
await newChat.click();
await fab(page).click();
await expect(page).toHaveURL(/\/coach$/);
await expect(
page.locator('[data-slot="coach-input-textarea"]'),
@@ -795,6 +857,13 @@ test.describe("Coach page frame", () => {
// Below 1280 px the panel waits to be asked.
await expect(panel(page)).toHaveCount(0);
await expect(toggle(page)).toHaveAttribute("aria-expanded", "false");
// New conversation sits above the composer, clear of its field.
await expectNewChatFab(page);
const fabBox = (await fab(page).boundingBox())!;
const composerBox = (await page
.locator('[data-slot="coach-page-composer"]')
.boundingBox())!;
expect(fabBox.y + fabBox.height).toBeLessThanOrEqual(composerBox.y);
await expectNoSidewaysScroll(page);
await shot(page, testInfo, "coach-frame-390-thread");
@@ -802,7 +871,9 @@ test.describe("Coach page frame", () => {
const sheet = page.getByRole("dialog", { name: "Conversations" });
await expect(sheet).toBeVisible();
await expect(toggle(page)).toHaveAttribute("aria-expanded", "true");
await expectNewChatInHeader(sheet);
await expect(
sheet.locator('[data-slot="coach-panel-new-chat"]'),
).toHaveCount(0);
// The title keeps its whole word beside four header controls.
const title = sheet.locator(
'[data-slot="coach-conversations-panel-header"] h2',
+90 -1
View File
@@ -1,4 +1,4 @@
import type { Page } from "@playwright/test";
import type { Page, TestInfo } from "@playwright/test";
import { mockDay } from "./setup/day-mock";
import { expect, test } from "./setup/test";
@@ -26,6 +26,8 @@ import { MOBILE_ROUTES_ANALYTE } from "./setup/mobile-routes-fixture";
* - the dashboard's today area offers no door; its charts below do.
*
* Stable data attributes only: `day-panel` (+ `data-shell`), `day-link`,
* the header's `day-date-button` and its `day-date-picker` (days keyed by
* `data-date-key`, dotted by `data-entries`, and `day-date-today`),
* `chart-plot[data-day-links]`, `chart-tooltip-open-day`, `day-prev` /
* `day-next` / `day-close`, and the collapsed edge `day-rail` (+ `data-day`)
* with its `day-expand`.
@@ -33,6 +35,18 @@ import { MOBILE_ROUTES_ANALYTE } from "./setup/mobile-routes-fixture";
const panel = (page: Page) => page.locator('[data-slot="day-panel"]');
async function shot(page: Page, testInfo: TestInfo, name: string) {
const path = testInfo.outputPath(`${name}.png`);
await page.screenshot({ path });
await testInfo.attach(name, { path, contentType: "image/png" });
const dir = process.env.DAY_SHOTS_DIR;
if (dir) {
await page.screenshot({
path: `${dir}/${name}-${testInfo.project.name}.png`,
});
}
}
function isoDaysAgo(days: number): string {
const d = new Date(Date.now() - days * 86_400_000);
return d.toISOString().slice(0, 10);
@@ -189,6 +203,81 @@ test.describe("the day view", () => {
await expect(rail).toHaveCount(0);
});
test("the date opens a calendar; a picked day opens, Today goes to today", async ({
page,
}, testInfo) => {
const desktop = testInfo.project.name === "chromium-desktop";
// The days of the month that hold anything carry a dot.
await mockDay(page, { index: "full" });
const day = isoDaysAgo(5);
await page.goto(`/mood?day=${day}`);
await expect(panel(page)).toHaveAttribute(
"data-shell",
desktop ? "docked" : "bottom",
);
const date = panel(page).locator('[data-slot="day-date-button"]');
const picker = page.locator('[data-slot="day-date-picker"]');
// The date stays one line in the header, the arrows beside it.
await expect(date).toHaveAttribute("aria-expanded", "false");
await expect(panel(page).locator('[data-slot="day-prev"]')).toBeVisible();
await shot(page, testInfo, "day-date-closed");
await date.click();
await expect(picker).toBeVisible();
await expect(date).toHaveAttribute("aria-expanded", "true");
await expect(picker.locator(`[data-date-key="${day}"]`)).toHaveAttribute(
"data-selected-single",
"true",
);
await expect(picker.locator('[data-entries="true"]').first()).toBeVisible();
// Nothing past today can be picked.
const tomorrow = picker.locator(`[data-date-key="${isoDaysAgo(-2)}"]`);
if ((await tomorrow.count()) > 0) await expect(tomorrow).toBeDisabled();
await shot(page, testInfo, "day-date-open");
// Another day of the same month: the 1st, or the 2nd when the open day
// is the 1st itself.
const first = `${day.slice(0, 8)}01`;
const target = first === day ? isoDaysAgo(4) : first;
const start = await page.evaluate(() => window.history.length);
await picker.locator(`[data-date-key="${target}"]`).click();
await expect(picker).toHaveCount(0);
await expect(page.locator('[data-slot="day-view"]')).toHaveAttribute(
"data-day",
target,
);
await expect(page).toHaveURL(new RegExp(`[?&]day=${target}`));
// Like a step: no new history entry, and focus back on the date.
expect(await page.evaluate(() => window.history.length)).toBe(start);
await expect(date).toBeFocused();
// The keyboard: Enter opens, Escape closes the calendar only.
await page.keyboard.press("Enter");
await expect(picker).toBeVisible();
// The calendar has the keyboard before Escape is pressed.
await expect
.poll(() =>
page.evaluate(
() =>
document.activeElement?.closest(
'[role="dialog"]:not([data-slot="day-panel"])',
) != null,
),
)
.toBe(true);
await page.keyboard.press("Escape");
await expect(picker).toHaveCount(0);
await expect(panel(page)).toBeVisible();
await expect(date).toBeFocused();
// Today.
await page.keyboard.press("Space");
await expect(picker).toBeVisible();
await picker.locator('[data-slot="day-date-today"]').click();
await expect(picker).toHaveCount(0);
await expect(panel(page).locator('[data-slot="day-next"]')).toBeDisabled();
});
test("a sheet closes for good and leaves no edge behind", async ({
page,
}, testInfo) => {
+1 -1
View File
@@ -427,7 +427,7 @@ test.describe("document vault — AI assist + content search", () => {
// The PDF is refused locally (tesseract reads images only) — the error
// shows and the server suggest route is never called.
await expect(
sheet.getByText("Local OCR reads images only — not this file."),
sheet.getByText("Local OCR reads images only, not this file."),
).toBeVisible();
await page.waitForTimeout(300);
expect(suggestCalls).toBe(0);
+1 -1
View File
@@ -771,7 +771,7 @@ test.describe("document vault", () => {
await uploadViaPicker(page, `${marker}-copy.pdf`, bytes);
await expect(
page.getByText("Already stored — highlighting the existing document."),
page.getByText("Already stored. The existing document is highlighted."),
).toBeVisible();
// One row, not two — the copy never landed.
await expect(openButton(page, `${marker}-copy.pdf`)).not.toBeVisible();