| author | |
| committer | |
| log | 4ca4a1f99c346456bd9434e9ae4e77f31b2c7aaf |
| tree | 4af437e54f9e7f86c7cce2a478a8c659eca66a21 |
| parent | 5470801c4aecca55ea426be9b110bc879266be32 |
- Replace Playwright e2e timeline-height test setup with Vitest browser config
- Add `ChatView.browser.tsx` parity tests for text wrapping and attachment height estimation
- Add row data attributes in `ChatView` to support in-browser measurement targeting8 files changed, 533 insertions(+), 169 deletions(-)
.gitignore+1| ... | ... | @@ -14,3 +14,4 @@ release/ |
| 14 | 14 | .t3 |
| 15 | 15 | apps/web/.playwright |
| 16 | 16 | apps/web/playwright-report |
| 17 | apps/web/src/components/__screenshots__ |
apps/web/browser-tests/timelineHeight.browser.e2e.ts deleted-148| ... | ... | @@ -1,148 +0,0 @@ |
| 1 | import { expect, test, type Page } from "@playwright/test"; | |
| 2 | ||
| 3 | import { estimateTimelineMessageHeight } from "../src/components/timelineHeight"; | |
| 4 | ||
| 5 | interface HeightCase { | |
| 6 | timelineWidthPx: number; | |
| 7 | text: string; | |
| 8 | attachmentCount: number; | |
| 9 | } | |
| 10 | ||
| 11 | async function measureUserRowHeight(page: Page, testCase: HeightCase) { | |
| 12 | const { timelineWidthPx, text, attachmentCount } = testCase; | |
| 13 | await page.setContent("<!doctype html><html><body style='margin:0'></body></html>"); | |
| 14 | ||
| 15 | return page.evaluate(({ width, messageText, attachments }) => { | |
| 16 | const timeline = document.createElement("div"); | |
| 17 | timeline.style.width = `${width}px`; | |
| 18 | timeline.style.maxWidth = `${width}px`; | |
| 19 | ||
| 20 | const row = document.createElement("div"); | |
| 21 | row.style.paddingBottom = "16px"; | |
| 22 | ||
| 23 | const alignment = document.createElement("div"); | |
| 24 | alignment.style.display = "flex"; | |
| 25 | alignment.style.justifyContent = "flex-end"; | |
| 26 | ||
| 27 | const bubble = document.createElement("div"); | |
| 28 | bubble.style.boxSizing = "border-box"; | |
| 29 | bubble.style.maxWidth = "80%"; | |
| 30 | bubble.style.padding = "12px 16px"; | |
| 31 | bubble.style.border = "1px solid rgba(0, 0, 0, 0.12)"; | |
| 32 | bubble.style.borderRadius = "16px"; | |
| 33 | bubble.style.background = "rgba(0, 0, 0, 0.03)"; | |
| 34 | ||
| 35 | if (attachments > 0) { | |
| 36 | const attachmentGrid = document.createElement("div"); | |
| 37 | attachmentGrid.style.marginBottom = "8px"; | |
| 38 | attachmentGrid.style.maxWidth = "420px"; | |
| 39 | attachmentGrid.style.display = "grid"; | |
| 40 | attachmentGrid.style.gridTemplateColumns = "repeat(2, minmax(0, 1fr))"; | |
| 41 | attachmentGrid.style.gap = "8px"; | |
| 42 | for (let index = 0; index < attachments; index += 1) { | |
| 43 | const tile = document.createElement("div"); | |
| 44 | tile.style.height = "220px"; | |
| 45 | tile.style.border = "1px solid rgba(0, 0, 0, 0.12)"; | |
| 46 | tile.style.borderRadius = "8px"; | |
| 47 | tile.style.background = "rgba(0, 0, 0, 0.06)"; | |
| 48 | attachmentGrid.append(tile); | |
| 49 | } | |
| 50 | bubble.append(attachmentGrid); | |
| 51 | } | |
| 52 | ||
| 53 | if (messageText.length > 0) { | |
| 54 | const pre = document.createElement("pre"); | |
| 55 | pre.textContent = messageText; | |
| 56 | pre.style.margin = "0"; | |
| 57 | pre.style.whiteSpace = "pre-wrap"; | |
| 58 | pre.style.overflowWrap = "anywhere"; | |
| 59 | pre.style.fontFamily = | |
| 60 | "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace"; | |
| 61 | pre.style.fontSize = "14px"; | |
| 62 | pre.style.lineHeight = "22px"; | |
| 63 | bubble.append(pre); | |
| 64 | } | |
| 65 | ||
| 66 | const meta = document.createElement("div"); | |
| 67 | meta.style.marginTop = "6px"; | |
| 68 | meta.style.height = "16px"; | |
| 69 | bubble.append(meta); | |
| 70 | ||
| 71 | alignment.append(bubble); | |
| 72 | row.append(alignment); | |
| 73 | timeline.append(row); | |
| 74 | document.body.append(timeline); | |
| 75 | return row.getBoundingClientRect().height; | |
| 76 | }, { width: timelineWidthPx, messageText: text, attachments: attachmentCount }); | |
| 77 | } | |
| 78 | ||
| 79 | function estimatedHeight(testCase: HeightCase): number { | |
| 80 | return estimateTimelineMessageHeight( | |
| 81 | { | |
| 82 | role: "user", | |
| 83 | text: testCase.text, | |
| 84 | attachments: Array.from({ length: testCase.attachmentCount }, (_value, index) => ({ | |
| 85 | id: String(index), | |
| 86 | })), | |
| 87 | }, | |
| 88 | { timelineWidthPx: testCase.timelineWidthPx }, | |
| 89 | ); | |
| 90 | } | |
| 91 | ||
| 92 | test.describe("timeline height estimator parity", () => { | |
| 93 | test("tracks long wrapped text growth at desktop width", async ({ page }) => { | |
| 94 | const baselineCase: HeightCase = { timelineWidthPx: 960, text: "", attachmentCount: 0 }; | |
| 95 | const longCase: HeightCase = { | |
| 96 | timelineWidthPx: 960, | |
| 97 | text: "x".repeat(1200), | |
| 98 | attachmentCount: 0, | |
| 99 | }; | |
| 100 | ||
| 101 | const baselineMeasured = await measureUserRowHeight(page, baselineCase); | |
| 102 | const longMeasured = await measureUserRowHeight(page, longCase); | |
| 103 | const measuredDelta = longMeasured - baselineMeasured; | |
| 104 | ||
| 105 | const estimatedDelta = estimatedHeight(longCase) - estimatedHeight(baselineCase); | |
| 106 | expect(Math.abs(measuredDelta - estimatedDelta)).toBeLessThanOrEqual(22); | |
| 107 | }); | |
| 108 | ||
| 109 | test("tracks additional wrapping when viewport narrows", async ({ page }) => { | |
| 110 | const desktopCase: HeightCase = { | |
| 111 | timelineWidthPx: 960, | |
| 112 | text: "x".repeat(1000), | |
| 113 | attachmentCount: 0, | |
| 114 | }; | |
| 115 | const mobileCase: HeightCase = { | |
| 116 | timelineWidthPx: 360, | |
| 117 | text: desktopCase.text, | |
| 118 | attachmentCount: 0, | |
| 119 | }; | |
| 120 | ||
| 121 | const desktopMeasured = await measureUserRowHeight(page, desktopCase); | |
| 122 | const mobileMeasured = await measureUserRowHeight(page, mobileCase); | |
| 123 | const measuredDelta = mobileMeasured - desktopMeasured; | |
| 124 | ||
| 125 | const estimatedDelta = estimatedHeight(mobileCase) - estimatedHeight(desktopCase); | |
| 126 | expect(Math.abs(measuredDelta - estimatedDelta)).toBeLessThanOrEqual(44); | |
| 127 | }); | |
| 128 | ||
| 129 | test("tracks attachment row growth", async ({ page }) => { | |
| 130 | const withoutAttachmentsCase: HeightCase = { | |
| 131 | timelineWidthPx: 960, | |
| 132 | text: "hello", | |
| 133 | attachmentCount: 0, | |
| 134 | }; | |
| 135 | const withAttachmentsCase: HeightCase = { | |
| 136 | ...withoutAttachmentsCase, | |
| 137 | attachmentCount: 3, | |
| 138 | }; | |
| 139 | ||
| 140 | const withoutMeasured = await measureUserRowHeight(page, withoutAttachmentsCase); | |
| 141 | const withMeasured = await measureUserRowHeight(page, withAttachmentsCase); | |
| 142 | const measuredDelta = withMeasured - withoutMeasured; | |
| 143 | ||
| 144 | const estimatedDelta = | |
| 145 | estimatedHeight(withAttachmentsCase) - estimatedHeight(withoutAttachmentsCase); | |
| 146 | expect(Math.abs(measuredDelta - estimatedDelta)).toBeLessThanOrEqual(4); | |
| 147 | }); | |
| 148 | }); |
apps/web/package.json+4-3| ... | ... | @@ -10,8 +10,8 @@ |
| 10 | 10 | "preview": "vite preview", |
| 11 | 11 | "typecheck": "tsc --noEmit", |
| 12 | 12 | "test": "vitest run --passWithNoTests", |
| 13 | "test:browser": "playwright test -c playwright.config.ts", | |
| 14 | "test:browser:install": "playwright install chromium" | |
| 13 | "test:browser": "vitest run --config vitest.browser.config.ts", | |
| 14 | "test:browser:install": "playwright install --with-deps chromium" | |
| 15 | 15 | }, |
| 16 | 16 | "dependencies": { |
| 17 | 17 | "@base-ui/react": "^1.2.0", |
| ... | ... | @@ -38,13 +38,14 @@ |
| 38 | 38 | }, |
| 39 | 39 | "devDependencies": { |
| 40 | 40 | "@effect/language-service": "catalog:", |
| 41 | "@playwright/test": "^1.58.2", | |
| 42 | 41 | "@tailwindcss/vite": "^4.0.0", |
| 43 | 42 | "@tanstack/router-plugin": "^1.161.0", |
| 44 | 43 | "@types/react": "^19.0.0", |
| 45 | 44 | "@types/react-dom": "^19.0.0", |
| 46 | 45 | "@vitejs/plugin-react": "^5.1.4", |
| 46 | "@vitest/browser-playwright": "^4.0.18", | |
| 47 | 47 | "babel-plugin-react-compiler": "^19.0.0-beta-e552027-20250112", |
| 48 | "playwright": "^1.58.2", | |
| 48 | 49 | "tailwindcss": "^4.0.0", |
| 49 | 50 | "typescript": "catalog:", |
| 50 | 51 | "vite": "^8.0.0-beta.12", |
apps/web/playwright.config.ts deleted-12| ... | ... | @@ -1,12 +0,0 @@ |
| 1 | import { defineConfig } from "@playwright/test"; | |
| 2 | ||
| 3 | export default defineConfig({ | |
| 4 | testDir: "./browser-tests", | |
| 5 | testMatch: "*.browser.e2e.ts", | |
| 6 | reporter: "list", | |
| 7 | fullyParallel: true, | |
| 8 | outputDir: "./.playwright/test-results", | |
| 9 | use: { | |
| 10 | headless: true, | |
| 11 | }, | |
| 12 | }); |
apps/web/src/components/ChatView.browser.tsx created+461| ... | ... | @@ -0,0 +1,461 @@ |
| 1 | import "../index.css"; | |
| 2 | ||
| 3 | import { type MessageId, type ThreadId } from "@t3tools/contracts"; | |
| 4 | import { createRoot, type Root } from "react-dom/client"; | |
| 5 | import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; | |
| 6 | ||
| 7 | import type { ChatMessage, Thread } from "../types"; | |
| 8 | import ChatView from "./ChatView"; | |
| 9 | import { estimateTimelineMessageHeight } from "./timelineHeight"; | |
| 10 | ||
| 11 | const mocks = vi.hoisted(() => { | |
| 12 | return { | |
| 13 | navigate: vi.fn(), | |
| 14 | markThreadVisited: vi.fn(), | |
| 15 | setThreadError: vi.fn(), | |
| 16 | setRuntimeMode: vi.fn(), | |
| 17 | setThreadBranch: vi.fn(), | |
| 18 | storeState: { | |
| 19 | projects: [] as unknown[], | |
| 20 | threads: [] as Thread[], | |
| 21 | runtimeMode: "full-access", | |
| 22 | markThreadVisited: vi.fn(), | |
| 23 | setError: vi.fn(), | |
| 24 | setRuntimeMode: vi.fn(), | |
| 25 | setThreadBranch: vi.fn(), | |
| 26 | }, | |
| 27 | composerDraft: { | |
| 28 | prompt: "", | |
| 29 | images: [] as unknown[], | |
| 30 | nonPersistedImageIds: [] as string[], | |
| 31 | model: null, | |
| 32 | effort: null, | |
| 33 | }, | |
| 34 | composerStore: { | |
| 35 | draftsByThreadId: {}, | |
| 36 | draftThreadsByThreadId: {}, | |
| 37 | setPrompt: vi.fn(), | |
| 38 | setModel: vi.fn(), | |
| 39 | setEffort: vi.fn(), | |
| 40 | addImage: vi.fn(), | |
| 41 | addImages: vi.fn(), | |
| 42 | removeImage: vi.fn(), | |
| 43 | clearPersistedAttachments: vi.fn(), | |
| 44 | syncPersistedAttachments: vi.fn(), | |
| 45 | clearComposerContent: vi.fn(), | |
| 46 | clearDraftThread: vi.fn(), | |
| 47 | setDraftThreadContext: vi.fn(), | |
| 48 | }, | |
| 49 | terminalStore: { | |
| 50 | terminalStateByThreadId: {} as Record<string, unknown>, | |
| 51 | setTerminalOpen: vi.fn(), | |
| 52 | setTerminalHeight: vi.fn(), | |
| 53 | splitTerminal: vi.fn(), | |
| 54 | newTerminal: vi.fn(), | |
| 55 | setActiveTerminal: vi.fn(), | |
| 56 | closeTerminal: vi.fn(), | |
| 57 | }, | |
| 58 | terminalState: { | |
| 59 | terminalOpen: false, | |
| 60 | terminalHeight: 280, | |
| 61 | terminalIds: ["default"], | |
| 62 | runningTerminalIds: [], | |
| 63 | activeTerminalId: "default", | |
| 64 | terminalGroups: [{ id: "group-default", terminalIds: ["default"] }], | |
| 65 | activeTerminalGroupId: "group-default", | |
| 66 | }, | |
| 67 | }; | |
| 68 | }); | |
| 69 | ||
| 70 | vi.mock("@tanstack/react-router", () => ({ | |
| 71 | useNavigate: () => mocks.navigate, | |
| 72 | useSearch: (options?: { select?: (params: Record<string, unknown>) => unknown }) => | |
| 73 | options?.select ? options.select({}) : {}, | |
| 74 | })); | |
| 75 | ||
| 76 | vi.mock("@tanstack/react-query", async () => { | |
| 77 | const actual = await vi.importActual<typeof import("@tanstack/react-query")>( | |
| 78 | "@tanstack/react-query", | |
| 79 | ); | |
| 80 | return { | |
| 81 | ...actual, | |
| 82 | useQueryClient: () => ({}), | |
| 83 | useMutation: () => ({ | |
| 84 | mutateAsync: vi.fn(async () => ({})), | |
| 85 | isPending: false, | |
| 86 | }), | |
| 87 | useQuery: () => ({ | |
| 88 | data: undefined, | |
| 89 | isLoading: false, | |
| 90 | error: null, | |
| 91 | }), | |
| 92 | }; | |
| 93 | }); | |
| 94 | ||
| 95 | vi.mock("../store", () => ({ | |
| 96 | useStore: (selector: (store: typeof mocks.storeState) => unknown) => selector(mocks.storeState), | |
| 97 | })); | |
| 98 | ||
| 99 | vi.mock("../composerDraftStore", () => ({ | |
| 100 | useComposerThreadDraft: () => mocks.composerDraft, | |
| 101 | useComposerDraftStore: (selector: (store: typeof mocks.composerStore) => unknown) => | |
| 102 | selector(mocks.composerStore), | |
| 103 | })); | |
| 104 | ||
| 105 | vi.mock("../terminalStateStore", () => ({ | |
| 106 | useTerminalStateStore: (selector: (store: typeof mocks.terminalStore) => unknown) => | |
| 107 | selector(mocks.terminalStore), | |
| 108 | selectThreadTerminalState: () => mocks.terminalState, | |
| 109 | })); | |
| 110 | ||
| 111 | vi.mock("../hooks/useTurnDiffSummaries", () => ({ | |
| 112 | useTurnDiffSummaries: () => ({ | |
| 113 | turnDiffSummaries: [], | |
| 114 | inferredCheckpointTurnCountByTurnId: {}, | |
| 115 | }), | |
| 116 | })); | |
| 117 | ||
| 118 | vi.mock("../hooks/useTheme", () => ({ | |
| 119 | useTheme: () => ({ resolvedTheme: "light" as const }), | |
| 120 | })); | |
| 121 | ||
| 122 | vi.mock("../nativeApi", () => ({ | |
| 123 | readNativeApi: () => null, | |
| 124 | ensureNativeApi: () => { | |
| 125 | throw new Error("Native API unavailable in browser test"); | |
| 126 | }, | |
| 127 | })); | |
| 128 | ||
| 129 | vi.mock("./BranchToolbar", () => ({ | |
| 130 | default: () => null, | |
| 131 | })); | |
| 132 | ||
| 133 | vi.mock("./GitActionsControl", () => ({ | |
| 134 | default: () => null, | |
| 135 | })); | |
| 136 | ||
| 137 | vi.mock("./ProjectScriptsControl", () => ({ | |
| 138 | default: () => null, | |
| 139 | })); | |
| 140 | ||
| 141 | vi.mock("./ThreadTerminalDrawer", () => ({ | |
| 142 | default: () => null, | |
| 143 | })); | |
| 144 | ||
| 145 | vi.mock("./ComposerPromptEditor", () => ({ | |
| 146 | ComposerPromptEditor: () => null, | |
| 147 | })); | |
| 148 | ||
| 149 | vi.mock("./ui/sidebar", () => ({ | |
| 150 | SidebarTrigger: () => null, | |
| 151 | })); | |
| 152 | ||
| 153 | const THREAD_ID = "thread-browser-test" as ThreadId; | |
| 154 | const NOW_ISO = "2026-03-04T12:00:00.000Z"; | |
| 155 | const BASE_TIME_MS = Date.parse(NOW_ISO); | |
| 156 | const TALL_IMAGE_DATA_URI = `data:image/svg+xml;charset=utf-8,${encodeURIComponent( | |
| 157 | "<svg xmlns='http://www.w3.org/2000/svg' width='120' height='300'></svg>", | |
| 158 | )}`; | |
| 159 | ||
| 160 | interface RenderMeasureOptions { | |
| 161 | timelineWidthPx: number; | |
| 162 | messages: ChatMessage[]; | |
| 163 | targetMessageId: MessageId; | |
| 164 | } | |
| 165 | ||
| 166 | function createThread(messages: ChatMessage[]): Thread { | |
| 167 | return { | |
| 168 | id: THREAD_ID, | |
| 169 | codexThreadId: null, | |
| 170 | projectId: "project-1" as Thread["projectId"], | |
| 171 | title: "Browser test thread", | |
| 172 | model: "gpt-5", | |
| 173 | session: null, | |
| 174 | messages, | |
| 175 | error: null, | |
| 176 | createdAt: NOW_ISO, | |
| 177 | latestTurn: null, | |
| 178 | lastVisitedAt: NOW_ISO, | |
| 179 | branch: null, | |
| 180 | worktreePath: null, | |
| 181 | turnDiffSummaries: [], | |
| 182 | activities: [], | |
| 183 | }; | |
| 184 | } | |
| 185 | ||
| 186 | function isoAt(offsetSeconds: number): string { | |
| 187 | return new Date(BASE_TIME_MS + offsetSeconds * 1_000).toISOString(); | |
| 188 | } | |
| 189 | ||
| 190 | function createUserMessage({ | |
| 191 | id, | |
| 192 | text, | |
| 193 | offsetSeconds, | |
| 194 | attachments, | |
| 195 | }: { | |
| 196 | id: MessageId; | |
| 197 | text: string; | |
| 198 | offsetSeconds: number; | |
| 199 | attachments?: ChatMessage["attachments"]; | |
| 200 | }): ChatMessage { | |
| 201 | return { | |
| 202 | id, | |
| 203 | role: "user", | |
| 204 | text, | |
| 205 | ...(attachments && attachments.length > 0 ? { attachments } : {}), | |
| 206 | createdAt: isoAt(offsetSeconds), | |
| 207 | completedAt: isoAt(offsetSeconds + 1), | |
| 208 | streaming: false, | |
| 209 | }; | |
| 210 | } | |
| 211 | ||
| 212 | function createAssistantMessage({ | |
| 213 | id, | |
| 214 | text, | |
| 215 | offsetSeconds, | |
| 216 | }: { | |
| 217 | id: MessageId; | |
| 218 | text: string; | |
| 219 | offsetSeconds: number; | |
| 220 | }): ChatMessage { | |
| 221 | return { | |
| 222 | id, | |
| 223 | role: "assistant", | |
| 224 | text, | |
| 225 | createdAt: isoAt(offsetSeconds), | |
| 226 | completedAt: isoAt(offsetSeconds + 1), | |
| 227 | streaming: false, | |
| 228 | }; | |
| 229 | } | |
| 230 | ||
| 231 | function createImageAttachments(count: number): NonNullable<ChatMessage["attachments"]> { | |
| 232 | return Array.from({ length: count }, (_, index) => ({ | |
| 233 | type: "image" as const, | |
| 234 | id: `attachment-${index + 1}`, | |
| 235 | name: `attachment-${index + 1}.svg`, | |
| 236 | mimeType: "image/svg+xml", | |
| 237 | sizeBytes: 128, | |
| 238 | previewUrl: TALL_IMAGE_DATA_URI, | |
| 239 | })); | |
| 240 | } | |
| 241 | ||
| 242 | function createConversationWithTargetUser(options: { | |
| 243 | targetMessageId: MessageId; | |
| 244 | targetText: string; | |
| 245 | targetAttachments?: ChatMessage["attachments"]; | |
| 246 | }): ChatMessage[] { | |
| 247 | const messages: ChatMessage[] = []; | |
| 248 | for (let index = 0; index < 22; index += 1) { | |
| 249 | const userId = (`msg-user-${index}` as MessageId); | |
| 250 | const assistantId = (`msg-assistant-${index}` as MessageId); | |
| 251 | const isTarget = index === 3; | |
| 252 | messages.push( | |
| 253 | createUserMessage({ | |
| 254 | id: isTarget ? options.targetMessageId : userId, | |
| 255 | text: isTarget ? options.targetText : `filler user message ${index}`, | |
| 256 | offsetSeconds: messages.length * 3, | |
| 257 | attachments: isTarget ? options.targetAttachments : undefined, | |
| 258 | }), | |
| 259 | ); | |
| 260 | messages.push( | |
| 261 | createAssistantMessage({ | |
| 262 | id: assistantId, | |
| 263 | text: `assistant filler ${index}`, | |
| 264 | offsetSeconds: messages.length * 3, | |
| 265 | }), | |
| 266 | ); | |
| 267 | } | |
| 268 | return messages; | |
| 269 | } | |
| 270 | ||
| 271 | async function nextFrame(): Promise<void> { | |
| 272 | await new Promise<void>((resolve) => { | |
| 273 | window.requestAnimationFrame(() => resolve()); | |
| 274 | }); | |
| 275 | } | |
| 276 | ||
| 277 | async function waitForLayout(): Promise<void> { | |
| 278 | await nextFrame(); | |
| 279 | await nextFrame(); | |
| 280 | await nextFrame(); | |
| 281 | } | |
| 282 | ||
| 283 | async function waitForImagesToLoad(scope: ParentNode): Promise<void> { | |
| 284 | const images = Array.from(scope.querySelectorAll("img")); | |
| 285 | await Promise.all( | |
| 286 | images.map( | |
| 287 | (image) => | |
| 288 | new Promise<void>((resolve) => { | |
| 289 | if (image.complete) { | |
| 290 | resolve(); | |
| 291 | return; | |
| 292 | } | |
| 293 | image.addEventListener("load", () => resolve(), { once: true }); | |
| 294 | image.addEventListener("error", () => resolve(), { once: true }); | |
| 295 | }), | |
| 296 | ), | |
| 297 | ); | |
| 298 | await waitForLayout(); | |
| 299 | } | |
| 300 | ||
| 301 | async function renderAndMeasureUserRow({ | |
| 302 | timelineWidthPx, | |
| 303 | messages, | |
| 304 | targetMessageId, | |
| 305 | }: RenderMeasureOptions): Promise<{ | |
| 306 | measuredRowHeightPx: number; | |
| 307 | timelineWidthMeasuredPx: number; | |
| 308 | renderedInVirtualizedRegion: boolean; | |
| 309 | }> { | |
| 310 | const host = document.createElement("div"); | |
| 311 | host.style.width = `${timelineWidthPx}px`; | |
| 312 | host.style.height = "920px"; | |
| 313 | host.style.display = "flex"; | |
| 314 | host.style.overflow = "hidden"; | |
| 315 | document.body.append(host); | |
| 316 | ||
| 317 | mocks.storeState.threads = [createThread(messages)]; | |
| 318 | ||
| 319 | const root: Root = createRoot(host); | |
| 320 | root.render(<ChatView threadId={THREAD_ID} />); | |
| 321 | await waitForLayout(); | |
| 322 | ||
| 323 | const scrollContainer = host.querySelector("div.overflow-y-auto.overscroll-y-contain"); | |
| 324 | if (!(scrollContainer instanceof HTMLDivElement)) { | |
| 325 | root.unmount(); | |
| 326 | throw new Error("Unable to find ChatView message scroll container."); | |
| 327 | } | |
| 328 | scrollContainer.scrollTop = 0; | |
| 329 | scrollContainer.dispatchEvent(new Event("scroll")); | |
| 330 | await waitForLayout(); | |
| 331 | ||
| 332 | const row = host.querySelector<HTMLElement>( | |
| 333 | `[data-message-id="${targetMessageId}"][data-message-role="user"]`, | |
| 334 | ); | |
| 335 | if (!(row instanceof HTMLElement)) { | |
| 336 | root.unmount(); | |
| 337 | throw new Error("Unable to locate targeted user message row."); | |
| 338 | } | |
| 339 | await waitForImagesToLoad(row); | |
| 340 | ||
| 341 | const timelineRoot = row.closest("div.max-w-3xl"); | |
| 342 | if (!(timelineRoot instanceof HTMLElement)) { | |
| 343 | root.unmount(); | |
| 344 | throw new Error("Unable to locate timeline root container."); | |
| 345 | } | |
| 346 | ||
| 347 | const measuredRowHeightPx = row.getBoundingClientRect().height; | |
| 348 | const timelineWidthMeasuredPx = timelineRoot.getBoundingClientRect().width; | |
| 349 | const renderedInVirtualizedRegion = row.closest("[data-index]") instanceof HTMLElement; | |
| 350 | ||
| 351 | root.unmount(); | |
| 352 | host.remove(); | |
| 353 | ||
| 354 | return { measuredRowHeightPx, timelineWidthMeasuredPx, renderedInVirtualizedRegion }; | |
| 355 | } | |
| 356 | ||
| 357 | describe("ChatView timeline estimator parity", () => { | |
| 358 | beforeEach(() => { | |
| 359 | document.body.innerHTML = ""; | |
| 360 | mocks.storeState.projects = []; | |
| 361 | mocks.storeState.threads = []; | |
| 362 | mocks.storeState.runtimeMode = "full-access"; | |
| 363 | mocks.composerDraft.prompt = ""; | |
| 364 | mocks.composerDraft.images = []; | |
| 365 | mocks.composerDraft.nonPersistedImageIds = []; | |
| 366 | mocks.composerDraft.model = null; | |
| 367 | mocks.composerDraft.effort = null; | |
| 368 | }); | |
| 369 | ||
| 370 | afterEach(() => { | |
| 371 | document.body.innerHTML = ""; | |
| 372 | }); | |
| 373 | ||
| 374 | it("keeps long user message estimate close to actual rendered virtualized ChatView row height", async () => { | |
| 375 | const userText = "x".repeat(3_200); | |
| 376 | const targetMessageId = "msg-user-target-long" as MessageId; | |
| 377 | const { measuredRowHeightPx, timelineWidthMeasuredPx, renderedInVirtualizedRegion } = | |
| 378 | await renderAndMeasureUserRow({ | |
| 379 | timelineWidthPx: 960, | |
| 380 | targetMessageId, | |
| 381 | messages: createConversationWithTargetUser({ | |
| 382 | targetMessageId, | |
| 383 | targetText: userText, | |
| 384 | }), | |
| 385 | }); | |
| 386 | ||
| 387 | expect(renderedInVirtualizedRegion).toBe(true); | |
| 388 | ||
| 389 | const estimatedHeightPx = estimateTimelineMessageHeight( | |
| 390 | { role: "user", text: userText, attachments: [] }, | |
| 391 | { timelineWidthPx: timelineWidthMeasuredPx }, | |
| 392 | ); | |
| 393 | ||
| 394 | expect(Math.abs(measuredRowHeightPx - estimatedHeightPx)).toBeLessThanOrEqual(44); | |
| 395 | }); | |
| 396 | ||
| 397 | it("tracks additional rendered wrapping when ChatView width narrows", async () => { | |
| 398 | const userText = "x".repeat(2_400); | |
| 399 | const targetMessageId = "msg-user-target-wrap" as MessageId; | |
| 400 | const messages = createConversationWithTargetUser({ | |
| 401 | targetMessageId, | |
| 402 | targetText: userText, | |
| 403 | }); | |
| 404 | const desktop = await renderAndMeasureUserRow({ | |
| 405 | timelineWidthPx: 960, | |
| 406 | targetMessageId, | |
| 407 | messages, | |
| 408 | }); | |
| 409 | const mobile = await renderAndMeasureUserRow({ | |
| 410 | timelineWidthPx: 360, | |
| 411 | targetMessageId, | |
| 412 | messages, | |
| 413 | }); | |
| 414 | ||
| 415 | const estimatedDesktopPx = estimateTimelineMessageHeight( | |
| 416 | { role: "user", text: userText, attachments: [] }, | |
| 417 | { timelineWidthPx: desktop.timelineWidthMeasuredPx }, | |
| 418 | ); | |
| 419 | const estimatedMobilePx = estimateTimelineMessageHeight( | |
| 420 | { role: "user", text: userText, attachments: [] }, | |
| 421 | { timelineWidthPx: mobile.timelineWidthMeasuredPx }, | |
| 422 | ); | |
| 423 | ||
| 424 | const measuredDeltaPx = mobile.measuredRowHeightPx - desktop.measuredRowHeightPx; | |
| 425 | const estimatedDeltaPx = estimatedMobilePx - estimatedDesktopPx; | |
| 426 | expect(measuredDeltaPx).toBeGreaterThan(0); | |
| 427 | expect(estimatedDeltaPx).toBeGreaterThan(0); | |
| 428 | const ratio = estimatedDeltaPx / measuredDeltaPx; | |
| 429 | expect(ratio).toBeGreaterThan(0.65); | |
| 430 | expect(ratio).toBeLessThan(1.35); | |
| 431 | }); | |
| 432 | ||
| 433 | it("keeps user attachment estimate close to actual rendered ChatView row height", async () => { | |
| 434 | const targetMessageId = "msg-user-target-attachments" as MessageId; | |
| 435 | const attachments = createImageAttachments(3); | |
| 436 | const userText = "message with image attachments"; | |
| 437 | const { measuredRowHeightPx, timelineWidthMeasuredPx, renderedInVirtualizedRegion } = | |
| 438 | await renderAndMeasureUserRow({ | |
| 439 | timelineWidthPx: 960, | |
| 440 | targetMessageId, | |
| 441 | messages: createConversationWithTargetUser({ | |
| 442 | targetMessageId, | |
| 443 | targetText: userText, | |
| 444 | targetAttachments: attachments, | |
| 445 | }), | |
| 446 | }); | |
| 447 | ||
| 448 | expect(renderedInVirtualizedRegion).toBe(true); | |
| 449 | ||
| 450 | const estimatedHeightPx = estimateTimelineMessageHeight( | |
| 451 | { | |
| 452 | role: "user", | |
| 453 | text: userText, | |
| 454 | attachments: attachments.map((attachment) => ({ id: attachment.id })), | |
| 455 | }, | |
| 456 | { timelineWidthPx: timelineWidthMeasuredPx }, | |
| 457 | ); | |
| 458 | ||
| 459 | expect(Math.abs(measuredRowHeightPx - estimatedHeightPx)).toBeLessThanOrEqual(56); | |
| 460 | }); | |
| 461 | }); |
apps/web/src/components/ChatView.tsx+6-1| ... | ... | @@ -3362,7 +3362,12 @@ const MessagesTimeline = memo(function MessagesTimeline({ |
| 3362 | 3362 | const nonVirtualizedRows = rows.slice(virtualizedRowCount); |
| 3363 | 3363 | |
| 3364 | 3364 | const renderRowContent = (row: TimelineRow) => ( |
| 3365 | <div className="pb-4"> | |
| 3365 | <div | |
| 3366 | className="pb-4" | |
| 3367 | data-timeline-row-kind={row.kind} | |
| 3368 | data-message-id={row.kind === "message" ? row.message.id : undefined} | |
| 3369 | data-message-role={row.kind === "message" ? row.message.role : undefined} | |
| 3370 | > | |
| 3366 | 3371 | {row.kind === "work" && |
| 3367 | 3372 | (() => { |
| 3368 | 3373 | const groupId = row.id; |
apps/web/vitest.browser.config.ts created+29| ... | ... | @@ -0,0 +1,29 @@ |
| 1 | import { fileURLToPath } from "node:url"; | |
| 2 | import { playwright } from "@vitest/browser-playwright"; | |
| 3 | import { defineConfig, mergeConfig } from "vitest/config"; | |
| 4 | ||
| 5 | import viteConfig from "./vite.config"; | |
| 6 | ||
| 7 | const srcPath = fileURLToPath(new URL("./src", import.meta.url)); | |
| 8 | ||
| 9 | export default mergeConfig( | |
| 10 | viteConfig, | |
| 11 | defineConfig({ | |
| 12 | resolve: { | |
| 13 | alias: { | |
| 14 | "~": srcPath, | |
| 15 | }, | |
| 16 | }, | |
| 17 | test: { | |
| 18 | include: ["src/components/ChatView.browser.tsx"], | |
| 19 | browser: { | |
| 20 | enabled: true, | |
| 21 | provider: playwright(), | |
| 22 | instances: [{ browser: "chromium" }], | |
| 23 | headless: true, | |
| 24 | }, | |
| 25 | testTimeout: 30_000, | |
| 26 | hookTimeout: 30_000, | |
| 27 | }, | |
| 28 | }), | |
| 29 | ); |
bun.lock+32-5| ... | ... | @@ -87,13 +87,14 @@ |
| 87 | 87 | }, |
| 88 | 88 | "devDependencies": { |
| 89 | 89 | "@effect/language-service": "catalog:", |
| 90 | "@playwright/test": "^1.58.2", | |
| 91 | 90 | "@tailwindcss/vite": "^4.0.0", |
| 92 | 91 | "@tanstack/router-plugin": "^1.161.0", |
| 93 | 92 | "@types/react": "^19.0.0", |
| 94 | 93 | "@types/react-dom": "^19.0.0", |
| 95 | 94 | "@vitejs/plugin-react": "^5.1.4", |
| 95 | "@vitest/browser-playwright": "^4.0.18", | |
| 96 | 96 | "babel-plugin-react-compiler": "^19.0.0-beta-e552027-20250112", |
| 97 | "playwright": "^1.58.2", | |
| 97 | 98 | "tailwindcss": "^4.0.0", |
| 98 | 99 | "typescript": "catalog:", |
| 99 | 100 | "vite": "^8.0.0-beta.12", |
| ... | ... | @@ -468,7 +469,7 @@ |
| 468 | 469 | |
| 469 | 470 | "@pierre/diffs": ["@pierre/diffs@1.1.0-beta.16", "", { "dependencies": { "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-McjTuEPuacSIcXdoI2O9W6VSHIOs9ApEHnEUwONKZnKqIo2GGv1vNg9Pr8tgBOL7lgBWNEHX5ROJ5z1X74sENQ=="], |
| 470 | 471 | |
| 471 | "@playwright/test": ["@playwright/test@1.58.2", "", { "dependencies": { "playwright": "1.58.2" }, "bin": { "playwright": "cli.js" } }, "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA=="], | |
| 472 | "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], | |
| 472 | 473 | |
| 473 | 474 | "@preact/signals-core": ["@preact/signals-core@1.13.0", "", {}, "sha512-slT6XeTCAbdql61GVLlGU4x7XHI7kCZV5Um5uhE4zLX4ApgiiXc0UYFvVOKq06xcovzp7p+61l68oPi563ARKg=="], |
| 474 | 475 | |
| ... | ... | @@ -702,6 +703,10 @@ |
| 702 | 703 | |
| 703 | 704 | "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA=="], |
| 704 | 705 | |
| 706 | "@vitest/browser": ["@vitest/browser@4.0.18", "", { "dependencies": { "@vitest/mocker": "4.0.18", "@vitest/utils": "4.0.18", "magic-string": "^0.30.21", "pixelmatch": "7.1.0", "pngjs": "^7.0.0", "sirv": "^3.0.2", "tinyrainbow": "^3.0.3", "ws": "^8.18.3" }, "peerDependencies": { "vitest": "4.0.18" } }, "sha512-gVQqh7paBz3gC+ZdcCmNSWJMk70IUjDeVqi+5m5vYpEHsIwRgw3Y545jljtajhkekIpIp5Gg8oK7bctgY0E2Ng=="], | |
| 707 | ||
| 708 | "@vitest/browser-playwright": ["@vitest/browser-playwright@4.0.18", "", { "dependencies": { "@vitest/browser": "4.0.18", "@vitest/mocker": "4.0.18", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "playwright": "*", "vitest": "4.0.18" } }, "sha512-gfajTHVCiwpxRj1qh0Sh/5bbGLG4F/ZH/V9xvFVoFddpITfMta9YGow0W6ZpTTORv2vdJuz9TnrNSmjKvpOf4g=="], | |
| 709 | ||
| 705 | 710 | "@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="], |
| 706 | 711 | |
| 707 | 712 | "@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="], |
| ... | ... | @@ -922,7 +927,7 @@ |
| 922 | 927 | |
| 923 | 928 | "fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], |
| 924 | 929 | |
| 925 | "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], | |
| 930 | "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], | |
| 926 | 931 | |
| 927 | 932 | "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], |
| 928 | 933 | |
| ... | ... | @@ -1194,6 +1199,8 @@ |
| 1194 | 1199 | |
| 1195 | 1200 | "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], |
| 1196 | 1201 | |
| 1202 | "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], | |
| 1203 | ||
| 1197 | 1204 | "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], |
| 1198 | 1205 | |
| 1199 | 1206 | "msgpackr": ["msgpackr@1.11.8", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA=="], |
| ... | ... | @@ -1252,10 +1259,14 @@ |
| 1252 | 1259 | |
| 1253 | 1260 | "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], |
| 1254 | 1261 | |
| 1262 | "pixelmatch": ["pixelmatch@7.1.0", "", { "dependencies": { "pngjs": "^7.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng=="], | |
| 1263 | ||
| 1255 | 1264 | "playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="], |
| 1256 | 1265 | |
| 1257 | 1266 | "playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="], |
| 1258 | 1267 | |
| 1268 | "pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="], | |
| 1269 | ||
| 1259 | 1270 | "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], |
| 1260 | 1271 | |
| 1261 | 1272 | "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], |
| ... | ... | @@ -1352,6 +1363,8 @@ |
| 1352 | 1363 | |
| 1353 | 1364 | "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], |
| 1354 | 1365 | |
| 1366 | "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], | |
| 1367 | ||
| 1355 | 1368 | "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], |
| 1356 | 1369 | |
| 1357 | 1370 | "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], |
| ... | ... | @@ -1418,6 +1431,8 @@ |
| 1418 | 1431 | |
| 1419 | 1432 | "toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="], |
| 1420 | 1433 | |
| 1434 | "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], | |
| 1435 | ||
| 1421 | 1436 | "tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="], |
| 1422 | 1437 | |
| 1423 | 1438 | "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], |
| ... | ... | @@ -1594,6 +1609,8 @@ |
| 1594 | 1609 | |
| 1595 | 1610 | "babel-dead-code-elimination/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], |
| 1596 | 1611 | |
| 1612 | "chokidar/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], | |
| 1613 | ||
| 1597 | 1614 | "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], |
| 1598 | 1615 | |
| 1599 | 1616 | "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], |
| ... | ... | @@ -1602,16 +1619,20 @@ |
| 1602 | 1619 | |
| 1603 | 1620 | "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], |
| 1604 | 1621 | |
| 1605 | "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], | |
| 1606 | ||
| 1607 | 1622 | "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], |
| 1608 | 1623 | |
| 1609 | 1624 | "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], |
| 1610 | 1625 | |
| 1611 | 1626 | "rolldown-plugin-dts/@babel/types": ["@babel/types@8.0.0-rc.1", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0-rc.1", "@babel/helper-validator-identifier": "^8.0.0-rc.1" } }, "sha512-ubmJ6TShyaD69VE9DQrlXcdkvJbmwWPB8qYj0H2kaJi29O7vJT9ajSdBd2W8CG34pwL9pYA74fi7RHC1qbLoVQ=="], |
| 1612 | 1627 | |
| 1628 | "rollup/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], | |
| 1629 | ||
| 1613 | 1630 | "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], |
| 1614 | 1631 | |
| 1632 | "tsx/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], | |
| 1633 | ||
| 1634 | "vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], | |
| 1635 | ||
| 1615 | 1636 | "vitest/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], |
| 1616 | 1637 | |
| 1617 | 1638 | "@babel/generator/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0-rc.1", "", {}, "sha512-vi/pfmbrOtQmqgfboaBhaCU50G7mcySVu69VU8z+lYoPPB6WzI9VgV7WQfL908M4oeSH5fDkmoupIqoE0SdApw=="], |
| ... | ... | @@ -1644,6 +1665,8 @@ |
| 1644 | 1665 | |
| 1645 | 1666 | "@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], |
| 1646 | 1667 | |
| 1668 | "@tailwindcss/vite/vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], | |
| 1669 | ||
| 1647 | 1670 | "@types/cacheable-request/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], |
| 1648 | 1671 | |
| 1649 | 1672 | "@types/keyv/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], |
| ... | ... | @@ -1654,6 +1677,10 @@ |
| 1654 | 1677 | |
| 1655 | 1678 | "@types/yauzl/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], |
| 1656 | 1679 | |
| 1680 | "@vitejs/plugin-react/vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], | |
| 1681 | ||
| 1657 | 1682 | "rolldown-plugin-dts/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0-rc.1", "", {}, "sha512-vi/pfmbrOtQmqgfboaBhaCU50G7mcySVu69VU8z+lYoPPB6WzI9VgV7WQfL908M4oeSH5fDkmoupIqoE0SdApw=="], |
| 1683 | ||
| 1684 | "vitest/vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], | |
| 1658 | 1685 | } |
| 1659 | 1686 | } |