test(e2e): harden PO form selectors

This commit is contained in:
Hardik 2026-05-22 17:15:17 +05:30
parent 32ea27331c
commit 934979750f
7 changed files with 92 additions and 56 deletions

View file

@ -3,6 +3,7 @@
* Covers: A-01 (view payment queue), A-02 (mark PO as paid with reference). * Covers: A-01 (view payment queue), A-02 (mark PO as paid with reference).
*/ */
import { test, expect, type Page } from "@playwright/test"; import { test, expect, type Page } from "@playwright/test";
import { fillFirstLineItem, fillPoHeader } from "./helpers/login";
const TECH = { email: "tech@pelagia.local", password: "tech1234" }; const TECH = { email: "tech@pelagia.local", password: "tech1234" };
const MGR = { email: "manager@pelagia.local", password: "manager1234" }; 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(/email/i).fill(creds.email);
await page.getByLabel(/password/i).fill(creds.password); await page.getByLabel(/password/i).fill(creds.password);
await page.getByRole("button", { name: /sign in/i }).click(); 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 */ /** 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 // Step 1: create + submit as tech
await login(page, TECH); await login(page, TECH);
await page.goto("/po/new"); await page.goto("/po/new");
await page.getByLabel(/title/i).fill(title); await fillPoHeader(page, title);
await page.getByLabel(/vessel/i).selectOption({ index: 1 }); await fillFirstLineItem(page, {
await page.getByLabel(/account/i).selectOption({ index: 1 }); name: "Deck paint",
await page.getByPlaceholder("Item description").fill("Deck paint"); quantity: "3",
await page.getByRole("spinbutton").first().fill("3"); unitPrice: "800",
await page.locator("input[placeholder='0.00']").fill("800"); });
await page.getByRole("button", { name: /submit for approval/i }).click(); await page.getByRole("button", { name: /submit for approval/i }).click();
await expect(page).toHaveURL(/\/po\//); await expect(page).toHaveURL(/\/po\//);
const poUrl = page.url(); const poUrl = page.url();

View file

@ -19,7 +19,7 @@
* Created: 2026-05-17 * Created: 2026-05-17
*/ */
import { test, expect, type BrowserContext } from "@playwright/test"; 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( async function driveEditHighlightFlow(
page: import("@playwright/test").Page, page: import("@playwright/test").Page,
@ -29,12 +29,12 @@ async function driveEditHighlightFlow(
// Step 1: Submit as tech // Step 1: Submit as tech
await login(page, USERS.TECH); await login(page, USERS.TECH);
await page.goto("/po/new"); await page.goto("/po/new");
await page.getByLabel(/title/i).fill(title); await fillPoHeader(page, title);
await page.getByLabel(/vessel/i).selectOption({ index: 1 }); await fillFirstLineItem(page, {
await page.getByLabel(/account/i).selectOption({ index: 1 }); name: "Original item",
await page.getByPlaceholder("Item description").fill("Original item"); quantity: "2",
await page.getByRole("spinbutton").first().fill("2"); unitPrice: "150",
await page.locator("input[placeholder='0.00']").fill("150"); });
await page.getByRole("button", { name: /submit for approval/i }).click(); await page.getByRole("button", { name: /submit for approval/i }).click();
await expect(page).toHaveURL(/\/po\//, { timeout: 15_000 }); await expect(page).toHaveURL(/\/po\//, { timeout: 15_000 });
const poUrl = page.url(); const poUrl = page.url();

View file

@ -28,20 +28,52 @@ export async function login(page: Page, creds: Credentials): Promise<void> {
console.log(`✓ Logged in as ${creds.email}`); console.log(`✓ Logged in as ${creds.email}`);
} }
export async function fillPoHeader(page: Page, title: string): Promise<void> {
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<void> {
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. * Create a minimal draft PO and return the absolute PO URL.
* Uses name-based selectors since the PO form labels have no htmlFor binding. * Uses name-based selectors since the PO form labels have no htmlFor binding.
*/ */
export async function createDraftPo(page: Page, title: string): Promise<string> { export async function createDraftPo(page: Page, title: string): Promise<string> {
await page.goto("/po/new"); await page.goto("/po/new");
// Wait for the form to be ready await fillPoHeader(page, title);
await page.waitForSelector('input[name="title"]', { timeout: 15_000 }); await fillFirstLineItem(page, {
await page.locator('input[name="title"]').fill(title); name: "Test item",
await page.locator('select[name="vesselId"]').selectOption({ index: 1 }); quantity: "1",
await page.locator('select[name="accountId"]').selectOption({ index: 1 }); unitPrice: "100",
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 page.getByRole("button", { name: /save as draft/i }).click(); await page.getByRole("button", { name: /save as draft/i }).click();
await expect(page).toHaveURL(/\/po\//, { timeout: 20_000 }); await expect(page).toHaveURL(/\/po\//, { timeout: 20_000 });
return page.url(); return page.url();
@ -50,13 +82,12 @@ export async function createDraftPo(page: Page, title: string): Promise<string>
/** Create a PO and submit it for approval; returns the PO URL. */ /** Create a PO and submit it for approval; returns the PO URL. */
export async function submitPo(page: Page, title: string): Promise<string> { export async function submitPo(page: Page, title: string): Promise<string> {
await page.goto("/po/new"); await page.goto("/po/new");
await page.waitForSelector('input[name="title"]', { timeout: 15_000 }); await fillPoHeader(page, title);
await page.locator('input[name="title"]').fill(title); await fillFirstLineItem(page, {
await page.locator('select[name="vesselId"]').selectOption({ index: 1 }); name: "Engine gasket",
await page.locator('select[name="accountId"]').selectOption({ index: 1 }); quantity: "2",
await page.getByPlaceholder("Item description").fill("Engine gasket"); unitPrice: "250",
await page.getByRole("spinbutton").first().fill("2"); });
await page.locator("input[placeholder='0.00']").first().fill("250");
await page.getByRole("button", { name: /submit for approval/i }).click(); await page.getByRole("button", { name: /submit for approval/i }).click();
await expect(page).toHaveURL(/\/po\//, { timeout: 20_000 }); await expect(page).toHaveURL(/\/po\//, { timeout: 20_000 });
return page.url(); return page.url();

View file

@ -4,6 +4,7 @@
* M-03 (reject with reason), M-04 (request edits, flag vendor ID). * M-03 (reject with reason), M-04 (request edits, flag vendor ID).
*/ */
import { test, expect, type Page } from "@playwright/test"; import { test, expect, type Page } from "@playwright/test";
import { fillFirstLineItem, fillPoHeader } from "./helpers/login";
const TECH = { email: "tech@pelagia.local", password: "tech1234" }; const TECH = { email: "tech@pelagia.local", password: "tech1234" };
const MGR = { email: "manager@pelagia.local", password: "manager1234" }; 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(/email/i).fill(creds.email);
await page.getByLabel(/password/i).fill(creds.password); await page.getByLabel(/password/i).fill(creds.password);
await page.getByRole("button", { name: /sign in/i }).click(); 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 */ /** Create + submit a PO as tech user, return the PO URL */
async function submitPoAsTech(page: Page, title: string): Promise<string> { async function submitPoAsTech(page: Page, title: string): Promise<string> {
await login(page, TECH); await login(page, TECH);
await page.goto("/po/new"); await page.goto("/po/new");
await page.getByLabel(/title/i).fill(title); await fillPoHeader(page, title);
await page.getByLabel(/vessel/i).selectOption({ index: 1 }); await fillFirstLineItem(page, {
await page.getByLabel(/account/i).selectOption({ index: 1 }); name: "Engine filter",
await page.getByPlaceholder("Item description").fill("Engine filter"); quantity: "2",
await page.getByRole("spinbutton").first().fill("2"); unitPrice: "500",
await page.locator("input[placeholder='0.00']").fill("500"); });
await page.getByRole("button", { name: /submit for approval/i }).click(); await page.getByRole("button", { name: /submit for approval/i }).click();
await expect(page).toHaveURL(/\/po\//); await expect(page).toHaveURL(/\/po\//);
return page.url(); return page.url();

View file

@ -3,6 +3,7 @@
* Verifies export buttons are present and the export endpoints respond correctly. * Verifies export buttons are present and the export endpoints respond correctly.
*/ */
import { test, expect, type Page } from "@playwright/test"; import { test, expect, type Page } from "@playwright/test";
import { fillFirstLineItem, fillPoHeader } from "./helpers/login";
const TECH = { email: "tech@pelagia.local", password: "tech1234" }; const TECH = { email: "tech@pelagia.local", password: "tech1234" };
const MGR = { email: "manager@pelagia.local", password: "manager1234" }; 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(/email/i).fill(creds.email);
await page.getByLabel(/password/i).fill(creds.password); await page.getByLabel(/password/i).fill(creds.password);
await page.getByRole("button", { name: /sign in/i }).click(); 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<string> { async function createDraftPo(page: Page, title: string): Promise<string> {
await page.goto("/po/new"); await page.goto("/po/new");
await page.getByLabel(/title/i).fill(title); await fillPoHeader(page, title);
await page.getByLabel(/vessel/i).selectOption({ index: 1 }); await fillFirstLineItem(page, {
await page.getByLabel(/account/i).selectOption({ index: 1 }); name: "Test item for export",
await page.getByPlaceholder("Item description").fill("Test item for export"); quantity: "1",
await page.getByRole("spinbutton").first().fill("1"); unitPrice: "100",
await page.locator("input[placeholder='0.00']").fill("100"); });
await page.getByRole("button", { name: /save as draft/i }).click(); await page.getByRole("button", { name: /save as draft/i }).click();
await expect(page).toHaveURL(/\/po\//); await expect(page).toHaveURL(/\/po\//);
return page.url(); return page.url();

View file

@ -83,7 +83,9 @@ test.describe("Feature 11 — User profile page & manager signature", () => {
await login(page, USERS.TECH); await login(page, USERS.TECH);
await page.goto("/profile"); 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"); console.log("✓ Change Password section visible on profile page");
}); });
}); });

View file

@ -4,6 +4,7 @@
* S-05 (view status), S-07 (edit and resubmit), S-08 (confirm receipt page visible). * S-05 (view status), S-07 (edit and resubmit), S-08 (confirm receipt page visible).
*/ */
import { test, expect, type Page } from "@playwright/test"; import { test, expect, type Page } from "@playwright/test";
import { fillFirstLineItem, fillPoHeader } from "./helpers/login";
const TECH = { email: "tech@pelagia.local", password: "tech1234" }; const TECH = { email: "tech@pelagia.local", password: "tech1234" };
const MGR = { email: "manager@pelagia.local", password: "manager1234" }; 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(/email/i).fill(creds.email);
await page.getByLabel(/password/i).fill(creds.password); await page.getByLabel(/password/i).fill(creds.password);
await page.getByRole("button", { name: /sign in/i }).click(); 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) { async function fillNewPoForm(page: Page, title: string) {
await page.goto("/po/new"); await page.goto("/po/new");
await page.getByLabel(/title/i).fill(title); await fillPoHeader(page, title);
await page.getByLabel(/vessel/i).selectOption({ index: 1 }); await fillFirstLineItem(page, {
await page.getByLabel(/account/i).selectOption({ index: 1 }); name: "Test marine part",
// Line item quantity: "5",
await page.getByPlaceholder("Item description").fill("Test marine part"); unitPrice: "100",
await page.getByRole("spinbutton").first().fill("5"); });
await page.locator("input[placeholder='0.00']").fill("100");
} }
// ── S-02: Save as draft ────────────────────────────────────────────────────── // ── 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 login(page, TECH);
await page.goto("/po/new"); await page.goto("/po/new");
await expect(page.getByText(/please quote this purchase order/i)).toBeVisible(); await expect(page.getByText(/please quote this purchase order/i)).toBeVisible();
await expect(page.getByLabel(/delivery/i).first()).toBeVisible(); await expect(page.locator('input[name="tcDelivery"]')).toBeVisible();
await expect(page.getByLabel(/payment terms/i)).toBeVisible(); await expect(page.locator('input[name="tcPaymentTerms"]')).toBeVisible();
}); });
// ── S-05: View status and activity ────────────────────────────────────────── // ── 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 page.getByRole("link", { name: /edit/i }).click();
await expect(page).toHaveURL(/\/edit$/); await expect(page).toHaveURL(/\/edit$/);
// Form should be pre-populated // Form should be pre-populated
await expect(page.getByLabel(/title/i)).toHaveValue(title); await expect(page.locator('input[name="title"]')).toHaveValue(title);
await expect(page.getByPlaceholder("Item description")).toHaveValue("Test marine part"); await expect(page.getByPlaceholder("Item name *")).toHaveValue("Test marine part");
}); });
// ── S-08: Confirm receipt page ─────────────────────────────────────────────── // ── S-08: Confirm receipt page ───────────────────────────────────────────────