pelagia-portal/App/tests/e2e/gst-rate.spec.ts
Hardik 1c5727850a fix(gst): 0% GST rate no longer falls back to 18%
parseFloat('0') is falsy in JS so `|| 0.18` silently discarded the user's
explicit 0% selection. Replaced with an explicit empty-string guard.
Adds e2e spec gst-rate.spec.ts covering all five GST rates (0/5/12/18/28%).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 00:00:51 +05:30

92 lines
4.3 KiB
TypeScript

/**
* E2E — GST rate selection correctness on the PO line-items editor.
*
* Bug covered: GST-0PCT-FALLBACK (fixed 2026-05-26)
* `parseFloat('0') || 0.18` is falsy, so selecting 0% GST silently fell back
* to 18%. Fixed by changing the expression to an explicit null/empty-string
* guard: `row.gstRate !== "" && row.gstRate != null ? parseFloat(row.gstRate) : 0.18`
*
* User story: S-01 (create PO, line items with correct GST calculation)
* Acceptance criterion: selecting any GST rate (0 / 5 / 12 / 18 / 28 %) on a
* line item must produce the mathematically correct Grand Total in the footer.
*
* Preconditions:
* - Dev server running at http://localhost:3000
* - Seed data present (at least one vessel and account selectable)
* - tech@pelagia.local / tech1234 credentials valid
*/
import { test, expect, type Page } from "@playwright/test";
import { login, fillPoHeader, USERS } from "./helpers/login";
// ── Helpers ──────────────────────────────────────────────────────────────────
/** Selects a GST rate in the first line-item row's GST dropdown. */
async function selectGstRate(page: Page, value: string): Promise<void> {
const lineItemsSection = page.locator("section").filter({
has: page.getByRole("heading", { name: /line items/i }),
});
const firstRow = lineItemsSection.locator("tbody tr").first();
// The GST select is the second <select> in the row (first is the UOM select)
await firstRow.locator("select").nth(1).selectOption(value);
}
/**
* Reads the Grand Total cell text from the line-items tfoot.
* The tfoot last row has a label cell containing "Grand Total" and a value cell.
*/
async function getGrandTotal(page: Page): Promise<string> {
const lineItemsSection = page.locator("section").filter({
has: page.getByRole("heading", { name: /line items/i }),
});
// The last row of the tfoot contains "Grand Total"
const grandTotalRow = lineItemsSection.locator("tfoot tr").last();
// The value is the last <td> in that row
return (await grandTotalRow.locator("td").last().textContent()) ?? "";
}
// ── Test data ─────────────────────────────────────────────────────────────────
const GST_CASES = [
{ label: "0%", value: "0", expected: "₹1,000.00" },
{ label: "5%", value: "0.05", expected: "₹1,050.00" },
{ label: "12%", value: "0.12", expected: "₹1,120.00" },
{ label: "18%", value: "0.18", expected: "₹1,180.00" },
{ label: "28%", value: "0.28", expected: "₹1,280.00" },
] as const;
// ── Tests ─────────────────────────────────────────────────────────────────────
test.describe("GST rate selection — Grand Total correctness (GST-0PCT-FALLBACK)", () => {
test.beforeEach(async ({ page }) => {
await login(page, USERS.TECH);
await page.goto("/po/new");
// Fill required header fields so the form is valid
await fillPoHeader(page, `E2E_GST_${Date.now()}`);
// Fill the first line item: name=Bearing Assembly, qty=1, unitPrice=1000
const lineItemsSection = page.locator("section").filter({
has: page.getByRole("heading", { name: /line items/i }),
});
const firstRow = lineItemsSection.locator("tbody tr").first();
await firstRow.getByPlaceholder("Item name *").fill("Bearing Assembly");
// qty input — the first number input in the row
await firstRow.locator('input[type="number"]').first().fill("1");
// unit price input — placeholder "0.00"
await firstRow.locator('input[placeholder="0.00"]').fill("1000");
});
for (const { label, value, expected } of GST_CASES) {
test(`GST-0PCT-FALLBACK: selecting ${label} GST shows Grand Total ${expected}`, async ({ page }) => {
await selectGstRate(page, value);
// The Grand Total footer cell must update reactively — assert with auto-retry
await expect(async () => {
const total = await getGrandTotal(page);
expect(total.trim()).toBe(expected);
}).toPass({ timeout: 5_000 });
});
}
});