feat(frontend): add intake journal and shared note flows (#648)

* feat(backend): add intake journal APIs and share note support

* feat(frontend): add intake journal and shared note flows
This commit is contained in:
Daniel Volz
2026-05-24 14:00:30 +02:00
committed by GitHub
parent e4a1b449c6
commit c78fc43083
67 changed files with 5414 additions and 580 deletions
+12 -2
View File
@@ -289,6 +289,7 @@ export interface TestShareToken {
token: string;
takenBy: string;
scheduleDays: number;
allowJournalNotes?: boolean;
expiresAt: string;
}
@@ -460,7 +461,11 @@ export async function deleteAllMedicationsViaAPI(): Promise<void> {
* Create a share token via the backend API.
* Requires a medication with takenBy to exist first.
*/
export async function createShareTokenViaAPI(takenBy: string, scheduleDays = 30): Promise<TestShareToken> {
export async function createShareTokenViaAPI(
takenBy: string,
scheduleDays = 30,
options: { allowJournalNotes?: boolean; expiryDays?: number | null } = {}
): Promise<TestShareToken> {
let token = await ensureAuthCookie();
const apiBase = await getRuntimeApiBase();
for (let attempt = 0; attempt < 5; attempt++) {
@@ -470,7 +475,12 @@ export async function createShareTokenViaAPI(takenBy: string, scheduleDays = 30)
"Content-Type": "application/json",
...(token ? { Cookie: `access_token=${token}` } : {}),
},
body: JSON.stringify({ takenBy, scheduleDays }),
body: JSON.stringify({
takenBy,
scheduleDays,
expiryDays: options.expiryDays ?? null,
allowJournalNotes: options.allowJournalNotes ?? false,
}),
});
if (res.status === 401) {
token = await refreshAuthCookieViaLogin();
+94
View File
@@ -0,0 +1,94 @@
import {
authFile,
createMedicationViaAPI,
createShareTokenViaAPI,
deleteAllMedicationsViaAPI,
expect,
navigateTo,
test,
} from "./fixtures";
test.describe("Mobile modal browser back", () => {
test.use({
storageState: authFile,
viewport: { width: 412, height: 915 },
isMobile: true,
hasTouch: true,
});
test("closes owner-side modals with browser back on a Pixel-width viewport", async ({ page }) => {
await navigateTo(page, "/dashboard");
const journalHistoryButton = page.locator(".journal-history-button").first();
await expect(journalHistoryButton).toBeVisible({ timeout: 10000 });
await journalHistoryButton.click();
const journalHistoryModal = page.locator(".journal-history-modal");
await expect(journalHistoryModal).toBeVisible({ timeout: 10000 });
await page.goBack();
await expect(journalHistoryModal).toBeHidden({ timeout: 10000 });
await navigateTo(page, "/settings");
const exportButton = page
.locator("button.secondary")
.filter({ hasText: /Export|Exportieren/i })
.first();
await expect(exportButton).toBeVisible({ timeout: 10000 });
await exportButton.click();
const exportModal = page.locator(".modal-content").filter({ hasText: /Export Options|Export-Optionen/i });
await expect(exportModal).toBeVisible({ timeout: 10000 });
await page.goBack();
await expect(exportModal).toBeHidden({ timeout: 10000 });
});
test("closes the shared intake journal modal with browser back on mobile", async ({ page }) => {
const uniqueSuffix = Date.now().toString(36);
const person = `Mobile Journal ${uniqueSuffix}`;
const medicationName = `Mobile Shared Journal ${uniqueSuffix}`;
const start = new Date();
start.setHours(8, 0, 0, 0);
const pad = (value: number) => value.toString().padStart(2, "0");
const startTime = `${start.getFullYear()}-${pad(start.getMonth() + 1)}-${pad(start.getDate())}T${pad(start.getHours())}:${pad(start.getMinutes())}`;
await deleteAllMedicationsViaAPI();
await createMedicationViaAPI({
name: medicationName,
takenBy: [person],
packageType: "blister",
packCount: 1,
blistersPerPack: 1,
pillsPerBlister: 10,
intakes: [{ usage: 1, every: 1, start: startTime, intakeRemindersEnabled: false, takenBy: person }],
});
const shareToken = await createShareTokenViaAPI(person, 30, { allowJournalNotes: true });
await page.goto(`/share/${shareToken.token}`);
await page.waitForLoadState("networkidle");
await expect(page.locator(".shared-schedule-loading-skeleton")).toBeHidden({ timeout: 10000 });
await expect(page.locator(".med-name-text").filter({ hasText: medicationName }).first()).toBeVisible({
timeout: 15000,
});
const doseItem = page.locator(".dose-item").first();
await expect(doseItem).toBeVisible({ timeout: 15000 });
await doseItem.locator(".dose-btn.take").click();
const collapsedTodayDivider = page.locator(".day-block.today.collapsed .day-divider.clickable").first();
if (await collapsedTodayDivider.isVisible().catch(() => false)) {
await collapsedTodayDivider.click();
}
const noteButton = page.locator(".dose-item").first().locator(".dose-btn.journal");
await expect(noteButton).toBeEnabled({ timeout: 10000 });
await noteButton.click();
const journalModal = page.locator(".journal-modal");
await expect(journalModal).toBeVisible({ timeout: 10000 });
await page.goBack();
await expect(journalModal).toBeHidden({ timeout: 10000 });
await expect(page.locator(".shared-schedule-container")).toBeVisible();
});
});
+56 -1
View File
@@ -18,7 +18,7 @@ import {
*/
test.describe("Share Schedule", () => {
test.use({ storageState: authFile });
test.describe.configure({ timeout: 90000 });
test.describe.configure({ mode: "serial", timeout: 90000 });
const MED_ALICE = "ShareTest AliceMed";
const MED_BOB = "ShareTest BobMed";
@@ -300,4 +300,59 @@ test.describe("Share Schedule", () => {
await page.locator("button.modal-close").click();
});
test("should let a shared recipient add and reopen a journal note", async ({ page }) => {
const uniqueSuffix = Date.now().toString(36);
const person = `Journal E2E ${uniqueSuffix}`;
const medicationName = `Share Journal E2E ${uniqueSuffix}`;
const journalNote = `Shared E2E note ${uniqueSuffix}`;
await createMedicationViaAPI({
name: medicationName,
takenBy: [person],
packageType: "blister",
packCount: 1,
blistersPerPack: 1,
pillsPerBlister: 10,
intakes: [{ usage: 1, every: 1, start: todayMorning, intakeRemindersEnabled: false, takenBy: person }],
});
const shareToken = await createShareTokenViaAPI(person, 30, { allowJournalNotes: true });
await page.goto(`/share/${shareToken.token}`);
await page.waitForLoadState("networkidle");
await expect(page.locator(".shared-schedule-loading-skeleton")).toBeHidden({ timeout: 10000 });
await expect(page.locator(".med-name-text").filter({ hasText: medicationName }).first()).toBeVisible({
timeout: 15000,
});
const doseItem = page.locator(".dose-item").first();
await expect(doseItem).toBeVisible({ timeout: 15000 });
await expect(doseItem.locator(".dose-btn.journal")).toBeDisabled();
await doseItem.locator(".dose-btn.take").click();
const collapsedTodayDivider = page.locator(".day-block.today.collapsed .day-divider.clickable").first();
if (await collapsedTodayDivider.isVisible().catch(() => false)) {
await collapsedTodayDivider.click();
}
const updatedDoseItem = page.locator(".dose-item").first();
const noteButton = updatedDoseItem.locator(".dose-btn.journal");
await expect(noteButton).toBeEnabled({ timeout: 10000 });
await noteButton.click();
const noteInput = page.locator("#journal-note-input");
await expect(noteInput).toBeVisible({ timeout: 10000 });
await expect(noteInput).toHaveValue("");
await noteInput.fill(journalNote);
await page.locator(".journal-modal-footer button.primary").click();
await expect(page.locator(".journal-modal")).toBeHidden({ timeout: 10000 });
await noteButton.click();
await expect(noteInput).toBeVisible({ timeout: 10000 });
await expect(noteInput).toHaveValue(journalNote, { timeout: 10000 });
});
});