feat(shell): run the docked strips the full height and make them the only fold

The docked panels' strips now run from the top edge of the window to the
bottom, and the top bar ends left of them. The panel header no longer
carries a fold button in docked mode: the strip opens and closes the day
and, on /coach, the conversations; opening moves focus to the panel's
heading and closing returns it to the strip. The sheets keep a close
button, the day's now an X at the end of its header.

The command palette's door is a magnifier at every width, named Search,
with the shortcut in its tooltip. g p opens or closes the day like a
click on its strip, and below the docking width opens the sheet on the
last day or closes it; the shortcuts list shows it under Day.
This commit is contained in:
Marc-André Bombeck committed 2026-10-10 20:04:19 +02:00
1 parent 023115b05c
commit 39fc4e4da2
26 files changed
+561 -229

No files matched your search

+59 -18
View File
@@ -21,9 +21,12 @@ import { mockDay } from "./setup/day-mock";
* panel left of the strips, a second click shuts it; the choice
* survives a reload. The panel is flush with the strips 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 strip, and Escape inside the open
* panel folds it.
* edge and the panel's header border is the top bar's. Both strips run
* the full height of the window, from its top edge, and the search
* magnifier ends the top bar in front of them, never covered. The strip
* is the docked panel's only fold control (the header has none); focus
* moves between the strip and the panel's heading, 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.
@@ -218,6 +221,8 @@ 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 heading = (page: Page) =>
page.locator('[data-slot="coach-panel-heading"]');
const expand = (page: Page) =>
page.locator('[data-slot="coach-panel-strip-toggle"]');
const strip = (page: Page) => page.locator('[data-slot="coach-panel-strip"]');
@@ -257,7 +262,9 @@ async function expectNoSidewaysScroll(page: Page) {
* right edge (no scrollbar gutter beside them). The panel runs from the top
* of the window to the bottom, the top bar ends at its left edge, an open
* panel's header row has the top bar's bottom border, and its own border
* is the left one only. Both strips start below the top bar's band.
* is the left one only. Both strips run the full height of the window, and
* the search magnifier at the end of the top bar sits in front of them,
* whole and on top.
*/
async function expectPanelFlush(page: Page, width: number, height: number) {
const geometry = await page.evaluate(() => {
@@ -283,6 +290,18 @@ async function expectPanelFlush(page: Page, width: number, height: number) {
list: box('[data-slot="coach-panel-strip"]')!,
listButton: box('[data-slot="coach-panel-strip-toggle"]')!,
day: box('[data-slot="day-strip"]')!,
dayButton: box('[data-slot="day-strip-toggle"]')!,
search: box('[data-slot="command-palette-trigger"]')!,
searchOnTop: (() => {
const r = document
.querySelector('[data-slot="command-palette-trigger"]')!
.getBoundingClientRect();
return (
document
.elementFromPoint(r.left + r.width / 2, r.top + r.height / 2)
?.closest('[data-slot="command-palette-trigger"]') != null
);
})(),
dayDock: box("#day-docked-panel")!,
gutter: main.offsetWidth - main.clientWidth,
borders: style
@@ -305,8 +324,17 @@ async function expectPanelFlush(page: Page, width: number, height: number) {
expect(geometry.panel.top).toBe(0);
expect(geometry.panel.bottom).toBe(height);
expect(geometry.bar.right).toBe(geometry.panel.left);
// The strips start below the band; the palette's corner stays the bar's.
expect(geometry.listButton.top).toBe(geometry.bar.bottom);
// The strips run the full height, from the top edge of the window.
for (const button of [geometry.listButton, geometry.dayButton]) {
expect(button.top).toBe(0);
expect(button.bottom).toBe(height);
}
// The magnifier ends the top bar, left of the strips, whole and on top.
expect(geometry.search.top).toBeGreaterThanOrEqual(0);
expect(geometry.search.bottom).toBeLessThanOrEqual(geometry.bar.bottom);
expect(geometry.search.right).toBeLessThanOrEqual(geometry.bar.right);
expect(geometry.search.right).toBeLessThanOrEqual(geometry.list.left);
expect(geometry.searchOnTop).toBe(true);
if (geometry.header !== null) {
expect(geometry.header).toBe(geometry.bar.bottom);
expect(geometry.borders).toEqual(["0px", "0px", "0px", "1px"]);
@@ -364,7 +392,6 @@ async function panelControlHeights(page: Page) {
.boundingBox())!.height,
);
return {
collapse: await height("coach-panel-collapse"),
plans: await height("coach-panel-plans"),
gear: await height("coach-settings"),
row: await height("coach-history-select"),
@@ -449,10 +476,11 @@ test.describe("Coach page frame", () => {
// First visit: open.
await expect(panel(page)).toHaveAttribute("data-state", "open");
await waitForWidth(panel(page), 288);
// Docked, the fold control lives in the panel; the top bar has none.
// Docked, the strip is the one fold control: the top bar and the
// panel's header carry none.
await expect(toggle(page)).toHaveCount(0);
await expect(collapse(page)).toHaveAttribute("aria-expanded", "true");
await expect(collapse(page)).toHaveAttribute(
await expect(collapse(page)).toHaveCount(0);
await expect(expand(page)).toHaveAttribute(
"aria-controls",
"coach-conversations-panel",
);
@@ -480,7 +508,7 @@ test.describe("Coach page frame", () => {
// Folded: the panel slides to nothing, the strip takes the focus, and
// the top bar ends at the strips.
await collapse(page).click();
await expand(page).click();
await expect(panel(page)).toHaveAttribute("data-state", "closed");
await waitForWidth(panel(page), 0);
expect(await stripBox(strip(page))).toEqual(stripOpen);
@@ -522,12 +550,17 @@ test.describe("Coach page frame", () => {
await expect(expand(page)).toBeFocused();
expect(await stripBox(strip(page))).toEqual(stripOpen);
// Opened from the keyboard, focus lands on the fold control; Escape
// Opened from the keyboard, focus lands on the panel's heading; Escape
// from inside folds it again and focus goes back to the strip.
await expand(page).focus();
await page.keyboard.press("Enter");
await expect(panel(page)).toHaveAttribute("data-state", "open");
await expect(collapse(page)).toBeFocused();
await expect(heading(page)).toBeFocused();
await page.keyboard.press("Escape");
await expect(panel(page)).toHaveAttribute("data-state", "closed");
await expect(expand(page)).toBeFocused();
await page.keyboard.press("Enter");
await expect(panel(page)).toHaveAttribute("data-state", "open");
await panel(page)
.locator('[data-slot="coach-history-select"]')
.first()
@@ -572,14 +605,14 @@ test.describe("Coach page frame", () => {
await shot(page, testInfo, "coach-frame-1440-day");
// Opening the conversations closes the day; its strip keeps the day,
// focus goes to the conversations' fold control, the day is out of the
// focus goes to the conversations' heading, 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(dayStrip(page)).toHaveAttribute("data-day", day);
await expect(dayStrip(page)).toHaveAttribute("data-state", "closed");
await expect(collapse(page)).toBeFocused();
await expect(heading(page)).toBeFocused();
await expect(page).toHaveURL(/\/coach\?c=frame-bp$/);
await expectNewChatFab(page);
await shot(page, testInfo, "coach-frame-1440-list");
@@ -627,7 +660,7 @@ test.describe("Coach page frame", () => {
await shot(page, testInfo, "coach-frame-1920-both");
// Folding one leaves the other alone.
await collapse(page).click();
await expand(page).click();
await expect(panel(page)).toHaveAttribute("data-state", "closed");
await expect(dayPanel(page)).toBeVisible();
await expand(page).click();
@@ -642,7 +675,6 @@ test.describe("Coach page frame", () => {
await mockCoach(page);
await openCoach(page);
expect(await panelControlHeights(page)).toEqual({
collapse: 28,
plans: 28,
gear: 28,
row: 36,
@@ -662,7 +694,6 @@ test.describe("Coach page frame", () => {
await openCoach(page);
await expect(panel(page)).toHaveAttribute("data-state", "open");
expect(await panelControlHeights(page)).toEqual({
collapse: 44,
plans: 44,
gear: 44,
row: 44,
@@ -935,6 +966,16 @@ test.describe("Coach page frame", () => {
await title.evaluate((el) => el.scrollWidth <= el.clientWidth),
"the sheet title is not truncated",
).toBe(true);
// Without a strip, the sheet keeps a clear close button and no fold.
await expect(
sheet.locator('[data-slot="coach-panel-close"]'),
).toBeVisible();
await expect(
sheet.locator('[data-slot="coach-panel-close"]'),
).toHaveAttribute("aria-label", "Close");
await expect(
sheet.locator('[data-slot="coach-panel-collapse"]'),
).toHaveCount(0);
await shot(page, testInfo, "coach-frame-390-panel");
await page.keyboard.press("Escape");
+23 -16
View File
@@ -12,13 +12,14 @@ import { expect, test } from "./setup/test";
* - a module's entry is a switch: Enter sends the module write and the
* palette stays open (the write is answered by a route mock, so the
* shared account's modules never move);
* - the top bar carries the search field on a wide desktop and a
* magnifier on a phone, which opens a full-height sheet with the field
* focused;
* - the top bar carries a magnifier at every width, named "Search",
* with a tooltip that gives the shortcut; on a phone it opens a
* full-height sheet with the field focused;
* - the open palette passes axe.
*
* Stable hooks only: `command-palette` (+ `data-variant`),
* `command-palette-input`, `command-palette-trigger` (+ `data-variant`),
* `command-palette-input`, `command-palette-trigger`,
* `command-palette-tooltip`,
* options by `data-entry`.
*/
@@ -109,20 +110,27 @@ test.describe("command palette", () => {
await expect(input(page)).toHaveValue("timeline");
});
test("the top bar: a search field on a wide desktop", async ({ page }) => {
test("the top bar: a magnifier on a wide desktop, with the shortcut in its tooltip", async ({
page,
}) => {
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto("/");
await shellReady(page);
const field = page.locator(
'[data-slot="command-palette-trigger"][data-variant="field"]',
const magnifier = page.locator('[data-slot="command-palette-trigger"]');
await expect(magnifier).toHaveCount(1);
await expect(magnifier).toBeVisible();
await expect(magnifier).toHaveAttribute("aria-label", "Search");
await expect(magnifier).toHaveAttribute(
"aria-keyshortcuts",
/^(Meta|Control)\+K$/,
);
await expect(field).toBeVisible();
// An icon, not a field: no visible words in the button.
expect((await magnifier.innerText()).trim()).toBe("");
await magnifier.hover();
await expect(
page.locator(
'[data-slot="command-palette-trigger"][data-variant="icon"]',
),
).toBeHidden();
await field.click();
page.locator('[data-slot="command-palette-tooltip"]').first(),
).toHaveText(/^Search \((⌘K|Ctrl K)\)$/);
await magnifier.click();
await expect(palette(page)).toBeVisible();
await expect(input(page)).toBeFocused();
@@ -142,10 +150,9 @@ test.describe("command palette", () => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/");
await shellReady(page);
const magnifier = page.locator(
'[data-slot="command-palette-trigger"][data-variant="icon"]',
);
const magnifier = page.locator('[data-slot="command-palette-trigger"]');
await expect(magnifier).toBeVisible();
await expect(magnifier).toHaveAttribute("aria-label", "Search");
await magnifier.click();
await expect(palette(page)).toHaveAttribute("data-variant", "sheet");
await expect(input(page)).toBeFocused();
+11 -3
View File
@@ -245,8 +245,15 @@ test.describe("the dashboard and the list of all values", () => {
.first();
await expect(plot).toBeVisible({ timeout: 20_000 });
await openFromPlot(page, plot, testInfo);
// Closing leaves the dashboard on today.
await page.locator('[data-slot="day-close"]').click();
// Closing leaves the dashboard on today: a sheet from its close button,
// the docked day from its strip, the only control it has.
await page
.locator(
isPhone(testInfo)
? '[data-slot="day-close"]'
: '[data-slot="day-strip-toggle"]',
)
.click();
await expect(panel(page)).toHaveCount(0);
await expect(page).toHaveURL(/\/$/);
});
@@ -292,7 +299,8 @@ test.describe("the dashboard and the list of all values", () => {
.toBeLessThanOrEqual(panelBox.x);
// Folded, the day keeps its strip; the launcher stays clear of it.
await page.locator('[data-slot="day-close"]').click();
await page.locator('[data-slot="day-strip-toggle"]').click();
await expect(panel(page)).toHaveCount(0);
const edge = (await page.locator('[data-slot="day-strip"]').boundingBox())!;
await expect
.poll(async () => {
+72 -5
View File
@@ -21,8 +21,11 @@ import { MOBILE_ROUTES_ANALYTE } from "./setup/mobile-routes-fixture";
* - `?day=` opens the layer on any page, Back and Escape close it, a date in
* the future is dropped;
* - stepping to the neighbouring days never adds history entries;
* - docked, the day collapses to a narrow edge that brings it back on any
* page; a sheet closes for good;
* - docked, the day folds to its strip, which runs the full height of the
* window right of the top bar, is the panel's only open and close
* control and brings the day back on any page; the search magnifier at
* the end of the top bar is never covered by it; a sheet closes for good
* from its close button;
* - the dashboard's today area offers no door; its charts below do.
*
* Stable data attributes only: `day-panel` (+ `data-shell`), `day-link`,
@@ -175,12 +178,15 @@ test.describe("the day view", () => {
await page.goto(`/mood?day=${day}`);
await expect(panel(page)).toHaveAttribute("data-shell", "docked");
await page.locator('[data-slot="day-close"]').click();
// Docked, the header has no fold button: the strip is the toggle.
await expect(page.locator('[data-slot="day-close"]')).toHaveCount(0);
const rail = page.locator('[data-slot="day-strip"]');
const expand = page.locator('[data-slot="day-strip-toggle"]');
await expect(expand).toHaveAttribute("aria-expanded", "true");
await expand.click();
await expect(panel(page)).toHaveCount(0);
await expect(page).toHaveURL(/\/mood$/);
// The strip stays, holding the day, and focus waits on it.
const rail = page.locator('[data-slot="day-strip"]');
const expand = page.locator('[data-slot="day-strip-toggle"]');
await expect(rail).toHaveAttribute("data-day", day);
await expect(rail).toHaveAttribute("data-state", "closed");
await expect(expand).toBeFocused();
@@ -305,6 +311,67 @@ test.describe("the day view", () => {
await expect(page.locator('[data-slot="day-strip"]')).toHaveCount(0);
});
test("the strip runs the full height; the search magnifier stays clear of it", async ({
page,
}, testInfo) => {
test.skip(
testInfo.project.name !== "chromium-desktop",
"docked from 1280 px",
);
const day = isoDaysAgo(3);
const strip = page.locator('[data-slot="day-strip"]');
const search = page.locator('[data-slot="command-palette-trigger"]');
const topBar = page.locator('[data-slot="top-bar"]');
for (const width of [1280, 1440, 1920]) {
await page.setViewportSize({ width, height: 900 });
for (const url of ["/mood", `/mood?day=${day}`]) {
await page.goto(url);
await expect(strip).toBeVisible();
if (url.includes("day=")) {
await expect(panel(page)).toHaveAttribute("data-shell", "docked");
// The open panel runs the full height beside its strip.
await expect
.poll(async () => (await panel(page).boundingBox())?.width)
.toBeGreaterThan(400);
const p = (await panel(page).boundingBox())!;
expect(p.y).toBe(0);
expect(Math.round(p.y + p.height)).toBe(900);
}
const s = (await strip.boundingBox())!;
// Top edge at the very top of the window, down to the bottom.
expect(s.y).toBe(0);
expect(Math.round(s.height)).toBe(900);
expect(Math.round(s.x + s.width)).toBe(width);
// The top bar ends left of the strips, so the magnifier sits fully
// inside it, in front of them.
const bar = (await topBar.boundingBox())!;
expect(bar.x + bar.width).toBeLessThanOrEqual(s.x + 0.5);
await expect(search).toBeVisible();
const m = (await search.boundingBox())!;
expect(m.x).toBeGreaterThanOrEqual(0);
expect(m.y).toBeGreaterThanOrEqual(0);
expect(m.x + m.width).toBeLessThanOrEqual(s.x);
expect(m.y + m.height).toBeLessThanOrEqual(bar.y + bar.height);
// Nothing paints over its centre: the hit test lands on it.
const hit = await page.evaluate(
([x, y]) =>
document
.elementFromPoint(x, y)
?.closest('[data-slot="command-palette-trigger"]') != null,
[m.x + m.width / 2, m.y + m.height / 2],
);
expect(hit).toBe(true);
// No horizontal scroll anywhere on the page.
const overflow = await page.evaluate(
() =>
document.documentElement.scrollWidth -
document.documentElement.clientWidth,
);
expect(overflow).toBeLessThanOrEqual(0);
}
}
});
test("a future or malformed ?day= is dropped without a word", async ({
page,
}) => {
+95 -1
View File
@@ -15,7 +15,10 @@ import { mockTimeline } from "./utils/mock-timeline";
* - `?` opens the list: a titled dialog that keeps focus inside and closes
* on Escape; the account menu opens the same list;
* - `n` opens the add menu;
* - `[` and `]` step the open day, never past today.
* - `[` and `]` step the open day, never past today;
* - `g p` opens and closes the day like a click on its strip (focus into
* the panel, back to the strip); below the docking width it opens the
* sheet on the last day and closes it again.
*
* The module map is the account's real `/api/auth/me` with only `timeline`
* rewritten, so the switch is never moved on a shared account. Desktop only:
@@ -149,6 +152,9 @@ test.describe("keyboard shortcuts", () => {
"g",
"d",
]);
await expect(
dialog.locator('[data-group="day"] [data-shortcut="day-toggle"] kbd'),
).toHaveText(["g", "p"]);
// Focus trap: Tab and Shift+Tab never leave the dialog.
for (const step of ["Tab", "Tab", "Tab", "Shift+Tab", "Shift+Tab"]) {
@@ -213,4 +219,92 @@ test.describe("keyboard shortcuts", () => {
await page.keyboard.press("]");
await expect(page).toHaveURL(new RegExp(`[?&]day=${TODAY}`));
});
test("g p opens and closes the docked day, focus like its strip", async ({
page,
}) => {
await page.setViewportSize({ width: 1440, height: 900 });
await mockDay(page);
await page.goto("/insights");
await shellReady(page);
const panel = page.locator('[data-slot="day-panel"]');
const strip = page.locator('[data-slot="day-strip-toggle"]');
await expect(strip).toHaveAttribute("aria-expanded", "false");
await expect(panel).toHaveCount(0);
await page.keyboard.press("g");
await page.keyboard.press("p");
await expect(panel).toHaveAttribute("data-shell", "docked");
await expect(page).toHaveURL(new RegExp(`[?&]day=${TODAY}`));
await expect(strip).toHaveAttribute("aria-expanded", "true");
// Into the panel, on its heading, the way the strip opens it.
await expect(panel.locator("h2").first()).toBeFocused();
await page.keyboard.press("g");
await page.keyboard.press("p");
await expect(panel).toHaveCount(0);
await expect(page).toHaveURL(/\/insights$/);
await expect(strip).toHaveAttribute("aria-expanded", "false");
// Back on the strip, which brings the day back.
await expect(strip).toBeFocused();
});
test("below the docking width g p opens the sheet on the last day and closes it", async ({
page,
}) => {
await page.setViewportSize({ width: 1024, height: 800 });
await mockDay(page);
const day = shiftDay(TODAY, -3);
// Open and close a day once, so it is the one remembered.
await page.goto(`/insights?day=${day}`);
await shellReady(page);
const panel = page.locator('[data-slot="day-panel"]');
await expect(panel).toHaveAttribute("data-shell", "sheet");
await page.keyboard.press("Escape");
await expect(panel).toHaveCount(0);
await page.locator("#main-content").focus();
await page.keyboard.press("g");
await page.keyboard.press("p");
await expect(panel).toHaveAttribute("data-shell", "sheet");
await expect(page.locator('[data-slot="day-view"]')).toHaveAttribute(
"data-day",
day,
);
// Over the sheet, the same keys close it.
await page.keyboard.press("g");
await page.keyboard.press("p");
await expect(panel).toHaveCount(0);
await expect(page).toHaveURL(/\/insights$/);
});
test("g p stays out of a field and out of another dialog", async ({
page,
}) => {
await page.setViewportSize({ width: 1440, height: 900 });
await mockDay(page);
await page.goto("/insights");
await shellReady(page);
const panel = page.locator('[data-slot="day-panel"]');
await expect(page.locator('[data-slot="day-strip"]')).toBeVisible();
await page.evaluate(() => {
const field = document.createElement("input");
field.setAttribute("data-testid", "probe-field");
field.setAttribute("aria-label", "Probe");
document.getElementById("main-content")?.prepend(field);
});
await page.getByTestId("probe-field").focus();
await page.keyboard.type("gp");
await expect(page.getByTestId("probe-field")).toHaveValue("gp");
await expect(panel).toHaveCount(0);
await page.locator("#main-content").focus();
await page.keyboard.press("Shift+?");
await expect(helpDialog(page)).toBeVisible();
await page.keyboard.press("g");
await page.keyboard.press("p");
await expect(panel).toHaveCount(0);
await page.keyboard.press("Escape");
await expect(helpDialog(page)).toHaveCount(0);
});
});
+72 -9
View File
@@ -306,6 +306,68 @@ test("value lines: quarterly means, gaps kept, short ones bridged, as many as th
await expect(last).toBeInViewport();
});
test("each mean's dot sits on the middle of its text and never wraps apart from it", async ({
page,
}, testInfo) => {
await setTimelineModuleOn(page);
await mockTimeline(page, "full", TODAY);
const check = async (slot: string) => {
const rows = page.locator(`[data-slot="${slot}"]`);
await expect(rows.first()).toBeVisible({ timeout: 20_000 });
const units = await rows.evaluateAll((els) =>
els.flatMap((row) =>
Array.from(
row.querySelectorAll('[data-slot="timeline-mean-part"]'),
).map((unit) => {
const dot = unit
.querySelector('[data-slot="timeline-series-dot"]')!
.getBoundingClientRect();
const text = unit.querySelector(".truncate")!.getBoundingClientRect();
return {
dotMid: dot.top + dot.height / 2,
textMid: text.top + text.height / 2,
dotRight: dot.right,
textLeft: text.left,
textHeight: text.height,
};
}),
),
);
expect(units.length).toBeGreaterThan(0);
for (const unit of units) {
// Centred on the text line, to the pixel.
expect(Math.abs(unit.dotMid - unit.textMid)).toBeLessThanOrEqual(1);
// On the same line, the value right after its dot: one line high.
expect(unit.textLeft).toBeGreaterThan(unit.dotRight);
expect(unit.textLeft - unit.dotRight).toBeLessThanOrEqual(8);
expect(unit.textHeight).toBeLessThanOrEqual(20);
}
};
// A narrow chart: the selection bar's means wrap between units only.
await page.setViewportSize({ width: 800, height: 900 });
await page.goto("/timeline");
await expect(page.locator('[data-slot="timeline-chart"]')).toBeVisible({
timeout: 20_000,
});
await check("timeline-selection-means");
await testInfo.attach("selection-means-800", {
body: await page
.locator('[data-slot="timeline-selection-bar"]')
.screenshot(),
contentType: "image/png",
});
// The phone chronicle's means follow the same rule.
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/timeline");
await check("timeline-chronicle-means");
await testInfo.attach("chronicle-means-390", {
body: await page.screenshot(),
contentType: "image/png",
});
});
test("one medication is one row: its doses cut the bar, its pause is a gap in it", async ({
page,
}) => {
@@ -621,8 +683,10 @@ test("an open day stays open from the timeline to another page", async ({
day,
);
// Folded by the person, it stays folded on the next page.
await page.locator('[data-slot="day-close"]').click();
// Folded by the person (the strip is the docked day's only fold control),
// it stays folded on the next page.
await expect(page.locator('[data-slot="day-close"]')).toHaveCount(0);
await page.locator('[data-slot="day-strip-toggle"]').click();
await expect(panel).toHaveCount(0);
await page.locator('a[href="/timeline"]').first().click();
await expect(page).toHaveURL(/\/timeline$/);
@@ -799,9 +863,6 @@ test("the day's strip stays at the right edge, opens and closes the day, and nev
docSw: doc.scrollWidth - doc.clientWidth,
docSh: doc.scrollHeight - doc.clientHeight,
mainSw: main.scrollWidth - main.clientWidth,
bar: document
.querySelector('[data-slot="top-bar"]')!
.getBoundingClientRect().bottom,
};
});
@@ -812,17 +873,19 @@ test("the day's strip stays at the right edge, opens and closes the day, and nev
page.locator('[data-slot="timeline-chart"] > svg'),
).toBeVisible({ timeout: 20_000 });
// Closed: the strip is there, at the right edge, below the top bar's
// band, holding today.
// Closed: the strip is there, at the right edge, the full height of the
// window from its top edge, holding today.
await expect(strip).toHaveAttribute("data-state", "closed");
await expect(toggle).toHaveAttribute("aria-expanded", "false");
const closed = await where();
expect(closed[0] + closed[2], `${width}px right edge`).toBe(width);
const before = await measure();
expect(closed[1], `${width}px from the top edge`).toBe(0);
expect(closed[3], `${width}px full height`).toBe(900);
expect(
Math.round((await toggle.boundingBox())!.y),
`${width}px below the band`,
).toBe(Math.round(before.bar));
`${width}px toggle from the top edge`,
).toBe(0);
expect(before.docSw).toBeLessThanOrEqual(0);
expect(before.mainSw).toBeLessThanOrEqual(0);