597 lines · 17.4 KB · TypeScript
RawDownload
import { scopeThreadRef } from "@t3tools/client-runtime";import { ThreadId } from "@t3tools/contracts";import * as Option from "effect/Option";import { useState } from "react";import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";import { render } from "vitest-browser-react"; const SHARED_THREAD_ID = ThreadId.make("thread-shared");const ENVIRONMENT_A = "environment-local" as never;const ENVIRONMENT_B = "environment-remote" as never;const GIT_CWD = "/repo/project";const BRANCH_NAME = "feature/toast-scope"; function createDeferredPromise<T>() {  let resolve!: (value: T) => void;  let reject!: (reason?: unknown) => void;   const promise = new Promise<T>((nextResolve, nextReject) => {    resolve = nextResolve;    reject = nextReject;  });   return { promise, resolve, reject };} const {  activeRunStackedActionDeferredRef,  activeDraftThreadRef,  hasServerThreadRef,  sourceControlDiscoveryRef,  vcsStatusRef,  invalidateSourceControlStateSpy,  refreshVcsStatusSpy,  runStackedActionSpy,  setDraftThreadContextSpy,  setThreadBranchSpy,  toastAddSpy,  toastCloseSpy,  toastPromiseSpy,  toastUpdateSpy,} = vi.hoisted(() => ({  activeRunStackedActionDeferredRef: { current: createDeferredPromise<never>() },  activeDraftThreadRef: { current: null as unknown },  hasServerThreadRef: { current: true },  sourceControlDiscoveryRef: { current: null as unknown },  vcsStatusRef: {    current: {      isRepo: true,      sourceControlProvider: {        kind: "github",        name: "GitHub",        baseUrl: "https://github.com",      },      hasPrimaryRemote: true,      isDefaultRef: false,      refName: "feature/toast-scope",      hasWorkingTreeChanges: false,      workingTree: { files: [], insertions: 0, deletions: 0 },      hasUpstream: true,      aheadCount: 1,      behindCount: 0,      pr: null,    },  },  invalidateSourceControlStateSpy: vi.fn(() => Promise.resolve()),  refreshVcsStatusSpy: vi.fn(() => Promise.resolve(null)),  runStackedActionSpy: vi.fn(() => activeRunStackedActionDeferredRef.current.promise),  setDraftThreadContextSpy: vi.fn(),  setThreadBranchSpy: vi.fn(),  toastAddSpy: vi.fn(() => "toast-1"),  toastCloseSpy: vi.fn(),  toastPromiseSpy: vi.fn(),  toastUpdateSpy: vi.fn(),})); vi.mock("~/components/ui/toast", () => ({  toastManager: {    add: toastAddSpy,    close: toastCloseSpy,    promise: toastPromiseSpy,    update: toastUpdateSpy,  },  stackedThreadToast: vi.fn((options: unknown) => options),})); vi.mock("~/editorPreferences", () => ({  openInPreferredEditor: vi.fn(),})); vi.mock("~/lib/sourceControlDiscoveryState", () => ({  useSourceControlDiscovery: vi.fn(() => sourceControlDiscoveryRef.current),})); vi.mock("~/lib/sourceControlActions", () => ({  invalidateSourceControlState: invalidateSourceControlStateSpy,  useGitStackedAction: vi.fn(() => ({    error: null,    isPending: false,    resetError: vi.fn(),    run: runStackedActionSpy,  })),  useSourceControlActionRunning: vi.fn(() => false),  useSourceControlPublishRepositoryAction: vi.fn(() => ({    error: null,    isPending: false,    resetError: vi.fn(),    run: vi.fn(),  })),  useVcsInitAction: vi.fn(() => ({    error: null,    isPending: false,    resetError: vi.fn(),    run: vi.fn(),  })),  useVcsPullAction: vi.fn(() => ({    error: null,    isPending: false,    resetError: vi.fn(),    run: vi.fn(),  })),})); vi.mock("~/lib/vcsStatusState", () => ({  refreshVcsStatus: refreshVcsStatusSpy,  resetVcsStatusStateForTests: () => undefined,  useVcsStatus: vi.fn(() => ({    data: vcsStatusRef.current,    error: null,    isPending: false,  })),})); vi.mock("~/localApi", () => ({  ensureLocalApi: vi.fn(() => {    throw new Error("ensureLocalApi not implemented in browser test");  }),  readLocalApi: vi.fn(() => null),})); vi.mock("~/composerDraftStore", async () => {  const draftStoreState = {    getDraftThreadByRef: () => activeDraftThreadRef.current,    getDraftSession: () => activeDraftThreadRef.current,    getDraftThread: () => activeDraftThreadRef.current,    getDraftSessionByLogicalProjectKey: () => null,    setDraftThreadContext: setDraftThreadContextSpy,    setLogicalProjectDraftThreadId: vi.fn(),    setProjectDraftThreadId: vi.fn(),    hasDraftThreadsInEnvironment: () => false,    clearDraftThread: vi.fn(),  };   return {    DraftId: {      makeUnsafe: (value: string) => value,    },    useComposerDraftStore: Object.assign(      (selector: (state: unknown) => unknown) => selector(draftStoreState),      { getState: () => draftStoreState },    ),    markPromotedDraftThread: vi.fn(),    markPromotedDraftThreadByRef: vi.fn(),    markPromotedDraftThreads: vi.fn(),    markPromotedDraftThreadsByRef: vi.fn(),    finalizePromotedDraftThreadByRef: vi.fn(),    finalizePromotedDraftThreadsByRef: vi.fn(),  };}); vi.mock("~/store", () => ({  selectEnvironmentState: (    state: { environmentStateById: Record<string, unknown> },    environmentId: string | null,  ) => {    if (!environmentId) {      throw new Error("Missing environment id");    }    const environmentState = state.environmentStateById[environmentId];    if (!environmentState) {      throw new Error(`Unknown environment: ${environmentId}`);    }    return environmentState;  },  selectProjectsForEnvironment: () => [],  selectProjectsAcrossEnvironments: () => [],  selectThreadsForEnvironment: () => [],  selectThreadsAcrossEnvironments: () => [],  selectThreadShellsAcrossEnvironments: () => [],  selectSidebarThreadsAcrossEnvironments: () => [],  selectSidebarThreadsForProjectRef: () => [],  selectSidebarThreadsForProjectRefs: () => [],  selectBootstrapCompleteForActiveEnvironment: () => true,  selectProjectByRef: () => null,  selectThreadByRef: () => null,  selectSidebarThreadSummaryByRef: () => null,  selectThreadIdsByProjectRef: () => [],  useStore: (selector: (state: unknown) => unknown) =>    selector({      setThreadBranch: setThreadBranchSpy,      environmentStateById: {        [ENVIRONMENT_A]: {          threadShellById: hasServerThreadRef.current            ? {                [SHARED_THREAD_ID]: {                  id: SHARED_THREAD_ID,                  branch: BRANCH_NAME,                  worktreePath: null,                },              }            : {},          threadSessionById: {},          threadTurnStateById: {},          messageIdsByThreadId: {},          messageByThreadId: {},          activityIdsByThreadId: {},          activityByThreadId: {},          proposedPlanIdsByThreadId: {},          proposedPlanByThreadId: {},          turnDiffIdsByThreadId: {},          turnDiffSummaryByThreadId: {},        },        [ENVIRONMENT_B]: {          threadShellById: hasServerThreadRef.current            ? {                [SHARED_THREAD_ID]: {                  id: SHARED_THREAD_ID,                  branch: BRANCH_NAME,                  worktreePath: null,                },              }            : {},          threadSessionById: {},          threadTurnStateById: {},          messageIdsByThreadId: {},          messageByThreadId: {},          activityIdsByThreadId: {},          activityByThreadId: {},          proposedPlanIdsByThreadId: {},          proposedPlanByThreadId: {},          turnDiffIdsByThreadId: {},          turnDiffSummaryByThreadId: {},        },      },    }),})); vi.mock("~/terminal-links", () => ({  resolvePathLinkTarget: vi.fn(),})); import GitActionsControl from "./GitActionsControl"; function findButtonByText(text: string): HTMLButtonElement | null {  return (Array.from(document.querySelectorAll("button")).find((button) =>    button.textContent?.includes(text),  ) ?? null) as HTMLButtonElement | null;} function findRadioByText(text: string): HTMLElement | null {  return (Array.from(document.querySelectorAll('[role="radio"]')).find((radio) =>    radio.textContent?.includes(text),  ) ?? null) as HTMLElement | null;} function setInputValue(input: HTMLInputElement, value: string) {  input.value = value;  input.dispatchEvent(new Event("input", { bubbles: true }));} function createDefaultVcsStatus() {  return {    isRepo: true,    sourceControlProvider: {      kind: "github",      name: "GitHub",      baseUrl: "https://github.com",    },    hasPrimaryRemote: true,    isDefaultRef: false,    refName: BRANCH_NAME,    hasWorkingTreeChanges: false,    workingTree: { files: [], insertions: 0, deletions: 0 },    hasUpstream: true,    aheadCount: 1,    behindCount: 0,    pr: null,  };} function createDefaultSourceControlDiscovery() {  return {    data: {      versionControlSystems: [],      sourceControlProviders: [        {          kind: "github",          label: "GitHub",          status: "available",          version: Option.some("2.85.0"),          installHint: "Install GitHub CLI.",          detail: Option.none(),          auth: {            status: "authenticated",            account: Option.some("octo"),            host: Option.some("github.com"),            detail: Option.none(),          },        },        {          kind: "gitlab",          label: "GitLab",          status: "available",          version: Option.some("19.0.0"),          installHint: "Install GitLab CLI.",          detail: Option.none(),          auth: {            status: "authenticated",            account: Option.some("tanuki"),            host: Option.some("gitlab.com"),            detail: Option.none(),          },        },      ],    },    error: null,    isPending: false,  };} function Harness() {  const [activeThreadRef, setActiveThreadRef] = useState(    scopeThreadRef(ENVIRONMENT_A, SHARED_THREAD_ID),  );   return (    <>      <button        type="button"        onClick={() => setActiveThreadRef(scopeThreadRef(ENVIRONMENT_B, SHARED_THREAD_ID))}      >        Switch environment      </button>      <GitActionsControl gitCwd={GIT_CWD} activeThreadRef={activeThreadRef} />    </>  );} describe("GitActionsControl thread-scoped progress toast", () => {  beforeEach(() => {    vcsStatusRef.current = createDefaultVcsStatus();    sourceControlDiscoveryRef.current = createDefaultSourceControlDiscovery();  });   afterEach(() => {    vi.useRealTimers();    vi.clearAllMocks();    activeRunStackedActionDeferredRef.current = createDeferredPromise<never>();    activeDraftThreadRef.current = null;    hasServerThreadRef.current = true;    vcsStatusRef.current = createDefaultVcsStatus();    sourceControlDiscoveryRef.current = createDefaultSourceControlDiscovery();    document.body.innerHTML = "";  });   it("keeps an in-flight git action toast pinned to the thread ref that started it", async () => {    vi.useFakeTimers();     const host = document.createElement("div");    document.body.append(host);    const screen = await render(<Harness />, { container: host });     try {      const quickActionButton = findButtonByText("Push & create PR");      expect(quickActionButton, 'Unable to find button containing "Push & create PR"').toBeTruthy();      if (!(quickActionButton instanceof HTMLButtonElement)) {        throw new Error('Unable to find button containing "Push & create PR"');      }      quickActionButton.click();       expect(toastAddSpy).toHaveBeenCalledWith(        expect.objectContaining({          data: { threadRef: scopeThreadRef(ENVIRONMENT_A, SHARED_THREAD_ID) },          title: "Pushing...",          type: "loading",        }),      );       await vi.advanceTimersByTimeAsync(1_000);       expect(toastUpdateSpy).toHaveBeenLastCalledWith(        "toast-1",        expect.objectContaining({          data: { threadRef: scopeThreadRef(ENVIRONMENT_A, SHARED_THREAD_ID) },          title: "Pushing...",          type: "loading",        }),      );       const switchEnvironmentButton = findButtonByText("Switch environment");      expect(        switchEnvironmentButton,        'Unable to find button containing "Switch environment"',      ).toBeTruthy();      if (!(switchEnvironmentButton instanceof HTMLButtonElement)) {        throw new Error('Unable to find button containing "Switch environment"');      }      switchEnvironmentButton.click();      await vi.advanceTimersByTimeAsync(1_000);       expect(toastUpdateSpy).toHaveBeenLastCalledWith(        "toast-1",        expect.objectContaining({          data: { threadRef: scopeThreadRef(ENVIRONMENT_A, SHARED_THREAD_ID) },          title: "Pushing...",          type: "loading",        }),      );    } finally {      activeRunStackedActionDeferredRef.current.reject(new Error("test cleanup"));      await Promise.resolve();      vi.useRealTimers();      await screen.unmount();      host.remove();    }  });   it("derives publish provider and repository defaults while preserving user edits", async () => {    vcsStatusRef.current = {      ...createDefaultVcsStatus(),      hasPrimaryRemote: false,      hasUpstream: false,      aheadCount: 0,    };     const host = document.createElement("div");    document.body.append(host);    const screen = await render(      <GitActionsControl        gitCwd={GIT_CWD}        activeThreadRef={scopeThreadRef(ENVIRONMENT_A, SHARED_THREAD_ID)}      />,      { container: host },    );     try {      findButtonByText("Publish repository")?.click();      await Promise.resolve();      expect(document.querySelector('[role="dialog"]')?.textContent).toContain(        "Publish repository",      );       findButtonByText("Next")?.click();      await Promise.resolve();      const repositoryInput = document.querySelector<HTMLInputElement>("#publish-repository-path");      expect(repositoryInput?.value).toBe("octo/");       if (!repositoryInput) {        throw new Error("Repository input was not rendered.");      }      setInputValue(repositoryInput, "octo/demo");      expect(repositoryInput.value).toBe("octo/demo");       findButtonByText("Back")?.click();      await Promise.resolve();      findRadioByText("GitLab")?.click();      await Promise.resolve();      findButtonByText("Next")?.click();      await Promise.resolve();       expect(document.querySelector<HTMLInputElement>("#publish-repository-path")?.value).toBe(        "tanuki/",      );    } finally {      await screen.unmount();      host.remove();    }  });   it("debounces focus-driven git status refreshes", async () => {    vi.useFakeTimers();     const originalVisibilityState = Object.getOwnPropertyDescriptor(document, "visibilityState");    let visibilityState: DocumentVisibilityState = "hidden";    Object.defineProperty(document, "visibilityState", {      configurable: true,      get: () => visibilityState,    });     const host = document.createElement("div");    document.body.append(host);    const screen = await render(      <GitActionsControl        gitCwd={GIT_CWD}        activeThreadRef={scopeThreadRef(ENVIRONMENT_A, SHARED_THREAD_ID)}      />,      {        container: host,      },    ); 

Lines 1–500 of 597.