test: cover the tile's rate attribute in the unit gate, and correct two notes

`data-latest-rate` is a production attribute whose only reader was the browser
suite, so a change to the rate's shape would have surfaced three minutes and a
whole server away from the gate that should own it. One unit test now renders
the chart over a loaded window and asserts the root carries the last day's
rate.

Two comments were wrong. The card's take / skip buttons do not collapse to
icons at narrow widths — both render their label unconditionally beside the
icon — so only the true half of the reason for addressing them by `data-slot`
survives: the labels are translated copy.

And the Enter press in the wizard's times-of-day helper is not decoration.
The field's overlay input holds what is being typed and hands it to the form
only on Enter or on blur (`src/components/ui/time-field.tsx:175-184`), while
the add button is disabled on an empty draft — so a fill followed straight by
a click waits out the actionability timeout on a button that never enables.
Proven by removing it: the flow died at exactly that click. The line stays,
with the reason written beside it.
This commit is contained in:
Marc-André Bombeck committed 2026-09-10 03:45:32 +02:00
1 parent c39bf0d0ed
commit db8ac95342
3 files changed
+54 -13

No files matched your search

+22 -9
View File
@@ -13,19 +13,23 @@
* 1. Journey — a twice-daily medication created through the real wizard, one
* dose taken and one skipped from the card, and the resulting figure read
* back on the card, in the intake history and on the dashboard tile.
* 2. Refusal — a dose recorded against a schedule that does not exist is
* refused, and the rate is exactly what it was before.
* 2. Refusal — an intake against a medication this account does not own, and
* one against a slot its schedule does not have, are both refused and the
* rate is exactly what it was before.
* 3. Cadence — a medication due on two weekdays does not count the other
* five as missed. Its daily twin, given the identical doses, does.
*
* Every assertion addresses a `data-slot` / `data-*` attribute rather than
* viewport text: the copy is i18n-driven, the wizard runs in German and the
* card's own labels collapse to icons at narrow widths.
* viewport text: the copy is i18n-driven and the wizard runs in German, so
* addressing a control by its label would assert the translation.
*
* The flows mutate the one seeded account and read counts back off it, so —
* like `visits.spec.ts` and `vaccinations.spec.ts` — this file runs in a
* single project (see `playwright.config.ts`) and serial, and clears the
* cabinet before each test so a rate counts only the doses the test wrote.
* The flows write rows and read counts back off them, so they run on their own
* account (`E2E_MEDICATION`) — the rate a card shows is a percentage over one
* cabinet, and a sibling spec mounting `/medications` in the other worker
* would otherwise be spending the same per-actor read allowance and sitting in
* the same dashboard denominator. The file still runs serial and in a single
* project (see `playwright.config.ts`), because its three tests share that one
* cabinet with each other.
*
* Why the medications are aged (`ageMedication`): compliance is reconstructed
* from the medication's creation stamp forward, so a medication created a
@@ -252,7 +256,16 @@ async function clockAnchoredDoseTimes(
});
}
/** Add one `HH:mm` to the wizard's times-of-day chips. */
/**
* Add one `HH:mm` to the wizard's times-of-day chips.
*
* The Enter press is the commit, not a flourish: the field's overlay input
* holds what is being typed and only hands it to the form on Enter or on blur
* (`src/components/ui/time-field.tsx:175-184`). Until then the draft is empty,
* and the add button is `disabled` on exactly that
* (`times-of-day-chips.tsx:267`) — so a fill followed straight by a click
* waits out the actionability timeout on a button that never enables.
*/
async function addDoseTime(page: Page, time: string): Promise<void> {
const chips = page.locator('[data-slot="times-of-day-chips"]');
const draft = chips.getByTestId("times-of-day-draft-input");
@@ -15,8 +15,16 @@ import {
* aggregation helper produces the correct rates.
*/
/**
* The daily points the mocked query serves. Mutable so one test can hand the
* chart a loaded window; every other test leaves it empty.
*/
const queryState = vi.hoisted(() => ({
data: [] as { date: string; scheduled: number; taken: number }[],
}));
vi.mock("@tanstack/react-query", () => ({
useQuery: () => ({ data: [], isLoading: false }),
useQuery: () => ({ data: queryState.data, isLoading: false }),
useQueryClient: () => ({
cancelQueries: () => Promise.resolve(),
getQueryData: () => undefined,
@@ -195,4 +203,24 @@ describe("<MedicationComplianceChart>", () => {
const html = render(<MedicationComplianceChart />);
expect(html).not.toContain('data-slot="medication-trend-chip"');
});
/**
* The tile's adherence number exists only as bar geometry, so the root's
* `data-latest-rate` is the one way to read it back — and the browser suite
* was its only reader, which put a production attribute's shape three
* minutes and a whole server away from the gate that should own it. Pinned
* here: the value is the LAST day's rate, as an integer.
*/
it("carries the last day's rate on the root as data-latest-rate", () => {
queryState.data = [
{ date: "2026-05-01", scheduled: 2, taken: 2 }, // 100 %
{ date: "2026-05-02", scheduled: 4, taken: 1 }, // 25 %
];
try {
const html = render(<MedicationComplianceChart />);
expect(html).toContain('data-latest-rate="25"');
} finally {
queryState.data = [];
}
});
});
@@ -24,9 +24,9 @@ interface MedicationIntakeActionsProps {
* structurally rather than kept byte-equivalent by hand.
*
* Both buttons carry a `data-slot`. They are the only controls on the card
* that write, and their labels are i18n copy that also collapses to an icon
* on the narrow card — an e2e journey addressing them by text would be
* asserting the translation, not the control.
* that write, and their labels are translated copy — an e2e journey
* addressing them by text would be asserting the translation, not the
* control.
*/
export function MedicationIntakeActions({
intakeLoading,