import { expect, test } from "@playwright/test"; import { captureBrowserFailures, clickUntilVisible, gotoLiveView, loginWithPassword, markerKinds, projectEmail, projectText, registerAndConfirm, selectOptionContaining, setRequestLocation, waitForMapReady, } from "./helpers"; test("two users complete medicine tracking, handover, realtime chat, and blind reviews", async ({ browser, request, }, testInfo) => { const runID = process.env.E2E_RUN_ID; const fixturePassword = process.env.E2E_FIXTURE_PASSWORD; const requestTitle = projectText( runID ? `Staging E2E medicine pickup ${runID}` : "E2E medicine pickup", testInfo.project.name, ); const requesterEmail = projectEmail("requester", testInfo.project.name); const helperEmail = projectEmail("helper", testInfo.project.name); const replacementEmail = projectEmail("replacement-helper", testInfo.project.name); const requester = runID && fixturePassword ? await loginWithPassword(browser, requesterEmail, fixturePassword) : await registerAndConfirm(browser, request, requesterEmail, "E2E Requester"); const helper = runID && fixturePassword ? await loginWithPassword(browser, helperEmail, fixturePassword) : await registerAndConfirm(browser, request, helperEmail, "E2E Helper"); const replacement = runID && fixturePassword ? await loginWithPassword(browser, replacementEmail, fixturePassword) : await registerAndConfirm( browser, request, replacementEmail, "E2E Replacement Helper", ); const assertRequesterClean = captureBrowserFailures(requester.page); const assertHelperClean = captureBrowserFailures(helper.page); const assertReplacementClean = captureBrowserFailures(replacement.page); 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: "E2E central district", 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).toHaveURL(/\/requests\/[0-9a-f-]+$/); const requestURL = requester.page.url(); await expect(requester.page.getByRole("heading", { name: requestTitle })).toBeVisible(); await gotoLiveView(helper.page, requestURL); await helper.page.getByRole("button", { name: "I can help" }).click(); await expect(helper.page.getByRole("heading", { name: "Private match chat" })).toBeVisible(); await expect(requester.page.getByRole("heading", { name: "Private match chat" })).toBeVisible(); await helper.page .getByPlaceholder("Write a safe coordination message…") .fill("I am on my way to the pharmacy."); await helper.page.getByRole("button", { name: "Send", exact: true }).click(); await expect(requester.page.getByText("I am on my way to the pharmacy.")).toBeVisible(); await requester.page .getByPlaceholder("Write a safe coordination message…") .fill("Thank you. The order is waiting at the counter."); await requester.page.getByRole("button", { name: "Send", exact: true }).click(); await expect(helper.page.getByText("Thank you. The order is waiting at the counter.")).toBeVisible(); const origin = new URL(process.env.BASE_URL!).origin; await helper.context.grantPermissions(["geolocation"], { origin }); await helper.context.setGeolocation({ latitude: 50.4504, longitude: 30.5237, accuracy: 5, }); await helper.page.getByRole("button", { name: "Share location" }).click(); await expect(helper.page.getByRole("button", { name: "Stop and delete position" })).toBeVisible(); await expect.poll(() => markerKinds(requester.page)).toContain("Shared location"); await helper.page.getByRole("button", { name: "Stop and delete position" }).click(); await expect(helper.page.getByRole("button", { name: "Share location" })).toBeVisible(); await expect.poll(() => markerKinds(requester.page)).not.toContain("Shared location"); await helper.page.getByRole("button", { name: "Start helping" }).click(); const handoverCode = ( await requester.page.locator("text=HANDOVER CODE FOR THE HELPER").locator("..").locator(".font-mono").innerText() ).replace(/\s/g, ""); expect(handoverCode).toMatch(/^\d{6}$/); await helper.page.getByLabel("Enter handover code").fill(handoverCode); await helper.page.getByRole("button", { name: "Verify handover" }).click(); await expect(helper.page.getByText("Handover code verified.")).toBeVisible(); await helper.page.getByRole("button", { name: "Confirm my part is complete" }).click(); await requester.page.getByRole("button", { name: "Confirm my part is complete" }).click(); await expect(requester.page.getByRole("heading", { name: "Double-blind review" })).toBeVisible(); await expect(helper.page.getByRole("heading", { name: "Double-blind review" })).toBeVisible(); await helper.page.getByLabel("Rating").selectOption("5"); await helper.page.getByLabel("Comment (optional)").fill("Clear and safe coordination."); await helper.page.getByRole("button", { name: "Submit review" }).click(); await expect( helper.page .locator("#main-content") .getByText("Review saved. It appears after both participants review."), ).toBeVisible(); await waitForMapReady(requester.page); await gotoLiveView(requester.page, "/profile"); await expect(requester.page.getByText("None revealed yet.")).toBeVisible(); await expect(requester.page.getByText("Clear and safe coordination.")).toHaveCount(0); await gotoLiveView(requester.page, requestURL); await requester.page.getByLabel("Rating").selectOption("5"); await requester.page.getByLabel("Comment (optional)").fill("Reliable volunteer."); await requester.page.getByRole("button", { name: "Submit review" }).click(); await expect( requester.page .locator("#main-content") .getByText("Review saved. It appears after both participants review."), ).toBeVisible(); await waitForMapReady(requester.page); await gotoLiveView(requester.page, "/profile"); await expect(requester.page.getByText("Clear and safe coordination.")).toBeVisible(); await waitForMapReady(helper.page); await gotoLiveView(helper.page, "/profile"); await expect(helper.page.getByText("Reliable volunteer.")).toBeVisible(); const roadsideTitle = projectText( runID ? `Staging E2E motorcycle chain ${runID}` : "E2E motorcycle chain", testInfo.project.name, ); await gotoLiveView(requester.page, "/requests/new"); await selectOptionContaining( requester.page, "Category", "Motorcycle roadside help", ); await requester.page.getByLabel("Motorcycle problem").selectOption("broken_chain"); await requester.page .getByLabel("Motorcycle is away from active traffic") .selectOption("true"); await requester.page.getByLabel("Short title").fill(roadsideTitle); await requester.page .getByLabel("What help do you need?") .fill("The motorcycle is secured and needs help with a broken chain."); await requester.page.getByLabel("Urgency").selectOption("today"); await requester.page.getByRole("button", { name: "In 3 hours" }).click(); await setRequestLocation(requester.page, { label: "E2E secured roadside area", mode: "hidden", }); await requester.page.locator("#request-form input[type=checkbox]").last().check(); await requester.page.getByRole("button", { name: "Publish request" }).click(); await expect(requester.page.getByRole("heading", { name: roadsideTitle })).toBeVisible(); const roadsideURL = requester.page.url(); await gotoLiveView(helper.page, roadsideURL); const withdrawalSummary = helper.page .locator("summary") .filter({ hasText: "Withdraw from this request" }); await clickUntilVisible( helper.page.getByRole("button", { name: "I can help" }), withdrawalSummary, ); await withdrawalSummary.click(); const withdrawalForm = helper.page.locator("#assignment-withdrawal-form"); await withdrawalForm.getByLabel("Reason").selectOption("requester_unreachable"); await withdrawalForm .getByLabel("Note (optional)") .fill("I could not reach the requester during the E2E workflow."); await withdrawalForm.getByRole("button", { name: "Confirm withdrawal" }).click(); await expect( helper.page.getByText( "You left this match. The request is open for another helper if time remains.", ), ).toBeVisible(); await expect(helper.page.getByRole("heading", { name: "Private match chat" })).toHaveCount(0); await expect(helper.page.getByRole("button", { name: "I can help" })).toBeVisible(); await expect(requester.page.getByText("Open", { exact: true })).toBeVisible(); await expect( requester.page.getByText( "The previous helper left. This request is open for a new helper again.", ), ).toBeVisible(); await gotoLiveView(replacement.page, roadsideURL); await replacement.page.getByRole("button", { name: "I can help" }).click(); await expect( replacement.page.getByRole("heading", { name: "Private match chat" }), ).toBeVisible(); await expect(requester.page.getByText("Helper: E2E Replacement Helper")).toBeVisible(); await expect(helper.page.getByRole("heading", { name: "Private match chat" })).toHaveCount(0); assertRequesterClean(); assertHelperClean(); assertReplacementClean(); await requester.context.close(); await helper.context.close(); await replacement.context.close(); });