mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
Merge branch 'feat/v144-userneeds' into release/v1.44.0
# Conflicts: # src/__tests__/sharing-surface-guard.test.ts # src/__tests__/workout-canonical-identity-guard.test.ts
This commit is contained in:
commit
aa899a9d54
86 files changed
+12949
-9
No files matched your search
@@ -0,0 +1,68 @@
|
||||
import { expect, test } from "./setup/test";
|
||||
|
||||
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
|
||||
/**
|
||||
* The activity calendar on the workouts page (D1036), at phone width: the
|
||||
* card renders a whole-week grid, pages back a period, switches to the week
|
||||
* view, and the goals sheet saves a goal the calendar then reports.
|
||||
*/
|
||||
test.describe("activity calendar", () => {
|
||||
test.use({
|
||||
storageState: STORAGE_STATE_PATH,
|
||||
viewport: { width: 390, height: 844 },
|
||||
});
|
||||
|
||||
test("draws the month, pages back and switches to a week", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/insights/workouts");
|
||||
const card = page.locator('[data-slot="activity-calendar"]');
|
||||
await expect(card).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[data-slot="activity-calendar-label"]'),
|
||||
).not.toHaveText("");
|
||||
|
||||
const days = page.locator('[data-slot="activity-calendar-day"]');
|
||||
const monthCount = await days.count();
|
||||
if (monthCount > 0) expect(monthCount % 7).toBe(0);
|
||||
|
||||
const label = page.locator('[data-slot="activity-calendar-label"]');
|
||||
const before = await label.textContent();
|
||||
await page.locator('[data-slot="activity-calendar-previous"]').click();
|
||||
await expect(label).not.toHaveText(before ?? "");
|
||||
|
||||
await card.getByRole("radio", { name: "Week" }).click();
|
||||
if ((await days.count()) > 0) await expect(days).toHaveCount(7);
|
||||
|
||||
// Nothing in the card pushes the page sideways on a phone.
|
||||
const overflow = await page.evaluate(
|
||||
() =>
|
||||
document.documentElement.scrollWidth -
|
||||
document.documentElement.clientWidth,
|
||||
);
|
||||
expect(overflow).toBeLessThanOrEqual(0);
|
||||
});
|
||||
|
||||
test("saves a goal and shows its progress", async ({ page }) => {
|
||||
await page.goto("/insights/workouts");
|
||||
await page.locator('[data-slot="activity-calendar-set-goals"]').click();
|
||||
await page.getByLabel("Steps per month").fill("150000");
|
||||
await page.locator('[data-slot="activity-goals-save"]').click();
|
||||
await expect(
|
||||
page
|
||||
.locator('[data-slot="activity-calendar-goals"]')
|
||||
.getByText("Steps per month"),
|
||||
).toBeVisible();
|
||||
|
||||
// Clear it again so the account is left as it was found.
|
||||
await page.locator('[data-slot="activity-calendar-set-goals"]').click();
|
||||
await page.getByLabel("Steps per month").fill("");
|
||||
await page.locator('[data-slot="activity-goals-save"]').click();
|
||||
await expect(
|
||||
page
|
||||
.locator('[data-slot="activity-calendar-goals"]')
|
||||
.getByText("Steps per month"),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import { expect, test } from "./setup/test";
|
||||
|
||||
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
|
||||
/**
|
||||
* Lab results from a spreadsheet (#1215), at phone width, against the real
|
||||
* server: the mapping is suggested from the headers, the check reports a
|
||||
* refused row by line and writes nothing, the import writes the rest, and a
|
||||
* second import of the same file only finds duplicates.
|
||||
*/
|
||||
function csv(): string {
|
||||
const day = new Date(Date.now() - 30 * 86_400_000).toISOString().slice(0, 10);
|
||||
return [
|
||||
"Analyte;Value;Unit;Date",
|
||||
`E2E Marker CSV;5,4;mmol/L;${day}`,
|
||||
`E2E Marker CSV;1.234;mmol/L;${day}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
test.describe("record CSV import", () => {
|
||||
test.use({
|
||||
storageState: STORAGE_STATE_PATH,
|
||||
viewport: { width: 390, height: 844 },
|
||||
});
|
||||
|
||||
test("checks, imports and then only finds duplicates", async ({ page }) => {
|
||||
await page.goto("/settings/export");
|
||||
await page.locator('[data-slot="record-import-open-labs"]').click();
|
||||
const sheet = page.locator('[data-slot="record-import-sheet"]');
|
||||
await expect(sheet).toBeVisible();
|
||||
const file = {
|
||||
name: "labs.csv",
|
||||
mimeType: "text/csv",
|
||||
buffer: Buffer.from(csv()),
|
||||
};
|
||||
await page.locator('[data-slot="record-import-file"]').setInputFiles(file);
|
||||
await expect(
|
||||
page.locator('[data-slot="record-import-map-analyte"]'),
|
||||
).toHaveValue("0");
|
||||
await expect(
|
||||
page.locator('[data-slot="record-import-map-value"]'),
|
||||
).toHaveValue("1");
|
||||
|
||||
await page.locator('[data-slot="record-import-check"]').click();
|
||||
await expect(
|
||||
page.locator('[data-slot="record-import-checked"]'),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[data-slot="record-import-error-row"]'),
|
||||
).toHaveCount(1);
|
||||
|
||||
await page.locator('[data-slot="record-import-run"]').click();
|
||||
await expect(
|
||||
page.locator('[data-slot="record-import-done"]'),
|
||||
).toBeVisible();
|
||||
|
||||
// Second run of the same file.
|
||||
await page.keyboard.press("Escape");
|
||||
await page.locator('[data-slot="record-import-open-labs"]').click();
|
||||
await page.locator('[data-slot="record-import-file"]').setInputFiles(file);
|
||||
await page.locator('[data-slot="record-import-check"]').click();
|
||||
await expect(
|
||||
page.locator('[data-slot="record-import-run"]'),
|
||||
).toBeDisabled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
import { expect, test } from "./setup/test";
|
||||
|
||||
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
|
||||
/**
|
||||
* The provenance page (#1204) at phone width, against mocked reads: the
|
||||
* day's sources with the used one marked, the comparison table, and the
|
||||
* link from Settings > Source priority.
|
||||
*/
|
||||
const DAY = {
|
||||
type: "ACTIVITY_STEPS",
|
||||
date: "2026-10-01",
|
||||
aggregation: "sum",
|
||||
ranked: true,
|
||||
metricKey: "steps",
|
||||
ladder: ["APPLE_HEALTH", "FITBIT"],
|
||||
picked: "APPLE_HEALTH",
|
||||
pickReason: "highest_on_ladder",
|
||||
sources: [
|
||||
{
|
||||
source: "APPLE_HEALTH",
|
||||
rank: 0,
|
||||
readings: 24,
|
||||
value: 9500,
|
||||
min: 10,
|
||||
max: 900,
|
||||
firstAt: "2026-10-01T06:00:00.000Z",
|
||||
lastAt: "2026-10-01T20:00:00.000Z",
|
||||
unit: "steps",
|
||||
devices: ["phone", "watch"],
|
||||
pass: null,
|
||||
},
|
||||
{
|
||||
source: "FITBIT",
|
||||
rank: 1,
|
||||
readings: 1,
|
||||
value: 8800,
|
||||
min: 8800,
|
||||
max: 8800,
|
||||
firstAt: "2026-10-01T23:00:00.000Z",
|
||||
lastAt: "2026-10-01T23:00:00.000Z",
|
||||
unit: "steps",
|
||||
devices: ["band"],
|
||||
pass: "lower_on_ladder",
|
||||
},
|
||||
],
|
||||
};
|
||||
const COMPARE = {
|
||||
type: "ACTIVITY_STEPS",
|
||||
from: "2026-09-02",
|
||||
to: "2026-10-01",
|
||||
aggregation: "sum",
|
||||
ranked: true,
|
||||
ladder: ["APPLE_HEALTH", "FITBIT"],
|
||||
unit: "steps",
|
||||
sources: [
|
||||
{ source: "APPLE_HEALTH", days: 30 },
|
||||
{ source: "FITBIT", days: 28 },
|
||||
],
|
||||
a: "APPLE_HEALTH",
|
||||
b: "FITBIT",
|
||||
days: [{ date: "2026-10-01", a: 9500, b: 8800, picked: "APPLE_HEALTH" }],
|
||||
summary: {
|
||||
daysA: 30,
|
||||
daysB: 28,
|
||||
daysBoth: 28,
|
||||
meanA: 9100,
|
||||
meanB: 8700,
|
||||
meanDifference: -400,
|
||||
meanAbsoluteDifference: 450,
|
||||
pickedA: 30,
|
||||
pickedB: 0,
|
||||
},
|
||||
};
|
||||
|
||||
test.describe("source provenance", () => {
|
||||
test.use({
|
||||
storageState: STORAGE_STATE_PATH,
|
||||
viewport: { width: 390, height: 844 },
|
||||
});
|
||||
|
||||
test("explains the day's pick and compares two sources", async ({ page }) => {
|
||||
await page.route(/\/api\/measurements\/provenance\/compare/, (route) =>
|
||||
route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ data: COMPARE, error: null }),
|
||||
}),
|
||||
);
|
||||
await page.route(/\/api\/measurements\/provenance\?/, (route) =>
|
||||
route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ data: DAY, error: null }),
|
||||
}),
|
||||
);
|
||||
// Yesterday in UTC: the page refuses nothing, but a day the date field
|
||||
// cannot pick (after today) would read as a fixture gone stale.
|
||||
const day = new Date(Date.now() - 86_400_000).toISOString().slice(0, 10);
|
||||
await page.goto(`/insights/sources?type=ACTIVITY_STEPS&date=${day}`);
|
||||
await expect(
|
||||
page.locator('[data-slot="provenance-source-row"][data-picked="true"]'),
|
||||
).toHaveAttribute("data-source", "APPLE_HEALTH");
|
||||
await expect(
|
||||
page.locator('[data-slot="provenance-verdict"]'),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[data-slot="provenance-compare-row"]'),
|
||||
).toHaveCount(1);
|
||||
const overflow = await page.evaluate(
|
||||
() =>
|
||||
document.documentElement.scrollWidth -
|
||||
document.documentElement.clientWidth,
|
||||
);
|
||||
expect(overflow).toBeLessThanOrEqual(0);
|
||||
});
|
||||
|
||||
test("is reachable from Settings > Source priority", async ({ page }) => {
|
||||
await page.goto("/settings/sources");
|
||||
await page.locator('[data-slot="sources-provenance-cross-link"]').click();
|
||||
await expect(page).toHaveURL(/\/insights\/sources/);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user