|
| 1 | +/** |
| 2 | + * One-off screenshot script for the dialectic playground. |
| 3 | + * Run with: pnpm exec playwright test packages/web/e2e/playground.screenshots.ts |
| 4 | + * Outputs are written to docs/screenshots/. |
| 5 | + */ |
| 6 | + |
| 7 | +import { mkdirSync } from "node:fs"; |
| 8 | +import { dirname, resolve } from "node:path"; |
| 9 | +import { fileURLToPath } from "node:url"; |
| 10 | +import { test } from "@playwright/test"; |
| 11 | + |
| 12 | +const __filename = fileURLToPath(import.meta.url); |
| 13 | +const __dirname = dirname(__filename); |
| 14 | +const OUT_DIR = resolve(__dirname, "../../../docs/screenshots"); |
| 15 | + |
| 16 | +const STORE_KEY = "openconcho:instances"; |
| 17 | +const STORE_VALUE = JSON.stringify({ |
| 18 | + instances: [ |
| 19 | + { |
| 20 | + id: "demo-inst", |
| 21 | + name: "Demo Honcho", |
| 22 | + baseUrl: "http://localhost:8001", |
| 23 | + token: "", |
| 24 | + }, |
| 25 | + ], |
| 26 | + activeId: "demo-inst", |
| 27 | +}); |
| 28 | + |
| 29 | +const WORKSPACE = "demo-workspace"; |
| 30 | +const PEER = "alice@example.com"; |
| 31 | + |
| 32 | +// Per-level mocked latency (ms) and answer. |
| 33 | +const FIXTURES: Record<string, { delayMs: number; content: string }> = { |
| 34 | + minimal: { |
| 35 | + delayMs: 140, |
| 36 | + content: |
| 37 | + "Quick gist: Alice prefers async standups, dislikes meetings on Mondays, and tracks priorities in Linear.", |
| 38 | + }, |
| 39 | + low: { |
| 40 | + delayMs: 410, |
| 41 | + content: |
| 42 | + "Alice runs the platform team. She prefers async standups, batches code review in the afternoons, and pushes back on meetings before 10am. Linear is her source of truth for priorities.", |
| 43 | + }, |
| 44 | + medium: { |
| 45 | + delayMs: 1180, |
| 46 | + content: |
| 47 | + "Alice leads the platform team and operates on async-by-default. Three recurring patterns:\n\n• Async over sync — she explicitly skips standups in favor of written status posts on Wednesdays.\n• Deep-work mornings — meetings before 10am are pushed back; she protects 9–11am for coding.\n• Single-source-of-truth in Linear — anything not tracked there is treated as not happening.", |
| 48 | + }, |
| 49 | + high: { |
| 50 | + delayMs: 2410, |
| 51 | + content: |
| 52 | + "Alice's working model has stayed remarkably stable over the last three months. She leads platform, treats async writing as the default communication mode, and resists synchronous coordination unless a decision is actively blocked. Three concrete patterns recur:\n\n1. Async-first standups — Wednesday written status, no daily sync.\n2. Morning deep work — calendar protected 9–11am, meetings pushed past 10.\n3. Linear as system-of-record — verbal commitments she hasn't written into Linear are treated as not real.\n\nShe also pushes back hard on cross-team meetings without a clear decision owner.", |
| 53 | + }, |
| 54 | + max: { |
| 55 | + delayMs: 3920, |
| 56 | + content: |
| 57 | + "Across her recent sessions Alice consistently surfaces three reinforcing patterns and one tension worth flagging.\n\nPatterns:\n1. Async-first communication — explicit preference for written status (Wednesday Linear updates) over standups; she's said \"if it's not in Linear it isn't real\" in three separate threads.\n2. Protected morning deep-work — calendar is blocked 9–11am every weekday; she'll move meetings rather than break the block.\n3. Decision-owner gating — she refuses cross-team meetings without a named decision owner; this has come up six times since March.\n\nTension to flag: Alice's async-default occasionally collides with newer hires who prefer synchronous onboarding. She's aware of this — last month she experimented with a weekly 30-min office hour — but the data is too thin to call it resolved.", |
| 58 | + }, |
| 59 | +}; |
| 60 | + |
| 61 | +// Default baseURL comes from playwright.config.ts (localhost:5173); override |
| 62 | +// with PLAYWRIGHT_BASE_URL=http://localhost:5184 if regenerating screenshots |
| 63 | +// against a worktree dev server on a different port. |
| 64 | +const BASE_URL = process.env.PLAYWRIGHT_BASE_URL; |
| 65 | + |
| 66 | +test.use({ |
| 67 | + viewport: { width: 1600, height: 1000 }, |
| 68 | + ...(BASE_URL ? { baseURL: BASE_URL } : {}), |
| 69 | +}); |
| 70 | + |
| 71 | +test("playground screenshots", async ({ page }) => { |
| 72 | + mkdirSync(OUT_DIR, { recursive: true }); |
| 73 | + |
| 74 | + await page.addInitScript( |
| 75 | + ([key, value]) => { |
| 76 | + window.localStorage.setItem(key, value); |
| 77 | + }, |
| 78 | + [STORE_KEY, STORE_VALUE], |
| 79 | + ); |
| 80 | + |
| 81 | + // Mock the Honcho health probe so the SPA doesn't show a disconnected banner. |
| 82 | + await page.route("**/v3/health*", (route) => |
| 83 | + route.fulfill({ |
| 84 | + status: 200, |
| 85 | + contentType: "application/json", |
| 86 | + body: JSON.stringify({ status: "ok" }), |
| 87 | + }), |
| 88 | + ); |
| 89 | + |
| 90 | + // Mock the chat POST with per-level fixtures. |
| 91 | + await page.route("**/v3/workspaces/*/peers/*/chat", async (route) => { |
| 92 | + const body = JSON.parse(route.request().postData() ?? "{}") as { |
| 93 | + reasoning_level?: keyof typeof FIXTURES; |
| 94 | + }; |
| 95 | + const level = body.reasoning_level ?? "low"; |
| 96 | + const fx = FIXTURES[level]; |
| 97 | + await new Promise((r) => setTimeout(r, fx.delayMs)); |
| 98 | + await route.fulfill({ |
| 99 | + status: 200, |
| 100 | + contentType: "application/json", |
| 101 | + body: JSON.stringify({ content: fx.content }), |
| 102 | + }); |
| 103 | + }); |
| 104 | + |
| 105 | + // 1. Idle: empty playground. |
| 106 | + await page.goto(`/workspaces/${WORKSPACE}/peers/${encodeURIComponent(PEER)}/playground`); |
| 107 | + await page.waitForSelector('[data-testid="column-minimal"]'); |
| 108 | + await page.screenshot({ |
| 109 | + path: `${OUT_DIR}/playground-idle.png`, |
| 110 | + fullPage: false, |
| 111 | + }); |
| 112 | + |
| 113 | + // 2. Mid-flight: type a query, fire, capture while columns are still pending. |
| 114 | + await page.getByLabel("Query").fill("What patterns does Alice show across her recent sessions?"); |
| 115 | + await page.getByLabel("Run selected levels").click(); |
| 116 | + await page.waitForSelector('[data-testid="column-minimal"][data-status="success"]'); |
| 117 | + // minimal returns at ~140ms; capture now so medium/high/max are still pending. |
| 118 | + await page.screenshot({ |
| 119 | + path: `${OUT_DIR}/playground-running.png`, |
| 120 | + fullPage: false, |
| 121 | + }); |
| 122 | + |
| 123 | + // 3. Settled: wait for max to finish. |
| 124 | + await page.waitForSelector('[data-testid="column-max"][data-status="success"]', { |
| 125 | + timeout: 10_000, |
| 126 | + }); |
| 127 | + await page.screenshot({ |
| 128 | + path: `${OUT_DIR}/playground-results.png`, |
| 129 | + fullPage: false, |
| 130 | + }); |
| 131 | +}); |
0 commit comments