import { _electron as electron, expect, test, type Page } from "@playwright/test"; import { mkdirSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { randomUUID } from "node:crypto"; import { DatabaseSync } from "node:sqlite"; import { buildFloorPlan } from "../../src/renderer/src/pages/office/floor-plan"; import { cameraFor } from "../../src/renderer/src/pages/office/simulation"; import { isoFrame, isoPixelSize, toIso } from "../../src/renderer/src/pages/office/iso"; import type { FloorPlan, Vec } from "../../src/renderer/src/pages/office/office-types"; import { seedBuiltinSkillsFixture } from "./builtin-skills-fixture"; test("the office floor opens agent details on click", async () => { const profileDir = join(tmpdir(), `agentcompany-new-office-${randomUUID()}`); mkdirSync(profileDir, { recursive: true }); const app = await electron.launch({ args: [resolve("out/main/index.js")], env: { ...process.env, AGENTCOMPANY_NEW_PROFILE_DIR: profileDir, AGENTCOMPANY_NEW_AUTORUN: "0", AGENTCOMPANY_NEW_BUILTIN_SKILLS_DIR: seedBuiltinSkillsFixture(join(profileDir, "builtin-src")), }, }); try { const page = await app.firstWindow(); // The office renders in a rAF loop; a throwing frame (e.g. a broken // effect draw) surfaces only as a page error, so collect them. const pageErrors: string[] = []; page.on("pageerror", (error) => pageErrors.push(error.message)); await page.setViewportSize({ width: 1440, height: 900 }); await page.getByRole("button", { name: "Get started" }).click(); await page.getByLabel("API key").fill("office-e2e-key"); await page.getByLabel("Model").fill("gpt-5.4"); await page.getByRole("button", { name: "Continue" }).click(); await page.getByLabel("Company name").fill("Pixel Works"); await page.getByLabel("Your name").fill("Morgan Reyes"); await page.getByLabel("Description").fill("Office e2e coverage."); await page.getByRole("button", { name: "Continue" }).click(); await page.getByRole("button", { name: "Continue" }).click(); await page.getByLabel("Goal").fill("Verify the office floor."); await page.getByLabel("Acceptance criteria").fill("Clicking an agent opens their card."); await page.getByRole("button", { name: "Launch" }).click(); await expect(page.getByRole("heading", { name: "Connectors" })).toBeVisible(); await page.getByRole("button", { name: "Skip" }).click(); await expect(page.getByRole("button", { name: /Pixel Works/ })).toBeVisible(); await page.getByRole("button", { name: "Office", exact: true }).click(); const canvas = page.locator("canvas"); await expect(canvas).toBeVisible(); await page.waitForTimeout(300); // Onboarding seeds exactly one agent (the CEO), so the floor plan and // camera are fully determined by the same pure modules the app uses. const box = await canvas.boundingBox(); expect(box).not.toBeNull(); const plan = buildFloorPlan(0); const ratio = await page.evaluate(() => window.devicePixelRatio); const { x: clickX, y: clickY } = onScreen(plan, box!, ratio, plan.ceoSeat, 20); await page.mouse.click(clickX, clickY); const card = page.getByRole("dialog", { name: "Agent CEO" }); await expect(card).toBeVisible(); await expect(card.getByText("CEO", { exact: true })).toBeVisible(); await expect(card.getByText("Chief Executive Officer")).toBeVisible(); await expect(card.getByText("Executive", { exact: true })).toBeVisible(); // The card sits beside the clicked agent, not pinned to a screen corner. const cardBox = await card.boundingBox(); expect(cardBox).not.toBeNull(); const gapToSprite = Math.min( Math.abs(cardBox!.x - clickX), Math.abs(cardBox!.x + cardBox!.width - clickX), ); expect(gapToSprite).toBeLessThan(80); expect(Math.abs(cardBox!.y + cardBox!.height / 2 - clickY)).toBeLessThan(160); await page.keyboard.press("Escape"); await expect(card).not.toBeVisible(); // The office shows real state: a blocked task makes its agent blocked, with the reason on the card. const db = new DatabaseSync(join(profileDir, "agentcompany.db")); const blocked = db.prepare("update tasks set blocked_reason = 'needs the staging key' where assignee_agent_id = (select id from agents where name = 'CEO') and status <> 'done'").run(); db.close(); expect(blocked.changes).toBeGreaterThan(0); await app.evaluate(({ BrowserWindow }) => { BrowserWindow.getAllWindows()[0]!.webContents.send("agentcompany:renderer-event", { type: "snapshot-changed", version: Number.MAX_SAFE_INTEGER }); }); await expect(page.getByLabel("Show a person").locator("option", { hasText: "CEO, Blocked" })).toHaveCount(1); await page.getByLabel("Show a person").selectOption({ label: "CEO, Blocked" }); await expect(card.getByText("Blocked: needs the staging key")).toBeVisible(); await expect(card.getByText("Waiting on you.")).toBeVisible(); await page.keyboard.press("Escape"); await expect(card).not.toBeVisible(); // Clicking empty corridor floor selects nothing. const corridor = onScreen(plan, box!, ratio, { x: 500, y: 320 }, 0); await page.mouse.click(corridor.x, corridor.y); await expect(page.getByRole("dialog", { name: /Agent / })).not.toBeVisible(); // WASD input is accepted without disturbing the page. for (const key of ["KeyD", "KeyS", "KeyA", "KeyW"]) { await page.keyboard.down(key); await page.waitForTimeout(120); await page.keyboard.up(key); } await expect(canvas).toBeVisible(); // The office offers no pretend controls over work. await expect(page.getByRole("button", { name: "Whip" })).toHaveCount(0); // Hired agents take open-floor desks in hire order and are clickable too. await hireAgent(page, "Ava Patel", "Designer", "Design"); await hireAgent(page, "Ken Ito", "Engineer", "Engineering"); // The org chart draws everyone as the office's pixel people: the chair, the CEO and both hires. await page.getByRole("button", { name: "Team", exact: true }).click(); const avatars = page.locator(".org-node-card canvas.pixel-avatar"); await expect(avatars).toHaveCount(4); const painted = await avatars.evaluateAll((canvases) => canvases.map((canvas) => { const { data } = (canvas as HTMLCanvasElement).getContext("2d")!.getImageData(0, 0, (canvas as HTMLCanvasElement).width, (canvas as HTMLCanvasElement).height); let opaque = 0; for (let i = 3; i < data.length; i += 4) if (data[i] === 255) opaque++; return opaque; })); for (const count of painted) expect(count).toBeGreaterThan(100); // Different people look different. const looks = await avatars.evaluateAll((canvases) => canvases.map((canvas) => (canvas as HTMLCanvasElement).toDataURL())); expect(new Set(looks).size).toBe(4); const avaLook = await page.locator(".org-node-card", { hasText: "Ava Patel" }).locator("canvas.pixel-avatar").evaluate((canvas) => (canvas as HTMLCanvasElement).toDataURL()); await page.getByRole("button", { name: "Office", exact: true }).click(); await expect(canvas).toBeVisible(); await page.waitForTimeout(300); const populated = buildFloorPlan(2); const populatedBox = await canvas.boundingBox(); const firstDesk = onScreen(populated, populatedBox!, ratio, populated.desks[0].seat, 20); await page.mouse.click(firstDesk.x, firstDesk.y); const avaCard = page.getByRole("dialog", { name: "Agent Ava Patel" }); await expect(avaCard).toBeVisible(); await expect(avaCard.getByText("Design", { exact: true })).toBeVisible(); // The card shows the same pixel face the org chart drew for Ava. expect(await avaCard.locator("canvas.pixel-avatar").evaluate((canvas) => (canvas as HTMLCanvasElement).toDataURL())).toBe(avaLook); const outDir = resolve("test-results/screenshots"); mkdirSync(outDir, { recursive: true }); await page.screenshot({ path: join(outDir, "19-office-populated.png"), fullPage: false }); expect(pageErrors).toEqual([]); } finally { await app.close(); } }); async function hireAgent(page: Page, name: string, role: string, department: string): Promise { await page.getByRole("button", { name: "Team", exact: true }).click(); await page.getByRole("button", { name: "Hire", exact: true }).click(); await page.getByLabel("Name").fill(name); await page.getByLabel("Role").fill(role); await page.getByLabel("Department").fill(department); await page.getByLabel("Monthly budget").fill("100"); await page.locator(".modal").getByRole("button", { name: "Hire" }).click(); await expect(page.getByText(name).first()).toBeVisible(); } /** Where a plan point, lifted by `lift` iso pixels, is drawn: the same projection, camera and pixel size the page uses, with the player at the entrance. */ function onScreen(plan: FloorPlan, box: { x: number; y: number; width: number; height: number }, ratio: number, point: Vec, lift: number): Vec { const pixel = isoPixelSize(ratio); const frame = isoFrame(plan.world); const camera = cameraFor(frame.size, box.width / pixel, box.height / pixel, toIso(frame, plan.entrance)); const at = toIso(frame, point, lift); return { x: box.x + (at.x - Math.round(camera.x)) * pixel, y: box.y + (at.y - Math.round(camera.y)) * pixel }; }