mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
With the day panel docked beside the blood-pressure page (1280 px and up), each half of the stat card is a narrow column, and its four cells still went side by side because the step followed the window's width. Every cell came out narrower than "112 mmHg": the systolic values broke onto two lines, the two-digit diastolic ones next to them did not. The stat strip now joins a value to its unit with a no-break space, and each series is a container whose cells go four-up only when the series itself has the room: from 28rem for a half-card column, from 36rem for a full-width strip (where the 640 px window step put it, so the longest unit written, mL/(kg·min), keeps a cell it fits in). The loading shell follows the same steps. Every page with a stat strip (pulse, weight, the HealthKit, lab and custom-metric pages) shares the component. A component test pins the no-break pair and the container steps; the day-view spec measures every blood-pressure value beside the docked day at 1280 px and fails on the old markup.
503 lines
20 KiB
TypeScript
503 lines
20 KiB
TypeScript
import type { Page, TestInfo } from "@playwright/test";
|
|
|
|
import { mockDay } from "./setup/day-mock";
|
|
import { expect, test } from "./setup/test";
|
|
import { MOBILE_ROUTES_STORAGE_STATE_PATH } from "./setup/global-setup";
|
|
import { MOBILE_ROUTES_ANALYTE } from "./setup/mobile-routes-fixture";
|
|
|
|
/**
|
|
* v1.42 — the day view and its doors.
|
|
*
|
|
* The record is the phone-sweep account `globalSetup` seeds: a week of blood
|
|
* pressure, two lab readings, mood and an episode. The day itself
|
|
* (`GET /api/day/{date}`, `GET /api/day/index`) is answered by a route mock in
|
|
* the contract's shape, so the journeys pin the client: which door opens which
|
|
* day, what the history does, and where no door is.
|
|
*
|
|
* - a chart point opens the day beside the chart (desktop);
|
|
* - on a touch screen the first tap shows the value and the tooltip offers
|
|
* the day, the second opens it as a bottom sheet;
|
|
* - a date in a list opens the day while the row keeps its own target;
|
|
* - `?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 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`,
|
|
* 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 docked day's strip `day-strip`
|
|
* (+ `data-day`, `data-state`) with its `day-strip-toggle`.
|
|
*/
|
|
|
|
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);
|
|
}
|
|
|
|
test.describe("the day view", () => {
|
|
test.use({ storageState: MOBILE_ROUTES_STORAGE_STATE_PATH });
|
|
|
|
test.beforeEach(async ({ page, context }, testInfo) => {
|
|
const baseURL = testInfo.project.use.baseURL ?? "http://localhost:3000";
|
|
await context.addCookies([
|
|
{ name: "healthlog-locale", value: "en", url: baseURL },
|
|
]);
|
|
await mockDay(page);
|
|
});
|
|
|
|
test("a chart point opens its day beside the chart; Back closes it", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
test.skip(testInfo.project.name !== "chromium-desktop", "desktop click");
|
|
await page.setViewportSize({ width: 1440, height: 900 });
|
|
await page.goto("/insights/blood-pressure");
|
|
const plot = page.locator(
|
|
'[data-slot="chart-plot"][data-day-links="true"]',
|
|
);
|
|
await expect(plot).toBeVisible();
|
|
// The plot's right half holds the most recent points.
|
|
const box = (await plot.boundingBox())!;
|
|
await page.mouse.move(box.x + box.width * 0.8, box.y + box.height / 2);
|
|
await page.mouse.click(box.x + box.width * 0.8, box.y + box.height / 2);
|
|
|
|
await expect(panel(page)).toBeVisible();
|
|
await expect(panel(page)).toHaveAttribute("data-shell", "docked");
|
|
// A landmark beside the page, named by its date.
|
|
await expect(panel(page)).toHaveJSProperty("tagName", "ASIDE");
|
|
await expect(page).toHaveURL(/[?&]day=\d{4}-\d{2}-\d{2}/);
|
|
await expect(page.locator('[data-slot="day-focus"]')).toBeVisible();
|
|
// The chart beside it stays usable: the panel is not modal.
|
|
await expect(page.locator('[data-slot="sheet-overlay"]')).toHaveCount(0);
|
|
|
|
await page.goBack();
|
|
await expect(panel(page)).toHaveCount(0);
|
|
await expect(page).not.toHaveURL(/[?&]day=/);
|
|
});
|
|
|
|
test("a date in a list opens the day; the row keeps its own target", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/labs");
|
|
await page
|
|
.locator('[data-slot="lab-list-analyte"]')
|
|
.filter({ hasText: MOBILE_ROUTES_ANALYTE })
|
|
.first()
|
|
.click();
|
|
// The marker's readings, each dated.
|
|
await page.locator('a[href$="/values"]').first().click();
|
|
const link = page.locator('[data-slot="day-link"]:visible').first();
|
|
await expect(link).toBeVisible();
|
|
const day = await link.getAttribute("data-day");
|
|
const before = page.url();
|
|
await link.click();
|
|
await expect(panel(page)).toBeVisible();
|
|
await expect(page.locator('[data-slot="day-view"]')).toHaveAttribute(
|
|
"data-day",
|
|
day!,
|
|
);
|
|
// Same page underneath: the click opened the day, not the reading.
|
|
expect(new URL(page.url()).pathname).toBe(new URL(before).pathname);
|
|
// The reading the person came from sits on top, with the one before it.
|
|
await expect(page.locator('[data-slot="day-focus"]')).toBeVisible();
|
|
});
|
|
|
|
test("?day= opens the layer on any page; Escape closes it", async ({
|
|
page,
|
|
}) => {
|
|
const day = isoDaysAgo(3);
|
|
await page.goto(`/mood?day=${day}`);
|
|
await expect(panel(page)).toBeVisible();
|
|
await expect(page.locator('[data-slot="day-view"]')).toHaveAttribute(
|
|
"data-day",
|
|
day,
|
|
);
|
|
await page.keyboard.press("Escape");
|
|
await expect(panel(page)).toHaveCount(0);
|
|
await expect(page).toHaveURL(/\/mood$/);
|
|
});
|
|
|
|
test("the day's scores sit below its values, each opening its own page", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto(`/mood?day=${isoDaysAgo(3)}`);
|
|
await expect(panel(page)).toBeVisible();
|
|
const scores = page.locator('[data-slot="day-scores"]');
|
|
await expect(scores).toBeVisible();
|
|
const tiles = scores.locator('[data-slot="day-score"]');
|
|
await expect(tiles).toHaveCount(2);
|
|
await expect(
|
|
scores.locator('[data-slot="day-score"][data-score="healthScore"]'),
|
|
).toHaveAttribute("href", "/insights/health-score");
|
|
await expect(
|
|
scores.locator('[data-slot="day-score"][data-score="strain"]'),
|
|
).toHaveAttribute("href", "/insights/scores/strain");
|
|
// A score the day does not hold has no tile.
|
|
await expect(
|
|
scores.locator('[data-slot="day-score"][data-score="readiness"]'),
|
|
).toHaveCount(0);
|
|
// Below the values, above what happened.
|
|
const order = await page
|
|
.locator(
|
|
'[data-slot="day-values"], [data-slot="day-scores"], [data-slot="day-events"]',
|
|
)
|
|
.evaluateAll((nodes) =>
|
|
nodes.map((node) => node.getAttribute("data-slot")),
|
|
);
|
|
expect(order).toEqual(["day-values", "day-scores", "day-events"]);
|
|
});
|
|
|
|
test("the docked day folds beside its strip and comes back on another page", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
test.skip(
|
|
testInfo.project.name !== "chromium-desktop",
|
|
"docked from 1280 px",
|
|
);
|
|
const day = isoDaysAgo(3);
|
|
await page.goto(`/mood?day=${day}`);
|
|
await expect(panel(page)).toHaveAttribute("data-shell", "docked");
|
|
|
|
// 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.
|
|
await expect(rail).toHaveAttribute("data-day", day);
|
|
await expect(rail).toHaveAttribute("data-state", "closed");
|
|
await expect(expand).toBeFocused();
|
|
await expect(expand).toHaveAttribute("aria-expanded", "false");
|
|
await expect(expand).toHaveAttribute("aria-controls", "day-docked-panel");
|
|
// Named by its visible date (WCAG 2.5.3); the action is the tooltip.
|
|
expect(await expand.getAttribute("aria-label")).toBeNull();
|
|
expect(await expand.getAttribute("title")).toMatch(/^Show /);
|
|
expect((await rail.boundingBox())!.width).toBeLessThanOrEqual(40);
|
|
|
|
// Another page: the edge is still there and brings the same day back,
|
|
// from the keyboard too.
|
|
await page.goto("/labs");
|
|
await expect(rail).toHaveAttribute("data-day", day);
|
|
await expand.focus();
|
|
await page.keyboard.press("Enter");
|
|
await expect(panel(page)).toHaveAttribute("data-shell", "docked");
|
|
await expect(page.locator('[data-slot="day-view"]')).toHaveAttribute(
|
|
"data-day",
|
|
day,
|
|
);
|
|
await expect(page).toHaveURL(new RegExp(`/labs\\?day=${day}$`));
|
|
await expect(panel(page).locator("h2").first()).toBeFocused();
|
|
// Open, the strip stays where it was and says so.
|
|
await expect(rail).toHaveAttribute("data-state", "open");
|
|
await expect(expand).toHaveAttribute("aria-expanded", "true");
|
|
});
|
|
|
|
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);
|
|
// 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<string>((resolve) =>
|
|
requestAnimationFrame(() =>
|
|
setTimeout(() => resolve(getComputedStyle(el).pointerEvents)),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
.toBe("none");
|
|
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 strip behind", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
const desktop = testInfo.project.name === "chromium-desktop";
|
|
// Below 1280 px the day is a sheet from the right; on a phone, from the
|
|
// bottom. Neither collapses.
|
|
if (desktop) await page.setViewportSize({ width: 1024, height: 800 });
|
|
const day = isoDaysAgo(3);
|
|
await page.goto(`/mood?day=${day}`);
|
|
await expect(panel(page)).toHaveAttribute(
|
|
"data-shell",
|
|
desktop ? "sheet" : "bottom",
|
|
);
|
|
await expect(page.locator('[data-slot="day-close"]')).toHaveAttribute(
|
|
"aria-label",
|
|
"Close day",
|
|
);
|
|
await page.locator('[data-slot="day-close"]').click();
|
|
await expect(panel(page)).toHaveCount(0);
|
|
await expect(page).toHaveURL(/\/mood$/);
|
|
await expect(page.locator('[data-slot="day-strip"]')).toHaveCount(0);
|
|
});
|
|
|
|
test("beside the docked day, every blood-pressure value keeps its unit on its line", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
test.skip(
|
|
testInfo.project.name !== "chromium-desktop",
|
|
"docked from 1280 px",
|
|
);
|
|
// The narrowest docked width: each half of the stat card is narrowest
|
|
// here. Four cells a row left "1xx mmHg" no room, so the systolic values
|
|
// broke onto two lines while the two-digit diastolic ones fitted.
|
|
await page.setViewportSize({ width: 1280, height: 900 });
|
|
await page.goto(`/insights/blood-pressure?day=${isoDaysAgo(3)}`);
|
|
await expect(panel(page)).toHaveAttribute("data-shell", "docked");
|
|
const values = page.locator(
|
|
'[data-slot="metric-stat-series"] [data-slot="metric-stat"] > p:nth-child(2)',
|
|
);
|
|
await expect(values).toHaveCount(8);
|
|
const cells = await values.evaluateAll((els) =>
|
|
els.map((el) => {
|
|
const lineHeight = parseFloat(getComputedStyle(el).lineHeight);
|
|
return {
|
|
text: el.textContent ?? "",
|
|
lines: Math.round(el.getBoundingClientRect().height / lineHeight),
|
|
overflow: el.scrollWidth - el.clientWidth,
|
|
};
|
|
}),
|
|
);
|
|
for (const cell of cells) {
|
|
expect(cell.text, cell.text).toMatch(/mmHg$/);
|
|
expect(cell.lines, cell.text).toBe(1);
|
|
expect(cell.overflow, cell.text).toBeLessThanOrEqual(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,
|
|
}) => {
|
|
await page.goto("/mood?day=2999-01-01");
|
|
await expect(page).toHaveURL(/\/mood$/);
|
|
await expect(panel(page)).toHaveCount(0);
|
|
await page.goto("/mood?day=2026-02-30");
|
|
await expect(page).toHaveURL(/\/mood$/);
|
|
await expect(panel(page)).toHaveCount(0);
|
|
});
|
|
|
|
test("stepping between days never adds a history entry", async ({ page }) => {
|
|
await page.goto("/mood");
|
|
await page.locator('[data-slot="day-link"]:visible').first().click();
|
|
await expect(panel(page)).toBeVisible();
|
|
const start = await page.evaluate(() => window.history.length);
|
|
const first = await page
|
|
.locator('[data-slot="day-view"]')
|
|
.getAttribute("data-day");
|
|
for (let i = 0; i < 3; i += 1) {
|
|
await page.locator('[data-slot="day-prev"]').click();
|
|
}
|
|
await expect(page.locator('[data-slot="day-view"]')).not.toHaveAttribute(
|
|
"data-day",
|
|
first!,
|
|
);
|
|
expect(await page.evaluate(() => window.history.length)).toBe(start);
|
|
// One Back leaves the layer, however far it stepped.
|
|
await page.goBack();
|
|
await expect(panel(page)).toHaveCount(0);
|
|
await expect(page).toHaveURL(/\/mood$/);
|
|
});
|
|
|
|
test("on a phone the first tap shows the value, the second opens the day", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
test.skip(testInfo.project.name !== "chromium-mobile", "touch only");
|
|
await page.goto("/insights/blood-pressure");
|
|
const plot = page.locator(
|
|
'[data-slot="chart-plot"][data-day-links="true"]',
|
|
);
|
|
await plot.scrollIntoViewIfNeeded();
|
|
await expect(plot).toBeVisible();
|
|
const box = (await plot.boundingBox())!;
|
|
await page.touchscreen.tap(box.x + box.width * 0.8, box.y + box.height / 2);
|
|
const open = page.locator('[data-slot="chart-tooltip-open-day"]');
|
|
await expect(open).toBeVisible();
|
|
// A tap alone never opens a sheet.
|
|
await expect(panel(page)).toHaveCount(0);
|
|
await open.tap();
|
|
await expect(panel(page)).toBeVisible();
|
|
await expect(panel(page)).toHaveAttribute("data-shell", "bottom");
|
|
// The tapped value heads the day.
|
|
await expect(page.locator('[data-slot="day-focus"]')).toBeVisible();
|
|
});
|
|
|
|
test("the dashboard's today area offers no day door", async ({ page }) => {
|
|
await page.goto("/");
|
|
await expect(
|
|
page.locator('[data-slot="main-content-wrapper"]'),
|
|
).toBeVisible();
|
|
// The hero is today: nothing in it opens another day. (The charts
|
|
// further down do; `day-doors.spec.ts` walks one.)
|
|
await expect(
|
|
page.locator('[data-slot^="today-hero"]').first(),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.locator(
|
|
'[data-slot^="today-hero"] [data-slot="day-link"], [data-slot^="today-hero"] [data-day-links="true"]',
|
|
),
|
|
).toHaveCount(0);
|
|
});
|
|
});
|