import { describe, expect, it } from "bun:test"; import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { unansweredDecisionsLabel } from "./decision-view-control"; import { acceptDecisionCounts, applyDecisionCounts, applyDecisionSnapshot, beginDocumentLoad, completeDocumentPage, failDocumentLoad, newestDocument, projectDocuments, removeLoadedDocument, replaceProjectTotal, settleHttpTotal, staleDecisionCounts, } from "./document-actions"; import { ProjectSidebar } from "./project-sidebar"; import { decisionWatchPlan, fallbackRefreshBatch } from "./sidebar-decision-plan"; import { MAX_WATCHED_DOCUMENTS, MAX_WATCHED_REPOSITORIES } from "./sidebar-decision-watch"; import type { ComponentProps } from "react"; import type * as Api from "./api"; import type { DocumentLoadState } from "./document-actions"; let parent: Api.Channel = { id: "channel-lease", repositoryId: "R_chopin", repositoryOwner: "githubnext", repositoryName: "chopin", title: "Postgres writer lease", slug: "postgres-writer-lease", createdBy: "user-one", revision: 3, createdAt: "2026-08-20T00:00:00.000Z", updatedAt: "2026-08-23T00:00:00.000Z", descriptionRevision: 0, unansweredDecisions: 4, }; let child: Api.Channel = { ...parent, id: "channel-fencing", parentChannelId: parent.id, title: "Fencing tokens", slug: "fencing-tokens", unansweredDecisions: 1, }; let quiet: Api.Channel = { ...parent, id: "channel-quiet", title: "Settled plan", slug: "settled-plan", unansweredDecisions: 0, }; function sidebar( documents: DocumentLoadState, overrides: Partial> = {}, ): string { return renderToStaticMarkup(createElement(ProjectSidebar, { canCreateDocument: true, pendingCreations: new Map(), onAccount: () => {}, onAddProject: () => {}, onCollapse: () => {}, onCreateDocument: () => {}, onDocumentAction: () => {}, onLoadMore: () => {}, onNewDocument: () => {}, onSearch: () => {}, onCatalogueModeChange: () => {}, projects: [{ documents, project: { available: true, position: 0, repositoryId: "R_chopin", repositoryName: "chopin", repositoryOwner: "githubnext", repository: { id: "R_chopin", owner: "githubnext", name: "chopin", fullName: "githubnext/chopin", permissions: { pull: true, push: true, admin: false }, }, }, }], catalogueMode: "active", user: { avatarUrl: "", id: "user-one", login: "octocat" }, ...overrides, })); } function counts(markup: string): string[] { return [...markup.matchAll(/data-sidebar-decision-count="">]*>(\d+)<\/span>/g)] .map(match => match[1]!); } describe("unanswered decision labels", () => { it("follows the Decisions tab wording and leaves zero unlabelled", () => { expect(unansweredDecisionsLabel("Postgres writer lease", 4)) .toBe("Postgres writer lease, 4 unanswered decisions"); expect(unansweredDecisionsLabel("chopin", 1)).toBe("chopin, 1 unanswered decision"); expect(unansweredDecisionsLabel("Settled plan", 0)).toBe("Settled plan"); }); }); describe("sidebar decision counts", () => { let loaded: DocumentLoadState = { status: "ready", channels: [parent, child, quiet], nextCursor: "later-page", unansweredDecisions: 10, }; it("labels document, child, and project rows with their unanswered counts", () => { let markup = sidebar(loaded); expect(markup).toContain('aria-label="Postgres writer lease, 4 unanswered decisions"'); expect(markup).toContain('aria-label="Fencing tokens, 1 unanswered decision"'); expect(markup).toMatch( /]*aria-expanded="true"[^>]*aria-label="chopin, 10 unanswered decisions"[^>]*class="project-sidebar-project-disclosure/, ); expect(counts(markup)).toEqual(["10", "4", "1"]); }); it("shows the server's project total rather than the loaded rows' sum", () => { let markup = sidebar(loaded); expect(markup).toContain("chopin, 10 unanswered decisions"); expect(markup).not.toContain("chopin, 5 unanswered"); }); it("shows the project total before any of its rows load", () => { let markup = sidebar({ ...loaded, channels: [] }); expect(markup).toContain("chopin, 10 unanswered decisions"); expect(counts(markup)).toEqual(["10"]); }); it("hides counts from accessible names and renders the shared quiet Count", () => { let markup = sidebar(loaded); let slots = [...markup.matchAll(/]*)data-sidebar-decision-count="">(]*>)/g)]; expect(slots).toHaveLength(3); for (let [, attributes, count] of slots) { expect(attributes).toContain('aria-hidden="true"'); expect(attributes).toContain('class="project-sidebar-count"'); expect(count).toContain("bg-count-quiet"); expect(count).toContain("text-text-tertiary"); expect(count).not.toContain("bg-brand"); } }); it("places each count after the row action in one trailing slot", () => { let markup = sidebar(loaded); expect(markup).toMatch( /aria-label="New document in chopin"[^>]*>.*?<\/button>