@@ -2,51 +2,47 @@ import { test, expect } from "@playwright/test";
22import fs from "node:fs" ;
33import path from "node:path" ;
44import { fileURLToPath } from "node:url" ;
5- import { DEFAULT_ROLE , startCloudOnboarding } from "./onboarding-flow" ;
65
76const __dirname = path . dirname ( fileURLToPath ( import . meta. url ) ) ;
87
98/**
109 * NUX Phase 4 — visual QA screenshot capture.
1110 *
1211 * Boots a throwaway local_trusted instance (see playwright.config.ts webServer)
13- * and captures screenshots of every integrated onboarding surface:
14- * - Welcome screen (path picker)
15- * - Company step (name + mission)
16- * - Create-your-first-agent step (role picker + capsule)
17- * - First-task step
18- * - "Add an agent to an existing company" entry (/:prefix/onboarding)
12+ * and captures screenshots of every surface integrated by NUX Phases 1–3:
13+ * - "Build a new company" step 1 (company name) + step 2 (mission)
14+ * - Team-lead hire step (capsule wizard, PAP-125)
15+ * - Onboarding front door (path picker)
16+ * - "Add agents to your org" growth intake
1917 * - Conference Room (BoardChat) shell + composer + activity feed
2018 * - Artifacts page
2119 *
2220 * These are structural/rendering checks — LLM-dependent streaming (CEO chat
2321 * responses, hiring-plan generation) is verified separately on an LLM-backed
24- * instance. Screenshots land in ./test-results for upload as evidence.
22+ * instance. Screenshots land in ./nux-phase4-shots for upload as evidence.
2523 */
2624
2725// Write under the gitignored test-results dir so re-runs leave no untracked
2826// noise; screenshots are uploaded to the issue as QA evidence, not committed.
2927const SHOT_DIR = path . join ( __dirname , "test-results" , "nux-phase4-shots" ) ;
3028
31- const SHOTS = [
32- "01-welcome.png" ,
33- "02-company.png" ,
34- "03-agent.png" ,
35- "04-first-task.png" ,
36- "05-add-agent.png" ,
37- "06-board-chat.png" ,
38- "07-artifacts.png" ,
39- ] ;
40-
4129function shot ( name : string ) {
4230 fs . mkdirSync ( SHOT_DIR , { recursive : true } ) ;
4331 return path . join ( SHOT_DIR , name ) ;
4432}
4533
34+ async function openWizard ( page : import ( "@playwright/test" ) . Page ) {
35+ await page . goto ( "/onboarding" ) ;
36+ const startBtn = page . getByRole ( "button" , { name : / S t a r t O n b o a r d i n g | N e w C o m p a n y | A d d A g e n t / } ) ;
37+ if ( await startBtn . count ( ) ) {
38+ await startBtn . first ( ) . click ( ) ;
39+ }
40+ }
41+
4642test . describe ( "NUX Phase 4 visual QA" , ( ) => {
4743 test ( "captures every integrated surface" , async ( { page } ) => {
48- // Conference Room is flag-gated default-OFF: turn the experimental flag on
49- // for this throwaway instance before driving that surface (Section C) .
44+ // New-NUX surfaces are flag-gated default-OFF (PAP-136/137/138) : turn the
45+ // experimental flag on for this throwaway instance before driving them .
5046 const flagRes = await page . request . patch ( "/api/instance/settings/experimental" , {
5147 data : { enableConferenceRoomChat : true } ,
5248 } ) ;
@@ -61,43 +57,36 @@ test.describe("NUX Phase 4 visual QA", () => {
6157 const baseUrl =
6258 "http://127.0.0.1:" + ( process . env . PAPERCLIP_E2E_PORT ?? "3199" ) ;
6359
64- // ── Section A: the cloud flow, step by step ───────────────────────────
65- await page . goto ( "/onboarding" ) ;
66-
60+ // ── Section A: create-company path (name → mission → hire) ────────────
61+ await openWizard ( page ) ;
62+ // Front door shows when the wizard doesn't open directly on the create
63+ // path (e.g. another spec already created a company on this instance).
64+ const createCard = page . getByRole ( "button" , { name : / B u i l d a n e w c o m p a n y / } ) ;
65+ if ( await createCard . count ( ) ) {
66+ await createCard . first ( ) . click ( ) ;
67+ }
6768 await expect (
68- page . getByRole ( "heading" , { name : "Welcome to Paperclip! " } ) ,
69+ page . getByRole ( "heading" , { name : "Name your company " } ) ,
6970 ) . toBeVisible ( { timeout : 15_000 } ) ;
70- await page . screenshot ( { path : shot ( "01-welcome.png" ) } ) ;
71+ await page . getByPlaceholder ( "Acme Corp" ) . fill ( "QA Robotics" ) ;
72+ await page . screenshot ( { path : shot ( "02-create-name.png" ) } ) ;
7173
72- await startCloudOnboarding ( page ) ;
73-
74- // Capture the company step populated but not yet submitted, then submit.
74+ await page . getByRole ( "button" , { name : / ^ N e x t / } ) . click ( ) ;
7575 await expect (
76- page . getByRole ( "heading" , { name : "What is the name of your company or team?" } ) ,
77- ) . toBeVisible ( { timeout : 15_000 } ) ;
78- await page . locator ( "#onboarding-company-name" ) . fill ( "QA Robotics" ) ;
76+ page . getByRole ( "heading" , { name : "Define your mission" } ) ,
77+ ) . toBeVisible ( { timeout : 10_000 } ) ;
7978 await page
80- . locator ( "#onboarding-mission ")
79+ . getByPlaceholder ( "What is your team trying to achieve? ")
8180 . fill ( "Build affordable home robots that handle household chores." ) ;
82- await page . screenshot ( { path : shot ( "02-company.png" ) } ) ;
83- await page . getByRole ( "button" , { name : / ^ N e x t / } ) . click ( ) ;
81+ await page . screenshot ( { path : shot ( "03-create-mission.png" ) } ) ;
8482
85- // Agent step: pick a role so the capsule + preview render, then capture
86- // before hiring.
87- await expect (
88- page . getByRole ( "heading" , { name : "Create your first agent" } ) ,
89- ) . toBeVisible ( { timeout : 30_000 } ) ;
90- await page . locator ( "#onboarding-agent-role" ) . click ( ) ;
91- await page . getByRole ( "option" , { name : DEFAULT_ROLE , exact : true } ) . click ( ) ;
92- await page . screenshot ( { path : shot ( "03-agent.png" ) } ) ;
93- await page . getByRole ( "button" , { name : / ^ C r e a t e / } ) . click ( ) ;
94-
95- // First-task step: select a choice so the card's selected state is visible.
96- await expect (
97- page . getByRole ( "heading" , { name : "Assign your agent a first task" } ) ,
98- ) . toBeVisible ( { timeout : 30_000 } ) ;
99- await page . getByRole ( "button" , { name : / C r e a t e a h i r i n g p l a n / } ) . click ( ) ;
100- await page . screenshot ( { path : shot ( "04-first-task.png" ) } ) ;
83+ // Step 2 advances via "Confirm mission" (creates the company + goal);
84+ // step 3 is the team-lead naming step of the capsule wizard.
85+ await page . getByRole ( "button" , { name : / C o n f i r m m i s s i o n / } ) . click ( ) ;
86+ await page . waitForSelector ( 'input[placeholder="Chief of staff"]' , {
87+ timeout : 30_000 ,
88+ } ) ;
89+ await page . screenshot ( { path : shot ( "04-hire-team-lead.png" ) } ) ;
10190
10291 // The company just created anchors the route-scoped sections below.
10392 const companiesRes = await page . request . get ( `${ baseUrl } /api/companies` ) ;
@@ -106,23 +95,39 @@ test.describe("NUX Phase 4 visual QA", () => {
10695 const qaCompany = ( Array . isArray ( companies ) ? companies : [ ] ) . find (
10796 ( c : { name : string } ) => c . name === "QA Robotics" ,
10897 ) ;
109- expect ( qaCompany , "onboarding should have created QA Robotics" ) . toBeTruthy ( ) ;
98+ expect ( qaCompany , "wizard should have created QA Robotics" ) . toBeTruthy ( ) ;
11099 const prefix : string = qaCompany . issuePrefix ;
111100
112- // ── Section B: "add an agent to an existing company" entry ────────────
113- // OnboardingWizardVariant renders outside <Routes> (App.tsx), so it never
114- // sees the :companyPrefix param and the company-scoped route still opens on
115- // the welcome screen. The real existing-company entry is the launcher card
116- // behind it: dismiss the overlay, then "Add Agent" opens onboarding scoped
117- // to this company, which skips company creation and starts at the agent step.
101+ // ── Section B: front door + growth intake ─────────────────────────────
118102 await page . evaluate ( ( ) => window . localStorage . clear ( ) ) ;
119- await page . goto ( `/${ prefix } /onboarding` ) ;
120- await page . getByRole ( "button" , { name : "Close onboarding" } ) . click ( ) ;
121- await page . getByRole ( "button" , { name : "Add Agent" } ) . click ( ) ;
103+ await openWizard ( page ) ;
104+ // Reach the full-screen front door (step 0): either it shows directly or
105+ // "← Back to start" returns to it from the create step.
106+ if ( ! ( await page . getByRole ( "heading" , { name : "Welcome to Paperclip" } ) . count ( ) ) ) {
107+ await page . getByRole ( "button" , { name : / B a c k t o s t a r t / } ) . click ( ) ;
108+ }
122109 await expect (
123- page . getByRole ( "heading" , { name : "Create your first agent" } ) ,
124- ) . toBeVisible ( { timeout : 20_000 } ) ;
125- await page . screenshot ( { path : shot ( "05-add-agent.png" ) } ) ;
110+ page . getByRole ( "heading" , { name : "Welcome to Paperclip" } ) ,
111+ ) . toBeVisible ( { timeout : 10_000 } ) ;
112+ await expect (
113+ page . getByRole ( "heading" , { name : "Build a new company" } ) ,
114+ ) . toBeVisible ( ) ;
115+ await expect (
116+ page . getByRole ( "heading" , { name : "Add agents to your org" } ) ,
117+ ) . toBeVisible ( ) ;
118+ await page . screenshot ( { path : shot ( "01-front-door.png" ) } ) ;
119+
120+ await page . getByRole ( "button" , { name : / A d d a g e n t s t o y o u r o r g / } ) . click ( ) ;
121+ // The grow path shares step 1 (company name) before its step-2 intake.
122+ await expect (
123+ page . getByRole ( "heading" , { name : "Name your company" } ) ,
124+ ) . toBeVisible ( { timeout : 10_000 } ) ;
125+ await page . getByPlaceholder ( "Acme Corp" ) . fill ( "QA Robotics Grow" ) ;
126+ await page . getByRole ( "button" , { name : / ^ N e x t / } ) . click ( ) ;
127+ await expect (
128+ page . getByRole ( "heading" , { name : / T e l l u s a b o u t y o u r t e a m / } ) ,
129+ ) . toBeVisible ( { timeout : 10_000 } ) ;
130+ await page . screenshot ( { path : shot ( "05-growth-intake.png" ) } ) ;
126131
127132 // ── Section C: Conference Room (BoardChat) ────────────────────────────
128133 // Visit the company dashboard first so CompanyContext selects the company
@@ -147,7 +152,15 @@ test.describe("NUX Phase 4 visual QA", () => {
147152 await page . waitForTimeout ( 1_000 ) ;
148153 await page . screenshot ( { path : shot ( "07-artifacts.png" ) } ) ;
149154
150- for ( const f of SHOTS ) {
155+ for ( const f of [
156+ "01-front-door.png" ,
157+ "02-create-name.png" ,
158+ "03-create-mission.png" ,
159+ "04-hire-team-lead.png" ,
160+ "05-growth-intake.png" ,
161+ "06-board-chat.png" ,
162+ "07-artifacts.png" ,
163+ ] ) {
151164 const p = shot ( f ) ;
152165 expect ( fs . existsSync ( p ) , `missing ${ f } ` ) . toBe ( true ) ;
153166 expect ( fs . statSync ( p ) . size , `empty ${ f } ` ) . toBeGreaterThan ( 1_000 ) ;
0 commit comments