mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
Thirty-one files carried internal build vocabulary and anchors pointing at notes that are not in the repository: a reader of `checklist.ts` in a year would find its rationale keyed to a research id they cannot open, inside a comment that also names an internal build stage. Two test titles carried the same anchors. The version tags and the whole of the reasoning stay; only the vocabulary and the dangling references go, with the few sentences that were built around an anchor rewritten to stand on their own.
213 lines
7.4 KiB
TypeScript
213 lines
7.4 KiB
TypeScript
/**
|
|
* v1.38.19 — the setup flow, path 5: a wearable on Q4.
|
|
*
|
|
* The connect arm is the only place in the flow that renders a claim about
|
|
* real account state, and until this file no journey reached it: bp answers
|
|
* `manual`, visit answers `file`, and the child path ends at confirm. The
|
|
* a11y sweep does open the screen, but asserted the connect card was visible
|
|
* — which it was, for the few hundred milliseconds before the status query
|
|
* resolved. So the sweep passed THROUGH the defect it should have caught.
|
|
*
|
|
* Both cases here assert AFTER `GET /api/integrations/status` has settled,
|
|
* which is the only moment at which the screen's claim means anything.
|
|
*
|
|
* Runs in one project (see `playwright.config.ts`): it resets and mutates its
|
|
* own account.
|
|
*/
|
|
import pg from "pg";
|
|
|
|
import {
|
|
E2E_SETUP_CONNECT,
|
|
SETUP_CONNECT_STORAGE_STATE_PATH,
|
|
} from "./setup/global-setup";
|
|
import { resetSetupFlow } from "./setup/setup-flow-fixture";
|
|
import type { Page, Response } from "@playwright/test";
|
|
|
|
import { expect, test } from "./setup/test";
|
|
import { answer, expectScreen, readMe } from "./setup-flow-helpers";
|
|
|
|
function pool(): pg.Pool {
|
|
const url = process.env.DATABASE_URL;
|
|
if (!url) throw new Error("[setup-flow-connect] DATABASE_URL is not set");
|
|
return new pg.Pool({ connectionString: url });
|
|
}
|
|
|
|
/**
|
|
* Put the account's Oura back to "never connected". The journey's second case
|
|
* writes a token column and a ledger row, neither of which `resetSetupFlow`
|
|
* knows about — it resets the FLOW, and a leftover connection would make the
|
|
* first case assert against the second case's state.
|
|
*/
|
|
async function clearOura(): Promise<void> {
|
|
const db = pool();
|
|
try {
|
|
const { rows } = await db.query<{ id: string }>(
|
|
`SELECT id FROM users WHERE username = $1`,
|
|
[E2E_SETUP_CONNECT.username],
|
|
);
|
|
const id = rows[0]?.id;
|
|
if (!id) throw new Error("[setup-flow-connect] no account");
|
|
await db.query(
|
|
`UPDATE users SET oura_access_token_encrypted = NULL WHERE id = $1`,
|
|
[id],
|
|
);
|
|
await db.query(
|
|
`DELETE FROM integration_statuses WHERE user_id = $1 AND integration = 'oura'`,
|
|
[id],
|
|
);
|
|
} finally {
|
|
await db.end();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A connection that has been delivering for an hour: a stored token (the
|
|
* envelope reads `connected` off the column's presence alone) and a ledger
|
|
* row whose last success is recent enough for the `fresh` verdict.
|
|
*/
|
|
async function seedDeliveringOura(): Promise<void> {
|
|
const db = pool();
|
|
try {
|
|
const { rows } = await db.query<{ id: string }>(
|
|
`SELECT id FROM users WHERE username = $1`,
|
|
[E2E_SETUP_CONNECT.username],
|
|
);
|
|
const id = rows[0]?.id;
|
|
if (!id) throw new Error("[setup-flow-connect] no account");
|
|
await db.query(
|
|
`UPDATE users SET oura_access_token_encrypted = 'e2e-placeholder' WHERE id = $1`,
|
|
[id],
|
|
);
|
|
await db.query(
|
|
`INSERT INTO integration_statuses
|
|
(id, user_id, integration, state, last_success_at, last_attempt_at, created_at, updated_at)
|
|
VALUES ($1, $2, 'oura', 'connected', NOW(), NOW(), NOW(), NOW())
|
|
ON CONFLICT (user_id, integration) DO UPDATE
|
|
SET state = 'connected',
|
|
last_success_at = NOW(),
|
|
last_attempt_at = NOW(),
|
|
updated_at = NOW()`,
|
|
[`e2e-connect-oura-${Date.now()}`, id],
|
|
);
|
|
} finally {
|
|
await db.end();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Walk the questions through the API, then open the screen under test.
|
|
*
|
|
* `areas` decides whether the priority has anywhere to fall to when the Q4
|
|
* source turns out to be connected already: a Q2 area is a `log-reading` task,
|
|
* an empty Q2 is nothing.
|
|
*/
|
|
async function reachFirstResult(
|
|
page: Page,
|
|
areas: readonly string[],
|
|
): Promise<void> {
|
|
await answer(page, { step: "who", recordTarget: "me" });
|
|
await answer(page, {
|
|
step: "areas",
|
|
...(areas.length > 0 ? { areas } : { status: "skipped" }),
|
|
});
|
|
await answer(page, { step: "medication", medication: "no" });
|
|
await answer(page, { step: "sources", sources: ["oura"] });
|
|
await answer(page, { step: "visit", visit: "no" });
|
|
const settled = page.waitForResponse(
|
|
(res: Response) =>
|
|
res.url().includes("/api/integrations/status") && res.status() === 200,
|
|
);
|
|
await page.goto("/onboarding/confirm");
|
|
await expectScreen(page, "confirm");
|
|
await page.locator('[data-slot="onboarding-next"]').click();
|
|
await expectScreen(page, "first-result");
|
|
// Every assertion below is about what the screen says once it KNOWS.
|
|
await settled;
|
|
}
|
|
|
|
// The three cases drive the one account, each setting up a different
|
|
// connection state. Run in parallel they would each walk the others' state.
|
|
test.describe.configure({ mode: "serial" });
|
|
|
|
test.describe("setup flow — a wearable on Q4", () => {
|
|
test.use({ storageState: SETUP_CONNECT_STORAGE_STATE_PATH });
|
|
|
|
test.beforeEach(async () => {
|
|
await resetSetupFlow(E2E_SETUP_CONNECT.username);
|
|
await clearOura();
|
|
});
|
|
|
|
test.afterAll(async () => {
|
|
await clearOura();
|
|
});
|
|
|
|
test("never claims a connection the account does not have", async ({
|
|
page,
|
|
}) => {
|
|
test.setTimeout(90_000);
|
|
await page.goto("/onboarding");
|
|
await reachFirstResult(page, ["sleep"]);
|
|
|
|
// The tile the screen settled on: either the connect card, or — on an
|
|
// instance with no Oura app configured — the credentials note. Never a
|
|
// result: nothing was connected.
|
|
const tile = page.locator("[data-connect-slot]");
|
|
await expect(tile).toBeVisible({ timeout: 15_000 });
|
|
const slot = await tile.getAttribute("data-connect-slot");
|
|
expect(["connect", "credentials"]).toContain(slot);
|
|
await expect(tile).toHaveAttribute("data-connect-state", "disconnected");
|
|
await expect(
|
|
page.locator('[data-slot="onboarding-first-result-done"]'),
|
|
).toHaveCount(0);
|
|
|
|
// And the ledger was not stamped on the person's behalf.
|
|
const me = await readMe(page);
|
|
expect(me.onboarding.firstResult?.completedAt ?? null).toBeNull();
|
|
});
|
|
|
|
test("offers the next task instead of a connection that already works", async ({
|
|
page,
|
|
}) => {
|
|
test.setTimeout(90_000);
|
|
await seedDeliveringOura();
|
|
await page.goto("/onboarding");
|
|
await reachFirstResult(page, ["sleep"]);
|
|
|
|
// The answers name one area as well, so the priority has somewhere to
|
|
// fall to and the flow ends on something the person can actually do.
|
|
await expect(page.locator("section[data-task]")).toHaveAttribute(
|
|
"data-task",
|
|
"log-reading",
|
|
);
|
|
await expect(
|
|
page.locator('[data-slot="onboarding-task-connect"]'),
|
|
).toHaveCount(0);
|
|
});
|
|
|
|
test("acknowledges a connection that is already delivering", async ({
|
|
page,
|
|
}) => {
|
|
test.setTimeout(90_000);
|
|
await seedDeliveringOura();
|
|
await page.goto("/onboarding");
|
|
// Q2 passed over, so the connection is the only task the answers name.
|
|
await reachFirstResult(page, []);
|
|
|
|
const tile = page.locator('[data-connect-slot="result"]');
|
|
await expect(tile).toBeVisible({ timeout: 15_000 });
|
|
await expect(tile).toHaveAttribute("data-connect-state", "fresh");
|
|
|
|
// The acknowledgment replaces the offer: the connect CTA is gone.
|
|
await expect(
|
|
page.locator('[data-slot="onboarding-task-connect"]'),
|
|
).toHaveCount(0);
|
|
|
|
// A working connection is a result the flow may record.
|
|
await expect
|
|
.poll(async () => (await readMe(page)).onboarding.firstResult?.task, {
|
|
timeout: 15_000,
|
|
})
|
|
.toBe("connect-source");
|
|
});
|
|
});
|