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:
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user