Files
PaperClipAI/tests/e2e/nux-phase4-screenshots.spec.ts
DottaandPaperclip c79d347abe Update company creation copy (#8653)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The onboarding and company switcher UI are the first places users
create or select an organization
> - Some of that surface still used older team/workspace wording even
though the product model is company-centric
> - Mixed wording makes the setup path feel inconsistent and can make
users wonder whether they are creating a team, workspace, or company
> - This pull request updates the affected UI copy to consistently say
company
> - The benefit is a clearer first-run and navigation experience without
changing behavior

## Linked Issues or Issue Description

No public issue exists for this small UI polish change.

### Problem or motivation

The company creation and switcher surfaces used mixed
team/workspace/company wording for the same concept, which makes the
setup path feel inconsistent.

### Proposed solution

Update the visible copy, accessibility label, inline comment, e2e
expectations, and matching test expectations to use company-centric
language consistently.

### Alternatives considered

Leave the existing wording alone, but that preserves inconsistent
terminology in a high-traffic setup path.

### Roadmap alignment

This is focused UI polish and does not overlap with a roadmap-level core
feature.

### Additional context

The create action keeps its trailing ellipsis because it opens the
onboarding wizard rather than completing immediately.

## What Changed

- Updated front door and onboarding wizard labels from team-oriented
copy to company-oriented copy.
- Updated the sidebar company menu from workspace/team wording to
company wording, including the trigger accessibility label and empty
fallback text.
- Kept the sidebar create action ellipsis for the dialog/wizard
affordance.
- Updated component and Playwright test expectations for the new copy.

## Verification

- `pnpm exec vitest run ui/src/components/SidebarCompanyMenu.test.tsx`
- Attempted `npx playwright test --config tests/e2e/playwright.config.ts
tests/e2e/onboarding.spec.ts tests/e2e/nux-phase4-screenshots.spec.ts
tests/e2e/planning-mode-visual-verification.spec.ts
tests/e2e/conference-room-typing-intro.spec.ts`; local browser launch is
blocked by missing host Chromium dependencies (`libatk1.0-0t64`,
`libatspi2.0-0t64`, `libxcomposite1`, `libxdamage1`, `libxfixes3`,
`libxrandr2`, `libgbm1`, `libasound2t64`).
- Screenshots intentionally omitted because this is a copy-only change
and no design screenshots are needed for review.

## Risks

Low risk. This is copy-only UI polish plus matching test updates; no
data model, API, migration, workflow, lockfile, or behavior changes are
included.

## Model Used

OpenAI GPT-5 Codex (`gpt-5`) via the Paperclip Codex agent, with
tool-assisted repository inspection, GitHub CLI usage, and local command
execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-06-26 12:01:02 -05:00

176 lines
7.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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) + step 2 (mission)
* - Team-lead hire step (capsule wizard, PAP-125)
* - Onboarding front door (path picker)
* - "Add agents to your org" growth intake
* - Conference Room (BoardChat) shell + composer + activity feed
* - Artifacts page
*
* 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 Company|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 → mission → hire) ────────────
await openWizard(page);
// Front door shows when the wizard doesn't open directly on the create
// path (e.g. another spec already created a company on this instance).
const createCard = page.getByRole("button", { name: /Build a new company/ });
if (await createCard.count()) {
await createCard.first().click();
}
await expect(
page.getByRole("heading", { name: "Name your company" }),
).toBeVisible({ timeout: 15_000 });
await page.getByPlaceholder("Acme Corp").fill("QA Robotics");
await page.screenshot({ path: shot("02-create-name.png") });
await page.getByRole("button", { name: /^Next/ }).click();
await expect(
page.getByRole("heading", { name: "Define your mission" }),
).toBeVisible({ timeout: 10_000 });
await page
.getByPlaceholder("What is your team trying to achieve?")
.fill("Build affordable home robots that handle household chores.");
await page.screenshot({ path: shot("03-create-mission.png") });
// Step 2 advances via "Confirm mission" (creates the company + goal);
// step 3 is the team-lead naming step of the capsule wizard.
await page.getByRole("button", { name: /Confirm mission/ }).click();
await page.waitForSelector('input[placeholder="Chief of staff"]', {
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: front door + growth intake ─────────────────────────────
await page.evaluate(() => window.localStorage.clear());
await openWizard(page);
// Reach the full-screen front door (step 0): either it shows directly or
// "← Back to start" returns to it from the create step.
if (!(await page.getByRole("heading", { name: "Welcome to Paperclip" }).count())) {
await page.getByRole("button", { name: /Back to start/ }).click();
}
await expect(
page.getByRole("heading", { name: "Welcome to Paperclip" }),
).toBeVisible({ timeout: 10_000 });
await expect(
page.getByRole("heading", { name: "Build a new company" }),
).toBeVisible();
await expect(
page.getByRole("heading", { name: "Add agents to your org" }),
).toBeVisible();
await page.screenshot({ path: shot("01-front-door.png") });
await page.getByRole("button", { name: /Add agents to your org/ }).click();
// The grow path shares step 1 (company name) before its step-2 intake.
await expect(
page.getByRole("heading", { name: "Name your company" }),
).toBeVisible({ timeout: 10_000 });
await page.getByPlaceholder("Acme Corp").fill("QA Robotics Grow");
await page.getByRole("button", { name: /^Next/ }).click();
await expect(
page.getByRole("heading", { name: /Tell us about your team/ }),
).toBeVisible({ timeout: 10_000 });
await page.screenshot({ path: shot("05-growth-intake.png") });
// ── Section C: 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 company..."),
).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 D: 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 [
"01-front-door.png",
"02-create-name.png",
"03-create-mission.png",
"04-hire-team-lead.png",
"05-growth-intake.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);
});
});