Say what the level means on both screens that decide it

The invitation form gains the choice, and the option labels say what actually
ships rather than the word "write": a delegate can add readings, results,
observations, a medication and a marked dose, and can edit or delete nothing,
including what they entered a moment ago. That gap between what "write access"
sounds like and what it does is where a support case lives, so it is on the
card rather than in a reply.

The delegate reads the same thing before they accept. A pending write
invitation states its level and one sentence about what accepting means, on the
screen where they accept it, because acceptance is where the two consents meet
and it happens once.

Ending an access says what stays: the entries remain and the owner can remove
any of them, and the activity view keeps saying who entered what for as long as
the instance's audit window reaches. That number comes from the server, so an
instance running ninety days says ninety, and a browser that has not been told
says nothing rather than a made-up year.

The browser journey now invites at the wider level and reads the posted body
for it. That is the only check in the tree that can: the component convention
is server-rendered only, so no test under src/ runs the form's submit path, and
every one of them stays green with the control wired to a constant.
This commit is contained in:
Marc-André Bombeck committed 2026-08-03 10:12:22 +02:00
1 parent 533400ce60
commit d82e29737b
13 files changed
+451 -7

No files matched your search

+29
View File
@@ -208,6 +208,21 @@ test.describe("account sharing", () => {
const chosenDay = isoDayFromNow(30);
await lapse.fill(chosenDay);
// The level, through the real control and onto the real wire.
//
// Same class of gap as the lapse date above, and the reason it is checked
// here rather than in a component test is worth stating: the project's
// component convention is SSR-only, so no test under `src/` can run this
// form's submit path at all. Wire the radio back to a constant and every
// one of those stays green. Only the posted body can tell the level a
// person picked from the level the card sends.
const writeOption = ownerPage.locator(
'[data-slot="grant-invite-access-option"][data-access="WRITE"]',
);
await expect(writeOption).toBeVisible();
await writeOption.click();
await expect(writeOption).toHaveAttribute("data-selected", "true");
const invitePost = ownerPage.waitForRequest(
(req) =>
req.method() === "POST" && req.url().endsWith("/api/account/grants"),
@@ -215,6 +230,7 @@ test.describe("account sharing", () => {
await submit.click();
const posted = JSON.parse((await invitePost).postData() ?? "{}") as {
expiresAt?: string | null;
access?: string;
};
expect(
posted.expiresAt,
@@ -222,6 +238,10 @@ test.describe("account sharing", () => {
).toBeTruthy();
// End of the chosen day, not the midnight that starts it.
expect(posted.expiresAt).toContain(chosenDay.slice(0, 8));
expect(
posted.access,
"the invitation must carry the level the owner chose",
).toBe("WRITE");
const row = ownerPage
.locator('[data-slot="grants-given-list"] [data-slot="grant-row"]')
@@ -229,6 +249,9 @@ test.describe("account sharing", () => {
await expect(row).toBeVisible();
// Pending, not active. The state is the server's answer, rendered.
await expect(row).toHaveAttribute("data-grant-state", "PENDING");
// And the level came back from the database on the list read, which is the
// far end of the same wire: form → POST → row → GET → attribute.
await expect(row).toHaveAttribute("data-grant-access", "WRITE");
});
test("the delegate is offered nothing until they accept", async ({
@@ -241,6 +264,12 @@ test.describe("account sharing", () => {
.first();
await expect(row).toBeVisible();
await expect(row).toHaveAttribute("data-grant-state", "PENDING");
// The delegate's half of the consent: what they are being offered, and
// what accepting it will mean, on the screen where they accept it.
await expect(row).toHaveAttribute("data-grant-access", "WRITE");
await expect(
row.locator('[data-slot="grant-write-consent"]'),
).toBeVisible();
// No switcher yet: `accountAccess.canSwitch` is false while the only
// grant is pending, and the menu binds that boolean.