Files
HealthLog/e2e/coach-conversation-rename.spec.ts
Marc-André Bombeck b7830c3a72 test(e2e): cover Insights and the Coach memory without AI
Adds the AI-optional journeys: the overview with AI switched off by the
operator and with no provider (no AI requests, no refusals, one setup
hint, remembered dismissal), and on an account of its own, withdrawing AI
consent deletes the stored briefing, and a hidden Coach's conversation is
read and deleted from Settings, AI.

Specs that drive AI surfaces against mocked routes now serve an available
capability map through /api/auth/me, since the web no longer assumes AI
is on until told otherwise.
2026-09-24 11:36:30 +02:00

124 lines
4.0 KiB
TypeScript

import { expect, test } from "./setup/test";
import { setTimeout as delay } from "node:timers/promises";
import { STORAGE_STATE_PATH } from "./setup/global-setup";
import { aiBlockAvailable, serveAiBlock } from "./setup/ai-capabilities";
const CONVERSATION_ID = "conversation-rename-e2e";
const INITIAL_TITLE = "Morning check-in";
const RENAMED_TITLE = "Weekly health review";
test.describe("Coach conversation rename", () => {
// AI surfaces render from the `ai` block of `/api/auth/me`, and the suite
// has no reachable provider; every capability reads available here so the
// mocked AI routes below are what the surfaces talk to.
test.beforeEach(async ({ page }) => {
await serveAiBlock(page, aiBlockAvailable());
});
test.use({ storageState: STORAGE_STATE_PATH });
test("renames by keyboard once and survives reload on desktop and mobile", async ({
page,
}) => {
let persistedTitle = INITIAL_TITLE;
let patchCount = 0;
await page.route(
/\/api\/insights\/chat(?:\/[^?]+)?(?:\?|$)/,
async (route) => {
const request = route.request();
const url = new URL(request.url());
if (
request.method() === "PATCH" &&
url.pathname === `/api/insights/chat/${CONVERSATION_ID}`
) {
patchCount += 1;
const payload = JSON.parse(request.postData() ?? "{}") as {
title: string;
};
persistedTitle = payload.title;
await delay(150);
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
data: { id: CONVERSATION_ID, title: persistedTitle },
error: null,
}),
});
return;
}
return route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
data: {
conversations: [
{
id: CONVERSATION_ID,
title: persistedTitle,
createdAt: "2026-07-20T10:00:00.000Z",
updatedAt: "2026-07-20T10:00:00.000Z",
messageCount: 2,
fenced: false,
attachments: [],
documentTitle: null,
},
],
nextCursor: null,
},
error: null,
}),
});
},
);
await page.goto("/coach/conversations", {
waitUntil: "domcontentloaded",
});
const item = page.locator('[data-slot="coach-conversations-item"]');
await expect(item).toContainText(INITIAL_TITLE, { timeout: 10_000 });
const renameButton = item.locator(
'[data-slot="coach-conversation-rename"]',
);
await renameButton.click();
const input = item.locator('[data-slot="coach-conversation-rename-input"]');
await expect(input).toBeFocused();
await input.fill("Discard this draft");
await input.press("Escape");
await expect(input).toHaveCount(0);
await expect(item).toContainText(INITIAL_TITLE);
expect(patchCount).toBe(0);
await renameButton.click();
const activeInput = item.locator(
'[data-slot="coach-conversation-rename-input"]',
);
await activeInput.fill(` ${RENAMED_TITLE} `);
await activeInput.evaluate((element) => {
element.dispatchEvent(
new KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
);
element.dispatchEvent(
new KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
);
});
await expect(activeInput).toBeDisabled();
await expect(item).toContainText(RENAMED_TITLE);
await expect(activeInput).toHaveCount(0, { timeout: 10_000 });
expect(patchCount).toBe(1);
await page.reload({ waitUntil: "domcontentloaded" });
await expect(
page.locator('[data-slot="coach-conversations-item"]'),
).toContainText(RENAMED_TITLE, { timeout: 10_000 });
expect(patchCount).toBe(1);
});
});