871 lines · 24.8 KB · TypeScript
RawDownload
// Production CSS is part of the behavior under test because row height depends on it.import "../index.css"; import {  ORCHESTRATION_WS_METHODS,  type MessageId,  type OrchestrationReadModel,  type ProjectId,  type ServerConfig,  type ThreadId,  type WsWelcomePayload,  WS_CHANNELS,  WS_METHODS,} from "@t3tools/contracts";import { RouterProvider, createMemoryHistory } from "@tanstack/react-router";import { HttpResponse, http, ws } from "msw";import { setupWorker } from "msw/browser";import { page } from "vitest/browser";import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";import { render } from "vitest-browser-react"; import { useComposerDraftStore } from "../composerDraftStore";import { getRouter } from "../router";import { useStore } from "../store";import { estimateTimelineMessageHeight } from "./timelineHeight"; const THREAD_ID = "thread-browser-test" as ThreadId;const PROJECT_ID = "project-1" as ProjectId;const NOW_ISO = "2026-03-04T12:00:00.000Z";const BASE_TIME_MS = Date.parse(NOW_ISO);const ATTACHMENT_SVG = "<svg xmlns='http://www.w3.org/2000/svg' width='120' height='300'></svg>"; interface WsRequestEnvelope {  id: string;  body: {    _tag: string;    [key: string]: unknown;  };} interface TestFixture {  snapshot: OrchestrationReadModel;  serverConfig: ServerConfig;  welcome: WsWelcomePayload;} let fixture: TestFixture;const wsRequests: WsRequestEnvelope["body"][] = [];const wsLink = ws.link(/ws(s)?:\/\/.*/); interface ViewportSpec {  name: string;  width: number;  height: number;  textTolerancePx: number;  attachmentTolerancePx: number;} const DEFAULT_VIEWPORT: ViewportSpec = {  name: "desktop",  width: 960,  height: 1_100,  textTolerancePx: 44,  attachmentTolerancePx: 56,};const TEXT_VIEWPORT_MATRIX = [  DEFAULT_VIEWPORT,  { name: "tablet", width: 720, height: 1_024, textTolerancePx: 44, attachmentTolerancePx: 56 },  { name: "mobile", width: 430, height: 932, textTolerancePx: 56, attachmentTolerancePx: 56 },  { name: "narrow", width: 320, height: 700, textTolerancePx: 84, attachmentTolerancePx: 56 },] as const satisfies readonly ViewportSpec[];const ATTACHMENT_VIEWPORT_MATRIX = [  DEFAULT_VIEWPORT,  { name: "mobile", width: 430, height: 932, textTolerancePx: 56, attachmentTolerancePx: 56 },  { name: "narrow", width: 320, height: 700, textTolerancePx: 84, attachmentTolerancePx: 56 },] as const satisfies readonly ViewportSpec[]; interface UserRowMeasurement {  measuredRowHeightPx: number;  timelineWidthMeasuredPx: number;  renderedInVirtualizedRegion: boolean;} interface MountedChatView {  cleanup: () => Promise<void>;  measureUserRow: (targetMessageId: MessageId) => Promise<UserRowMeasurement>;  setViewport: (viewport: ViewportSpec) => Promise<void>;} function isoAt(offsetSeconds: number): string {  return new Date(BASE_TIME_MS + offsetSeconds * 1_000).toISOString();} function createBaseServerConfig(): ServerConfig {  return {    cwd: "/repo/project",    keybindingsConfigPath: "/repo/project/.t3code-keybindings.json",    keybindings: [],    issues: [],    providers: [      {        provider: "codex",        status: "ready",        available: true,        authStatus: "authenticated",        checkedAt: NOW_ISO,      },    ],    availableEditors: [],  };} function createUserMessage(options: {  id: MessageId;  text: string;  offsetSeconds: number;  attachments?: Array<{    type: "image";    id: string;    name: string;    mimeType: string;    sizeBytes: number;  }>;}) {  return {    id: options.id,    role: "user" as const,    text: options.text,    ...(options.attachments ? { attachments: options.attachments } : {}),    turnId: null,    streaming: false,    createdAt: isoAt(options.offsetSeconds),    updatedAt: isoAt(options.offsetSeconds + 1),  };} function createAssistantMessage(options: { id: MessageId; text: string; offsetSeconds: number }) {  return {    id: options.id,    role: "assistant" as const,    text: options.text,    turnId: null,    streaming: false,    createdAt: isoAt(options.offsetSeconds),    updatedAt: isoAt(options.offsetSeconds + 1),  };} function createSnapshotForTargetUser(options: {  targetMessageId: MessageId;  targetText: string;  targetAttachmentCount?: number;}): OrchestrationReadModel {  const messages: Array<OrchestrationReadModel["threads"][number]["messages"][number]> = [];   for (let index = 0; index < 22; index += 1) {    const isTarget = index === 3;    const userId = `msg-user-${index}` as MessageId;    const assistantId = `msg-assistant-${index}` as MessageId;    const attachments =      isTarget && (options.targetAttachmentCount ?? 0) > 0        ? Array.from({ length: options.targetAttachmentCount ?? 0 }, (_, attachmentIndex) => ({            type: "image" as const,            id: `attachment-${attachmentIndex + 1}`,            name: `attachment-${attachmentIndex + 1}.png`,            mimeType: "image/png",            sizeBytes: 128,          }))        : undefined;     messages.push(      createUserMessage({        id: isTarget ? options.targetMessageId : userId,        text: isTarget ? options.targetText : `filler user message ${index}`,        offsetSeconds: messages.length * 3,        ...(attachments ? { attachments } : {}),      }),    );    messages.push(      createAssistantMessage({        id: assistantId,        text: `assistant filler ${index}`,        offsetSeconds: messages.length * 3,      }),    );  }   return {    snapshotSequence: 1,    projects: [      {        id: PROJECT_ID,        title: "Project",        workspaceRoot: "/repo/project",        defaultModel: "gpt-5",        scripts: [],        createdAt: NOW_ISO,        updatedAt: NOW_ISO,        deletedAt: null,      },    ],    threads: [      {        id: THREAD_ID,        projectId: PROJECT_ID,        title: "Browser test thread",        model: "gpt-5",        interactionMode: "default",        runtimeMode: "full-access",        branch: "main",        worktreePath: null,        latestTurn: null,        createdAt: NOW_ISO,        updatedAt: NOW_ISO,        deletedAt: null,        messages,        activities: [],        proposedPlans: [],        checkpoints: [],        session: {          threadId: THREAD_ID,          status: "ready",          providerName: "codex",          runtimeMode: "full-access",          activeTurnId: null,          lastError: null,          updatedAt: NOW_ISO,        },      },    ],    updatedAt: NOW_ISO,  };} function buildFixture(snapshot: OrchestrationReadModel): TestFixture {  return {    snapshot,    serverConfig: createBaseServerConfig(),    welcome: {      cwd: "/repo/project",      projectName: "Project",      bootstrapProjectId: PROJECT_ID,      bootstrapThreadId: THREAD_ID,    },  };} function createDraftOnlySnapshot(): OrchestrationReadModel {  const snapshot = createSnapshotForTargetUser({    targetMessageId: "msg-user-draft-target" as MessageId,    targetText: "draft thread",  });  return {    ...snapshot,    threads: [],  };} function resolveWsRpc(tag: string): unknown {  if (tag === ORCHESTRATION_WS_METHODS.getSnapshot) {    return fixture.snapshot;  }  if (tag === WS_METHODS.serverGetConfig) {    return fixture.serverConfig;  }  if (tag === WS_METHODS.gitListBranches) {    return {      isRepo: true,      branches: [        {          name: "main",          current: true,          isDefault: true,          worktreePath: null,        },      ],    };  }  if (tag === WS_METHODS.gitStatus) {    return {      branch: "main",      hasWorkingTreeChanges: false,      workingTree: {        files: [],        insertions: 0,        deletions: 0,      },      hasUpstream: true,      aheadCount: 0,      behindCount: 0,      pr: null,    };  }  if (tag === WS_METHODS.projectsSearchEntries) {    return {      entries: [],      truncated: false,    };  }  return {};} const worker = setupWorker(  wsLink.addEventListener("connection", ({ client }) => {    client.send(      JSON.stringify({        type: "push",        sequence: 1,        channel: WS_CHANNELS.serverWelcome,        data: fixture.welcome,      }),    );    client.addEventListener("message", (event) => {      const rawData = event.data;      if (typeof rawData !== "string") return;      let request: WsRequestEnvelope;      try {        request = JSON.parse(rawData) as WsRequestEnvelope;      } catch {        return;      }      const method = request.body?._tag;      if (typeof method !== "string") return;      wsRequests.push(request.body);      client.send(        JSON.stringify({          id: request.id,          result: resolveWsRpc(method),        }),      );    });  }),  http.get("*/attachments/:attachmentId", () =>    HttpResponse.text(ATTACHMENT_SVG, {      headers: {        "Content-Type": "image/svg+xml",      },    }),  ),  http.get("*/api/project-favicon", () => new HttpResponse(null, { status: 204 })),); async function nextFrame(): Promise<void> {  await new Promise<void>((resolve) => {    window.requestAnimationFrame(() => resolve());  });} async function waitForLayout(): Promise<void> {  await nextFrame();  await nextFrame();  await nextFrame();} async function setViewport(viewport: ViewportSpec): Promise<void> {  await page.viewport(viewport.width, viewport.height);  await waitForLayout();} async function waitForProductionStyles(): Promise<void> {  await vi.waitFor(    () => {      expect(getComputedStyle(document.documentElement).getPropertyValue("--background").trim()).not.toBe(        "",      );      expect(getComputedStyle(document.body).marginTop).toBe("0px");    },    {      timeout: 4_000,      interval: 16,    },  );} async function waitForElement<T extends Element>(  query: () => T | null,  errorMessage: string,): Promise<T> {  let element: T | null = null;  await vi.waitFor(    () => {      element = query();      expect(element, errorMessage).toBeTruthy();    },    {      timeout: 8_000,      interval: 16,    },  );  if (!element) {    throw new Error(errorMessage);  }  return element;} async function waitForComposerEditor(): Promise<HTMLElement> {  return waitForElement(    () => document.querySelector<HTMLElement>('[contenteditable="true"]'),    "Unable to find composer editor.",  );} async function waitForInteractionModeButton(expectedLabel: "Chat" | "Plan"): Promise<HTMLButtonElement> {  return waitForElement(    () =>      Array.from(document.querySelectorAll("button")).find(        (button) => button.textContent?.trim() === expectedLabel,      ) as HTMLButtonElement | null,    `Unable to find ${expectedLabel} interaction mode button.`,  );} async function waitForImagesToLoad(scope: ParentNode): Promise<void> {  const images = Array.from(scope.querySelectorAll("img"));  if (images.length === 0) {    return;  }  await Promise.all(    images.map(      (image) =>        new Promise<void>((resolve) => {          if (image.complete) {            resolve();            return;          }          image.addEventListener("load", () => resolve(), { once: true });          image.addEventListener("error", () => resolve(), { once: true });        }),    ),  );  await waitForLayout();} async function measureUserRow(options: {  host: HTMLElement;  targetMessageId: MessageId;}): Promise<UserRowMeasurement> {  const { host, targetMessageId } = options;  const rowSelector = `[data-message-id="${targetMessageId}"][data-message-role="user"]`;   const scrollContainer = await waitForElement(    () => host.querySelector<HTMLDivElement>("div.overflow-y-auto.overscroll-y-contain"),    "Unable to find ChatView message scroll container.",  );   let row: HTMLElement | null = null;  await vi.waitFor(    async () => {      scrollContainer.scrollTop = 0;      scrollContainer.dispatchEvent(new Event("scroll"));      await waitForLayout();      row = host.querySelector<HTMLElement>(rowSelector);      expect(row, "Unable to locate targeted user message row.").toBeTruthy();    },    {      timeout: 8_000,      interval: 16,    },  );   await waitForImagesToLoad(row!);  scrollContainer.scrollTop = 0;  scrollContainer.dispatchEvent(new Event("scroll"));  await nextFrame();   const timelineRoot =    row!.closest<HTMLElement>('[data-timeline-root="true"]') ??    host.querySelector<HTMLElement>('[data-timeline-root="true"]');  if (!(timelineRoot instanceof HTMLElement)) {    throw new Error("Unable to locate timeline root container.");  }   let timelineWidthMeasuredPx = 0;  let measuredRowHeightPx = 0;  let renderedInVirtualizedRegion = false;  await vi.waitFor(    async () => {      scrollContainer.scrollTop = 0;      scrollContainer.dispatchEvent(new Event("scroll"));      await nextFrame();      const measuredRow = host.querySelector<HTMLElement>(rowSelector);      expect(measuredRow, "Unable to measure targeted user row height.").toBeTruthy();      timelineWidthMeasuredPx = timelineRoot.getBoundingClientRect().width;      measuredRowHeightPx = measuredRow!.getBoundingClientRect().height;      renderedInVirtualizedRegion = measuredRow!.closest("[data-index]") instanceof HTMLElement;      expect(timelineWidthMeasuredPx, "Unable to measure timeline width.").toBeGreaterThan(0);      expect(measuredRowHeightPx, "Unable to measure targeted user row height.").toBeGreaterThan(0);    },    {      timeout: 4_000,      interval: 16,    },  );   return { measuredRowHeightPx, timelineWidthMeasuredPx, renderedInVirtualizedRegion };} async function mountChatView(options: {  viewport: ViewportSpec;  snapshot: OrchestrationReadModel;

Lines 1–500 of 871.