WFDASH-WEBAPP/e2e/management.spec.ts

163 lines
8.1 KiB
TypeScript
Raw Normal View History

import { test, expect, HAS_AUTH_FIXTURE } from "./_helpers";
import PocketBase from "pocketbase";
import { randomUUID } from "node:crypto";
const POCKETBASE_URL = process.env.E2E_POCKETBASE_URL ?? "http://127.0.0.1:8090";
/**
* End-to-end coverage for the Management section: add a Company, then a
* Customer linked to that company (via the searchable combobox + rep
* users dropdown + lat/lng inputs), then a Supplier. Verifies the new
* field shapes (geoPoint + relations) round-trip through the live PB.
*
* Requires the same auth fixture as `auth.spec.ts` — a seeded
* PocketBase user with E2E_TEST_EMAIL / E2E_TEST_PASSWORD. Without
* that we skip the whole file rather than silently passing.
*
* Cleanup: each spec records the ids it created into a per-test
* `cleanup` array and deletes them in afterEach. The auth user itself
* is created by the existing test fixture (see e2e/_helpers.ts).
*/
test.describe("management flow", () => {
test.skip(!HAS_AUTH_FIXTURE, "E2E_TEST_EMAIL / E2E_TEST_PASSWORD not set — skipping management specs");
// Track ids we create per-test for cleanup.
const cleanup: { collection: string; id: string }[] = [];
test.afterEach(async ({ request }) => {
// Use the Next.js dev server's admin PB via /api/mcp? No — MCP
// doesn't expose a delete. Use the test page context: easiest is
// to sign in as the admin PB directly.
const admin = new PocketBase(POCKETBASE_URL);
await admin.admins.authWithPassword(
process.env.PB_ADMIN_EMAIL!,
process.env.PB_ADMIN_PASSWORD!
);
for (const { collection, id } of cleanup.splice(0)) {
try {
await admin.collection(collection).delete(id);
} catch {
// Best-effort — already-deleted rows are fine.
}
}
});
test("add company → customer → supplier via the UI", async ({ page }) => {
const stamp = randomUUID().slice(0, 8);
const companyName = `PW-CO-${stamp}`;
const customerName = `PW-CU-${stamp}`;
const supplierName = `PW-SU-${stamp}`;
// ── Sign in ───────────────────────────────────────────────────
await page.goto("/login");
await page.getByLabel(/email/i).fill(process.env.E2E_TEST_EMAIL!);
await page.getByLabel(/password/i).fill(process.env.E2E_TEST_PASSWORD!);
await page.getByRole("button", { name: /sign in|log in|login/i }).click();
await page.waitForURL((u) => !u.pathname.startsWith("/login"), { timeout: 10_000 });
// ── Add Company ──────────────────────────────────────────────
await page.goto("/companies");
await expect(page.getByRole("heading", { name: /companies/i })).toBeVisible();
await page.getByRole("button", { name: /add company/i }).click();
// The Sheet form opens.
await page.getByLabel(/company name/i).fill(companyName);
await page.getByLabel(/address/i).fill("1 PW Test Road");
await page.getByLabel(/vendor number/i).fill(`PW-V-${stamp}`);
// Lat / lng inputs — leave empty (PB default {0,0} → UI masks as empty).
await page.getByRole("button", { name: /create company/i }).click();
await expect(page.getByText(companyName)).toBeVisible({ timeout: 10_000 });
// Track for cleanup. We re-read the id from PB by filter.
const admin = new PocketBase(POCKETBASE_URL);
await admin.admins.authWithPassword(
process.env.PB_ADMIN_EMAIL!,
process.env.PB_ADMIN_PASSWORD!
);
const companyRows = await admin.collection("companies").getFullList({
filter: `company = "${companyName}"`,
});
expect(companyRows).toHaveLength(1);
cleanup.push({ collection: "companies", id: companyRows[0].id });
// ── Add Customer linked to the company ───────────────────────
await page.goto("/customers");
await expect(page.getByRole("heading", { name: /customers/i })).toBeVisible();
await page.getByRole("button", { name: /add customer/i }).click();
await page.getByLabel(/customer name/i).fill(customerName);
await page.getByLabel(/^email$/i).fill(`pw-cu-${stamp}@example.com`);
await page.getByLabel(/^phone$/i).fill("0123456789");
// Company combobox — opens popover, types to filter, picks the row.
await page.getByRole("button", { name: /company/i, exact: true }).click();
await page.getByPlaceholder(/search companies/i).fill(companyName);
await page.getByRole("option", { name: new RegExp(companyName) }).click();
// Rep users dropdown — opens, waits for the lazy fetch, picks the
// first user.
await page.getByRole("button", { name: /user/i, exact: false }).click();
// The combobox shows "Loading reps…" until /api/v1/users/lookup
// returns; wait for at least one CommandItem option.
await page.waitForSelector('[cmdk-item], [role="option"]', { timeout: 10_000 });
await page.locator('[cmdk-item], [role="option"]').first().click();
// Lat / lng — Johannesburg coords.
await page.getByLabel(/latitude/i).fill("-26.2041");
await page.getByLabel(/longitude/i).fill("28.0473");
await page.getByRole("button", { name: /create customer/i }).click();
await expect(page.getByText(customerName)).toBeVisible({ timeout: 10_000 });
const customerRows = await admin.collection("customers").getFullList({
filter: `customer = "${customerName}"`,
});
expect(customerRows).toHaveLength(1);
const customer = customerRows[0];
cleanup.push({ collection: "customers", id: customer.id });
// Assert the relations + geoPoint persisted correctly.
expect(customer.company).toBe(companyRows[0].id);
expect(customer.rep).toBeTruthy(); // any real user id
expect(customer.coordinates).toEqual({ lon: 28.0473, lat: -26.2041 });
// Customer detail page should render the expanded company name in
// the header subtitle.
await page.goto(`/customers/${customer.id}`);
await expect(page.getByRole("heading", { name: customerName })).toBeVisible();
await expect(page.getByText(companyName)).toBeVisible();
// ── Add Supplier (unchanged fields — sanity check) ───────────
await page.goto("/suppliers");
await page.getByRole("button", { name: /add supplier|new supplier/i }).click();
// The supplier form is a Sheet (older pattern); use placeholder
// text to find the inputs.
await page.getByPlaceholder(/supplier.*name|name.*supplier/i).first().fill(supplierName).catch(async () => {
// Fallback: label-based.
await page.getByLabel(/supplier name/i).fill(supplierName);
});
await page.getByRole("button", { name: /create supplier|add supplier/i }).click();
await expect(page.getByText(supplierName)).toBeVisible({ timeout: 10_000 });
const supplierRows = await admin.collection("suppliers").getFullList({
filter: `suppliername = "${supplierName}"`,
});
expect(supplierRows).toHaveLength(1);
cleanup.push({ collection: "suppliers", id: supplierRows[0].id });
});
test("rejects partial coordinates on customer form", async ({ page }) => {
await page.goto("/login");
await page.getByLabel(/email/i).fill(process.env.E2E_TEST_EMAIL!);
await page.getByLabel(/password/i).fill(process.env.E2E_TEST_PASSWORD!);
await page.getByRole("button", { name: /sign in|log in|login/i }).click();
await page.waitForURL((u) => !u.pathname.startsWith("/login"), { timeout: 10_000 });
await page.goto("/customers");
await page.getByRole("button", { name: /add customer/i }).click();
await page.getByLabel(/customer name/i).fill(`PW-PARTIAL-${randomUUID().slice(0, 6)}`);
// Fill only latitude, not longitude.
await page.getByLabel(/latitude/i).fill("-26.2041");
await page.getByRole("button", { name: /create customer/i }).click();
// Should NOT save; should surface a validation error.
await expect(page.getByText(/enter both latitude and longitude/i)).toBeVisible({
timeout: 5_000,
});
// Sheet stays open.
await expect(page.getByRole("button", { name: /create customer/i })).toBeVisible();
});
});