// UI test for the end-to-end order placement and cancellation flow that lives // across artifacts/tx-os/src/pages/services.tsx, the order modal in // artifacts/tx-os/src/components/order-service-modal.tsx, and // artifacts/tx-os/src/pages/my-orders.tsx. // // Mirrors the API-side coverage in // artifacts/api-server/tests/service-orders.test.mjs but at the UI layer: // - Logs in as a regular user (seeded via the database). // - Navigates to /services and clicks an available service tile. // - Submits the order modal and waits for the POST /api/orders 201 response. // - Confirms the new order shows up at the top of /my-orders with the // "Awaiting receiver" pill (pending status). // - Cancels it via the in-card Cancel button + confirmation dialog and waits // for the PATCH /api/orders/:id/status 200 response. // - Asserts the red cancelled pill replaces the timeline (no timeline step // circles remain) and the Cancel button is gone from the card. // // Both Arabic and English label paths are exercised via separate tests; the // language is forced through localStorage before the page loads. // // 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 // // The test seeds its own user + uses an existing available service, then // cleans up the user, their orders, and any notifications it produced. 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 order placement / cancellation UI test", ); } // Same convention as artifacts/api-server/tests/service-orders.test.mjs: // 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, preferredLanguage = "en") { 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, 'Order UI Test', $4, true) RETURNING id`, [ username, `${username}@example.com`, TEST_PASSWORD_HASH, preferredLanguage, ], ); 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 = 'user'`, [id], ); return { id, 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) { // Catch any orders the user created that we didn't track (defensive). 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 placeAndCancelFlow(page, lang, labels) { // Seed the user with a server-side preferredLanguage matching the test // language, because AuthContext / login.tsx call `i18n.changeLanguage( // user.preferredLanguage)` after login and would otherwise override our // localStorage tx-lang seed. const user = await createUser(`order_ui_${lang}`, lang); const service = await getAvailableService(); const serviceName = lang === "ar" ? service.nameAr : service.nameEn; await setLanguage(page, lang); await loginViaUi(page, user.username, TEST_PASSWORD); // ---- Place an order from /services ---- await page.goto("/services"); // Service tiles render as