// Repository placard: Git leaves the tool palette and becomes an always-visible // compact block under the tool strip. These tests cover the projection logic, // the rendering contract (including "show nothing rather than a placeholder"), // and the wiring that keeps one Git request path and one Git surface. var test = require("node:test"); var assert = require("node:assert/strict"); var fs = require("node:fs"); var path = require("node:path"); var root = path.join(__dirname, ".."); function read(relative) { return fs.readFileSync(path.join(root, relative), "utf8"); } var placardSource = read("lib/public/modules/git-placard.js"); var panelSource = read("lib/public/modules/git-panel.js"); var sidebarSource = read("lib/public/modules/sidebar.js"); var appSource = read("lib/public/app.js"); var indexHtml = read("lib/public/index.html"); var placardCss = read("lib/public/css/git-placard.css"); var inputCss = read("lib/public/css/input.css"); var styleCss = read("lib/public/style.css"); // --- Fake DOM ------------------------------------------------------------- // // There is no DOM harness in this suite, so the module is evaluated with its // imports stubbed and a minimal element model. Only what the placard touches // is modelled; anything else would be pretending to test a browser. function makeElement(id) { var classes = {}; return { id: id, innerHTML: "", attributes: {}, classList: { add: function (name) { classes[name] = true; }, remove: function (name) { delete classes[name]; }, contains: function (name) { return !!classes[name]; }, }, setAttribute: function (name, value) { this.attributes[name] = value; }, removeAttribute: function (name) { delete this.attributes[name]; }, }; } function loadPlacard() { var body = placardSource .replace(/^import[\s\S]*?;$/gm, "") .replace(/^export function/gm, "function"); var elements = { "git-placard": makeElement("git-placard"), "git-placard-body": makeElement("git-placard-body") }; var iconCalls = []; var refreshCalls = { count: 0 }; var fakeDocument = { getElementById: function (id) { return elements[id] || null; }, }; var factory = new Function( "document", "escapeHtml", "iconHtml", "refreshIcons", body + "\nreturn { mergeStatusIntoSummary: mergeStatusIntoSummary, accessibleLabel: accessibleLabel," + " renderGitPlacard: renderGitPlacard, hideGitPlacard: hideGitPlacard };" ); var api = factory( fakeDocument, function (value) { return String(value == null ? "" : value) .replace(/&/g, "&").replace(//g, ">"); }, function (name) { iconCalls.push(name); return ""; }, function () { refreshCalls.count++; } ); api.elements = elements; api.iconCalls = iconCalls; api.refreshCalls = refreshCalls; return api; } function fullStatus(overrides) { return Object.assign({ isRepository: true, name: "throughline-portal", root: "/Users/chad/clay-projects/throughline-portal", gitDir: "/Users/chad/clay-projects/throughline-portal/.git", mainWorktree: "/Users/chad/clay-projects/throughline-portal", origin: "git@github.com:ThroughLineCare/throughline-portal.git", branch: "codex/centralize-sdk-login", detached: false, oid: "6cdcc4109e91570867f8848c0e04e8b52d3b32e7", upstream: "origin/codex/centralize-sdk-login", ahead: 2, behind: 0, isWorktree: false, files: [], }, overrides || {}); } // --- Projection ----------------------------------------------------------- test("a non-repository status projects to nothing at all", function () { var api = loadPlacard(); assert.equal(api.mergeStatusIntoSummary(null, null), null); assert.equal(api.mergeStatusIntoSummary({ isRepository: false }, null), null); assert.equal(api.mergeStatusIntoSummary({ isRepository: false, loadError: "boom" }, null), null); }); test("the projection carries identity, branch and bounded counts", function () { var api = loadPlacard(); var summary = api.mergeStatusIntoSummary(fullStatus({ files: [ { path: "a.js", staged: true, unstaged: false, untracked: false, conflicted: false }, { path: "b.js", staged: false, unstaged: true, untracked: false, conflicted: false }, { path: "c.js", staged: false, unstaged: true, untracked: true, conflicted: false }, { path: "d.js", staged: true, unstaged: true, untracked: false, conflicted: true }, ], }), null); assert.equal(summary.name, "throughline-portal"); assert.equal(summary.branch, "codex/centralize-sdk-login"); assert.equal(summary.changed, 4); assert.equal(summary.staged, 2); assert.equal(summary.unstaged, 2, "an untracked file is not double counted as unstaged"); assert.equal(summary.untracked, 1); assert.equal(summary.conflicted, 1); assert.equal(summary.hasUpstream, true); assert.equal(summary.ahead, 2); assert.equal(summary.behind, 0); }); test("the projection never carries a filesystem path", function () { var api = loadPlacard(); var summary = api.mergeStatusIntoSummary(fullStatus(), null); var keys = Object.keys(summary); assert.equal(keys.indexOf("root"), -1); assert.equal(keys.indexOf("gitDir"), -1); assert.equal(keys.indexOf("commonDir"), -1); assert.equal(keys.indexOf("mainWorktree"), -1); assert.equal(keys.indexOf("origin"), -1, "the raw remote URL is not projected either"); assert.equal(keys.indexOf("files"), -1); for (var i = 0; i < keys.length; i++) { var value = summary[keys[i]]; if (typeof value !== "string") continue; assert.equal(value.indexOf("/Users/"), -1, keys[i] + " must not contain an absolute path"); } }); test("the remote label comes only from the server summary, never parsed client-side", function () { var api = loadPlacard(); var withoutPrevious = api.mergeStatusIntoSummary(fullStatus(), null); assert.equal(withoutPrevious.remote, null, "a raw origin URL in the panel status is never turned into a label here"); var withPrevious = api.mergeStatusIntoSummary(fullStatus(), { remote: "github.com/ThroughLineCare/throughline-portal", name: "stale-name", }); assert.equal(withPrevious.remote, "github.com/ThroughLineCare/throughline-portal"); assert.equal(withPrevious.name, "throughline-portal", "the fresher status wins for identity"); assert.equal(/git@|https:\/\//.test(placardSource), false, "the client does not parse remote URLs"); }); test("a detached head reports the short oid instead of a branch", function () { var api = loadPlacard(); var summary = api.mergeStatusIntoSummary(fullStatus({ detached: true, branch: null }), null); assert.equal(summary.branch, null); assert.equal(summary.detached, true); assert.equal(summary.shortOid, "6cdcc410"); assert.match(api.accessibleLabel(summary), /Detached at 6cdcc410/); }); // --- Accessible label ----------------------------------------------------- test("the placard announces one full sentence rather than bare numbers", function () { var api = loadPlacard(); var label = api.accessibleLabel(api.mergeStatusIntoSummary(fullStatus({ isWorktree: true, behind: 3, files: [{ path: "a.js", staged: false, unstaged: true, untracked: false, conflicted: false }], }), { remote: "github.com/ThroughLineCare/throughline-portal" })); assert.match(label, /Repository throughline-portal/); assert.match(label, /branch codex\/centralize-sdk-login/); assert.match(label, /linked worktree/); assert.match(label, /1 changed files/); assert.match(label, /2 ahead/); assert.match(label, /3 behind/); assert.match(label, /remote github\.com\/ThroughLineCare\/throughline-portal/); var clean = api.accessibleLabel(api.mergeStatusIntoSummary(fullStatus({ ahead: 0 }), null)); assert.match(clean, /working tree clean/); assert.equal(/ahead|behind/.test(clean), false, "a synced branch says nothing about sync"); assert.equal(api.accessibleLabel(null), ""); }); // --- Rendering ------------------------------------------------------------ test("no repository renders no placeholder and no flash", function () { var api = loadPlacard(); api.renderGitPlacard(null); assert.equal(api.elements["git-placard"].classList.contains("hidden"), true); assert.equal(api.elements["git-placard-body"].innerHTML, ""); assert.equal(api.elements["git-placard"].attributes["aria-label"], undefined); api.renderGitPlacard({ isRepository: false }); assert.equal(api.elements["git-placard"].classList.contains("hidden"), true); assert.equal(api.elements["git-placard-body"].innerHTML, ""); }); test("a real repository renders identity, branch and a bounded status glance", function () { var api = loadPlacard(); api.renderGitPlacard(api.mergeStatusIntoSummary(fullStatus({ files: [ { path: "a.js", staged: true, unstaged: false, untracked: false, conflicted: false }, { path: "b.js", staged: false, unstaged: true, untracked: false, conflicted: false }, ], }), { remote: "github.com/ThroughLineCare/throughline-portal" })); var el = api.elements["git-placard"]; var html = api.elements["git-placard-body"].innerHTML; assert.equal(el.classList.contains("hidden"), false); assert.match(html, /git-placard-repo">throughline-portal); assert.match(html, /git-placard-branch">codex\/centralize-sdk-login); assert.match(html, /2 changed/); assert.match(html, /git-placard-remote/); assert.match(html, /github\.com\/ThroughLineCare\/throughline-portal/); assert.equal(html.indexOf("/Users/"), -1, "no filesystem path is ever rendered"); assert.ok(el.attributes["aria-label"].length > 0, "the group carries its own label"); }); test("a clean tree says so, and sync counts appear only when there is an upstream", function () { var api = loadPlacard(); api.renderGitPlacard(api.mergeStatusIntoSummary(fullStatus({ ahead: 0, behind: 0 }), null)); assert.match(api.elements["git-placard-body"].innerHTML, /Clean/); api.renderGitPlacard(api.mergeStatusIntoSummary(fullStatus({ ahead: 4, behind: 1 }), null)); var synced = api.elements["git-placard-body"].innerHTML; assert.match(synced, /arrow-up"><\/i>4/); assert.match(synced, /arrow-down"><\/i>1/); api.renderGitPlacard(api.mergeStatusIntoSummary(fullStatus({ upstream: null, ahead: 0, behind: 0 }), null)); var noUpstream = api.elements["git-placard-body"].innerHTML; assert.equal(/arrow-up|arrow-down/.test(noUpstream), false, "without an upstream there is no ahead/behind claim to make"); }); test("conflicts and linked worktrees are surfaced as bounded labels", function () { var api = loadPlacard(); api.renderGitPlacard(api.mergeStatusIntoSummary(fullStatus({ isWorktree: true, files: [{ path: "a.js", staged: true, unstaged: true, untracked: false, conflicted: true }], }), null)); var html = api.elements["git-placard-body"].innerHTML; assert.match(html, /Linked worktree/); assert.match(html, /git-placard-stat-warn">1 conflicted); api.renderGitPlacard(api.mergeStatusIntoSummary(fullStatus({ detached: true, branch: null }), null)); assert.match(api.elements["git-placard-body"].innerHTML, /git-placard-badge-warn">Detached); }); test("an unchanged reading does not rewrite the DOM", function () { var api = loadPlacard(); var summary = api.mergeStatusIntoSummary(fullStatus(), null); api.renderGitPlacard(summary); var firstRefresh = api.refreshCalls.count; api.renderGitPlacard(api.mergeStatusIntoSummary(fullStatus(), null)); assert.equal(api.refreshCalls.count, firstRefresh, "identical data re-renders nothing"); }); test("hiding clears the label and the body so nothing stale survives a switch", function () { var api = loadPlacard(); api.renderGitPlacard(api.mergeStatusIntoSummary(fullStatus(), null)); api.hideGitPlacard(); assert.equal(api.elements["git-placard"].classList.contains("hidden"), true); assert.equal(api.elements["git-placard-body"].innerHTML, ""); assert.equal(api.elements["git-placard"].attributes["aria-label"], undefined); // And the next render after a hide repaints rather than short-circuiting. api.renderGitPlacard(api.mergeStatusIntoSummary(fullStatus(), null)); assert.match(api.elements["git-placard-body"].innerHTML, /throughline-portal/); }); // --- Markup and wiring ---------------------------------------------------- test("the placard sits directly below the tool strip and starts hidden", function () { var toolsBlock = indexHtml.slice(indexHtml.indexOf('