import AxeBuilder from "@axe-core/playwright"; import { expect, test } from "@playwright/test"; import { captureBrowserFailures, gotoLiveView, gotoWithTransientRetry, projectEmail, projectText, registerAndConfirm, selectOptionContaining, setRequestLocation, waitForMapIdle, } from "./helpers"; test("nearby alerts, private notification inbox, preferences, and data export work end to end", async ({ browser, request, }, testInfo) => { const scope = `${testInfo.project.name}-${Date.now()}-${process.pid}`; const subscriber = await registerAndConfirm( browser, request, projectEmail(`notification-subscriber-${scope}`, testInfo.project.name), "E2E Notification Subscriber", ); const requester = await registerAndConfirm( browser, request, projectEmail(`notification-requester-${scope}`, testInfo.project.name), "E2E Notification Requester", ); const assertSubscriberClean = captureBrowserFailures(subscriber.page); const assertRequesterClean = captureBrowserFailures(requester.page); const alertName = projectText("Urgent medicine nearby", scope); const privateArea = projectText("Private subscriber center", scope); const requestTitle = projectText("Nearby notification request", scope); await gotoLiveView(subscriber.page, "/notifications"); await subscriber.page.getByRole("link", { name: "Delivery settings" }).click(); const preferenceForm = subscriber.page.locator("#notification-preferences-form"); await preferenceForm.getByLabel("Allow email notifications").check(); await preferenceForm.getByLabel("Nearby requests by email").check(); await preferenceForm.getByLabel("Use quiet hours").check(); await preferenceForm.getByLabel("From").fill("22:00"); await preferenceForm.getByLabel("Until").fill("07:00"); await preferenceForm.getByRole("button", { name: "Save preferences" }).click(); await expect(subscriber.page.getByText("Notification preferences saved.")).toBeVisible(); await subscriber.page .getByRole("link", { name: "Nearby alerts", exact: true }) .click(); const subscriptionForm = subscriber.page.locator("#nearby-subscription-form"); await subscriptionForm.getByLabel("Alert name").fill(alertName); await subscriptionForm.getByLabel("Private area label").fill(privateArea); await subscriptionForm .getByText("Enter coordinates manually", { exact: true }) .click(); await subscriptionForm.getByLabel("Latitude").fill("50.4501"); await subscriptionForm.getByLabel("Longitude").fill("30.5234"); await subscriptionForm .locator('input[name="nearby_subscription[radius_meters]"][value="3000"]') .check(); await expect .poll( async () => { const map = subscriber.page.locator("#nearby-subscription-map"); if ((await map.getAttribute("data-map-idle")) === "true") { return "idle"; } if ((await map.locator("[data-map-fallback=true]").count()) === 1) { return "fallback"; } return null; }, { timeout: 15_000 }, ) .not.toBeNull(); await subscriptionForm.getByLabel("Email notification").check(); await subscriptionForm.getByRole("button", { name: "Create nearby alert" }).click(); await expect(subscriber.page.getByText("Nearby alert created.")).toBeVisible(); const savedAlert = subscriber.page .getByRole("heading", { name: "Your nearby alerts" }) .locator(".."); await expect(savedAlert.getByRole("heading", { name: alertName })).toBeVisible(); await expect(savedAlert.getByText(`${privateArea} ยท 3 km`)).toBeVisible(); await expect(savedAlert.getByText("Push", { exact: true })).toBeVisible(); await expect(savedAlert.getByText("Email", { exact: true })).toBeVisible(); const accessibility = await new AxeBuilder({ page: subscriber.page }).analyze(); expect( accessibility.violations, accessibility.violations .map((violation) => `${violation.id}: ${violation.help}`) .join("\n"), ).toEqual([]); await gotoLiveView(requester.page, "/requests/new"); await selectOptionContaining(requester.page, "Category", "Medicine pickup"); await requester.page.getByLabel("Medicine pickup status").selectOption("reserved"); await requester.page.getByLabel("Short title").fill(requestTitle); await requester.page .getByLabel("What help do you need?") .fill("Please collect the legal medicine that is already reserved."); await requester.page.getByLabel("Urgency").selectOption("now"); await requester.page.getByRole("button", { name: "In 3 hours" }).click(); await setRequestLocation(requester.page, { label: "Public notification test area", mode: "approximate_public", latitude: "50.4501", longitude: "30.5234", radiusMeters: 1000, }); await requester.page.locator("#request-form input[type=checkbox]").check(); await requester.page.getByRole("button", { name: "Publish request" }).click(); await expect(requester.page.getByRole("heading", { name: requestTitle })).toBeVisible(); const requestURL = requester.page.url(); await subscriber.page.getByRole("link", { name: "Inbox" }).click(); const inbox = subscriber.page .getByRole("heading", { name: "Inbox" }) .locator("xpath=ancestor::section"); const notification = inbox.getByRole("link", { name: /New help request nearby/ }); await expect(notification).toBeVisible({ timeout: 20_000 }); await expect(inbox.getByText(privateArea)).toHaveCount(0); await notification.click(); await expect(subscriber.page).toHaveURL(requestURL); await expect(subscriber.page.getByRole("heading", { name: requestTitle })).toBeVisible(); // WebKit reports cancelled image decoding as a page error when a document // destroys a raster map before its worker reaches idle. Waiting here tests // the real rendered state and avoids navigating away mid-decode. await waitForMapIdle(subscriber.page); await gotoWithTransientRetry(subscriber.page, "/users/settings"); await expect(subscriber.page.getByRole("heading", { name: "Account Settings" })).toBeVisible(); assertSubscriberClean(); assertRequesterClean(); const exportLink = subscriber.page.getByRole("link", { name: "Download my data" }); await expect(exportLink).toHaveAttribute("href", "/users/data-export"); const exportURL = new URL("/users/data-export", subscriber.page.url()).toString(); const exportResponse = await subscriber.context.request.get(exportURL); expect(exportResponse.status()).toBe(200); expect(exportResponse.headers()["content-disposition"]).toMatch( /^attachment; filename="who-need-help-account-export-\d{4}-\d{2}-\d{2}\.json"$/, ); expect((await exportResponse.json()).format).toBe("who-need-help-account-export"); await subscriber.context.close(); await requester.context.close(); });