Skip to content

Commit 8770c4b

Browse files
committed
test(unit): expand UI renderer tests for rich layout and fallback
Add tests for Unicode rich layout, ASCII fallback, explain and validation blocks, and updated expectations for new UI components.
1 parent 2021201 commit 8770c4b

File tree

1 file changed

+256
-34
lines changed

1 file changed

+256
-34
lines changed

tests/unit/ui.test.ts

Lines changed: 256 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
import { describe, expect, it } from "vitest";
22
import type { CandidateDiagnostics, ContextDiagnostics } from "../../src/diagnostics.js";
33
import type { DoctorResult } from "../../src/doctor.js";
4-
import { createTerminalUi, renderDoctorReport, renderErrorBlock, renderReviewScreen } from "../../src/ui.js";
4+
import {
5+
createTerminalUi,
6+
renderActionSummary,
7+
renderDoctorReport,
8+
renderErrorBlock,
9+
renderExplainBlock,
10+
renderReviewScreen,
11+
renderStateCard,
12+
renderValidationBlock
13+
} from "../../src/ui.js";
514

615
function baseContext(): ContextDiagnostics {
716
return {
@@ -62,51 +71,74 @@ function baseCandidate(overrides: Partial<CandidateDiagnostics> = {}): Candidate
6271

6372
describe("ui renderers", () => {
6473
it("renders the primary review screen as a stable rich-layout snapshot", () => {
65-
const ui = createTerminalUi({ isTTY: false, columns: 80 }, { forceRichLayout: true });
74+
const ui = createTerminalUi({ isTTY: false, columns: 80 }, {
75+
forceRichLayout: true,
76+
forceUnicode: true
77+
});
6678

6779
expect(renderReviewScreen(ui, baseContext(), baseCandidate(), {
6880
explain: true,
6981
alternativesCount: 1
7082
})).toBe([
71-
"== Review commit message ==",
83+
"╭──────────────────────────────────────────────────────────────────────────────╮",
84+
"│ Review commit │",
85+
"├──────────────────────────────────────────────────────────────────────────────┤",
86+
"│ [VALID] [REPAIRED] │",
87+
"│ │",
88+
"│ feat(cli): add baseline │",
89+
"│ │",
90+
"│ Refs ABC-123 │",
91+
"│ │",
92+
"│ expected feat · scope cli · ticket ABC-123 · source repaired │",
93+
"╰──────────────────────────────────────────────────────────────────────────────╯",
7294
"",
73-
"== Context ==",
74-
" Expected type : feat (diff inference)",
75-
" Scope : cli (changed files)",
76-
" Ticket : ABC-123 (branch inference)",
77-
"",
78-
"== Commit preview ==",
79-
" [VALID] [REPAIRED] [SCOPE cli] [TICKET ABC-123]",
80-
" Subject : feat(cli): add baseline",
81-
" Body :",
82-
" Refs ABC-123",
83-
"",
84-
"== Why this message ==",
85-
" Source : repaired",
86-
" Expected type : feat (diff inference)",
87-
" Selected scope : cli (changed files)",
88-
" Selected ticket : ABC-123 (branch inference)",
89-
" Validation : valid",
90-
" Ranking : 1111100 (valid, subject-fit, type-match, scope-match,",
91-
" ticket-footer)",
92-
" Alternatives : 1"
95+
"╭──────────────────────────────────────────────────────────────────────────────╮",
96+
"│ Why it won │",
97+
"├──────────────────────────────────────────────────────────────────────────────┤",
98+
"│ signals valid · type-match · scope-match · ticket-footer · subject-fit │",
99+
"│ score 1111100 · 1 alternative │",
100+
"│ type diff · scope files · ticket branch │",
101+
"╰──────────────────────────────────────────────────────────────────────────────╯"
93102
].join("\n"));
94103
});
95104

96105
it("renders a representative failure block as a stable snapshot", () => {
97-
const ui = createTerminalUi({ isTTY: false, columns: 80 }, { forceRichLayout: true });
106+
const ui = createTerminalUi({ isTTY: false, columns: 80 }, {
107+
forceRichLayout: true,
108+
forceUnicode: true
109+
});
98110

99111
expect(renderErrorBlock(
100112
ui,
101113
"Generated message failed validation",
102114
"Scope is required and must be one of: cli.",
103115
"Revise, edit, regenerate, or rerun with `--allow-invalid` if you want to override."
104116
)).toBe([
105-
"== Problem ==",
106-
" Problem : Generated message failed validation",
107-
" Why : Scope is required and must be one of: cli.",
108-
" Next step : Revise, edit, regenerate, or rerun with `--allow-invalid` if you",
109-
" want to override."
117+
"╭──────────────────────────────────────────────────────────────────────────────╮",
118+
"│ Generated message failed validation │",
119+
"├──────────────────────────────────────────────────────────────────────────────┤",
120+
"│ Scope is required and must be one of: cli. │",
121+
"│ │",
122+
"│ next Revise, edit, regenerate, or rerun with `--allow-invalid` if you want │",
123+
"│ to override. │",
124+
"╰──────────────────────────────────────────────────────────────────────────────╯"
125+
].join("\n"));
126+
});
127+
128+
it("supports an ASCII rich-layout fallback for card rendering", () => {
129+
const ui = createTerminalUi({ isTTY: false, columns: 72 }, {
130+
forceRichLayout: true,
131+
forceUnicode: false
132+
});
133+
134+
expect(renderActionSummary(ui, "Hooks installed", [
135+
"/repo/.git/hooks/prepare-commit-msg"
136+
])).toBe([
137+
"+----------------------------------------------------------------------+",
138+
"| Hooks installed |",
139+
"+----------------------------------------------------------------------+",
140+
"| - /repo/.git/hooks/prepare-commit-msg |",
141+
"+----------------------------------------------------------------------+"
110142
].join("\n"));
111143
});
112144

@@ -118,8 +150,159 @@ describe("ui renderers", () => {
118150
})).toBe("feat(cli): add baseline\n\nRefs ABC-123");
119151
});
120152

153+
it("renders compact plain explain and validation output without extra sections", () => {
154+
const ui = createTerminalUi({ isTTY: false, columns: 80 });
155+
const invalidCandidate = baseCandidate({
156+
message: "bad message",
157+
subject: "bad message",
158+
source: "model",
159+
final: {
160+
type: null,
161+
scope: {
162+
value: null,
163+
source: "none"
164+
},
165+
ticket: {
166+
value: null,
167+
source: "none"
168+
}
169+
},
170+
validation: {
171+
ok: false,
172+
errors: [
173+
"Not Conventional Commits format",
174+
"Message must reference a ticket."
175+
]
176+
},
177+
ranking: {
178+
valid: false,
179+
validPoints: 0,
180+
subjectWithinLimit: false,
181+
subjectWithinLimitPoints: 0,
182+
expectedTypeMatch: false,
183+
expectedTypePoints: 0,
184+
expectedScopeMatch: false,
185+
expectedScopePoints: 0,
186+
ticketFooterPresent: false,
187+
ticketFooterPoints: 0,
188+
genericDescriptionPenalty: true,
189+
genericDescriptionPoints: -10,
190+
total: -10
191+
}
192+
});
193+
194+
expect(renderReviewScreen(ui, baseContext(), invalidCandidate, {
195+
explain: true,
196+
validationNextStep: "Regenerate the message and retry."
197+
})).toBe([
198+
"bad message",
199+
"",
200+
"errors:",
201+
"- Not Conventional Commits format",
202+
"- Message must reference a ticket.",
203+
"next: Regenerate the message and retry."
204+
].join("\n"));
205+
206+
expect(renderValidationBlock(ui, ["Missing ticket footer"], {
207+
note: "score -10",
208+
nextStep: "Add a ticket footer."
209+
})).toBe([
210+
"errors:",
211+
"- Missing ticket footer",
212+
"score -10",
213+
"next: Add a ticket footer."
214+
].join("\n"));
215+
});
216+
217+
it("renders baseline rich explain output when no ranking signals or origins apply", () => {
218+
const ui = createTerminalUi({ isTTY: false, columns: 72 }, {
219+
forceRichLayout: true,
220+
forceUnicode: false
221+
});
222+
const minimalCandidate = baseCandidate({
223+
message: "chore: bump deps",
224+
subject: "chore: bump deps",
225+
source: "model",
226+
final: {
227+
type: "chore",
228+
scope: {
229+
value: null,
230+
source: "message"
231+
},
232+
ticket: {
233+
value: null,
234+
source: "none"
235+
}
236+
},
237+
ranking: {
238+
valid: false,
239+
validPoints: 0,
240+
subjectWithinLimit: false,
241+
subjectWithinLimitPoints: 0,
242+
expectedTypeMatch: false,
243+
expectedTypePoints: 0,
244+
expectedScopeMatch: false,
245+
expectedScopePoints: 0,
246+
ticketFooterPresent: false,
247+
ticketFooterPoints: 0,
248+
genericDescriptionPenalty: false,
249+
genericDescriptionPoints: 0,
250+
total: 0
251+
}
252+
});
253+
254+
expect(renderExplainBlock(ui, {
255+
expectedType: {
256+
value: null,
257+
source: "none"
258+
},
259+
scope: {
260+
suggested: null,
261+
effective: null,
262+
source: "none"
263+
},
264+
ticket: {
265+
value: null,
266+
source: "none"
267+
}
268+
}, minimalCandidate)).toBe([
269+
"+----------------------------------------------------------------------+",
270+
"| Why it won |",
271+
"+----------------------------------------------------------------------+",
272+
"| signals baseline fit |",
273+
"| score 0 |",
274+
"+----------------------------------------------------------------------+"
275+
].join("\n"));
276+
});
277+
278+
it("supports plain error output and tty-colored rich state cards", () => {
279+
const plainUi = createTerminalUi({ isTTY: false, columns: 80 });
280+
expect(renderErrorBlock(plainUi, "Hook install failed", "Existing hook is unmanaged")).toBe([
281+
"Problem: Hook install failed",
282+
"Why: Existing hook is unmanaged",
283+
"Next: Review the error details and retry."
284+
].join("\n"));
285+
286+
const ttyUi = createTerminalUi({ isTTY: true, columns: 68 }, {
287+
forceRichLayout: true,
288+
forceUnicode: false
289+
});
290+
const rendered = renderStateCard(ttyUi, {
291+
title: "Commit created",
292+
headline: "fix(cli): tighten output",
293+
meta: ["scope cli"],
294+
tone: "success"
295+
});
296+
297+
expect(rendered).toContain("\u001b[1m\u001b[32mCommit created\u001b[0m");
298+
expect(rendered).toContain("\u001b[1m\u001b[36mfix(cli): tighten output\u001b[0m");
299+
});
300+
121301
it("includes doctor sections and next steps in the report", () => {
122-
const ui = createTerminalUi({ isTTY: false, columns: 80 }, { forceRichLayout: true });
302+
const ui = createTerminalUi({ isTTY: false, columns: 80 }, {
303+
forceRichLayout: true,
304+
forceUnicode: false
305+
});
123306
const result: DoctorResult = {
124307
ok: false,
125308
exitCode: 3,
@@ -141,9 +324,48 @@ describe("ui renderers", () => {
141324
};
142325

143326
const report = renderDoctorReport(ui, result);
144-
expect(report).toContain("== Doctor ==");
145-
expect(report).toContain("== Environment ==");
146-
expect(report).toContain("== Ollama ==");
147-
expect(report).toContain("next: Run `ollama pull gpt-oss:120b-cloud` and retry.");
327+
expect(report).toContain("| Doctor");
328+
expect(report).toContain("| Environment");
329+
expect(report).toContain("| Ollama");
330+
expect(report).toContain("next Run `ollama pull gpt-oss:120b-cloud` and retry.");
331+
});
332+
333+
it("renders plain doctor sections and default width fallback cleanly", () => {
334+
const ui = createTerminalUi(undefined);
335+
const result: DoctorResult = {
336+
ok: false,
337+
exitCode: 3,
338+
checks: [
339+
{
340+
section: "Environment",
341+
name: "Node.js",
342+
ok: true,
343+
detail: "Detected 20.12.0"
344+
},
345+
{
346+
section: "Ollama",
347+
name: "Configured model",
348+
ok: false,
349+
detail: "Model not found",
350+
nextStep: "Run `ollama pull gpt-oss:120b-cloud` and retry."
351+
}
352+
]
353+
};
354+
355+
expect(ui.width).toBe(88);
356+
expect(ui.richLayout).toBe(false);
357+
expect(ui.unicode).toBe(false);
358+
expect(renderDoctorReport(ui, result)).toBe([
359+
"Doctor",
360+
"1 check need attention",
361+
"review the failing section below",
362+
"",
363+
"Environment:",
364+
"- OK Node.js: Detected 20.12.0",
365+
"",
366+
"Ollama:",
367+
"- FAIL Configured model: Model not found",
368+
" next: Run `ollama pull gpt-oss:120b-cloud` and retry."
369+
].join("\n"));
148370
});
149371
});

0 commit comments

Comments
 (0)