Release Git Tools 0.1.0
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
|
||||
const ORIGIN = "http://127.0.0.1:4173";
|
||||
async function localOnly(page: Page) {
|
||||
const external: string[] = [];
|
||||
await page.route("**/*", async (route) => {
|
||||
const url = new URL(route.request().url());
|
||||
if (url.origin !== ORIGIN) {
|
||||
external.push(url.href);
|
||||
await route.abort();
|
||||
} else await route.continue();
|
||||
});
|
||||
return external;
|
||||
}
|
||||
|
||||
test("runs from a nested path without external requests", async ({ page }) => {
|
||||
const errors: string[] = [];
|
||||
page.on("pageerror", (error) => errors.push(error.message));
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === "error") errors.push(message.text());
|
||||
});
|
||||
const external = await localOnly(page);
|
||||
await page.goto("/deep/nested/git/");
|
||||
await expect(
|
||||
page.getByRole("banner").getByRole("heading", { name: "Git Tools" }),
|
||||
).toBeVisible();
|
||||
expect(external).toEqual([]);
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
test("authors and re-inspects a patch without flashing state", async ({
|
||||
page,
|
||||
}) => {
|
||||
const external = await localOnly(page);
|
||||
await page.goto("/deep/nested/git/");
|
||||
await page.getByRole("tab", { 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");
|
||||
await page.getByRole("button", { name: "Inspect generated patch" }).click();
|
||||
await expect(page.getByText("src/example.ts", { exact: true })).toBeVisible();
|
||||
expect(external).toEqual([]);
|
||||
});
|
||||
|
||||
test("serves release identity and hardened headers", async ({ request }) => {
|
||||
const index = await request.get("/deep/nested/git/");
|
||||
expect(index.ok()).toBe(true);
|
||||
expect(index.headers()["content-security-policy"]).toContain(
|
||||
"connect-src 'self'",
|
||||
);
|
||||
expect(await index.text()).not.toMatch(/\b(?:src|href)=["']\//u);
|
||||
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",
|
||||
entry: "./",
|
||||
privacy: { processing: "local", telemetry: false },
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { App } from "../../src/App";
|
||||
|
||||
describe("Git Tools", () => {
|
||||
it("renders the local workbench and ignore explanations", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => new Response("Not found", { status: 404 })),
|
||||
);
|
||||
render(<App />);
|
||||
expect(
|
||||
await screen.findByRole("heading", { name: "Git Tools" }),
|
||||
).toBeVisible();
|
||||
await userEvent.click(
|
||||
await screen.findByRole("tab", { name: ".gitignore" }),
|
||||
);
|
||||
expect(screen.getAllByText(/Last matching rule/u).length).toBeGreaterThan(
|
||||
0,
|
||||
);
|
||||
expect(screen.getByText("Browser-local")).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildConventionalCommit,
|
||||
compareSemVer,
|
||||
createUnifiedPatch,
|
||||
explainGitignore,
|
||||
lintConventionalCommit,
|
||||
normalizeChangelog,
|
||||
parseSemVer,
|
||||
parseUnifiedPatch,
|
||||
satisfiesSemVer,
|
||||
} from "../../src/git/tools";
|
||||
|
||||
describe("Git interchange tools", () => {
|
||||
it("parses patch files, renames, hunks, and statistics", () => {
|
||||
const source = `diff --git a/old.txt b/new.txt\nsimilarity index 90%\nrename from old.txt\nrename to new.txt\n--- a/old.txt\n+++ b/new.txt\n@@ -1,2 +1,2 @@ section\n same\n-old\n+new\n`;
|
||||
const parsed = parseUnifiedPatch(source);
|
||||
expect(parsed).toMatchObject({ added: 1, deleted: 1 });
|
||||
expect(parsed.files[0]).toMatchObject({
|
||||
status: "renamed",
|
||||
oldPath: "old.txt",
|
||||
newPath: "new.txt",
|
||||
similarity: 90,
|
||||
});
|
||||
expect(parsed.files[0]?.hunks[0]?.validCounts).toBe(true);
|
||||
});
|
||||
|
||||
it("decodes Git C-quoted octal paths", () => {
|
||||
const source = `diff --git "a/foo\\040bar.txt" "b/foo\\040bar.txt"\n--- "a/foo\\040bar.txt"\n+++ "b/foo\\040bar.txt"\n@@ -1 +1 @@\n-old\n+new\n`;
|
||||
expect(parseUnifiedPatch(source).files[0]).toMatchObject({
|
||||
oldPath: "foo bar.txt",
|
||||
newPath: "foo bar.txt",
|
||||
});
|
||||
});
|
||||
|
||||
it("authors a patch that parses with valid counts", () => {
|
||||
const output = createUnifiedPatch(
|
||||
"a\nb\nc\n",
|
||||
"a\nB\nc\nd\n",
|
||||
"src/a file.txt",
|
||||
1,
|
||||
);
|
||||
expect(output).toContain("-b");
|
||||
expect(output).toContain("+B");
|
||||
const parsed = parseUnifiedPatch(output);
|
||||
expect(parsed.diagnostics).toEqual([]);
|
||||
expect(parsed).toMatchObject({ added: 2, deleted: 1 });
|
||||
});
|
||||
|
||||
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, [
|
||||
"dist/app.js",
|
||||
"debug.log",
|
||||
"keep.log",
|
||||
"cache/readme.md",
|
||||
]);
|
||||
expect(results.map((item) => item.ignored)).toEqual([
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
]);
|
||||
expect(results[3]?.explanation).toMatch(/parent cache/u);
|
||||
});
|
||||
|
||||
it("propagates ordinary directory matches but respects directory-only inputs", () => {
|
||||
const results = explainGitignore(`dist/\nbuild\n`, [
|
||||
"dist",
|
||||
"dist/",
|
||||
"dist/app.js",
|
||||
"build",
|
||||
"build/",
|
||||
"build/app.js",
|
||||
]);
|
||||
expect(results.map((item) => item.ignored)).toEqual([
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
]);
|
||||
});
|
||||
|
||||
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);
|
||||
expect(satisfiesSemVer("1.4.2", "^1.2.0 <2.0.0")).toBe(true);
|
||||
expect(satisfiesSemVer("2.0.0", "^1.2.0 || >=3.0.0")).toBe(false);
|
||||
expect(satisfiesSemVer("1.3.0-beta.1", "^1.2.0")).toBe(false);
|
||||
expect(satisfiesSemVer("1.9.9", "~1")).toBe(true);
|
||||
expect(satisfiesSemVer("0.8.0", "^0")).toBe(true);
|
||||
expect(satisfiesSemVer("0.0.9", "^0.0")).toBe(true);
|
||||
expect(satisfiesSemVer("2.3.9", "1.2 - 2.3")).toBe(true);
|
||||
expect(satisfiesSemVer("1.99.0", "<=1")).toBe(true);
|
||||
expect(satisfiesSemVer("2.0.0", "<=1")).toBe(false);
|
||||
expect(() => parseSemVer("9007199254740992.0.0")).toThrow(/safe integer/u);
|
||||
});
|
||||
|
||||
it("builds and lints conventional commits", () => {
|
||||
const message = buildConventionalCommit({
|
||||
type: "feat",
|
||||
scope: "api",
|
||||
breaking: true,
|
||||
subject: "change response shape",
|
||||
breakingDescription: "Clients must read data.items.",
|
||||
issues: "#42",
|
||||
});
|
||||
expect(message).toContain("feat(api)!");
|
||||
expect(message).toContain("BREAKING CHANGE:");
|
||||
expect(lintConventionalCommit(message).valid).toBe(true);
|
||||
expect(lintConventionalCommit("bad header").valid).toBe(false);
|
||||
});
|
||||
|
||||
it("normalizes changelog headings, order, and duplicate bullets", () => {
|
||||
const result = normalizeChangelog(
|
||||
`# Changelog\n\n## 1.0.0 - 2026-01-01\n\n### fixed\n\n- B\n- B\n\n### Added\n\n- A\n`,
|
||||
);
|
||||
expect(result.deduplicated).toBe(1);
|
||||
expect(result.output.indexOf("### Added")).toBeLessThan(
|
||||
result.output.indexOf("### Fixed"),
|
||||
);
|
||||
expect(result.output).toContain("## [1.0.0] - 2026-01-01");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user