mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
Output of `node scripts/strip-planning-tags.mjs --write`: 3,157 comment lines in 1,308 files lose the work-item identifier at their head. Release versions and issue references stay; nothing outside comment text changes, which the script checks per file against a comment-free printout. healthlog/no-planning-tags is enabled at error level with this commit, so the tree it lands on is clean.
155 lines
5.3 KiB
TypeScript
155 lines
5.3 KiB
TypeScript
/**
|
|
* v1.39 — driving the setup flow in a browser, shared by its journeys.
|
|
*
|
|
* Everything here addresses stable `data-slot` / `data-*` attributes, never
|
|
* viewport text: the copy is i18n-driven and one of the specs runs every
|
|
* locale. The API helpers ride the page's own cookie jar, so what they write
|
|
* lands on the account the browser is signed in as.
|
|
*/
|
|
import { expect, type Page } from "@playwright/test";
|
|
|
|
export type SetupScreen =
|
|
| "welcome"
|
|
| "who"
|
|
| "areas"
|
|
| "medication"
|
|
| "sources"
|
|
| "visit"
|
|
| "units"
|
|
| "confirm"
|
|
| "first-result"
|
|
| "done";
|
|
|
|
export const SHELL = '[data-slot="onboarding-shell"]';
|
|
|
|
export async function expectScreen(page: Page, screen: SetupScreen) {
|
|
await expect(page.locator(SHELL)).toHaveAttribute("data-screen", screen, {
|
|
timeout: 15_000,
|
|
});
|
|
}
|
|
|
|
/** Tick one or more chips of the question on screen. */
|
|
export async function choose(
|
|
page: Page,
|
|
question: string,
|
|
values: readonly string[],
|
|
): Promise<void> {
|
|
for (const value of values) {
|
|
await page
|
|
.locator(
|
|
`[data-slot="onboarding-question-${question}"] [data-slot="onboarding-choice"][data-value="${value}"]`,
|
|
)
|
|
.click();
|
|
}
|
|
}
|
|
|
|
export async function next(page: Page): Promise<void> {
|
|
await page.locator('[data-slot="onboarding-next"]').click();
|
|
}
|
|
|
|
export async function skip(page: Page): Promise<void> {
|
|
await page.locator('[data-slot="onboarding-skip"]').click();
|
|
}
|
|
|
|
/** The welcome screen's acknowledgment plus "Set up". */
|
|
export async function acceptAndSetUp(page: Page): Promise<void> {
|
|
await expectScreen(page, "welcome");
|
|
const box = page.locator('[data-slot="onboarding-disclaimer"]');
|
|
if ((await box.getAttribute("data-state")) !== "checked") {
|
|
await box.click();
|
|
}
|
|
await page.locator('[data-slot="onboarding-set-up"]').click();
|
|
await expectScreen(page, "who");
|
|
}
|
|
|
|
/**
|
|
* One call on the browser's own session, sent with the page's own fetch.
|
|
*
|
|
* Every Playwright API context in the runner shares one keep-alive agent,
|
|
* and it can hand out a socket the server already closed on its idle
|
|
* timeout; the call then dies with `read ECONNRESET` and CI counts the retry
|
|
* as a failure. The page opens its own connection. A page that has not
|
|
* navigated yet has no origin to fetch against, so it first opens the public
|
|
* version endpoint: the specs call these helpers before their first real
|
|
* navigation, and routing that case back through the API context is what
|
|
* kept the flake alive.
|
|
*/
|
|
async function sessionCall(
|
|
page: Page,
|
|
method: "GET" | "POST" | "PATCH",
|
|
path: string,
|
|
body?: unknown,
|
|
): Promise<{ status: number; json: unknown }> {
|
|
if (!page.url().startsWith("http")) {
|
|
await page.goto("/api/version", { waitUntil: "domcontentloaded" });
|
|
}
|
|
return page.evaluate(
|
|
async ({ method, path, body }) => {
|
|
const res = await fetch(path, {
|
|
method,
|
|
headers:
|
|
body === undefined
|
|
? undefined
|
|
: { "Content-Type": "application/json" },
|
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
});
|
|
return { status: res.status, json: await res.json().catch(() => null) };
|
|
},
|
|
{ method, path, body },
|
|
);
|
|
}
|
|
|
|
/** The account payload, as the browser's session sees it. */
|
|
export async function readMe(page: Page): Promise<MePayload> {
|
|
const res = await sessionCall(page, "GET", "/api/auth/me");
|
|
expect(res.status, "reading the account payload").toBe(200);
|
|
return (res.json as { data: MePayload }).data;
|
|
}
|
|
|
|
export interface MePayload {
|
|
modules: Record<string, boolean>;
|
|
onboarding: {
|
|
steps: Array<{ id: string; status: string }>;
|
|
needs: { recordTarget: string | null; areas: string[] };
|
|
completedAt: string | null;
|
|
firstResult: { task: string; completedAt: string | null } | null;
|
|
};
|
|
accountAccess?: {
|
|
accounts: Array<{ accountId: string; recordKind: string }>;
|
|
};
|
|
}
|
|
|
|
/** The dashboard layout's tile ids, in order. */
|
|
export async function readTileOrder(page: Page): Promise<string[]> {
|
|
const res = await sessionCall(page, "GET", "/api/dashboard/widgets");
|
|
expect(res.status, "reading the dashboard layout").toBe(200);
|
|
const { data } = res.json as {
|
|
data: { widgets: Array<{ id: string; order: number }> };
|
|
};
|
|
return [...data.widgets].sort((a, b) => a.order - b.order).map((w) => w.id);
|
|
}
|
|
|
|
/** One answer through the answers route, on the browser's own session. */
|
|
export async function answer(page: Page, body: unknown): Promise<void> {
|
|
const res = await sessionCall(page, "PATCH", "/api/onboarding/answers", body);
|
|
expect(res.status, `answering ${JSON.stringify(body)}`).toBe(200);
|
|
}
|
|
|
|
export async function complete(page: Page): Promise<void> {
|
|
const res = await sessionCall(page, "POST", "/api/onboarding/complete", {});
|
|
expect(res.status, "completing the flow").toBe(200);
|
|
}
|
|
|
|
/**
|
|
* Answer every question through the API so a later screen can be visited
|
|
* directly. The answers name a glucose area (so Q6 applies) and a wearable
|
|
* (so the first-result task is a connection the screen can wait for).
|
|
*/
|
|
export async function answerEverything(page: Page): Promise<void> {
|
|
await answer(page, { step: "who", recordTarget: "me" });
|
|
await answer(page, { step: "areas", areas: ["glucose", "weight-body"] });
|
|
await answer(page, { step: "medication", medication: "yes" });
|
|
await answer(page, { step: "sources", sources: ["oura"] });
|
|
await answer(page, { step: "visit", visit: "within-a-month" });
|
|
}
|