diff --git a/.gitignore b/.gitignore index 5c2ec0b4a0b47998d272a0f9f10650892f9bae5f..ac08d39161f1f177d18b5a138852567935126e82 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ release/ .t3 apps/web/.playwright apps/web/playwright-report +apps/web/src/components/__screenshots__ diff --git a/apps/web/browser-tests/timelineHeight.browser.e2e.ts b/apps/web/browser-tests/timelineHeight.browser.e2e.ts deleted file mode 100644 index 185ca6ce0681ba1ef94aca7e93a3c58dc62315c0..0000000000000000000000000000000000000000 --- a/apps/web/browser-tests/timelineHeight.browser.e2e.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { expect, test, type Page } from "@playwright/test"; - -import { estimateTimelineMessageHeight } from "../src/components/timelineHeight"; - -interface HeightCase { - timelineWidthPx: number; - text: string; - attachmentCount: number; -} - -async function measureUserRowHeight(page: Page, testCase: HeightCase) { - const { timelineWidthPx, text, attachmentCount } = testCase; - await page.setContent("
"); - - return page.evaluate(({ width, messageText, attachments }) => { - const timeline = document.createElement("div"); - timeline.style.width = `${width}px`; - timeline.style.maxWidth = `${width}px`; - - const row = document.createElement("div"); - row.style.paddingBottom = "16px"; - - const alignment = document.createElement("div"); - alignment.style.display = "flex"; - alignment.style.justifyContent = "flex-end"; - - const bubble = document.createElement("div"); - bubble.style.boxSizing = "border-box"; - bubble.style.maxWidth = "80%"; - bubble.style.padding = "12px 16px"; - bubble.style.border = "1px solid rgba(0, 0, 0, 0.12)"; - bubble.style.borderRadius = "16px"; - bubble.style.background = "rgba(0, 0, 0, 0.03)"; - - if (attachments > 0) { - const attachmentGrid = document.createElement("div"); - attachmentGrid.style.marginBottom = "8px"; - attachmentGrid.style.maxWidth = "420px"; - attachmentGrid.style.display = "grid"; - attachmentGrid.style.gridTemplateColumns = "repeat(2, minmax(0, 1fr))"; - attachmentGrid.style.gap = "8px"; - for (let index = 0; index < attachments; index += 1) { - const tile = document.createElement("div"); - tile.style.height = "220px"; - tile.style.border = "1px solid rgba(0, 0, 0, 0.12)"; - tile.style.borderRadius = "8px"; - tile.style.background = "rgba(0, 0, 0, 0.06)"; - attachmentGrid.append(tile); - } - bubble.append(attachmentGrid); - } - - if (messageText.length > 0) { - const pre = document.createElement("pre"); - pre.textContent = messageText; - pre.style.margin = "0"; - pre.style.whiteSpace = "pre-wrap"; - pre.style.overflowWrap = "anywhere"; - pre.style.fontFamily = - "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace"; - pre.style.fontSize = "14px"; - pre.style.lineHeight = "22px"; - bubble.append(pre); - } - - const meta = document.createElement("div"); - meta.style.marginTop = "6px"; - meta.style.height = "16px"; - bubble.append(meta); - - alignment.append(bubble); - row.append(alignment); - timeline.append(row); - document.body.append(timeline); - return row.getBoundingClientRect().height; - }, { width: timelineWidthPx, messageText: text, attachments: attachmentCount }); -} - -function estimatedHeight(testCase: HeightCase): number { - return estimateTimelineMessageHeight( - { - role: "user", - text: testCase.text, - attachments: Array.from({ length: testCase.attachmentCount }, (_value, index) => ({ - id: String(index), - })), - }, - { timelineWidthPx: testCase.timelineWidthPx }, - ); -} - -test.describe("timeline height estimator parity", () => { - test("tracks long wrapped text growth at desktop width", async ({ page }) => { - const baselineCase: HeightCase = { timelineWidthPx: 960, text: "", attachmentCount: 0 }; - const longCase: HeightCase = { - timelineWidthPx: 960, - text: "x".repeat(1200), - attachmentCount: 0, - }; - - const baselineMeasured = await measureUserRowHeight(page, baselineCase); - const longMeasured = await measureUserRowHeight(page, longCase); - const measuredDelta = longMeasured - baselineMeasured; - - const estimatedDelta = estimatedHeight(longCase) - estimatedHeight(baselineCase); - expect(Math.abs(measuredDelta - estimatedDelta)).toBeLessThanOrEqual(22); - }); - - test("tracks additional wrapping when viewport narrows", async ({ page }) => { - const desktopCase: HeightCase = { - timelineWidthPx: 960, - text: "x".repeat(1000), - attachmentCount: 0, - }; - const mobileCase: HeightCase = { - timelineWidthPx: 360, - text: desktopCase.text, - attachmentCount: 0, - }; - - const desktopMeasured = await measureUserRowHeight(page, desktopCase); - const mobileMeasured = await measureUserRowHeight(page, mobileCase); - const measuredDelta = mobileMeasured - desktopMeasured; - - const estimatedDelta = estimatedHeight(mobileCase) - estimatedHeight(desktopCase); - expect(Math.abs(measuredDelta - estimatedDelta)).toBeLessThanOrEqual(44); - }); - - test("tracks attachment row growth", async ({ page }) => { - const withoutAttachmentsCase: HeightCase = { - timelineWidthPx: 960, - text: "hello", - attachmentCount: 0, - }; - const withAttachmentsCase: HeightCase = { - ...withoutAttachmentsCase, - attachmentCount: 3, - }; - - const withoutMeasured = await measureUserRowHeight(page, withoutAttachmentsCase); - const withMeasured = await measureUserRowHeight(page, withAttachmentsCase); - const measuredDelta = withMeasured - withoutMeasured; - - const estimatedDelta = - estimatedHeight(withAttachmentsCase) - estimatedHeight(withoutAttachmentsCase); - expect(Math.abs(measuredDelta - estimatedDelta)).toBeLessThanOrEqual(4); - }); -}); diff --git a/apps/web/package.json b/apps/web/package.json index a9c4b5451d9f8fed557563f1bceea7fc09cda6d3..7b23de3679d04f64be5519c899c4ac725a07d6bd 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -10,8 +10,8 @@ "preview": "vite preview", "typecheck": "tsc --noEmit", "test": "vitest run --passWithNoTests", - "test:browser": "playwright test -c playwright.config.ts", - "test:browser:install": "playwright install chromium" + "test:browser": "vitest run --config vitest.browser.config.ts", + "test:browser:install": "playwright install --with-deps chromium" }, "dependencies": { "@base-ui/react": "^1.2.0", @@ -38,13 +38,14 @@ }, "devDependencies": { "@effect/language-service": "catalog:", - "@playwright/test": "^1.58.2", "@tailwindcss/vite": "^4.0.0", "@tanstack/router-plugin": "^1.161.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^5.1.4", + "@vitest/browser-playwright": "^4.0.18", "babel-plugin-react-compiler": "^19.0.0-beta-e552027-20250112", + "playwright": "^1.58.2", "tailwindcss": "^4.0.0", "typescript": "catalog:", "vite": "^8.0.0-beta.12", diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts deleted file mode 100644 index a0d93c553cf90c743fb735d5e9c36b4602ebd36d..0000000000000000000000000000000000000000 --- a/apps/web/playwright.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineConfig } from "@playwright/test"; - -export default defineConfig({ - testDir: "./browser-tests", - testMatch: "*.browser.e2e.ts", - reporter: "list", - fullyParallel: true, - outputDir: "./.playwright/test-results", - use: { - headless: true, - }, -}); diff --git a/apps/web/src/components/ChatView.browser.tsx b/apps/web/src/components/ChatView.browser.tsx new file mode 100644 index 0000000000000000000000000000000000000000..167f95f2f3d75827788d18f9ddc6e59120c2954f --- /dev/null +++ b/apps/web/src/components/ChatView.browser.tsx @@ -0,0 +1,461 @@ +import "../index.css"; + +import { type MessageId, type ThreadId } from "@t3tools/contracts"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import type { ChatMessage, Thread } from "../types"; +import ChatView from "./ChatView"; +import { estimateTimelineMessageHeight } from "./timelineHeight"; + +const mocks = vi.hoisted(() => { + return { + navigate: vi.fn(), + markThreadVisited: vi.fn(), + setThreadError: vi.fn(), + setRuntimeMode: vi.fn(), + setThreadBranch: vi.fn(), + storeState: { + projects: [] as unknown[], + threads: [] as Thread[], + runtimeMode: "full-access", + markThreadVisited: vi.fn(), + setError: vi.fn(), + setRuntimeMode: vi.fn(), + setThreadBranch: vi.fn(), + }, + composerDraft: { + prompt: "", + images: [] as unknown[], + nonPersistedImageIds: [] as string[], + model: null, + effort: null, + }, + composerStore: { + draftsByThreadId: {}, + draftThreadsByThreadId: {}, + setPrompt: vi.fn(), + setModel: vi.fn(), + setEffort: vi.fn(), + addImage: vi.fn(), + addImages: vi.fn(), + removeImage: vi.fn(), + clearPersistedAttachments: vi.fn(), + syncPersistedAttachments: vi.fn(), + clearComposerContent: vi.fn(), + clearDraftThread: vi.fn(), + setDraftThreadContext: vi.fn(), + }, + terminalStore: { + terminalStateByThreadId: {} as Record