fix(web): retain eligible providers during refresh (#3498)

Co-authored-by: ayattara <alassane.yattara@savoirfairelinux.com>
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
This commit is contained in:
Alassane
2026-09-10 06:09:25 -04:00
committed by GitHub
parent b703ab352f
commit 39e3ce2d52
2 changed files with 112 additions and 3 deletions
@@ -33,6 +33,7 @@ import { useRemoteRepositories } from "./use-remote-repositories";
import { pluginRegistry } from "@/lib/plugins/registry";
const WORKSPACE_ID = "workspace-1";
const NEXT_WORKSPACE_ID = "workspace-2";
const PLUGIN_ID = "test-bitbucket-provider";
const GITLAB_UNAVAILABLE = "GitLab unavailable";
@@ -84,6 +85,14 @@ function expectWorkspaceScopedGitHubRequest() {
});
}
const githubRepo = (name: string) => ({
owner: "acme",
name,
full_name: `acme/${name}`,
default_branch: "main",
private: false,
});
describe("useRemoteRepositories registered providers", () => {
it("isolates a provider URL matcher failure", async () => {
pluginRegistry.forPlugin(PLUGIN_ID).registerRepositoryProvider({
@@ -412,13 +421,15 @@ describe("useRemoteRepositories provider eligibility changes", () => {
expect(result.current.repos[0]?.provider).toBe("github");
expect(mocks.listUserProjects).not.toHaveBeenCalled();
rerender({ workspaceId: "workspace-2" });
rerender({ workspaceId: NEXT_WORKSPACE_ID });
await waitFor(() => expect(result.current.repos[0]?.provider).toBe("gitlab"));
expect(mocks.fetchAccessibleRepos).toHaveBeenCalledTimes(1);
expect(mocks.listUserProjects).toHaveBeenCalledTimes(1);
});
});
describe("useRemoteRepositories provider refreshes", () => {
it("re-evaluates provider eligibility before a manual refresh", async () => {
mocks.fetchAccessibleRepos.mockResolvedValue([]);
mocks.listUserProjects.mockResolvedValue({ projects: [] });
@@ -438,6 +449,54 @@ describe("useRemoteRepositories provider eligibility changes", () => {
expect(mocks.listAzureDevOpsProjects).toHaveBeenCalledTimes(1);
await waitFor(() => expect(result.current.availableProviders).toEqual(["github"]));
});
it("keeps currently eligible providers available while a refresh is loading", async () => {
mocks.fetchAccessibleRepos.mockResolvedValue([]);
mocks.listUserProjects.mockResolvedValue({ projects: [] });
mocks.listAzureDevOpsProjects.mockResolvedValue({ projects: [] });
const { result } = renderHook(() => useRemoteRepositories(WORKSPACE_ID));
await waitFor(() => expect(result.current.loading).toBe(false));
let resolveRefresh: ((repos: never[]) => void) | undefined;
mocks.fetchAccessibleRepos.mockImplementationOnce(
() => new Promise((resolve) => (resolveRefresh = resolve)),
);
setBuiltInAvailability({ azureDevOps: false, gitlab: false });
act(() => result.current.refresh?.());
await waitFor(() => expect(mocks.fetchAccessibleRepos).toHaveBeenCalledTimes(2));
expect(result.current.availableProviders).toEqual(["github"]);
expect(result.current.repos).toEqual([]);
act(() => resolveRefresh?.([]));
await waitFor(() => expect(result.current.loading).toBe(false));
});
it("clears a provider error while its refresh retry is loading", async () => {
setBuiltInAvailability({ gitlab: false, azureDevOps: false });
mocks.fetchAccessibleRepos.mockRejectedValueOnce(new Error("GitHub unavailable"));
const { result } = renderHook(() => useRemoteRepositories(WORKSPACE_ID));
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.sourceErrors).toEqual([
{ provider: "github", error: new Error("GitHub unavailable") },
]);
let resolveRetry: ((repos: never[]) => void) | undefined;
mocks.fetchAccessibleRepos.mockImplementationOnce(
() => new Promise((resolve) => (resolveRetry = resolve)),
);
act(() => result.current.refresh?.());
await waitFor(() => expect(mocks.fetchAccessibleRepos).toHaveBeenCalledTimes(2));
expect(result.current.loading).toBe(true);
expect(result.current.sourceErrors).toEqual([]);
act(() => resolveRetry?.([]));
await waitFor(() => expect(result.current.loading).toBe(false));
});
});
describe("useRemoteRepositories workspace scope", () => {
@@ -486,11 +545,56 @@ describe("useRemoteRepositories workspace scope", () => {
);
await waitFor(() => expect(result.current.repos).toHaveLength(1));
rerender({ workspaceId: "workspace-2" });
rerender({ workspaceId: NEXT_WORKSPACE_ID });
await waitFor(() => expect(result.current.loading).toBe(true));
expect(result.current.repos).toEqual([]);
act(() => resolveNext?.({ projects: [] }));
await waitFor(() => expect(result.current.loading).toBe(false));
});
it("does not publish a stale repository result after a workspace change", async () => {
setBuiltInAvailability({ gitlab: false, azureDevOps: false });
let resolveFirst: ((value: unknown) => void) | undefined;
let resolveSecond: ((value: unknown) => void) | undefined;
mocks.fetchAccessibleRepos
.mockImplementationOnce(() => new Promise((resolve) => (resolveFirst = resolve)))
.mockImplementationOnce(() => new Promise((resolve) => (resolveSecond = resolve)));
const { result, rerender } = renderHook(
({ workspaceId }) => useRemoteRepositories(workspaceId),
{ initialProps: { workspaceId: WORKSPACE_ID } },
);
await waitFor(() => expect(mocks.fetchAccessibleRepos).toHaveBeenCalledTimes(1));
rerender({ workspaceId: NEXT_WORKSPACE_ID });
await waitFor(() => expect(mocks.fetchAccessibleRepos).toHaveBeenCalledTimes(2));
await act(async () => resolveFirst?.([githubRepo("stale")]));
expect(result.current.repos).toEqual([]);
await act(async () => resolveSecond?.([githubRepo("current")]));
await waitFor(() =>
expect(result.current.repos.map((repo) => repo.fullName)).toEqual(["acme/current"]),
);
});
it("ignores a result resolved before the next workspace request starts", async () => {
setBuiltInAvailability({ gitlab: false, azureDevOps: false });
let resolveFirst: ((value: unknown) => void) | undefined;
mocks.fetchAccessibleRepos
.mockImplementationOnce(() => new Promise((resolve) => (resolveFirst = resolve)))
.mockImplementationOnce(() => new Promise(() => {}));
const { result, rerender } = renderHook(
({ workspaceId }) => useRemoteRepositories(workspaceId),
{ initialProps: { workspaceId: WORKSPACE_ID } },
);
await waitFor(() => expect(mocks.fetchAccessibleRepos).toHaveBeenCalledTimes(1));
await act(async () => {
resolveFirst?.([githubRepo("stale")]);
rerender({ workspaceId: NEXT_WORKSPACE_ID });
});
expect(result.current.repos).toEqual([]);
await waitFor(() => expect(mocks.fetchAccessibleRepos).toHaveBeenCalledTimes(2));
});
});
@@ -307,11 +307,16 @@ function useBuiltInRepositorySource(
const [loading, setLoading] = useState(true);
const [sourceErrors, setSourceErrors] = useState<RemoteRepositorySourceError[]>([]);
const [availableProviders, setAvailableProviders] = useState<RemoteRepositoryProvider[]>([]);
const workspaceRef = useRef(workspaceId);
useEffect(() => {
let cancelled = false;
const sameWorkspace = workspaceRef.current === workspaceId;
workspaceRef.current = workspaceId;
setRepos([]);
setAvailableProviders([]);
setAvailableProviders((current) =>
sameWorkspace ? current.filter((provider) => eligibility.providers.has(provider)) : [],
);
setSourceErrors([]);
setLoading(true);
if (eligibility.loading) {