From 934979750f7b2dc2d99e9c2ae4148fa125245b1d Mon Sep 17 00:00:00 2001 From: Hardik Date: Fri, 22 May 2026 17:15:17 +0530 Subject: [PATCH] test(e2e): harden PO form selectors --- App/tests/e2e/accounts-payment.spec.ts | 15 ++--- .../e2e/approvals-edit-highlight.spec.ts | 14 ++--- App/tests/e2e/helpers/login.ts | 61 ++++++++++++++----- App/tests/e2e/manager-approvals.spec.ts | 15 ++--- App/tests/e2e/po-export.spec.ts | 15 ++--- App/tests/e2e/profile.spec.ts | 4 +- App/tests/e2e/submitter-journey.spec.ts | 24 ++++---- 7 files changed, 92 insertions(+), 56 deletions(-) diff --git a/App/tests/e2e/accounts-payment.spec.ts b/App/tests/e2e/accounts-payment.spec.ts index 8d6bde5..79d28ff 100644 --- a/App/tests/e2e/accounts-payment.spec.ts +++ b/App/tests/e2e/accounts-payment.spec.ts @@ -3,6 +3,7 @@ * Covers: A-01 (view payment queue), A-02 (mark PO as paid with reference). */ import { test, expect, type Page } from "@playwright/test"; +import { fillFirstLineItem, fillPoHeader } from "./helpers/login"; const TECH = { email: "tech@pelagia.local", password: "tech1234" }; const MGR = { email: "manager@pelagia.local", password: "manager1234" }; @@ -13,7 +14,7 @@ async function login(page: Page, creds: typeof TECH) { await page.getByLabel(/email/i).fill(creds.email); await page.getByLabel(/password/i).fill(creds.password); await page.getByRole("button", { name: /sign in/i }).click(); - await expect(page).not.toHaveURL(/login/); + await expect(page).not.toHaveURL(/\/login/, { timeout: 20_000 }); } /** Full flow: create PO as tech → approve as manager → return PO URL */ @@ -21,12 +22,12 @@ async function createApprovedPo(page: Page, context: import("@playwright/test"). // Step 1: create + submit as tech await login(page, TECH); await page.goto("/po/new"); - await page.getByLabel(/title/i).fill(title); - await page.getByLabel(/vessel/i).selectOption({ index: 1 }); - await page.getByLabel(/account/i).selectOption({ index: 1 }); - await page.getByPlaceholder("Item description").fill("Deck paint"); - await page.getByRole("spinbutton").first().fill("3"); - await page.locator("input[placeholder='0.00']").fill("800"); + await fillPoHeader(page, title); + await fillFirstLineItem(page, { + name: "Deck paint", + quantity: "3", + unitPrice: "800", + }); await page.getByRole("button", { name: /submit for approval/i }).click(); await expect(page).toHaveURL(/\/po\//); const poUrl = page.url(); diff --git a/App/tests/e2e/approvals-edit-highlight.spec.ts b/App/tests/e2e/approvals-edit-highlight.spec.ts index 98c739d..30069b6 100644 --- a/App/tests/e2e/approvals-edit-highlight.spec.ts +++ b/App/tests/e2e/approvals-edit-highlight.spec.ts @@ -19,7 +19,7 @@ * Created: 2026-05-17 */ import { test, expect, type BrowserContext } from "@playwright/test"; -import { login, USERS } from "./helpers/login"; +import { fillFirstLineItem, fillPoHeader, login, USERS } from "./helpers/login"; async function driveEditHighlightFlow( page: import("@playwright/test").Page, @@ -29,12 +29,12 @@ async function driveEditHighlightFlow( // Step 1: Submit as tech await login(page, USERS.TECH); await page.goto("/po/new"); - await page.getByLabel(/title/i).fill(title); - await page.getByLabel(/vessel/i).selectOption({ index: 1 }); - await page.getByLabel(/account/i).selectOption({ index: 1 }); - await page.getByPlaceholder("Item description").fill("Original item"); - await page.getByRole("spinbutton").first().fill("2"); - await page.locator("input[placeholder='0.00']").fill("150"); + await fillPoHeader(page, title); + await fillFirstLineItem(page, { + name: "Original item", + quantity: "2", + unitPrice: "150", + }); await page.getByRole("button", { name: /submit for approval/i }).click(); await expect(page).toHaveURL(/\/po\//, { timeout: 15_000 }); const poUrl = page.url(); diff --git a/App/tests/e2e/helpers/login.ts b/App/tests/e2e/helpers/login.ts index f462223..f5d0a69 100644 --- a/App/tests/e2e/helpers/login.ts +++ b/App/tests/e2e/helpers/login.ts @@ -28,20 +28,52 @@ export async function login(page: Page, creds: Credentials): Promise { console.log(`✓ Logged in as ${creds.email}`); } +export async function fillPoHeader(page: Page, title: string): Promise { + await page.waitForSelector('input[name="title"]', { timeout: 15_000 }); + await page.locator('input[name="title"]').fill(title); + await page.locator('select[name="vesselId"]').selectOption({ index: 1 }); + await page.locator('select[name="accountId"]').selectOption({ index: 1 }); +} + +export async function fillFirstLineItem( + page: Page, + { + name, + quantity, + unitPrice, + description, + }: { + name: string; + quantity: string; + unitPrice: string; + description?: string; + } +): Promise { + const lineItems = page.locator("section").filter({ + has: page.getByRole("heading", { name: /line items/i }), + }); + const firstRow = lineItems.locator("tbody tr").first(); + + await firstRow.getByPlaceholder("Item name *").fill(name); + if (description) { + await firstRow.getByPlaceholder("Description (optional)").fill(description); + } + await firstRow.locator('input[type="number"]').first().fill(quantity); + await firstRow.locator('input[placeholder="0.00"]').fill(unitPrice); +} + /** * Create a minimal draft PO and return the absolute PO URL. * Uses name-based selectors since the PO form labels have no htmlFor binding. */ export async function createDraftPo(page: Page, title: string): Promise { await page.goto("/po/new"); - // Wait for the form to be ready - await page.waitForSelector('input[name="title"]', { timeout: 15_000 }); - await page.locator('input[name="title"]').fill(title); - await page.locator('select[name="vesselId"]').selectOption({ index: 1 }); - await page.locator('select[name="accountId"]').selectOption({ index: 1 }); - await page.getByPlaceholder("Item description").fill("Test item"); - await page.getByRole("spinbutton").first().fill("1"); - await page.locator("input[placeholder='0.00']").first().fill("100"); + await fillPoHeader(page, title); + await fillFirstLineItem(page, { + name: "Test item", + quantity: "1", + unitPrice: "100", + }); await page.getByRole("button", { name: /save as draft/i }).click(); await expect(page).toHaveURL(/\/po\//, { timeout: 20_000 }); return page.url(); @@ -50,13 +82,12 @@ export async function createDraftPo(page: Page, title: string): Promise /** Create a PO and submit it for approval; returns the PO URL. */ export async function submitPo(page: Page, title: string): Promise { await page.goto("/po/new"); - await page.waitForSelector('input[name="title"]', { timeout: 15_000 }); - await page.locator('input[name="title"]').fill(title); - await page.locator('select[name="vesselId"]').selectOption({ index: 1 }); - await page.locator('select[name="accountId"]').selectOption({ index: 1 }); - await page.getByPlaceholder("Item description").fill("Engine gasket"); - await page.getByRole("spinbutton").first().fill("2"); - await page.locator("input[placeholder='0.00']").first().fill("250"); + await fillPoHeader(page, title); + await fillFirstLineItem(page, { + name: "Engine gasket", + quantity: "2", + unitPrice: "250", + }); await page.getByRole("button", { name: /submit for approval/i }).click(); await expect(page).toHaveURL(/\/po\//, { timeout: 20_000 }); return page.url(); diff --git a/App/tests/e2e/manager-approvals.spec.ts b/App/tests/e2e/manager-approvals.spec.ts index b9d05fc..7bf5ba2 100644 --- a/App/tests/e2e/manager-approvals.spec.ts +++ b/App/tests/e2e/manager-approvals.spec.ts @@ -4,6 +4,7 @@ * M-03 (reject with reason), M-04 (request edits, flag vendor ID). */ import { test, expect, type Page } from "@playwright/test"; +import { fillFirstLineItem, fillPoHeader } from "./helpers/login"; const TECH = { email: "tech@pelagia.local", password: "tech1234" }; const MGR = { email: "manager@pelagia.local", password: "manager1234" }; @@ -13,19 +14,19 @@ async function login(page: Page, creds: typeof TECH) { await page.getByLabel(/email/i).fill(creds.email); await page.getByLabel(/password/i).fill(creds.password); await page.getByRole("button", { name: /sign in/i }).click(); - await expect(page).not.toHaveURL(/login/); + await expect(page).not.toHaveURL(/\/login/, { timeout: 20_000 }); } /** Create + submit a PO as tech user, return the PO URL */ async function submitPoAsTech(page: Page, title: string): Promise { await login(page, TECH); await page.goto("/po/new"); - await page.getByLabel(/title/i).fill(title); - await page.getByLabel(/vessel/i).selectOption({ index: 1 }); - await page.getByLabel(/account/i).selectOption({ index: 1 }); - await page.getByPlaceholder("Item description").fill("Engine filter"); - await page.getByRole("spinbutton").first().fill("2"); - await page.locator("input[placeholder='0.00']").fill("500"); + await fillPoHeader(page, title); + await fillFirstLineItem(page, { + name: "Engine filter", + quantity: "2", + unitPrice: "500", + }); await page.getByRole("button", { name: /submit for approval/i }).click(); await expect(page).toHaveURL(/\/po\//); return page.url(); diff --git a/App/tests/e2e/po-export.spec.ts b/App/tests/e2e/po-export.spec.ts index f2ee153..066ad4b 100644 --- a/App/tests/e2e/po-export.spec.ts +++ b/App/tests/e2e/po-export.spec.ts @@ -3,6 +3,7 @@ * Verifies export buttons are present and the export endpoints respond correctly. */ import { test, expect, type Page } from "@playwright/test"; +import { fillFirstLineItem, fillPoHeader } from "./helpers/login"; const TECH = { email: "tech@pelagia.local", password: "tech1234" }; const MGR = { email: "manager@pelagia.local", password: "manager1234" }; @@ -12,17 +13,17 @@ async function login(page: Page, creds: typeof TECH) { await page.getByLabel(/email/i).fill(creds.email); await page.getByLabel(/password/i).fill(creds.password); await page.getByRole("button", { name: /sign in/i }).click(); - await expect(page).not.toHaveURL(/login/); + await expect(page).not.toHaveURL(/\/login/, { timeout: 20_000 }); } async function createDraftPo(page: Page, title: string): Promise { await page.goto("/po/new"); - await page.getByLabel(/title/i).fill(title); - await page.getByLabel(/vessel/i).selectOption({ index: 1 }); - await page.getByLabel(/account/i).selectOption({ index: 1 }); - await page.getByPlaceholder("Item description").fill("Test item for export"); - await page.getByRole("spinbutton").first().fill("1"); - await page.locator("input[placeholder='0.00']").fill("100"); + await fillPoHeader(page, title); + await fillFirstLineItem(page, { + name: "Test item for export", + quantity: "1", + unitPrice: "100", + }); await page.getByRole("button", { name: /save as draft/i }).click(); await expect(page).toHaveURL(/\/po\//); return page.url(); diff --git a/App/tests/e2e/profile.spec.ts b/App/tests/e2e/profile.spec.ts index 90cb496..85b75f4 100644 --- a/App/tests/e2e/profile.spec.ts +++ b/App/tests/e2e/profile.spec.ts @@ -83,7 +83,9 @@ test.describe("Feature 11 — User profile page & manager signature", () => { await login(page, USERS.TECH); await page.goto("/profile"); - await expect(page.getByText(/change password/i)).toBeVisible(); + await expect( + page.locator("section h2").filter({ hasText: /change password/i }) + ).toBeVisible(); console.log("✓ Change Password section visible on profile page"); }); }); diff --git a/App/tests/e2e/submitter-journey.spec.ts b/App/tests/e2e/submitter-journey.spec.ts index 60c08c5..0df7d8e 100644 --- a/App/tests/e2e/submitter-journey.spec.ts +++ b/App/tests/e2e/submitter-journey.spec.ts @@ -4,6 +4,7 @@ * S-05 (view status), S-07 (edit and resubmit), S-08 (confirm receipt page visible). */ import { test, expect, type Page } from "@playwright/test"; +import { fillFirstLineItem, fillPoHeader } from "./helpers/login"; const TECH = { email: "tech@pelagia.local", password: "tech1234" }; const MGR = { email: "manager@pelagia.local", password: "manager1234" }; @@ -13,18 +14,17 @@ async function login(page: Page, creds: typeof TECH) { await page.getByLabel(/email/i).fill(creds.email); await page.getByLabel(/password/i).fill(creds.password); await page.getByRole("button", { name: /sign in/i }).click(); - await expect(page).not.toHaveURL(/login/); + await expect(page).not.toHaveURL(/\/login/, { timeout: 20_000 }); } async function fillNewPoForm(page: Page, title: string) { await page.goto("/po/new"); - await page.getByLabel(/title/i).fill(title); - await page.getByLabel(/vessel/i).selectOption({ index: 1 }); - await page.getByLabel(/account/i).selectOption({ index: 1 }); - // Line item - await page.getByPlaceholder("Item description").fill("Test marine part"); - await page.getByRole("spinbutton").first().fill("5"); - await page.locator("input[placeholder='0.00']").fill("100"); + await fillPoHeader(page, title); + await fillFirstLineItem(page, { + name: "Test marine part", + quantity: "5", + unitPrice: "100", + }); } // ── S-02: Save as draft ────────────────────────────────────────────────────── @@ -67,8 +67,8 @@ test("S-01 — new PO form shows structured T&C section with fixed first line", await login(page, TECH); await page.goto("/po/new"); await expect(page.getByText(/please quote this purchase order/i)).toBeVisible(); - await expect(page.getByLabel(/delivery/i).first()).toBeVisible(); - await expect(page.getByLabel(/payment terms/i)).toBeVisible(); + await expect(page.locator('input[name="tcDelivery"]')).toBeVisible(); + await expect(page.locator('input[name="tcPaymentTerms"]')).toBeVisible(); }); // ── S-05: View status and activity ────────────────────────────────────────── @@ -100,8 +100,8 @@ test("S-07 — submitter sees edit form pre-populated with existing values", asy await page.getByRole("link", { name: /edit/i }).click(); await expect(page).toHaveURL(/\/edit$/); // Form should be pre-populated - await expect(page.getByLabel(/title/i)).toHaveValue(title); - await expect(page.getByPlaceholder("Item description")).toHaveValue("Test marine part"); + await expect(page.locator('input[name="title"]')).toHaveValue(title); + await expect(page.getByPlaceholder("Item name *")).toHaveValue("Test marine part"); }); // ── S-08: Confirm receipt page ───────────────────────────────────────────────