227 lines
8.6 KiB
TypeScript
227 lines
8.6 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import { t } from "@/lib/i18n";
|
|
import {
|
|
buildVcsSplitCallbacks,
|
|
determinePrimaryAction,
|
|
hasOpenChangeRequest,
|
|
} from "./vcs-split-button";
|
|
import { buildSingleRepoContributionCallbacks } from "./vcs-split-button-parts";
|
|
|
|
const SELECTED_PR_URL = "https://github.com/acme/widget-a/pull/42";
|
|
|
|
/**
|
|
* The VCS tooltips used to build their plural by hand:
|
|
*
|
|
* `Create PR (${aheadCount} commit${aheadCount !== 1 ? "s" : ""} ahead)`
|
|
*
|
|
* That puts the plural rule at the call site where no other locale can reach
|
|
* it, so the copy moved to `count` + `_one`/`_other`. These assertions pin the
|
|
* English to exactly what the concatenation emitted — E2E specs and the
|
|
* accessible-name queries depend on it, and a plural key is the easiest place
|
|
* to silently reword a string while every gate stays green.
|
|
*/
|
|
describe("vcs split-button count-bearing copy", () => {
|
|
const legacyCommit = (n: number) => `Commit ${n} changed file${n !== 1 ? "s" : ""}`;
|
|
const legacyPr = (n: number) => `Create PR (${n} commit${n !== 1 ? "s" : ""} ahead)`;
|
|
const legacyPush = (n: number) => `Push ${n} commit${n !== 1 ? "s" : ""} to remote`;
|
|
|
|
// 1 is the only value the old ternary treated specially; 0 and 2 both take
|
|
// the "s" branch, and 0 is the one a naive `n > 1` port would get wrong.
|
|
for (const n of [0, 1, 2, 5]) {
|
|
it(`matches the pre-migration English for ${n} commit(s)`, () => {
|
|
expect(t("integrations:commitChangedFiles", { count: n })).toBe(legacyCommit(n));
|
|
expect(t("integrations:createPrCommitsAhead", { count: n })).toBe(legacyPr(n));
|
|
expect(t("integrations:pushCommitsToRemote", { count: n })).toBe(legacyPush(n));
|
|
});
|
|
}
|
|
|
|
it("interpolates the git ref rather than translating it", () => {
|
|
expect(t("integrations:ontoBranch", { branch: "origin/main" })).toBe("onto origin/main");
|
|
expect(t("integrations:fromBranch", { branch: "release/1.2" })).toBe("from release/1.2");
|
|
expect(t("integrations:rebaseOntoBranchBehind", { branch: "origin/main", behind: 3 })).toBe(
|
|
"Rebase onto origin/main (3 behind)",
|
|
);
|
|
});
|
|
|
|
it("keeps the divergence aria-labels count-invariant, as the shipped English was", () => {
|
|
expect(t("integrations:commitsAheadAriaLabel", { value: 1 })).toBe("1 commits ahead");
|
|
expect(t("integrations:commitsBehindAriaLabel", { value: 2 })).toBe("2 commits behind");
|
|
});
|
|
});
|
|
|
|
describe("hasOpenChangeRequest", () => {
|
|
it("recognizes a registered provider review as the native task change request", () => {
|
|
expect(
|
|
hasOpenChangeRequest(undefined, [
|
|
{
|
|
providerId: "bitbucket",
|
|
reviewKey: "workspace/repo#42",
|
|
title: "Fix auth",
|
|
url: "https://bitbucket.test/pull-requests/42",
|
|
connectionScope: "https://bitbucket.test",
|
|
repositoryId: "repository-1",
|
|
changeRequestNumber: 42,
|
|
state: "OPEN",
|
|
},
|
|
]),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("does not treat merged or closed reviews as open", () => {
|
|
expect(
|
|
hasOpenChangeRequest("closed", [
|
|
{
|
|
providerId: "bitbucket",
|
|
reviewKey: "workspace/repo#42",
|
|
title: "Fix auth",
|
|
url: "https://bitbucket.test/pull-requests/42",
|
|
connectionScope: "https://bitbucket.test",
|
|
repositoryId: "repository-1",
|
|
changeRequestNumber: 42,
|
|
state: "MERGED",
|
|
},
|
|
]),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("vcs split-button remote action semantics", () => {
|
|
it("uses the upstream-relative count for a linked PR push", () => {
|
|
expect(determinePrimaryAction(0, 1, 7, true)).toBe("push");
|
|
});
|
|
|
|
it("does not select Push when divergence policy removes unsafe push evidence", () => {
|
|
expect(determinePrimaryAction(0, 0, 7, true)).toBe("rebase");
|
|
});
|
|
|
|
it("passes the blocked repository to single-repository contribution actions", async () => {
|
|
const requestReplace = vi.fn();
|
|
const requestUse = vi.fn();
|
|
const openExternalLinkFn = vi.fn().mockResolvedValue("browser");
|
|
const resolutionTarget = {
|
|
expectedRemoteHead: "a".repeat(40),
|
|
repo: "widget-a",
|
|
repositoryName: "widget-a",
|
|
};
|
|
const callbacks = buildVcsSplitCallbacks({
|
|
openCommitDialog: vi.fn(),
|
|
openPRDialog: vi.fn(),
|
|
handlePull: vi.fn(),
|
|
handlePush: vi.fn(),
|
|
handleRebase: vi.fn(),
|
|
handleMerge: vi.fn(),
|
|
resolution: { requestReplace, requestUse },
|
|
resolutionTarget,
|
|
selectedPR: { pr_url: SELECTED_PR_URL },
|
|
openExternalLinkFn,
|
|
});
|
|
|
|
callbacks.onReplaceContribution("widget-a");
|
|
callbacks.onUseContribution("widget-a");
|
|
callbacks.onViewContribution("widget-a");
|
|
await Promise.resolve();
|
|
|
|
expect(requestReplace).toHaveBeenCalledWith(resolutionTarget);
|
|
expect(requestUse).toHaveBeenCalledWith(resolutionTarget);
|
|
expect(openExternalLinkFn).toHaveBeenCalledWith(SELECTED_PR_URL);
|
|
});
|
|
|
|
it("scopes the single-repository menu callbacks to the blocked repository", () => {
|
|
const onReplaceContribution = vi.fn();
|
|
const onUseContribution = vi.fn();
|
|
const onViewContribution = vi.fn();
|
|
const onCompareContribution = vi.fn();
|
|
const callbacks = buildSingleRepoContributionCallbacks(
|
|
{ onReplaceContribution, onUseContribution, onViewContribution, onCompareContribution },
|
|
"widget-a",
|
|
);
|
|
|
|
callbacks.onReplaceContribution();
|
|
callbacks.onUseContribution();
|
|
callbacks.onViewContribution();
|
|
callbacks.onCompareContribution();
|
|
|
|
expect(onReplaceContribution).toHaveBeenCalledWith("widget-a");
|
|
expect(onUseContribution).toHaveBeenCalledWith("widget-a");
|
|
expect(onViewContribution).toHaveBeenCalledWith("widget-a");
|
|
expect(onCompareContribution).toHaveBeenCalledWith("widget-a");
|
|
});
|
|
|
|
it("normalizes a root checkout scope for single-repository menu callbacks", () => {
|
|
const onReplaceContribution = vi.fn();
|
|
const onUseContribution = vi.fn();
|
|
const onViewContribution = vi.fn();
|
|
const onCompareContribution = vi.fn();
|
|
const callbacks = buildSingleRepoContributionCallbacks(
|
|
{ onReplaceContribution, onUseContribution, onViewContribution, onCompareContribution },
|
|
undefined,
|
|
);
|
|
|
|
callbacks.onReplaceContribution();
|
|
callbacks.onUseContribution();
|
|
callbacks.onViewContribution();
|
|
callbacks.onCompareContribution();
|
|
|
|
expect(onReplaceContribution).toHaveBeenCalledWith("");
|
|
expect(onUseContribution).toHaveBeenCalledWith("");
|
|
expect(onViewContribution).toHaveBeenCalledWith("");
|
|
expect(onCompareContribution).toHaveBeenCalledWith("");
|
|
});
|
|
});
|
|
|
|
describe("vcs split-button root repository scope", () => {
|
|
it("matches the root checkout scope when callbacks receive no repository name", async () => {
|
|
const requestReplace = vi.fn();
|
|
const requestUse = vi.fn();
|
|
const openExternalLinkFn = vi.fn().mockResolvedValue("browser");
|
|
const onCompareContribution = vi.fn();
|
|
const resolutionTarget = {
|
|
expectedRemoteHead: "a".repeat(40),
|
|
repo: "",
|
|
repositoryName: "testorg/testrepo",
|
|
};
|
|
const callbacks = buildVcsSplitCallbacks({
|
|
openCommitDialog: vi.fn(),
|
|
openPRDialog: vi.fn(),
|
|
handlePull: vi.fn(),
|
|
handlePush: vi.fn(),
|
|
handleRebase: vi.fn(),
|
|
handleMerge: vi.fn(),
|
|
resolution: { requestReplace, requestUse },
|
|
resolutionTarget,
|
|
selectedPR: { pr_url: SELECTED_PR_URL },
|
|
openExternalLinkFn,
|
|
onCompareContribution,
|
|
});
|
|
|
|
callbacks.onReplaceContribution();
|
|
callbacks.onUseContribution();
|
|
callbacks.onViewContribution();
|
|
callbacks.onCompareContribution();
|
|
await Promise.resolve();
|
|
|
|
expect(requestReplace).toHaveBeenCalledWith(resolutionTarget);
|
|
expect(requestUse).toHaveBeenCalledWith(resolutionTarget);
|
|
expect(openExternalLinkFn).toHaveBeenCalledWith(SELECTED_PR_URL);
|
|
expect(onCompareContribution).toHaveBeenCalledWith("");
|
|
});
|
|
});
|
|
|
|
/**
|
|
* The discard dialog used to concatenate a " in your local clone at X" tail
|
|
* onto a shared stem, which froze the tail's position in the sentence. Each
|
|
* branch is now a whole message; these pin both to the previous rendering.
|
|
*/
|
|
describe("discard-local-changes description", () => {
|
|
it("reads identically to the old stem + tail concatenation", () => {
|
|
const stem = "Starting this task will permanently discard the uncommitted changes";
|
|
const tail = "Back up anything you want to keep before continuing.";
|
|
|
|
expect(t("common:discardLocalChangesDescription")).toBe(`${stem} in your local clone. ${tail}`);
|
|
expect(t("common:discardLocalChangesAtPathDescription", { repoPath: "/srv/repo" })).toBe(
|
|
`${stem} in your local clone at /srv/repo. ${tail}`,
|
|
);
|
|
});
|
|
});
|