@@ -33,7 +33,7 @@ test("authors and re-inspects a patch without flashing state", async ({
|
||||
}) => {
|
||||
const external = await localOnly(page);
|
||||
await page.goto("/deep/nested/git/");
|
||||
await page.getByRole("tab", { name: "Author" }).click();
|
||||
await page.getByRole("button", { name: "Author" }).click();
|
||||
await page.getByLabel("After text").fill("alpha\nbeta\n");
|
||||
await page.getByRole("button", { name: "Generate patch" }).click();
|
||||
await expect(page.getByLabel("Generated patch")).toContainText("+alpha");
|
||||
@@ -52,7 +52,7 @@ test("serves release identity and hardened headers", async ({ request }) => {
|
||||
const manifest = await request.get("/deep/nested/git/toolbox-app.json");
|
||||
await expect(manifest.json()).resolves.toMatchObject({
|
||||
id: "de.add-ideas.git-tools",
|
||||
version: "0.1.0",
|
||||
version: "0.2.0",
|
||||
entry: "./",
|
||||
privacy: { processing: "local", telemetry: false },
|
||||
});
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("keeps the primary workspace inside a narrow viewport", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/deep/nested/git/");
|
||||
await expect(page.locator("main").first()).toBeVisible();
|
||||
await expect(
|
||||
page.locator("main .loading, main .workbench-loading"),
|
||||
).toHaveCount(0);
|
||||
|
||||
const widths = await page.evaluate(() => ({
|
||||
content: document.documentElement.scrollWidth,
|
||||
viewport: document.documentElement.clientWidth,
|
||||
}));
|
||||
expect(widths.viewport).toBeLessThanOrEqual(430);
|
||||
expect(widths.content).toBeLessThanOrEqual(widths.viewport + 1);
|
||||
});
|
||||
@@ -14,7 +14,7 @@ describe("Git Tools", () => {
|
||||
await screen.findByRole("heading", { name: "Git Tools" }),
|
||||
).toBeVisible();
|
||||
await userEvent.click(
|
||||
await screen.findByRole("tab", { name: ".gitignore" }),
|
||||
await screen.findByRole("button", { name: ".gitignore" }),
|
||||
);
|
||||
expect(screen.getAllByText(/Last matching rule/u).length).toBeGreaterThan(
|
||||
0,
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
applyPatchToSnapshot,
|
||||
buildConventionalCommit,
|
||||
compareSemVer,
|
||||
createSnapshotZip,
|
||||
createUnifiedPatch,
|
||||
evaluateGitAttributes,
|
||||
explainGitignore,
|
||||
explainGitignoreWorkspace,
|
||||
lintConventionalCommit,
|
||||
normalizeChangelog,
|
||||
parseSemVer,
|
||||
parseUnifiedPatch,
|
||||
planRelease,
|
||||
satisfiesSemVer,
|
||||
} from "../../src/git/tools";
|
||||
|
||||
@@ -47,6 +52,33 @@ describe("Git interchange tools", () => {
|
||||
expect(parsed).toMatchObject({ added: 2, deleted: 1 });
|
||||
});
|
||||
|
||||
it("applies and reverses exact text patches atomically", () => {
|
||||
const before = "a\nb\nc\n";
|
||||
const after = "a\nB\nc\nd\n";
|
||||
const patch = createUnifiedPatch(before, after, "src/file.txt", 1);
|
||||
const applied = applyPatchToSnapshot(patch, {
|
||||
"src/file.txt": before,
|
||||
"README.md": "keep\n",
|
||||
});
|
||||
expect(applied.files).toEqual({
|
||||
"src/file.txt": after,
|
||||
"README.md": "keep\n",
|
||||
});
|
||||
expect(
|
||||
applyPatchToSnapshot(patch, applied.files, true).files["src/file.txt"],
|
||||
).toBe(before);
|
||||
expect(() =>
|
||||
applyPatchToSnapshot(patch, { "src/file.txt": "different\n" }),
|
||||
).toThrow(/exact hunk match/u);
|
||||
});
|
||||
|
||||
it("builds a deterministic text snapshot ZIP", () => {
|
||||
const one = createSnapshotZip({ "b.txt": "B\n", "a.txt": "A\n" });
|
||||
const two = createSnapshotZip({ "a.txt": "A\n", "b.txt": "B\n" });
|
||||
expect([...one.slice(0, 4)]).toEqual([0x50, 0x4b, 0x03, 0x04]);
|
||||
expect(one).toEqual(two);
|
||||
});
|
||||
|
||||
it("explains ordered ignore rules and blocked parents", () => {
|
||||
const source = `dist/\n*.log\n!keep.log\ncache/\n!cache/readme.md\n`;
|
||||
const results = explainGitignore(source, [
|
||||
@@ -83,6 +115,76 @@ describe("Git interchange tools", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("evaluates global, info, nested, and tracked precedence with winning source", () => {
|
||||
const results = explainGitignoreWorkspace(
|
||||
{
|
||||
globalExclude: "*.tmp\n*.log\n",
|
||||
infoExclude: "private/\n",
|
||||
root: "!keep.log\nsrc/*.tmp\n",
|
||||
nested: {
|
||||
"src/.gitignore": "!generated.tmp\n*.cache\n",
|
||||
"src/deep/.gitignore": "!keep.cache\n",
|
||||
},
|
||||
tracked: ["private/tracked.txt"],
|
||||
},
|
||||
[
|
||||
"debug.log",
|
||||
"keep.log",
|
||||
"src/generated.tmp",
|
||||
"src/other.tmp",
|
||||
"src/deep/drop.cache",
|
||||
"src/deep/keep.cache",
|
||||
"private/tracked.txt",
|
||||
],
|
||||
);
|
||||
expect(results.map((item) => item.ignored)).toEqual([
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
]);
|
||||
expect(results[2]?.explanation).toContain("src/.gitignore:1");
|
||||
expect(results[5]?.explanation).toContain("src/deep/.gitignore:1");
|
||||
expect(results[6]).toMatchObject({ tracked: true });
|
||||
});
|
||||
|
||||
it("evaluates .gitattributes states with per-attribute last matches", () => {
|
||||
const [script, secret, image] = evaluateGitAttributes(
|
||||
`* text=auto\n*.sh text eol=lf\nsecret.sh -text !eol\n*.png -text binary\n`,
|
||||
["scripts/build.sh", "secret.sh", "logo.png"],
|
||||
);
|
||||
expect(script?.attributes).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: "eol",
|
||||
state: { kind: "value", value: "lf" },
|
||||
}),
|
||||
expect.objectContaining({ name: "text", state: { kind: "set" } }),
|
||||
]),
|
||||
);
|
||||
expect(image?.attributes).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ name: "binary", state: { kind: "set" } }),
|
||||
expect.objectContaining({ name: "text", state: { kind: "unset" } }),
|
||||
]),
|
||||
);
|
||||
expect(secret?.attributes).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: "eol",
|
||||
state: { kind: "unspecified" },
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(
|
||||
evaluateGitAttributes("docs/ text\n", ["docs/readme.md"])[0]
|
||||
?.diagnostics[0],
|
||||
).toContain("trailing slash");
|
||||
});
|
||||
|
||||
it("implements SemVer precedence and basic ranges", () => {
|
||||
expect(compareSemVer("1.0.0-alpha.1", "1.0.0-alpha.beta")).toBeLessThan(0);
|
||||
expect(compareSemVer("1.0.0+one", "1.0.0+two")).toBe(0);
|
||||
@@ -123,4 +225,26 @@ describe("Git interchange tools", () => {
|
||||
);
|
||||
expect(result.output).toContain("## [1.0.0] - 2026-01-01");
|
||||
});
|
||||
|
||||
it("plans a release from valid conventional commit evidence", () => {
|
||||
const plan = planRelease({
|
||||
currentVersion: "1.4.2",
|
||||
date: "2026-09-01",
|
||||
commits: [
|
||||
"fix(api): retain response headers",
|
||||
"feat(ui): add timeline",
|
||||
"bad commit",
|
||||
],
|
||||
});
|
||||
expect(plan).toMatchObject({
|
||||
bump: "minor",
|
||||
nextVersion: "1.5.0",
|
||||
tag: "v1.5.0",
|
||||
validCommits: 2,
|
||||
});
|
||||
expect(plan.releaseNotes).toContain("### Added");
|
||||
expect(plan.releaseNotes).toContain("### Fixed");
|
||||
expect(plan.ignoredCommits).toHaveLength(1);
|
||||
expect(plan.commands.at(-1)).toContain("v1.5.0");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user