163 lines
8.1 KiB
TypeScript
163 lines
8.1 KiB
TypeScript
|
|
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();
|
||
|
|
});
|
||
|
|
});
|