129 lines
5.6 KiB
TypeScript
129 lines
5.6 KiB
TypeScript
import { test, expect } from "@playwright/test";
|
||
import fs from "node:fs";
|
||
import path from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
|
||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||
|
||
/**
|
||
* NUX Phase 4 — visual QA screenshot capture.
|
||
*
|
||
* Boots a throwaway local_trusted instance (see playwright.config.ts webServer)
|
||
* and captures screenshots of every surface integrated by NUX Phases 1–3:
|
||
* - "Build a new company" step 1 (company name)
|
||
* - Team-lead hire step (capsule wizard, PAP-125)
|
||
* - Conference Room (BoardChat) shell + composer + activity feed
|
||
* - Artifacts page
|
||
*
|
||
* The onboarding front door and the "Add agents to your org" growth intake
|
||
* were removed with the four-step wizard, so the shots that captured them
|
||
* are gone too.
|
||
*
|
||
* These are structural/rendering checks — LLM-dependent streaming (CEO chat
|
||
* responses, hiring-plan generation) is verified separately on an LLM-backed
|
||
* instance. Screenshots land in ./nux-phase4-shots for upload as evidence.
|
||
*/
|
||
|
||
// Write under the gitignored test-results dir so re-runs leave no untracked
|
||
// noise; screenshots are uploaded to the issue as QA evidence, not committed.
|
||
const SHOT_DIR = path.join(__dirname, "test-results", "nux-phase4-shots");
|
||
|
||
function shot(name: string) {
|
||
fs.mkdirSync(SHOT_DIR, { recursive: true });
|
||
return path.join(SHOT_DIR, name);
|
||
}
|
||
|
||
async function openWizard(page: import("@playwright/test").Page) {
|
||
await page.goto("/onboarding");
|
||
const startBtn = page.getByRole("button", { name: /Start Onboarding|New Organization|Add Agent/ });
|
||
if (await startBtn.count()) {
|
||
await startBtn.first().click();
|
||
}
|
||
}
|
||
|
||
test.describe("NUX Phase 4 visual QA", () => {
|
||
test("captures every integrated surface", async ({ page }) => {
|
||
// New-NUX surfaces are flag-gated default-OFF (PAP-136/137/138): turn the
|
||
// experimental flag on for this throwaway instance before driving them.
|
||
const flagRes = await page.request.patch("/api/instance/settings/experimental", {
|
||
data: { enableConferenceRoomChat: true },
|
||
});
|
||
expect(flagRes.ok()).toBe(true);
|
||
|
||
const consoleErrors: string[] = [];
|
||
page.on("console", (msg) => {
|
||
if (msg.type() === "error") consoleErrors.push(msg.text());
|
||
});
|
||
page.on("pageerror", (err) => consoleErrors.push("PAGEERROR: " + err.message));
|
||
|
||
const baseUrl =
|
||
"http://127.0.0.1:" + (process.env.PAPERCLIP_E2E_PORT ?? "3199");
|
||
|
||
// ── Section A: create-company path (name → hire) ──────────────────────
|
||
await openWizard(page);
|
||
await expect(
|
||
page.getByRole("heading", { name: "What is the name of your organization?" }),
|
||
).toBeVisible({ timeout: 15_000 });
|
||
await page.getByPlaceholder("e.g. Northwind Labs").fill("QA Robotics");
|
||
await page.screenshot({ path: shot("02-create-name.png") });
|
||
|
||
await page.getByRole("button", { name: /^Continue/ }).click();
|
||
// Step 1's "Next" creates the company and goes straight to the team lead.
|
||
// The mission screenshot that sat here is gone with the step it captured.
|
||
await page.waitForSelector("#onboarding-agent-name", {
|
||
timeout: 30_000,
|
||
});
|
||
await page.screenshot({ path: shot("04-hire-team-lead.png") });
|
||
|
||
// The company just created anchors the route-scoped sections below.
|
||
const companiesRes = await page.request.get(`${baseUrl}/api/companies`);
|
||
expect(companiesRes.ok()).toBe(true);
|
||
const companies = await companiesRes.json();
|
||
const qaCompany = (Array.isArray(companies) ? companies : []).find(
|
||
(c: { name: string }) => c.name === "QA Robotics",
|
||
);
|
||
expect(qaCompany, "wizard should have created QA Robotics").toBeTruthy();
|
||
const prefix: string = qaCompany.issuePrefix;
|
||
|
||
// ── Section B: Conference Room (BoardChat) ────────────────────────────
|
||
// Visit the company dashboard first so CompanyContext selects the company
|
||
// from the route before we land on the board-chat surface.
|
||
await page.evaluate(() => window.localStorage.clear());
|
||
await page.goto(`/${prefix}/dashboard`);
|
||
await page.waitForLoadState("networkidle");
|
||
await page.goto(`/${prefix}/board-chat`);
|
||
await expect(page).toHaveURL(new RegExp(`/${prefix}/board-chat`));
|
||
// Composer renders once a company is selected. (Regression guard for the
|
||
// Rules-of-Hooks crash that previously blanked this page — see PAP-50.)
|
||
await expect(
|
||
page.getByPlaceholder("Ask anything about your organization..."),
|
||
).toBeVisible({ timeout: 20_000 });
|
||
await page.waitForTimeout(2_000); // let welcome bubble + suggestion chips stage in
|
||
await page.screenshot({ path: shot("06-board-chat.png") });
|
||
|
||
// ── Section C: Artifacts ──────────────────────────────────────────────
|
||
await page.goto(`/${prefix}/artifacts`);
|
||
await expect(page).toHaveURL(new RegExp(`/${prefix}/artifacts`));
|
||
await page.waitForLoadState("networkidle");
|
||
await page.waitForTimeout(1_000);
|
||
await page.screenshot({ path: shot("07-artifacts.png") });
|
||
|
||
for (const f of [
|
||
"02-create-name.png",
|
||
"04-hire-team-lead.png",
|
||
"06-board-chat.png",
|
||
"07-artifacts.png",
|
||
]) {
|
||
const p = shot(f);
|
||
expect(fs.existsSync(p), `missing ${f}`).toBe(true);
|
||
expect(fs.statSync(p).size, `empty ${f}`).toBeGreaterThan(1_000);
|
||
}
|
||
|
||
// No React Rules-of-Hooks / render crashes on any surface we visited.
|
||
const hookErrors = consoleErrors.filter(
|
||
(e) => /Rendered more hooks|change in the order of Hooks/i.test(e),
|
||
);
|
||
expect(hookErrors, hookErrors.join("\n")).toHaveLength(0);
|
||
});
|
||
});
|