mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-24 02:02:42 +00:00
* fix(composer): keep background context out of loading state Pull request link resolution can block a new workspace until its checkout target is known, but it is not submission work. Keep that safety gate while reserving the send-button spinner and drop lockout for actual submission or upload progress. * test(composer): cover link resolution at browser boundary Exercise the real new-workspace composer while pull request context resolves, including the submit presentation and file-drop path. Keep the two presentation booleans local to their only caller instead of exporting policy solely for an internal-state unit test. * fix(composer): preserve background lookup ownership Composer edits could release the resolving gate while a pull request lookup still owned the request, briefly allowing workspace creation from the wrong branch. Track logical candidates independently of unrelated draft changes and settle each reference independently so only invalid work releases early. * fix(composer): preserve pasted PR selection order * fix(composer): renew re-added PR lookups * fix(composer): track pasted PR source order * fix(composer): preserve resolved attachment order * fix(composer): coordinate active lookup order
651 lines
20 KiB
TypeScript
651 lines
20 KiB
TypeScript
/**
|
|
* @vitest-environment jsdom
|
|
*/
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import { act, renderHook } from "@testing-library/react";
|
|
import React, { type ReactNode } from "react";
|
|
import { useState } from "react";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import type { UserComposerAttachment } from "@/attachments/types";
|
|
import type { ForgeSearchClient } from "@/git/use-forge-search-query";
|
|
import type { ForgeSearchItem, ForgeSearchResponse } from "@getpaseo/protocol/messages";
|
|
import { useComposerGithubAutoAttach } from "./auto-attach";
|
|
|
|
type ForgeSearchPayload = ForgeSearchResponse["payload"];
|
|
|
|
const remoteUrl = "git@github.com:acme/paseo.git";
|
|
const cwd = "/repo";
|
|
|
|
const pr101: ForgeSearchItem = {
|
|
kind: "change_request",
|
|
number: 101,
|
|
title: "Attach PR",
|
|
url: "https://github.com/acme/paseo/pull/101",
|
|
state: "open",
|
|
body: null,
|
|
labels: [],
|
|
baseRefName: "main",
|
|
headRefName: "feature",
|
|
};
|
|
|
|
const pr202: ForgeSearchItem = {
|
|
...pr101,
|
|
number: 202,
|
|
title: "Attach second PR",
|
|
url: "https://github.com/acme/paseo/pull/202",
|
|
headRefName: "feature-two",
|
|
};
|
|
|
|
const issue202: ForgeSearchItem = {
|
|
kind: "issue",
|
|
number: 202,
|
|
title: "Attach issue",
|
|
url: "https://github.com/acme/paseo/issues/202",
|
|
state: "open",
|
|
body: null,
|
|
labels: [],
|
|
baseRefName: null,
|
|
headRefName: null,
|
|
};
|
|
|
|
interface SearchCall {
|
|
cwd: string;
|
|
query: string;
|
|
limit?: number;
|
|
}
|
|
|
|
interface HarnessInput {
|
|
initialAttachments?: UserComposerAttachment[];
|
|
initialCwd?: string;
|
|
initialText?: string;
|
|
onPullRequestDetected?: () => void;
|
|
onPullRequestAdded?: (item: ForgeSearchItem) => void;
|
|
remote?: string | null;
|
|
}
|
|
|
|
function githubPayload(items: ForgeSearchItem[], requestId: string): ForgeSearchPayload {
|
|
return {
|
|
items,
|
|
authState: "authenticated",
|
|
error: null,
|
|
requestId,
|
|
};
|
|
}
|
|
|
|
function createSearchClient(items: ForgeSearchItem[]): ForgeSearchClient & { calls: SearchCall[] } {
|
|
const calls: SearchCall[] = [];
|
|
return {
|
|
calls,
|
|
async searchForge(options) {
|
|
calls.push(options);
|
|
return githubPayload(items, `search-${options.query}`);
|
|
},
|
|
};
|
|
}
|
|
|
|
function deferred<T>() {
|
|
let resolve!: (value: T) => void;
|
|
const promise = new Promise<T>((resolvePromise) => {
|
|
resolve = resolvePromise;
|
|
});
|
|
return { promise, resolve };
|
|
}
|
|
|
|
function createWrapper() {
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
retry: false,
|
|
},
|
|
},
|
|
});
|
|
return function Wrapper({ children }: { children: ReactNode }) {
|
|
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
|
};
|
|
}
|
|
|
|
function useHarness(client: ForgeSearchClient, input: HarnessInput = {}) {
|
|
const [text, setText] = useState(input.initialText ?? "");
|
|
const [searchClient, setSearchClient] = useState(client);
|
|
const [workingDirectory, setWorkingDirectory] = useState(input.initialCwd ?? cwd);
|
|
const [attachments, setAttachments] = useState<UserComposerAttachment[]>(
|
|
input.initialAttachments ?? [],
|
|
);
|
|
const autoAttach = useComposerGithubAutoAttach({
|
|
text,
|
|
remoteUrl: input.remote ?? remoteUrl,
|
|
attachments,
|
|
client: searchClient,
|
|
isConnected: true,
|
|
serverId: "server-1",
|
|
cwd: workingDirectory,
|
|
setAttachments,
|
|
onPullRequestDetected: input.onPullRequestDetected,
|
|
onPullRequestAdded: input.onPullRequestAdded,
|
|
});
|
|
|
|
return {
|
|
text,
|
|
setText,
|
|
setSearchClient,
|
|
setWorkingDirectory,
|
|
attachments,
|
|
setAttachments,
|
|
isResolving: autoAttach.isResolving,
|
|
markGithubAttachmentRemoved: autoAttach.markGithubAttachmentRemoved,
|
|
};
|
|
}
|
|
|
|
async function flushDebounce() {
|
|
await act(async () => {
|
|
await vi.advanceTimersByTimeAsync(300);
|
|
await Promise.resolve();
|
|
});
|
|
}
|
|
|
|
describe("useComposerGithubAutoAttach", () => {
|
|
it("adds a matching pasted GitHub PR URL as a composer attachment", async () => {
|
|
vi.useFakeTimers();
|
|
const client = createSearchClient([pr101]);
|
|
const onPullRequestDetected = vi.fn();
|
|
const { result } = renderHook(() => useHarness(client, { onPullRequestDetected }), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
act(() => {
|
|
result.current.setText("Please review https://github.com/acme/paseo/pull/101");
|
|
});
|
|
expect(result.current.isResolving).toBe(true);
|
|
expect(onPullRequestDetected).toHaveBeenCalledTimes(1);
|
|
await flushDebounce();
|
|
|
|
expect(result.current.attachments).toEqual([{ kind: "forge_change_request", item: pr101 }]);
|
|
expect(result.current.isResolving).toBe(false);
|
|
expect(client.calls).toEqual([{ cwd, query: "101", limit: 20 }]);
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("ignores URLs that do not match the current remote", async () => {
|
|
vi.useFakeTimers();
|
|
const client = createSearchClient([pr101]);
|
|
const { result } = renderHook(() => useHarness(client), { wrapper: createWrapper() });
|
|
|
|
act(() => {
|
|
result.current.setText("Other repo https://github.com/other/paseo/pull/101");
|
|
});
|
|
await flushDebounce();
|
|
|
|
expect(result.current.attachments).toEqual([]);
|
|
expect(client.calls).toEqual([]);
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("does not add a second pill when the ref is already attached", async () => {
|
|
vi.useFakeTimers();
|
|
const client = createSearchClient([pr101]);
|
|
const initialAttachments: UserComposerAttachment[] = [{ kind: "github_pr", item: pr101 }];
|
|
const { result } = renderHook(() => useHarness(client, { initialAttachments }), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
act(() => {
|
|
result.current.setText("Already here https://github.com/acme/paseo/pull/101");
|
|
});
|
|
await flushDebounce();
|
|
|
|
expect(result.current.attachments).toEqual(initialAttachments);
|
|
expect(client.calls).toEqual([]);
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("does not re-add a GitHub ref removed earlier in the same composer session", async () => {
|
|
vi.useFakeTimers();
|
|
const client = createSearchClient([pr101]);
|
|
const initialAttachments: UserComposerAttachment[] = [{ kind: "github_pr", item: pr101 }];
|
|
const { result } = renderHook(() => useHarness(client, { initialAttachments }), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
act(() => {
|
|
result.current.markGithubAttachmentRemoved(initialAttachments[0]);
|
|
result.current.setAttachments([]);
|
|
result.current.setText("Re-pasted https://github.com/acme/paseo/pull/101");
|
|
});
|
|
await flushDebounce();
|
|
|
|
expect(result.current.attachments).toEqual([]);
|
|
expect(client.calls).toEqual([]);
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("handles multiple matching URLs from one paste", async () => {
|
|
vi.useFakeTimers();
|
|
const client = createSearchClient([pr101, issue202]);
|
|
const { result } = renderHook(() => useHarness(client), { wrapper: createWrapper() });
|
|
|
|
act(() => {
|
|
result.current.setText(
|
|
"Refs https://github.com/acme/paseo/pull/101 and https://github.com/acme/paseo/issues/202",
|
|
);
|
|
});
|
|
await flushDebounce();
|
|
|
|
expect(result.current.attachments).toEqual([
|
|
{ kind: "forge_change_request", item: pr101 },
|
|
{ kind: "forge_issue", item: issue202 },
|
|
]);
|
|
expect(client.calls).toEqual([
|
|
{ cwd, query: "101", limit: 20 },
|
|
{ cwd, query: "202", limit: 20 },
|
|
]);
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("reports pasted pull requests in source order when lookups finish out of order", async () => {
|
|
vi.useFakeTimers();
|
|
const firstLookup = deferred<ForgeSearchPayload>();
|
|
const secondLookup = deferred<ForgeSearchPayload>();
|
|
const client: ForgeSearchClient = {
|
|
searchForge: vi
|
|
.fn()
|
|
.mockReturnValueOnce(firstLookup.promise)
|
|
.mockReturnValueOnce(secondLookup.promise),
|
|
};
|
|
const onPullRequestAdded = vi.fn();
|
|
const { result } = renderHook(() => useHarness(client, { onPullRequestAdded }), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
act(() => {
|
|
result.current.setText(
|
|
"Refs https://github.com/acme/paseo/pull/101 and https://github.com/acme/paseo/pull/202",
|
|
);
|
|
});
|
|
await flushDebounce();
|
|
|
|
await act(async () => {
|
|
secondLookup.resolve(githubPayload([pr202], "search-202"));
|
|
await Promise.resolve();
|
|
});
|
|
expect(onPullRequestAdded).not.toHaveBeenCalled();
|
|
expect(result.current.attachments).toEqual([]);
|
|
|
|
await act(async () => {
|
|
firstLookup.resolve(githubPayload([pr101], "search-101"));
|
|
await Promise.resolve();
|
|
});
|
|
await vi.waitFor(() => {
|
|
expect(onPullRequestAdded.mock.calls).toEqual([[pr101], [pr202]]);
|
|
expect(result.current.attachments).toEqual([
|
|
{ kind: "forge_change_request", item: pr101 },
|
|
{ kind: "forge_change_request", item: pr202 },
|
|
]);
|
|
});
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("preserves pull request order across lookup batches", async () => {
|
|
vi.useFakeTimers();
|
|
const firstLookup = deferred<ForgeSearchPayload>();
|
|
const secondLookup = deferred<ForgeSearchPayload>();
|
|
const client: ForgeSearchClient = {
|
|
searchForge: vi
|
|
.fn()
|
|
.mockReturnValueOnce(firstLookup.promise)
|
|
.mockReturnValueOnce(secondLookup.promise),
|
|
};
|
|
const onPullRequestAdded = vi.fn();
|
|
const { result } = renderHook(() => useHarness(client, { onPullRequestAdded }), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
act(() => {
|
|
result.current.setText("Review https://github.com/acme/paseo/pull/101");
|
|
});
|
|
await flushDebounce();
|
|
act(() => {
|
|
result.current.setText(
|
|
"Review https://github.com/acme/paseo/pull/101 and https://github.com/acme/paseo/pull/202",
|
|
);
|
|
});
|
|
expect(result.current.isResolving).toBe(true);
|
|
await flushDebounce();
|
|
|
|
await act(async () => {
|
|
secondLookup.resolve(githubPayload([pr202], "search-202"));
|
|
await Promise.resolve();
|
|
});
|
|
expect(onPullRequestAdded).not.toHaveBeenCalled();
|
|
expect(result.current.attachments).toEqual([]);
|
|
expect(result.current.isResolving).toBe(true);
|
|
|
|
await act(async () => {
|
|
firstLookup.resolve(githubPayload([pr101], "search-101"));
|
|
await Promise.resolve();
|
|
});
|
|
await vi.waitFor(() => {
|
|
expect(onPullRequestAdded.mock.calls).toEqual([[pr101], [pr202]]);
|
|
expect(result.current.attachments).toEqual([
|
|
{ kind: "forge_change_request", item: pr101 },
|
|
{ kind: "forge_change_request", item: pr202 },
|
|
]);
|
|
expect(result.current.isResolving).toBe(false);
|
|
});
|
|
expect(client.searchForge).toHaveBeenCalledTimes(2);
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("uses the latest pull request order when URLs are reordered during debounce", async () => {
|
|
vi.useFakeTimers();
|
|
const client = createSearchClient([pr101, pr202]);
|
|
const onPullRequestAdded = vi.fn();
|
|
const { result } = renderHook(() => useHarness(client, { onPullRequestAdded }), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
act(() => {
|
|
result.current.setText(
|
|
"Refs https://github.com/acme/paseo/pull/101 and https://github.com/acme/paseo/pull/202",
|
|
);
|
|
});
|
|
act(() => {
|
|
result.current.setText(
|
|
"Refs https://github.com/acme/paseo/pull/202 and https://github.com/acme/paseo/pull/101",
|
|
);
|
|
});
|
|
await flushDebounce();
|
|
|
|
expect(onPullRequestAdded.mock.calls).toEqual([[pr202], [pr101]]);
|
|
expect(client.calls).toEqual([
|
|
{ cwd, query: "202", limit: 20 },
|
|
{ cwd, query: "101", limit: 20 },
|
|
]);
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("uses the latest pull request order when active lookups are reordered", async () => {
|
|
vi.useFakeTimers();
|
|
const firstLookup = deferred<ForgeSearchPayload>();
|
|
const secondLookup = deferred<ForgeSearchPayload>();
|
|
const client: ForgeSearchClient = {
|
|
searchForge: vi
|
|
.fn()
|
|
.mockReturnValueOnce(firstLookup.promise)
|
|
.mockReturnValueOnce(secondLookup.promise),
|
|
};
|
|
const onPullRequestAdded = vi.fn();
|
|
const { result } = renderHook(() => useHarness(client, { onPullRequestAdded }), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
act(() => {
|
|
result.current.setText(
|
|
"Refs https://github.com/acme/paseo/pull/101 and https://github.com/acme/paseo/pull/202",
|
|
);
|
|
});
|
|
await flushDebounce();
|
|
act(() => {
|
|
result.current.setText(
|
|
"Refs https://github.com/acme/paseo/pull/202 and https://github.com/acme/paseo/pull/101",
|
|
);
|
|
});
|
|
await flushDebounce();
|
|
|
|
await act(async () => {
|
|
firstLookup.resolve(githubPayload([pr101], "search-101"));
|
|
await Promise.resolve();
|
|
});
|
|
expect(onPullRequestAdded).not.toHaveBeenCalled();
|
|
expect(result.current.attachments).toEqual([]);
|
|
expect(result.current.isResolving).toBe(true);
|
|
|
|
await act(async () => {
|
|
secondLookup.resolve(githubPayload([pr202], "search-202"));
|
|
await Promise.resolve();
|
|
});
|
|
await vi.waitFor(() => {
|
|
expect(onPullRequestAdded.mock.calls).toEqual([[pr202], [pr101]]);
|
|
expect(result.current.attachments).toEqual([
|
|
{ kind: "forge_change_request", item: pr202 },
|
|
{ kind: "forge_change_request", item: pr101 },
|
|
]);
|
|
expect(result.current.isResolving).toBe(false);
|
|
});
|
|
expect(client.searchForge).toHaveBeenCalledTimes(2);
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("does not report a buffered pull request after its URL is removed", async () => {
|
|
vi.useFakeTimers();
|
|
const firstLookup = deferred<ForgeSearchPayload>();
|
|
const secondLookup = deferred<ForgeSearchPayload>();
|
|
const client: ForgeSearchClient = {
|
|
searchForge: vi
|
|
.fn()
|
|
.mockReturnValueOnce(firstLookup.promise)
|
|
.mockReturnValueOnce(secondLookup.promise),
|
|
};
|
|
const onPullRequestAdded = vi.fn();
|
|
const { result } = renderHook(() => useHarness(client, { onPullRequestAdded }), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
act(() => {
|
|
result.current.setText(
|
|
"Refs https://github.com/acme/paseo/pull/101 and https://github.com/acme/paseo/pull/202",
|
|
);
|
|
});
|
|
await flushDebounce();
|
|
|
|
await act(async () => {
|
|
secondLookup.resolve(githubPayload([pr202], "search-202"));
|
|
await Promise.resolve();
|
|
});
|
|
act(() => {
|
|
result.current.setText("Still https://github.com/acme/paseo/pull/101");
|
|
});
|
|
await flushDebounce();
|
|
|
|
await act(async () => {
|
|
firstLookup.resolve(githubPayload([pr101], "search-101"));
|
|
await Promise.resolve();
|
|
});
|
|
await vi.waitFor(() => {
|
|
expect(onPullRequestAdded.mock.calls).toEqual([[pr101]]);
|
|
});
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("keeps resolving when a pending pull request URL is removed and re-added", async () => {
|
|
vi.useFakeTimers();
|
|
const lookup = deferred<ForgeSearchPayload>();
|
|
const client: ForgeSearchClient = {
|
|
searchForge: vi.fn().mockReturnValue(lookup.promise),
|
|
};
|
|
const onPullRequestAdded = vi.fn();
|
|
const { result } = renderHook(() => useHarness(client, { onPullRequestAdded }), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
act(() => {
|
|
result.current.setText("Review https://github.com/acme/paseo/pull/101");
|
|
});
|
|
await flushDebounce();
|
|
|
|
act(() => {
|
|
result.current.setText("");
|
|
});
|
|
expect(result.current.isResolving).toBe(false);
|
|
|
|
act(() => {
|
|
result.current.setText("Review https://github.com/acme/paseo/pull/101");
|
|
});
|
|
expect(result.current.isResolving).toBe(true);
|
|
await flushDebounce();
|
|
expect(client.searchForge).toHaveBeenCalledTimes(1);
|
|
|
|
await act(async () => {
|
|
lookup.resolve(githubPayload([pr101], "search-101"));
|
|
await Promise.resolve();
|
|
});
|
|
await vi.waitFor(() => {
|
|
expect(result.current.attachments).toEqual([{ kind: "forge_change_request", item: pr101 }]);
|
|
expect(result.current.isResolving).toBe(false);
|
|
expect(onPullRequestAdded.mock.calls).toEqual([[pr101]]);
|
|
});
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("releases a removed ref without waiting for its lookup to settle", async () => {
|
|
vi.useFakeTimers();
|
|
const firstLookup = deferred<ForgeSearchPayload>();
|
|
const secondLookup = deferred<ForgeSearchPayload>();
|
|
const client: ForgeSearchClient = {
|
|
searchForge: vi
|
|
.fn()
|
|
.mockReturnValueOnce(firstLookup.promise)
|
|
.mockReturnValueOnce(secondLookup.promise),
|
|
};
|
|
const onPullRequestAdded = vi.fn();
|
|
const { result } = renderHook(() => useHarness(client, { onPullRequestAdded }), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
act(() => {
|
|
result.current.setText(
|
|
"Refs https://github.com/acme/paseo/pull/101 and https://github.com/acme/paseo/pull/202",
|
|
);
|
|
});
|
|
await flushDebounce();
|
|
|
|
await act(async () => {
|
|
secondLookup.resolve(githubPayload([pr202], "search-202"));
|
|
await Promise.resolve();
|
|
});
|
|
expect(onPullRequestAdded).not.toHaveBeenCalled();
|
|
expect(result.current.isResolving).toBe(true);
|
|
|
|
act(() => {
|
|
result.current.setText("Still https://github.com/acme/paseo/pull/202");
|
|
});
|
|
await flushDebounce();
|
|
|
|
await vi.waitFor(() => {
|
|
expect(onPullRequestAdded.mock.calls).toEqual([[pr202]]);
|
|
expect(result.current.isResolving).toBe(false);
|
|
});
|
|
|
|
await act(async () => {
|
|
firstLookup.resolve(githubPayload([], "search-101"));
|
|
await Promise.resolve();
|
|
});
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("stays resolving when an unrelated attachment is added during lookup", async () => {
|
|
vi.useFakeTimers();
|
|
const lookup = deferred<ForgeSearchPayload>();
|
|
const client: ForgeSearchClient = {
|
|
searchForge: vi.fn().mockReturnValue(lookup.promise),
|
|
};
|
|
const { result } = renderHook(() => useHarness(client), { wrapper: createWrapper() });
|
|
|
|
act(() => {
|
|
result.current.setText("Review https://github.com/acme/paseo/pull/101");
|
|
});
|
|
await flushDebounce();
|
|
|
|
act(() => {
|
|
result.current.setAttachments([{ kind: "forge_issue", item: issue202 }]);
|
|
result.current.setText("Review https://github.com/acme/paseo/pull/101 please");
|
|
});
|
|
|
|
expect(result.current.isResolving).toBe(true);
|
|
|
|
await act(async () => {
|
|
lookup.resolve(githubPayload([pr101], "search-101"));
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(result.current.attachments).toEqual([
|
|
{ kind: "forge_issue", item: issue202 },
|
|
{ kind: "forge_change_request", item: pr101 },
|
|
]);
|
|
expect(result.current.isResolving).toBe(false);
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("stops resolving when an in-flight PR URL is removed", async () => {
|
|
vi.useFakeTimers();
|
|
const lookup = deferred<ForgeSearchPayload>();
|
|
const client: ForgeSearchClient = {
|
|
searchForge: vi.fn().mockReturnValue(lookup.promise),
|
|
};
|
|
const { result } = renderHook(() => useHarness(client), { wrapper: createWrapper() });
|
|
|
|
act(() => {
|
|
result.current.setText("Review https://github.com/acme/paseo/pull/101");
|
|
});
|
|
await flushDebounce();
|
|
act(() => {
|
|
result.current.setText("");
|
|
});
|
|
|
|
expect(result.current.isResolving).toBe(false);
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("ignores a lookup that finishes after the target changes", async () => {
|
|
vi.useFakeTimers();
|
|
const lookup = deferred<ForgeSearchPayload>();
|
|
const client: ForgeSearchClient = {
|
|
searchForge: vi.fn().mockReturnValue(lookup.promise),
|
|
};
|
|
const { result } = renderHook(() => useHarness(client), { wrapper: createWrapper() });
|
|
|
|
act(() => {
|
|
result.current.setText("Review https://github.com/acme/paseo/pull/101");
|
|
});
|
|
await flushDebounce();
|
|
|
|
act(() => {
|
|
result.current.setWorkingDirectory("/other-repo");
|
|
});
|
|
await flushDebounce();
|
|
await act(async () => {
|
|
lookup.resolve(githubPayload([pr101], "search-101"));
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(result.current.attachments).toEqual([]);
|
|
expect(client.searchForge).toHaveBeenCalledTimes(1);
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("accepts a lookup after the transport client is replaced for the same target", async () => {
|
|
vi.useFakeTimers();
|
|
const lookup = deferred<ForgeSearchPayload>();
|
|
const firstClient: ForgeSearchClient = {
|
|
searchForge: vi.fn().mockReturnValue(lookup.promise),
|
|
};
|
|
const replacementClient = createSearchClient([pr101]);
|
|
const { result } = renderHook(() => useHarness(firstClient), { wrapper: createWrapper() });
|
|
|
|
act(() => {
|
|
result.current.setText("Review https://github.com/acme/paseo/pull/101");
|
|
});
|
|
await flushDebounce();
|
|
act(() => {
|
|
result.current.setSearchClient(replacementClient);
|
|
});
|
|
await act(async () => {
|
|
lookup.resolve(githubPayload([pr101], "search-101"));
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(result.current.attachments).toEqual([{ kind: "forge_change_request", item: pr101 }]);
|
|
expect(replacementClient.calls).toEqual([]);
|
|
vi.useRealTimers();
|
|
});
|
|
});
|