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. Load more
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, }, );