Skip to content

Commit 66d0076

Browse files
committed
test: make TUI render assertions fail on an empty render
Assertions that indexed or folded over a render result treated a missing row as an empty string or an empty array, so they passed when the component rendered nothing. They now assert a row exists first. Also match braille loader frames when checking that a terminal workflow stopped animating.
1 parent ebfb674 commit 66d0076

5 files changed

Lines changed: 40 additions & 31 deletions

File tree

apps/pythinker-code/test/tui/activity-pane.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ describe('updateActivityPane terminal progress', () => {
220220
expect(state.activityContainer.children).toHaveLength(0);
221221
expect(vi.getTimerCount()).toBe(timersBeforeMissionControl);
222222
const output = strip(missionControl.render(100).join('\n'));
223-
expect(output).toMatch(/[] Orchestrating/);
223+
expect(output).toMatch(/[] Orchestrating/u);
224224
expect(output).not.toContain(formatThinkingSpinnerLabel());
225225

226226
state.activitySpinner?.instance.stop();
@@ -289,6 +289,7 @@ describe('updateActivityPane terminal progress', () => {
289289
const output = strip(missionControl.render(100).join('\n'));
290290
expect(output).toContain('✓ Completed');
291291
expect(output).not.toMatch(/[] Orchestrating/);
292+
for (const frame of BRAILLE_SPINNER_FRAMES) expect(output).not.toContain(frame);
292293

293294
state.activitySpinner?.instance.stop();
294295
driver.sessionEventHandler.clearDynamicWorkflowMissionControls();
@@ -370,7 +371,7 @@ describe('updateActivityPane terminal progress', () => {
370371
driver.updateActivityPane();
371372
const missionControl = startDynamicWorkflow(driver, state);
372373
expect(strip(missionControl.render(100).join('\n'))).toMatch(
373-
/[] Orchestrating/,
374+
/[] Orchestrating/u,
374375
);
375376

376377
cleanup(driver);
@@ -379,6 +380,7 @@ describe('updateActivityPane terminal progress', () => {
379380
const output = strip(missionControl.render(100).join('\n'));
380381
expect(output).toContain('– Cancelled');
381382
expect(output).not.toMatch(/[] Orchestrating/);
383+
for (const frame of BRAILLE_SPINNER_FRAMES) expect(output).not.toContain(frame);
382384
state.activitySpinner?.instance.stop();
383385
} finally {
384386
vi.useRealTimers();

apps/pythinker-code/test/tui/components/messages/thinking.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,15 +90,18 @@ describe('ThinkingComponent', () => {
9090

9191
vi.advanceTimersByTime(BRAILLE_SPINNER_INTERVAL_MS * (BRAILLE_SPINNER_FRAMES.length - 1));
9292
const fullCycleHeader = component.render(80)[1];
93-
expect(strip(fullCycleHeader ?? '')).toBe(strip(firstHeader ?? ''));
93+
expect(fullCycleHeader).toBeDefined();
94+
expect(firstHeader).toBeDefined();
95+
expect(strip(fullCycleHeader as string)).toBe(strip(firstHeader as string));
9496

9597
const shimmerHeaders = [firstHeader, fullCycleHeader];
9698
for (let sample = 0; sample < 3; sample++) {
9799
vi.advanceTimersByTime(BRAILLE_SPINNER_INTERVAL_MS * BRAILLE_SPINNER_FRAMES.length);
98100
shimmerHeaders.push(component.render(80)[1]);
99101
}
100-
expect(shimmerHeaders.map((header) => strip(header ?? ''))).toEqual(
101-
shimmerHeaders.map(() => strip(firstHeader ?? '')),
102+
for (const header of shimmerHeaders) expect(header).toBeDefined();
103+
expect(shimmerHeaders.map((header) => strip(header as string))).toEqual(
104+
shimmerHeaders.map(() => strip(firstHeader as string)),
102105
);
103106
expect(new Set(shimmerHeaders).size).toBeGreaterThan(1);
104107

apps/pythinker-code/test/tui/components/messages/tool-call.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ describe('ToolCallComponent', () => {
6060
},
6161
undefined,
6262
);
63-
6463
try {
6564
const pending = component.render(40);
6665
const pendingBody = pending.slice(1);
@@ -96,15 +95,16 @@ describe('ToolCallComponent', () => {
9695
},
9796
undefined,
9897
);
99-
10098
try {
10199
const backgrounds = [
102100
'\u001B[48;2;29;33;41m',
103101
'\u001B[48;2;20;23;27m',
104102
'\u001B[48;2;41;29;29m',
105103
];
104+
const lines = component.render(40);
105+
expect(lines.length).toBeGreaterThan(0);
106106
expect(
107-
component.render(40).every((line) => backgrounds.every((code) => !line.includes(code))),
107+
lines.every((line) => backgrounds.every((code) => !line.includes(code))),
108108
).toBe(true);
109109
} finally {
110110
chalk.level = previousLevel;

apps/pythinker-code/test/tui/components/panels/footer-bg-agents.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,16 @@ describe('FooterComponent — background task / agent badges', () => {
5353
const footer = new FooterComponent(baseState());
5454
footer.setBackgroundCounts({ bashTasks: 1, agentTasks: 0 });
5555
const out = backgroundExtras(1, 0);
56-
expect(out).toMatch(/\[1 task running\]/);
57-
expect(out).not.toMatch(/agents? running/);
56+
expect(out).toMatch(/\[1 task running\]/u);
57+
expect(out).not.toMatch(/agents? running/u);
5858
});
5959

6060
it('renders the agent badge alone when only agent tasks are running', () => {
6161
const footer = new FooterComponent(baseState());
6262
footer.setBackgroundCounts({ bashTasks: 0, agentTasks: 1 });
6363
const out = backgroundExtras(0, 1);
64-
expect(out).toMatch(/\[1 agent running\]/);
65-
expect(out).not.toMatch(/tasks? running/);
64+
expect(out).toMatch(/\[1 agent running\]/u);
65+
expect(out).not.toMatch(/tasks? running/u);
6666
});
6767

6868
it('renders both badges side by side when both are non-zero', () => {

apps/pythinker-code/test/tui/components/status-bar.test.ts

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ function stripAnsi(text: string): string {
1313
return text.replaceAll(/\u001B\[[0-9;]*m/gu, '');
1414
}
1515

16+
function renderRow(component: StatusBarComponent, width: number): string {
17+
const rows = component.render(width);
18+
expect(rows.length).toBeGreaterThan(0);
19+
return rows[0] as string;
20+
}
21+
1622
function status(overrides: Partial<StatusBarStatus> = {}): StatusBarStatus {
1723
return {
1824
model: 'Model Alpha',
@@ -40,14 +46,14 @@ describe('StatusBarComponent', () => {
4046
const lines = component.render(80);
4147

4248
expect(lines).toHaveLength(1);
43-
expect(stripAnsi(lines[0] ?? '')).toContain('Model Alpha · high');
49+
expect(stripAnsi(lines[0] as string)).toContain('Model Alpha · high');
4450
});
4551

4652
it('omits the effort suffix when thinking is off', () => {
4753
const component = new StatusBarComponent();
4854
component.update(status({ thinkingLevel: 'off' }));
4955

50-
const line = stripAnsi(component.render(80)[0] ?? '');
56+
const line = stripAnsi(renderRow(component, 80));
5157

5258
expect(line).toContain('Model Alpha');
5359
expect(line).not.toContain('· off');
@@ -58,17 +64,15 @@ describe('StatusBarComponent', () => {
5864
component.update(status({
5965
statusLine: { ...DEFAULT_STATUS_LINE_CONFIG, showModel: false },
6066
}));
61-
62-
expect(stripAnsi(component.render(80)[0] ?? '')).not.toContain('Model Alpha');
67+
expect(stripAnsi(renderRow(component, 80))).not.toContain('Model Alpha');
6368
});
6469

6570
it('hides the modes chip when showModes is false', () => {
6671
const component = new StatusBarComponent();
6772
component.update(status({
6873
statusLine: { ...DEFAULT_STATUS_LINE_CONFIG, showModes: false },
6974
}));
70-
71-
const line = stripAnsi(component.render(80)[0] ?? '');
75+
const line = stripAnsi(renderRow(component, 80));
7276

7377
expect(line).not.toContain('plan');
7478
expect(line).not.toContain('auto');
@@ -81,7 +85,7 @@ describe('StatusBarComponent', () => {
8185
statusLine: { ...DEFAULT_STATUS_LINE_CONFIG, showEffort: false },
8286
}));
8387

84-
const line = stripAnsi(component.render(80)[0] ?? '');
88+
const line = stripAnsi(renderRow(component, 80));
8589

8690
expect(line).toContain('Model Alpha');
8791
expect(line).not.toContain('· high');
@@ -97,7 +101,7 @@ describe('StatusBarComponent', () => {
97101
const component = new StatusBarComponent();
98102
component.update(status({ permissionMode: 'yolo' }));
99103

100-
expect(component.render(80)[0] ?? '').toContain(chalk.hex(darkColors.error)('yolo'));
104+
expect(renderRow(component, 80)).toContain(chalk.hex(darkColors.error)('yolo'));
101105
} finally {
102106
chalk.level = previousLevel;
103107
currentTheme.setPalette(previousPalette);
@@ -108,10 +112,10 @@ describe('StatusBarComponent', () => {
108112
const component = new StatusBarComponent();
109113
component.update(status());
110114

111-
const wide = stripAnsi(component.render(60)[0] ?? '');
112-
const withoutGap = stripAnsi(component.render(53)[0] ?? '');
113-
const withoutModes = stripAnsi(component.render(45)[0] ?? '');
114-
const modelOnly = stripAnsi(component.render(25)[0] ?? '');
115+
const wide = stripAnsi(renderRow(component, 60));
116+
const withoutGap = stripAnsi(renderRow(component, 53));
117+
const withoutModes = stripAnsi(renderRow(component, 45));
118+
const modelOnly = stripAnsi(renderRow(component, 25));
115119

116120
expect(wide).toContain('─');
117121
expect(withoutGap).not.toContain('─');
@@ -141,7 +145,7 @@ describe('StatusBarComponent', () => {
141145
component.update(status({ fastMode: true }));
142146

143147
try {
144-
expect(stripAnsi(component.render(80)[0] ?? '')).toContain('↯ fast');
148+
expect(stripAnsi(renderRow(component, 80))).toContain('↯ fast');
145149
} finally {
146150
chalk.level = previousLevel;
147151
}
@@ -155,7 +159,7 @@ describe('StatusBarComponent', () => {
155159
tokenSpeedEstimated: true,
156160
}));
157161

158-
const modelChip = stripAnsi(component.render(120)[0] ?? '').split(' ')[0]?.trim();
162+
const modelChip = stripAnsi(renderRow(component, 120)).split(' ')[0]?.trim();
159163

160164
expect(modelChip).toBe('Model Alpha · high · ↯ fast · ~75.7 t/s');
161165
});
@@ -167,7 +171,7 @@ describe('StatusBarComponent', () => {
167171
statusLine: { ...DEFAULT_STATUS_LINE_CONFIG, showTokenSpeed: false },
168172
}));
169173

170-
const modelChip = stripAnsi(component.render(120)[0] ?? '').split(' ')[0]?.trim();
174+
const modelChip = stripAnsi(renderRow(component, 120)).split(' ')[0]?.trim();
171175

172176
expect(modelChip).toBe('Model Alpha · high');
173177
});
@@ -176,7 +180,7 @@ describe('StatusBarComponent', () => {
176180
const component = new StatusBarComponent();
177181
component.update(status({ fastMode: true, tokenSpeed: null }));
178182

179-
const modelChip = stripAnsi(component.render(120)[0] ?? '').split(' ')[0]?.trim();
183+
const modelChip = stripAnsi(renderRow(component, 120)).split(' ')[0]?.trim();
180184

181185
expect(modelChip).toBe('Model Alpha · high · ↯ fast');
182186
});
@@ -185,7 +189,7 @@ describe('StatusBarComponent', () => {
185189
const component = new StatusBarComponent();
186190
component.update(status({ extras: ['6% · 55.6k/1M', 'main ± [PR#1]'] }));
187191

188-
const line = stripAnsi(component.render(160)[0] ?? '');
192+
const line = stripAnsi(renderRow(component, 160));
189193

190194
expect(line.indexOf('workflow')).toBeLessThan(line.indexOf('6% · 55.6k/1M'));
191195
expect(line.indexOf('6% · 55.6k/1M')).toBeLessThan(line.indexOf('main ± [PR#1]'));
@@ -196,7 +200,7 @@ describe('StatusBarComponent', () => {
196200
const component = new StatusBarComponent();
197201
component.update(status({ extras: ['first', 'second'] }));
198202

199-
const line = stripAnsi(component.render(62)[0] ?? '');
203+
const line = stripAnsi(renderRow(component, 62));
200204

201205
expect(line).toContain('Model Alpha');
202206
expect(line).toContain('first');
@@ -218,6 +222,6 @@ describe('StatusBarComponent', () => {
218222
const component = new StatusBarComponent();
219223
component.update(status({ cwd, homeDir }));
220224

221-
expect(stripAnsi(component.render(240)[0] ?? '')).toContain(expected);
225+
expect(stripAnsi(renderRow(component, 240))).toContain(expected);
222226
});
223227
});

0 commit comments

Comments
 (0)