From 2ffd63832a9eac68c8da20fdaf42bc2c97809289 Mon Sep 17 00:00:00 2001 From: riyadhafraa <49757212-riyadhafraa@users.noreply.replit.com> Date: Wed, 29 Apr 2026 13:45:24 +0000 Subject: [PATCH] Add e2e tests for the receiver-side incoming-orders flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task #102: Adds Playwright UI coverage for the receiver side of the service-orders flow, mirroring the existing API-level matrix in artifacts/api-server/tests/service-orders.test.mjs. What's added - artifacts/tx-os/tests/order-receiver-flow.spec.mjs with two tests: 1) "receiver claims, prepares, completes — requester sees live status updates": seeds a requester (role "user") and a receiver (role "order_receiver"), runs each in its own browser context, has the requester place an order via /services, then has the receiver claim it from /orders/incoming and walk it through Mark Preparing → Mark Completed. After every receiver action it asserts that the requester's /my-orders status pill ("Received", "Preparing", "Completed") updates without a manual refresh — exercising the order_updated socket.io push wired through use-notifications-socket. 2) "receiver cancels after claiming — requester sees Cancelled pill": covers the receiver-side cancel path required by the task by claiming the order and then cancelling from /orders/incoming, and verifies the requester's card flips to the red "Cancelled" pill and loses the timeline step circles. Test design notes - English UI only; the language matrix is already covered for the requester side by order-place-cancel.spec.mjs. - Each test creates a unique requester display name so that incoming cards can be uniquely located by service name + "From " even if other cards exist on the page. - Status assertions use exact-text matching on the pill to avoid the "Received" / "Awaiting receiver" substring overlap. - afterAll cleans up service_orders, notifications, user_roles and the seeded users (defensive: also deletes any orders referencing those users that weren't explicitly tracked). Other - The new spec is automatically picked up by the existing `pnpm --filter @workspace/tx-os test:e2e` run (testMatch in artifacts/tx-os/playwright.config.mjs already globs *.spec.mjs). - Verified locally: full tx-os e2e suite (9 specs) passes. Replit-Task-Id: bdbee3d1-20e0-4a40-b2b1-51ea16a15bb7 --- artifacts/tx-os/public/opengraph.jpg | Bin 25922 -> 24651 bytes .../tx-os/tests/order-receiver-flow.spec.mjs | 419 ++++++++++++++++++ 2 files changed, 419 insertions(+) create mode 100644 artifacts/tx-os/tests/order-receiver-flow.spec.mjs diff --git a/artifacts/tx-os/public/opengraph.jpg b/artifacts/tx-os/public/opengraph.jpg index b018b84cb830e6fcf8d14e4ac85f97a57df08cb6..723b79962120d8c4f2f3d6cdb26e5f6e3e5eda74 100644 GIT binary patch literal 24651 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 H-@E?Hp>a+7-_!Ei;C5JvPe$SBdd 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 diff --git a/artifacts/tx-os/tests/order-receiver-flow.spec.mjs b/artifacts/tx-os/tests/order-receiver-flow.spec.mjs new file mode 100644 index 00000000..1b391a10 --- /dev/null +++ b/artifacts/tx-os/tests/order-receiver-flow.spec.mjs @@ -0,0 +1,419 @@ +// UI test for the receiver-side incoming-orders flow that lives across +// artifacts/tx-os/src/pages/orders-incoming.tsx and the requester's +// artifacts/tx-os/src/pages/my-orders.tsx. +// +// Mirrors part of the API matrix in +// artifacts/api-server/tests/service-orders.test.mjs but at the UI layer: +// - Seeds a requester (role "user") and a receiver (role "order_receiver"). +// - Logs each into their own browser context. +// - Has the requester place an order via /services. +// - Switches to the receiver's /orders-incoming page, claims the order +// (Confirm Receipt), then walks it through Mark Preparing → Mark +// Completed. +// - After each receiver action, asserts that the requester's /my-orders +// card reflects the new status pill in real time. The push happens via +// the socket.io "order_updated" event the API server emits to the +// requester (see artifacts/api-server/src/routes/service-orders.ts + +// artifacts/tx-os/src/hooks/use-notifications-socket.ts), which +// invalidates the my-orders query and triggers a refetch. +// - Separately exercises the receiver-side cancel path: the receiver +// claims a second order, then cancels it from /orders-incoming, and +// the requester sees the red "Cancelled" pill. +// +// English UI only — the order-place-cancel.spec.mjs neighbour already +// exercises Arabic translations on the requester side, and the flow under +// test here is identical between languages save for label strings. +// +// Run with: +// DATABASE_URL=... pnpm --filter @workspace/tx-os test:e2e +// +// Browser binaries can be installed once with: +// pnpm --filter @workspace/tx-os exec playwright install chromium + +import { test, expect } from "@playwright/test"; +import pg from "pg"; + +const DATABASE_URL = process.env.DATABASE_URL; +if (!DATABASE_URL) { + throw new Error( + "DATABASE_URL must be set to run the receiver-side incoming-orders UI test", + ); +} + +// Same convention as the other UI/API tests: a precomputed bcrypt hash of +// the literal password "TestPass123!". +const TEST_PASSWORD = "TestPass123!"; +const TEST_PASSWORD_HASH = + "$2b$10$Bs636ukPMyz01nKrsi.5m.JlDXSN22AVCvn8cgPWWDbo5yJRQX2vu"; + +const pool = new pg.Pool({ connectionString: DATABASE_URL }); + +const createdUserIds = []; +const createdOrderIds = []; + +function uniqueSuffix() { + return `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`; +} + +async function createUser(prefix, roleName, displayNameEn) { + const username = `${prefix}_${uniqueSuffix()}`; + const { rows } = await pool.query( + `INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active) + VALUES ($1, $2, $3, $4, 'en', true) RETURNING id`, + [ + username, + `${username}@example.com`, + TEST_PASSWORD_HASH, + displayNameEn ?? username, + ], + ); + const id = rows[0].id; + createdUserIds.push(id); + await pool.query( + `INSERT INTO user_roles (user_id, role_id) + SELECT $1, id FROM roles WHERE name = $2`, + [id, roleName], + ); + return { id, username, displayName: displayNameEn ?? username }; +} + +async function getAvailableService() { + const { rows } = await pool.query( + `SELECT id, + name_en AS "nameEn", + name_ar AS "nameAr" + FROM services + WHERE is_available = true + ORDER BY id ASC + LIMIT 1`, + ); + if (rows.length === 0) { + throw new Error( + "No available service found in DB to test ordering against", + ); + } + return rows[0]; +} + +test.afterAll(async () => { + if (createdOrderIds.length > 0) { + await pool.query(`DELETE FROM service_orders WHERE id = ANY($1::int[])`, [ + createdOrderIds, + ]); + } + if (createdUserIds.length > 0) { + // Defensive cleanup: catch any orders created against these users that + // we didn't explicitly track. + await pool.query( + `DELETE FROM service_orders + WHERE user_id = ANY($1::int[]) OR assigned_to = ANY($1::int[])`, + [createdUserIds], + ); + await pool.query( + `DELETE FROM notifications WHERE user_id = ANY($1::int[])`, + [createdUserIds], + ); + await pool.query( + `DELETE FROM user_roles WHERE user_id = ANY($1::int[])`, + [createdUserIds], + ); + await pool.query(`DELETE FROM users WHERE id = ANY($1::int[])`, [ + createdUserIds, + ]); + } + await pool.end(); +}); + +async function setLanguage(page, lang) { + // i18n.ts reads `localStorage.getItem("tx-lang")` at module-init time, so + // seed it before any page script runs. + await page.addInitScript((l) => { + try { + window.localStorage.setItem("tx-lang", l); + } catch { + /* localStorage may not be available before navigation; safe to ignore */ + } + }, lang); +} + +async function loginViaUi(page, username, password) { + await page.goto("/login"); + await page.locator("#username").fill(username); + await page.locator("#password").fill(password); + await Promise.all([ + page.waitForURL((url) => !url.pathname.endsWith("/login"), { + timeout: 15_000, + }), + page.locator('form button[type="submit"]').click(), + ]); +} + +async function placeOrderViaUi(page, serviceNameEn) { + await page.goto("/services"); + await page + .getByRole("button", { name: serviceNameEn, exact: true }) + .first() + .click(); + const orderModal = page.getByRole("dialog"); + await expect(orderModal).toBeVisible(); + + const placePromise = page.waitForResponse( + (resp) => { + const path = new URL(resp.url()).pathname; + return path === "/api/orders" && resp.request().method() === "POST"; + }, + { timeout: 15_000 }, + ); + await orderModal + .getByRole("button", { name: "Send", exact: true }) + .click(); + const placeResp = await placePromise; + expect(placeResp.status()).toBe(201); + const placedOrder = await placeResp.json(); + createdOrderIds.push(placedOrder.id); + expect(placedOrder.status).toBe("pending"); + await expect(orderModal).toBeHidden(); + return placedOrder; +} + +// On /my-orders, the StatusPill renders the localized status text in a +// compact rounded-full span (see artifacts/tx-os/src/pages/my-orders.tsx). +// Asserting on the pill text avoids ambiguity with other text on the card +// (notably "Awaiting receiver" containing the substring "Received"). +async function expectMyOrdersStatus(card, expectedText) { + await expect( + card.getByText(expectedText, { exact: true }).first(), + ).toBeVisible({ timeout: 15_000 }); +} + +// On /orders-incoming, locate the card belonging to the order placed by +// `requesterDisplayName` for `serviceNameEn`. The card includes both +// (e.g. "" and "From "), so combining +// them uniquely identifies the order even if the page already has other +// cards from prior runs / unrelated users. +function incomingCardFor(page, serviceNameEn, requesterDisplayName) { + return page + .locator(".glass-panel.rounded-xl") + .filter({ hasText: serviceNameEn }) + .filter({ hasText: requesterDisplayName }); +} + +async function waitForStatusPatch(page, orderId) { + return page.waitForResponse( + (resp) => { + const path = new URL(resp.url()).pathname; + return ( + path === `/api/orders/${orderId}/status` && + resp.request().method() === "PATCH" + ); + }, + { timeout: 15_000 }, + ); +} + +async function waitForConfirmReceipt(page, orderId) { + return page.waitForResponse( + (resp) => { + const path = new URL(resp.url()).pathname; + return ( + path === `/api/orders/${orderId}/confirm-receipt` && + resp.request().method() === "PATCH" + ); + }, + { timeout: 15_000 }, + ); +} + +test.describe("Receiver-side incoming-orders flow", () => { + test("receiver claims, prepares, completes — requester sees live status updates", async ({ + browser, + }) => { + const requester = await createUser( + "ord_req", + "user", + `Req ${uniqueSuffix()}`, + ); + const receiver = await createUser( + "ord_recv", + "order_receiver", + `Recv ${uniqueSuffix()}`, + ); + const service = await getAvailableService(); + + const requesterContext = await browser.newContext(); + const receiverContext = await browser.newContext(); + try { + const requesterPage = await requesterContext.newPage(); + const receiverPage = await receiverContext.newPage(); + + await setLanguage(requesterPage, "en"); + await setLanguage(receiverPage, "en"); + + await loginViaUi(requesterPage, requester.username, TEST_PASSWORD); + await loginViaUi(receiverPage, receiver.username, TEST_PASSWORD); + + // ---- Requester places an order via /services ---- + const placedOrder = await placeOrderViaUi(requesterPage, service.nameEn); + + // ---- Requester opens /my-orders so the live updates have a target ---- + await requesterPage.goto("/my-orders"); + const requesterCard = requesterPage + .locator(".glass-panel.rounded-xl") + .first(); + await expect(requesterCard).toContainText(service.nameEn); + await expectMyOrdersStatus(requesterCard, "Awaiting receiver"); + + // ---- Receiver opens /orders/incoming and claims it ---- + await receiverPage.goto("/orders/incoming"); + const incomingCard = incomingCardFor( + receiverPage, + service.nameEn, + requester.displayName, + ); + await expect(incomingCard.first()).toBeVisible(); + + const claimPromise = waitForConfirmReceipt(receiverPage, placedOrder.id); + await incomingCard + .first() + .getByRole("button", { name: "Confirm Receipt", exact: true }) + .click(); + const claimResp = await claimPromise; + expect(claimResp.status()).toBe(200); + + // Requester should see "Received" pill via the order_updated socket + // push that invalidates the my-orders query. + await expectMyOrdersStatus(requesterCard, "Received"); + + // ---- Receiver moves it to Preparing ---- + // After the refetch, the card has moved to the "My active orders" + // section but still uniquely identifiable by service + requester. + const mineCardReceived = incomingCardFor( + receiverPage, + service.nameEn, + requester.displayName, + ); + await expect( + mineCardReceived + .first() + .getByRole("button", { name: "Mark Preparing", exact: true }), + ).toBeVisible(); + + const preparingPromise = waitForStatusPatch(receiverPage, placedOrder.id); + await mineCardReceived + .first() + .getByRole("button", { name: "Mark Preparing", exact: true }) + .click(); + const preparingResp = await preparingPromise; + expect(preparingResp.status()).toBe(200); + + await expectMyOrdersStatus(requesterCard, "Preparing"); + + // ---- Receiver moves it to Completed ---- + const mineCardPreparing = incomingCardFor( + receiverPage, + service.nameEn, + requester.displayName, + ); + await expect( + mineCardPreparing + .first() + .getByRole("button", { name: "Mark Completed", exact: true }), + ).toBeVisible(); + + const completedPromise = waitForStatusPatch(receiverPage, placedOrder.id); + await mineCardPreparing + .first() + .getByRole("button", { name: "Mark Completed", exact: true }) + .click(); + const completedResp = await completedPromise; + expect(completedResp.status()).toBe(200); + + await expectMyOrdersStatus(requesterCard, "Completed"); + } finally { + await requesterContext.close(); + await receiverContext.close(); + } + }); + + test("receiver cancels after claiming — requester sees Cancelled pill", async ({ + browser, + }) => { + const requester = await createUser( + "ord_req_cx", + "user", + `ReqCx ${uniqueSuffix()}`, + ); + const receiver = await createUser( + "ord_recv_cx", + "order_receiver", + `RecvCx ${uniqueSuffix()}`, + ); + const service = await getAvailableService(); + + const requesterContext = await browser.newContext(); + const receiverContext = await browser.newContext(); + try { + const requesterPage = await requesterContext.newPage(); + const receiverPage = await receiverContext.newPage(); + + await setLanguage(requesterPage, "en"); + await setLanguage(receiverPage, "en"); + + await loginViaUi(requesterPage, requester.username, TEST_PASSWORD); + await loginViaUi(receiverPage, receiver.username, TEST_PASSWORD); + + const placedOrder = await placeOrderViaUi(requesterPage, service.nameEn); + + await requesterPage.goto("/my-orders"); + const requesterCard = requesterPage + .locator(".glass-panel.rounded-xl") + .first(); + await expect(requesterCard).toContainText(service.nameEn); + await expectMyOrdersStatus(requesterCard, "Awaiting receiver"); + + // Receiver claims first so the cancel path under test exercises the + // "received → cancelled" transition (the only cancel path the + // receiver UI offers, see orders-incoming.tsx handleCancel guard). + await receiverPage.goto("/orders/incoming"); + const incomingCard = incomingCardFor( + receiverPage, + service.nameEn, + requester.displayName, + ); + const claimPromise = waitForConfirmReceipt(receiverPage, placedOrder.id); + await incomingCard + .first() + .getByRole("button", { name: "Confirm Receipt", exact: true }) + .click(); + const claimResp = await claimPromise; + expect(claimResp.status()).toBe(200); + await expectMyOrdersStatus(requesterCard, "Received"); + + // ---- Receiver cancels the claimed order ---- + const mineCard = incomingCardFor( + receiverPage, + service.nameEn, + requester.displayName, + ); + const cancelBtn = mineCard + .first() + .getByRole("button", { name: "Cancel", exact: true }); + await expect(cancelBtn).toBeVisible(); + + const cancelPromise = waitForStatusPatch(receiverPage, placedOrder.id); + await cancelBtn.click(); + const cancelResp = await cancelPromise; + expect(cancelResp.status()).toBe(200); + const cancelBody = await cancelResp.json(); + expect(cancelBody.status).toBe("cancelled"); + + // Requester sees the cancelled pill via the live socket update; the + // timeline circles are replaced with a centered "Cancelled" badge. + await expectMyOrdersStatus(requesterCard, "Cancelled"); + await expect(requesterCard.locator("div.h-7.w-7")).toHaveCount(0); + } finally { + await requesterContext.close(); + await receiverContext.close(); + } + }); +});