115 lines
4.3 KiB
TypeScript
115 lines
4.3 KiB
TypeScript
// @vitest-environment happy-dom
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import { createWorktreesView } from "./worktreesView.js";
|
|
import type { RetainedWorktree } from "./types.js";
|
|
|
|
function worktree(overrides: Partial<RetainedWorktree>): RetainedWorktree {
|
|
return {
|
|
id: "11111111-2222-3333-4444-555555555555",
|
|
path: "/home/naps/tea/maestro/worktrees/feat/login",
|
|
base_repo: "/home/naps/tea/maestro",
|
|
branch: "feat/login",
|
|
reason: "force-killed",
|
|
retained_at: "2026-08-24T10:00:00Z",
|
|
in_the_way: {
|
|
uncommitted_files: 2,
|
|
untracked_files: 1,
|
|
unpushed_commits: 3,
|
|
},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
/** Mount a view over a scripted store and return (el, reclaimed-ids). */
|
|
function mount(items: RetainedWorktree[], homeDir = "/home/naps") {
|
|
const reclaimed: string[] = [];
|
|
const view = createWorktreesView({
|
|
load: async () => items,
|
|
reclaim: async (id) => {
|
|
reclaimed.push(id);
|
|
items.splice(
|
|
items.findIndex((w) => w.id === id),
|
|
1,
|
|
);
|
|
},
|
|
homeDir: () => homeDir,
|
|
});
|
|
document.body.appendChild(view.el);
|
|
return { view, reclaimed };
|
|
}
|
|
|
|
describe("retained-worktrees view (#84)", () => {
|
|
it("lists branch, abbreviated path, reason and what is in the way", async () => {
|
|
const { view } = mount([
|
|
worktree({
|
|
path: "/home/naps/tea/maestro/worktrees/feat/login",
|
|
}),
|
|
]);
|
|
await view.refresh();
|
|
const text = view.el.textContent ?? "";
|
|
expect(text).toContain("feat/login");
|
|
// Sidebar-style abbreviation of the path against the daemon's home dir,
|
|
// full path kept in the tooltip.
|
|
expect(text).toContain("t/m/w/f/login");
|
|
const pathEl = view.el.querySelector<HTMLElement>(".wt-path")!;
|
|
expect(pathEl.title).toBe("/home/naps/tea/maestro/worktrees/feat/login");
|
|
// Same reason labels as `maestro worktrees list` and the TUI overlay.
|
|
expect(text).toContain("force-killed");
|
|
// Same phrasing as InTheWay's Display impl — shared with the kill refusal.
|
|
expect(text).toContain("2 uncommitted files, 1 untracked file, 3 unpushed commits");
|
|
});
|
|
|
|
it("says a tree whose work has since been pushed is clear", async () => {
|
|
const { view } = mount([worktree({ in_the_way: {} })]);
|
|
await view.refresh();
|
|
expect(view.el.textContent).toContain("no unsaved work");
|
|
});
|
|
|
|
it("renders an empty store as a normal state, not an error", async () => {
|
|
const { view } = mount([]);
|
|
await view.refresh();
|
|
const text = view.el.textContent ?? "";
|
|
expect(text).toContain("no retained worktrees");
|
|
expect(text.toLowerCase()).not.toContain("error");
|
|
expect(text.toLowerCase()).not.toContain("failed");
|
|
});
|
|
|
|
it("guards reclaim with a confirmation that names what is lost and that the branch survives", async () => {
|
|
const wt = worktree({ branch: "feat/login" });
|
|
const { view, reclaimed } = mount([wt]);
|
|
await view.refresh();
|
|
|
|
view.el.querySelector<HTMLButtonElement>(".wt-reclaim-btn")!.click();
|
|
const confirmText = view.el.querySelector<HTMLElement>(".wt-confirm")!.textContent ?? "";
|
|
expect(confirmText).toContain("Reclaim worktree feat/login?");
|
|
// Names what will be lost before it happens…
|
|
expect(confirmText).toContain(
|
|
"2 uncommitted files, 1 untracked file, 3 unpushed commits",
|
|
);
|
|
// …and says reclaim does not take the commits too.
|
|
expect(confirmText).toContain("never deleted");
|
|
|
|
view.el.querySelector<HTMLButtonElement>(".wt-confirm-cancel")!.click();
|
|
expect(reclaimed).toEqual([]);
|
|
|
|
view.el.querySelector<HTMLButtonElement>(".wt-reclaim-btn")!.click();
|
|
view.el.querySelector<HTMLButtonElement>(".wt-confirm-go")!.click();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
expect(reclaimed).toEqual(["11111111-2222-3333-4444-555555555555"]);
|
|
// The row is gone once the refresh lands.
|
|
expect(view.el.textContent).toContain("no retained worktrees");
|
|
});
|
|
|
|
it("sorts oldest-retained first, like every other client", async () => {
|
|
const { view } = mount([
|
|
worktree({ branch: "newer", retained_at: "2026-08-24T12:00:00Z" }),
|
|
worktree({ branch: "older", retained_at: "2026-08-24T08:00:00Z" }),
|
|
]);
|
|
await view.refresh();
|
|
const branches = [...view.el.querySelectorAll(".wt-branch")].map((n) => n.textContent);
|
|
expect(branches).toEqual(["older", "newer"]);
|
|
});
|
|
});
|