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, + setTerminalOpen: vi.fn(), + setTerminalHeight: vi.fn(), + splitTerminal: vi.fn(), + newTerminal: vi.fn(), + setActiveTerminal: vi.fn(), + closeTerminal: vi.fn(), + }, + terminalState: { + terminalOpen: false, + terminalHeight: 280, + terminalIds: ["default"], + runningTerminalIds: [], + activeTerminalId: "default", + terminalGroups: [{ id: "group-default", terminalIds: ["default"] }], + activeTerminalGroupId: "group-default", + }, + }; +}); + +vi.mock("@tanstack/react-router", () => ({ + useNavigate: () => mocks.navigate, + useSearch: (options?: { select?: (params: Record) => unknown }) => + options?.select ? options.select({}) : {}, +})); + +vi.mock("@tanstack/react-query", async () => { + const actual = await vi.importActual( + "@tanstack/react-query", + ); + return { + ...actual, + useQueryClient: () => ({}), + useMutation: () => ({ + mutateAsync: vi.fn(async () => ({})), + isPending: false, + }), + useQuery: () => ({ + data: undefined, + isLoading: false, + error: null, + }), + }; +}); + +vi.mock("../store", () => ({ + useStore: (selector: (store: typeof mocks.storeState) => unknown) => selector(mocks.storeState), +})); + +vi.mock("../composerDraftStore", () => ({ + useComposerThreadDraft: () => mocks.composerDraft, + useComposerDraftStore: (selector: (store: typeof mocks.composerStore) => unknown) => + selector(mocks.composerStore), +})); + +vi.mock("../terminalStateStore", () => ({ + useTerminalStateStore: (selector: (store: typeof mocks.terminalStore) => unknown) => + selector(mocks.terminalStore), + selectThreadTerminalState: () => mocks.terminalState, +})); + +vi.mock("../hooks/useTurnDiffSummaries", () => ({ + useTurnDiffSummaries: () => ({ + turnDiffSummaries: [], + inferredCheckpointTurnCountByTurnId: {}, + }), +})); + +vi.mock("../hooks/useTheme", () => ({ + useTheme: () => ({ resolvedTheme: "light" as const }), +})); + +vi.mock("../nativeApi", () => ({ + readNativeApi: () => null, + ensureNativeApi: () => { + throw new Error("Native API unavailable in browser test"); + }, +})); + +vi.mock("./BranchToolbar", () => ({ + default: () => null, +})); + +vi.mock("./GitActionsControl", () => ({ + default: () => null, +})); + +vi.mock("./ProjectScriptsControl", () => ({ + default: () => null, +})); + +vi.mock("./ThreadTerminalDrawer", () => ({ + default: () => null, +})); + +vi.mock("./ComposerPromptEditor", () => ({ + ComposerPromptEditor: () => null, +})); + +vi.mock("./ui/sidebar", () => ({ + SidebarTrigger: () => null, +})); + +const THREAD_ID = "thread-browser-test" as ThreadId; +const NOW_ISO = "2026-03-04T12:00:00.000Z"; +const BASE_TIME_MS = Date.parse(NOW_ISO); +const TALL_IMAGE_DATA_URI = `data:image/svg+xml;charset=utf-8,${encodeURIComponent( + "", +)}`; + +interface RenderMeasureOptions { + timelineWidthPx: number; + messages: ChatMessage[]; + targetMessageId: MessageId; +} + +function createThread(messages: ChatMessage[]): Thread { + return { + id: THREAD_ID, + codexThreadId: null, + projectId: "project-1" as Thread["projectId"], + title: "Browser test thread", + model: "gpt-5", + session: null, + messages, + error: null, + createdAt: NOW_ISO, + latestTurn: null, + lastVisitedAt: NOW_ISO, + branch: null, + worktreePath: null, + turnDiffSummaries: [], + activities: [], + }; +} + +function isoAt(offsetSeconds: number): string { + return new Date(BASE_TIME_MS + offsetSeconds * 1_000).toISOString(); +} + +function createUserMessage({ + id, + text, + offsetSeconds, + attachments, +}: { + id: MessageId; + text: string; + offsetSeconds: number; + attachments?: ChatMessage["attachments"]; +}): ChatMessage { + return { + id, + role: "user", + text, + ...(attachments && attachments.length > 0 ? { attachments } : {}), + createdAt: isoAt(offsetSeconds), + completedAt: isoAt(offsetSeconds + 1), + streaming: false, + }; +} + +function createAssistantMessage({ + id, + text, + offsetSeconds, +}: { + id: MessageId; + text: string; + offsetSeconds: number; +}): ChatMessage { + return { + id, + role: "assistant", + text, + createdAt: isoAt(offsetSeconds), + completedAt: isoAt(offsetSeconds + 1), + streaming: false, + }; +} + +function createImageAttachments(count: number): NonNullable { + return Array.from({ length: count }, (_, index) => ({ + type: "image" as const, + id: `attachment-${index + 1}`, + name: `attachment-${index + 1}.svg`, + mimeType: "image/svg+xml", + sizeBytes: 128, + previewUrl: TALL_IMAGE_DATA_URI, + })); +} + +function createConversationWithTargetUser(options: { + targetMessageId: MessageId; + targetText: string; + targetAttachments?: ChatMessage["attachments"]; +}): ChatMessage[] { + const messages: ChatMessage[] = []; + for (let index = 0; index < 22; index += 1) { + const userId = (`msg-user-${index}` as MessageId); + const assistantId = (`msg-assistant-${index}` as MessageId); + const isTarget = index === 3; + messages.push( + createUserMessage({ + id: isTarget ? options.targetMessageId : userId, + text: isTarget ? options.targetText : `filler user message ${index}`, + offsetSeconds: messages.length * 3, + attachments: isTarget ? options.targetAttachments : undefined, + }), + ); + messages.push( + createAssistantMessage({ + id: assistantId, + text: `assistant filler ${index}`, + offsetSeconds: messages.length * 3, + }), + ); + } + return messages; +} + +async function nextFrame(): Promise { + await new Promise((resolve) => { + window.requestAnimationFrame(() => resolve()); + }); +} + +async function waitForLayout(): Promise { + await nextFrame(); + await nextFrame(); + await nextFrame(); +} + +async function waitForImagesToLoad(scope: ParentNode): Promise { + const images = Array.from(scope.querySelectorAll("img")); + await Promise.all( + images.map( + (image) => + new Promise((resolve) => { + if (image.complete) { + resolve(); + return; + } + image.addEventListener("load", () => resolve(), { once: true }); + image.addEventListener("error", () => resolve(), { once: true }); + }), + ), + ); + await waitForLayout(); +} + +async function renderAndMeasureUserRow({ + timelineWidthPx, + messages, + targetMessageId, +}: RenderMeasureOptions): Promise<{ + measuredRowHeightPx: number; + timelineWidthMeasuredPx: number; + renderedInVirtualizedRegion: boolean; +}> { + const host = document.createElement("div"); + host.style.width = `${timelineWidthPx}px`; + host.style.height = "920px"; + host.style.display = "flex"; + host.style.overflow = "hidden"; + document.body.append(host); + + mocks.storeState.threads = [createThread(messages)]; + + const root: Root = createRoot(host); + root.render(); + await waitForLayout(); + + const scrollContainer = host.querySelector("div.overflow-y-auto.overscroll-y-contain"); + if (!(scrollContainer instanceof HTMLDivElement)) { + root.unmount(); + throw new Error("Unable to find ChatView message scroll container."); + } + scrollContainer.scrollTop = 0; + scrollContainer.dispatchEvent(new Event("scroll")); + await waitForLayout(); + + const row = host.querySelector( + `[data-message-id="${targetMessageId}"][data-message-role="user"]`, + ); + if (!(row instanceof HTMLElement)) { + root.unmount(); + throw new Error("Unable to locate targeted user message row."); + } + await waitForImagesToLoad(row); + + const timelineRoot = row.closest("div.max-w-3xl"); + if (!(timelineRoot instanceof HTMLElement)) { + root.unmount(); + throw new Error("Unable to locate timeline root container."); + } + + const measuredRowHeightPx = row.getBoundingClientRect().height; + const timelineWidthMeasuredPx = timelineRoot.getBoundingClientRect().width; + const renderedInVirtualizedRegion = row.closest("[data-index]") instanceof HTMLElement; + + root.unmount(); + host.remove(); + + return { measuredRowHeightPx, timelineWidthMeasuredPx, renderedInVirtualizedRegion }; +} + +describe("ChatView timeline estimator parity", () => { + beforeEach(() => { + document.body.innerHTML = ""; + mocks.storeState.projects = []; + mocks.storeState.threads = []; + mocks.storeState.runtimeMode = "full-access"; + mocks.composerDraft.prompt = ""; + mocks.composerDraft.images = []; + mocks.composerDraft.nonPersistedImageIds = []; + mocks.composerDraft.model = null; + mocks.composerDraft.effort = null; + }); + + afterEach(() => { + document.body.innerHTML = ""; + }); + + it("keeps long user message estimate close to actual rendered virtualized ChatView row height", async () => { + const userText = "x".repeat(3_200); + const targetMessageId = "msg-user-target-long" as MessageId; + const { measuredRowHeightPx, timelineWidthMeasuredPx, renderedInVirtualizedRegion } = + await renderAndMeasureUserRow({ + timelineWidthPx: 960, + targetMessageId, + messages: createConversationWithTargetUser({ + targetMessageId, + targetText: userText, + }), + }); + + expect(renderedInVirtualizedRegion).toBe(true); + + const estimatedHeightPx = estimateTimelineMessageHeight( + { role: "user", text: userText, attachments: [] }, + { timelineWidthPx: timelineWidthMeasuredPx }, + ); + + expect(Math.abs(measuredRowHeightPx - estimatedHeightPx)).toBeLessThanOrEqual(44); + }); + + it("tracks additional rendered wrapping when ChatView width narrows", async () => { + const userText = "x".repeat(2_400); + const targetMessageId = "msg-user-target-wrap" as MessageId; + const messages = createConversationWithTargetUser({ + targetMessageId, + targetText: userText, + }); + const desktop = await renderAndMeasureUserRow({ + timelineWidthPx: 960, + targetMessageId, + messages, + }); + const mobile = await renderAndMeasureUserRow({ + timelineWidthPx: 360, + targetMessageId, + messages, + }); + + const estimatedDesktopPx = estimateTimelineMessageHeight( + { role: "user", text: userText, attachments: [] }, + { timelineWidthPx: desktop.timelineWidthMeasuredPx }, + ); + const estimatedMobilePx = estimateTimelineMessageHeight( + { role: "user", text: userText, attachments: [] }, + { timelineWidthPx: mobile.timelineWidthMeasuredPx }, + ); + + const measuredDeltaPx = mobile.measuredRowHeightPx - desktop.measuredRowHeightPx; + const estimatedDeltaPx = estimatedMobilePx - estimatedDesktopPx; + expect(measuredDeltaPx).toBeGreaterThan(0); + expect(estimatedDeltaPx).toBeGreaterThan(0); + const ratio = estimatedDeltaPx / measuredDeltaPx; + expect(ratio).toBeGreaterThan(0.65); + expect(ratio).toBeLessThan(1.35); + }); + + it("keeps user attachment estimate close to actual rendered ChatView row height", async () => { + const targetMessageId = "msg-user-target-attachments" as MessageId; + const attachments = createImageAttachments(3); + const userText = "message with image attachments"; + const { measuredRowHeightPx, timelineWidthMeasuredPx, renderedInVirtualizedRegion } = + await renderAndMeasureUserRow({ + timelineWidthPx: 960, + targetMessageId, + messages: createConversationWithTargetUser({ + targetMessageId, + targetText: userText, + targetAttachments: attachments, + }), + }); + + expect(renderedInVirtualizedRegion).toBe(true); + + const estimatedHeightPx = estimateTimelineMessageHeight( + { + role: "user", + text: userText, + attachments: attachments.map((attachment) => ({ id: attachment.id })), + }, + { timelineWidthPx: timelineWidthMeasuredPx }, + ); + + expect(Math.abs(measuredRowHeightPx - estimatedHeightPx)).toBeLessThanOrEqual(56); + }); +}); diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index d7da2fb29bab52ec1df2f1a5cfeed14dc79bef24..e9f81c44534e8b44c440f1a59487827d285531cc 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -3362,7 +3362,12 @@ const MessagesTimeline = memo(function MessagesTimeline({ const nonVirtualizedRows = rows.slice(virtualizedRowCount); const renderRowContent = (row: TimelineRow) => ( -
+
{row.kind === "work" && (() => { const groupId = row.id; diff --git a/apps/web/vitest.browser.config.ts b/apps/web/vitest.browser.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..6083d6735e49e4ac7d5b6c36f4781b5a26bd9e7f --- /dev/null +++ b/apps/web/vitest.browser.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from "node:url"; +import { playwright } from "@vitest/browser-playwright"; +import { defineConfig, mergeConfig } from "vitest/config"; + +import viteConfig from "./vite.config"; + +const srcPath = fileURLToPath(new URL("./src", import.meta.url)); + +export default mergeConfig( + viteConfig, + defineConfig({ + resolve: { + alias: { + "~": srcPath, + }, + }, + test: { + include: ["src/components/ChatView.browser.tsx"], + browser: { + enabled: true, + provider: playwright(), + instances: [{ browser: "chromium" }], + headless: true, + }, + testTimeout: 30_000, + hookTimeout: 30_000, + }, + }), +); diff --git a/bun.lock b/bun.lock index d5e9b1e534fe6175b435379b6f00fb61f11252bf..5178098e6545799438643731ac3d4076360451f7 100644 --- a/bun.lock +++ b/bun.lock @@ -87,13 +87,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", @@ -468,7 +469,7 @@ "@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=="], - "@playwright/test": ["@playwright/test@1.58.2", "", { "dependencies": { "playwright": "1.58.2" }, "bin": { "playwright": "cli.js" } }, "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA=="], + "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], "@preact/signals-core": ["@preact/signals-core@1.13.0", "", {}, "sha512-slT6XeTCAbdql61GVLlGU4x7XHI7kCZV5Um5uhE4zLX4ApgiiXc0UYFvVOKq06xcovzp7p+61l68oPi563ARKg=="], @@ -702,6 +703,10 @@ "@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=="], + "@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=="], + + "@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=="], + "@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=="], "@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 @@ "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=="], - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -1194,6 +1199,8 @@ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "msgpackr": ["msgpackr@1.11.8", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA=="], @@ -1252,10 +1259,14 @@ "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "pixelmatch": ["pixelmatch@7.1.0", "", { "dependencies": { "pngjs": "^7.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng=="], + "playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="], "playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="], + "pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="], + "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=="], "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], @@ -1352,6 +1363,8 @@ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "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=="], + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -1418,6 +1431,8 @@ "toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="], + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + "tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="], "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], @@ -1594,6 +1609,8 @@ "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=="], + "chokidar/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "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=="], "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], @@ -1602,16 +1619,20 @@ "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], - "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "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=="], + "rollup/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], + "tsx/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "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=="], "@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 @@ "@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=="], + "@tailwindcss/vite/vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "@types/cacheable-request/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "@types/keyv/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], @@ -1654,6 +1677,10 @@ "@types/yauzl/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@vitejs/plugin-react/vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "rolldown-plugin-dts/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0-rc.1", "", {}, "sha512-vi/pfmbrOtQmqgfboaBhaCU50G7mcySVu69VU8z+lYoPPB6WzI9VgV7WQfL908M4oeSH5fDkmoupIqoE0SdApw=="], + + "vitest/vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], } }