Files
HealthLog/e2e/v138-managed-record-capabilities.spec.ts
T
Marc-André Bombeck 9cefaecf94 test(e2e): anchor absence claims on a rendered surface
The protected shell renders its hydration gate for every child until
/api/auth/me resolves, so after a goto, a reload or an exit click nothing
exists yet: no nav, no top bar, no banner, no page body. Fifteen absence
assertions across twelve specs were asserted inside that window. Each was
true because the surface had not rendered, and each would have stayed green
if the thing it forbids came back.

Every site now waits for a positive marker of the loaded state first: the
document sheet's footer download action, the settled integrations card
rather than the family attribute its skeleton shares, the top bar after an
exit, the page's own heading, the delegate's own marker, the Coach FAB. Two
skeleton-absence waits are replaced by waits on the content that follows.

delegated-writes had a different cause: it asserted the bulk selection bar
absent although no row was ever selected, so it passed for an owner too. The
gate is one level earlier - below MANAGE no row carries a checkbox at all -
so the test asserts that instead, with the bar and the row delete kept as
backstops.

No sleeps added, no timeouts raised, no claim weakened.
2026-09-11 13:00:56 +02:00

157 lines
5.6 KiB
TypeScript

/**
* v1.38.12 — the controls a guardian sees inside a managed profile (#939).
*
* A person acting on a managed profile holds a MANAGE grant, and the server
* has accepted their mood entries, screeners, visits, allergies and lab edits
* since v1.37.0. The interface withheld every one of those controls anyway,
* because the client answered `canManage: false` for every shared record. It
* answers per section now, from the two lists `GET /api/auth/me` publishes.
*
* Two pages, one each way:
*
* - Mood is a section whose create answers at MANAGE, so the add control
* is there and an entry can be added — asserted on the POST's status, not
* on the paint, because the paint is what lied before.
* - Documents is the section with no delegated write route, so the upload
* control is absent and the page says why, in a sentence, where the
* control would have been.
*
* Modelled on `v137-sharing-managed-profiles.spec.ts`; the switch helpers are
* the same ones, for the same reasons written there.
*/
import type { Page } from "@playwright/test";
import { expect, test } from "./setup/test";
import {
E2E_LEVEL_RECORDS,
SCOPE_CAPABILITIES_STORAGE_STATE_PATH,
} from "./setup/test-helpers";
async function openSwitcher(page: Page) {
await page.getByRole("button", { name: "User menu" }).first().click();
await page.locator('[data-slot="account-switcher-trigger"]').click();
await expect(
page.locator('[data-slot="account-switcher-menu"]'),
).toBeVisible();
}
/**
* Run `act` and wait for the document it replaces to be gone. See the
* v1.37.0 spec for why waiting on the banner alone settles nothing.
*/
async function withDocumentReplacement(
page: Page,
act: () => Promise<void>,
): Promise<void> {
await page.evaluate(() => {
(window as Window & { __hlNavToken?: true }).__hlNavToken = true;
});
await act();
await page.waitForFunction(
() =>
(window as Window & { __hlNavToken?: true }).__hlNavToken === undefined,
undefined,
{ timeout: 30_000 },
);
}
async function openRecord(page: Page, username: string) {
await page.goto("/");
await openSwitcher(page);
const entry = page.locator(
`[data-slot="account-switcher-entry"][data-account-username="${username}"]`,
);
await expect(entry).toHaveCount(1);
await withDocumentReplacement(page, () => entry.click());
await expect(page.locator('[data-slot="shared-record-banner"]')).toBeVisible({
timeout: 30_000,
});
}
/**
* Leave the record and prove the own-record shell is back.
*
* Leaving replaces the document, and the protected shell renders its
* hydration gate — no nav, no banner, no page body — until `/api/auth/me`
* resolves. "The banner is gone" is true of that gate as well, so on its own
* it is satisfied by the document on its way out and is not a wait at all.
* The top bar only exists past the gate, so it is the anchor the absence
* hangs on.
*/
async function leaveRecord(page: Page) {
await withDocumentReplacement(page, () =>
page.locator('[data-slot="shared-record-banner-exit"]').click(),
);
await expect(
page.locator('[data-slot="record-scope-hydration-gate"]'),
).toHaveCount(0, { timeout: 30_000 });
await expect(page.locator('[data-slot="top-bar"]')).toBeVisible({
timeout: 30_000,
});
await expect(page.locator('[data-slot="shared-record-banner"]')).toHaveCount(
0,
{ timeout: 30_000 },
);
}
test.describe.serial("a guardian's controls inside a managed profile", () => {
// Its own session row: this journey moves the record selector, and the
// scoped-sharing journey drives the same account from another worker.
test.use({ storageState: SCOPE_CAPABILITIES_STORAGE_STATE_PATH });
const managed = E2E_LEVEL_RECORDS.find(
(record) => record.recordKind === "managed",
);
if (!managed) throw new Error("managed record fixture is missing");
test("offers the mood add control and lands the entry", async ({ page }) => {
await openRecord(page, managed.username);
const banner = page.locator('[data-slot="shared-record-banner"]');
await expect(banner).toHaveAttribute("data-record-kind", "managed");
await expect(banner).toHaveAttribute("data-access-level", "manage");
await page.goto("/mood");
const add = page.locator('[data-slot="mood-add-entry"]');
await expect(add).toBeVisible({ timeout: 30_000 });
await add.click();
const face = page.locator('[data-slot="mood-face"][data-mood="GUT"]');
await expect(face).toBeVisible();
await face.click();
const posted = page.waitForResponse(
(response) =>
response.request().method() === "POST" &&
/\/api\/mood-entries(\?|$)/.test(response.url()),
{ timeout: 30_000 },
);
await page.getByRole("button", { name: "Save", exact: true }).click();
const response = await posted;
// The route, not the paint: a control that renders and 403s is the exact
// failure this release ends.
expect(response.status()).toBe(201);
await expect(page.locator('[data-slot="mood-rows"]').first()).toBeVisible({
timeout: 30_000,
});
await leaveRecord(page);
});
test("withholds the vault's upload control and says why", async ({
page,
}) => {
await openRecord(page, managed.username);
await page.goto("/documents");
const note = page.locator('[data-slot="documents-owner-only"]');
await expect(note).toBeVisible({ timeout: 30_000 });
await expect(note).toHaveText(
"Documents can only be added by the record owner.",
);
await expect(page.getByRole("button", { name: "Upload" })).toHaveCount(0);
await expect(page.locator('input[type="file"]')).toHaveCount(0);
await leaveRecord(page);
});
});