chore: make Windows paths better

This commit is contained in:
Jack Wotherspoon
2026-02-26 00:30:46 -05:00
committed by Keith Guerin
parent 450cb19ee9
commit 8d112f55b3
2 changed files with 544 additions and 382 deletions
+17 -1
View File
@@ -17,6 +17,7 @@ import { vi } from 'vitest';
import stripAnsi from 'strip-ansi'; import stripAnsi from 'strip-ansi';
import { act, useState } from 'react'; import { act, useState } from 'react';
import os from 'node:os'; import os from 'node:os';
import path from 'node:path';
import { LoadedSettings } from '../config/settings.js'; import { LoadedSettings } from '../config/settings.js';
import { KeypressProvider } from '../ui/contexts/KeypressContext.js'; import { KeypressProvider } from '../ui/contexts/KeypressContext.js';
import { SettingsContext } from '../ui/contexts/SettingsContext.js'; import { SettingsContext } from '../ui/contexts/SettingsContext.js';
@@ -502,7 +503,22 @@ const configProxy = new Proxy({} as Config, {
get(_target, prop) { get(_target, prop) {
if (prop === 'getTargetDir') { if (prop === 'getTargetDir') {
return () => return () =>
'/Users/test/project/foo/bar/and/some/more/directories/to/make/it/long'; path.join(
path.parse(process.cwd()).root,
'Users',
'test',
'project',
'foo',
'bar',
'and',
'some',
'more',
'directories',
'to',
'make',
'it',
'long',
);
} }
if (prop === 'getUseBackgroundColor') { if (prop === 'getUseBackgroundColor') {
return () => true; return () => true;
+404 -258
View File
@@ -4,12 +4,19 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { describe, it, expect, vi, afterEach } from 'vitest'; import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
import { renderWithProviders } from '../../test-utils/render.js'; import { renderWithProviders } from '../../test-utils/render.js';
import { Footer } from './Footer.js'; import { Footer } from './Footer.js';
import { createMockSettings } from '../../test-utils/settings.js'; import { createMockSettings } from '../../test-utils/settings.js';
import path from 'node:path';
const customMockSessionStats = { // Normalize paths to POSIX slashes for stable cross-platform snapshots.
const normalizeFrame = (frame: string | undefined) => {
if (!frame) return frame;
return frame.replace(/\\/g, '/').replace(/[A-Za-z]:\/(Users)/g, '/$1');
};
const mockSessionStats = {
sessionId: 'test-session-id', sessionId: 'test-session-id',
sessionStartTime: new Date(), sessionStartTime: new Date(),
promptCount: 0, promptCount: 0,
@@ -50,6 +57,7 @@ const customMockSessionStats = {
thoughts: 0, thoughts: 0,
tool: 0, tool: 0,
}, },
roles: {},
}, },
}, },
}, },
@@ -64,65 +72,97 @@ const defaultProps = {
}; };
describe('<Footer />', () => { describe('<Footer />', () => {
it('renders the component', () => { it('renders the component', async () => {
const { lastFrame } = renderWithProviders(<Footer />, { const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
width: 120, <Footer />,
uiState: { sessionStats: customMockSessionStats }, {
});
expect(lastFrame()).toBeDefined();
});
describe('path display', () => {
it('should display a shortened path on a narrow terminal', () => {
const { lastFrame } = renderWithProviders(<Footer />, {
width: 40,
uiState: {
sessionStats: customMockSessionStats,
},
});
const output = lastFrame();
expect(output).toBeDefined();
expect(output!.length).toBeLessThanOrEqual(120); // 40 width * 3? it depends.
});
it('should use wide layout at 80 columns', () => {
const { lastFrame } = renderWithProviders(<Footer />, {
width: 80,
uiState: {
sessionStats: customMockSessionStats,
},
});
const output = lastFrame();
expect(output).toBeDefined();
});
});
it('displays the branch name when provided', () => {
const { lastFrame } = renderWithProviders(<Footer />, {
width: 120, width: 120,
uiState: { uiState: {
branchName: defaultProps.branchName, branchName: defaultProps.branchName,
sessionStats: customMockSessionStats, sessionStats: mockSessionStats,
}, },
}); },
expect(lastFrame()).toContain(defaultProps.branchName); );
await waitUntilReady();
expect(lastFrame()).toBeDefined();
unmount();
}); });
it('does not display the branch name when not provided', () => { describe('path display', () => {
const { lastFrame } = renderWithProviders(<Footer />, { it('should display a shortened path on a narrow terminal', async () => {
width: 120, const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
uiState: { branchName: undefined, sessionStats: customMockSessionStats }, <Footer />,
}); {
expect(lastFrame()).not.toContain('('); width: 79,
uiState: { sessionStats: mockSessionStats },
},
);
await waitUntilReady();
const output = lastFrame();
expect(output).toBeDefined();
// Should contain some part of the path, likely shortened
expect(output).toContain(
path.join('directories', 'to', 'make', 'it', 'long'),
);
unmount();
}); });
it('displays the model name and context percentage', () => { it('should use wide layout at 80 columns', async () => {
const { lastFrame } = renderWithProviders(<Footer />, { const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<Footer />,
{
width: 80,
uiState: { sessionStats: mockSessionStats },
},
);
await waitUntilReady();
const output = lastFrame();
expect(output).toBeDefined();
expect(output).toContain(
path.join('directories', 'to', 'make', 'it', 'long'),
);
unmount();
});
});
it('displays the branch name when provided', async () => {
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<Footer />,
{
width: 120, width: 120,
uiState: { uiState: {
currentModel: 'gemini-pro', branchName: defaultProps.branchName,
sessionStats: mockSessionStats,
},
},
);
await waitUntilReady();
expect(lastFrame()).toContain(defaultProps.branchName);
unmount();
});
it('does not display the branch name when not provided', async () => {
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<Footer />,
{
width: 120,
uiState: { branchName: undefined, sessionStats: mockSessionStats },
},
);
await waitUntilReady();
expect(lastFrame()).not.toContain('Branch');
unmount();
});
it('displays the model name and context percentage', async () => {
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<Footer />,
{
width: 120,
uiState: {
currentModel: defaultProps.model,
sessionStats: { sessionStats: {
...customMockSessionStats, ...mockSessionStats,
lastPromptTokenCount: 1000, lastPromptTokenCount: 1000,
}, },
}, },
@@ -133,71 +173,98 @@ describe('<Footer />', () => {
}, },
}, },
}), }),
}); },
expect(lastFrame()).toContain('gemini-pro'); );
await waitUntilReady();
expect(lastFrame()).toContain(defaultProps.model);
expect(lastFrame()).toMatch(/\d+% left/);
unmount();
}); });
it('displays the usage indicator when usage is low', () => { it('displays the usage indicator when usage is low', async () => {
const { lastFrame } = renderWithProviders(<Footer />, { const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<Footer />,
{
width: 120, width: 120,
uiState: { uiState: {
sessionStats: mockSessionStats,
quota: { quota: {
stats: { remaining: 15, limit: 100 }, userTier: undefined,
userTier: 'free', stats: {
remaining: 15,
limit: 100,
resetTime: undefined,
},
proQuotaRequest: null, proQuotaRequest: null,
validationRequest: null, validationRequest: null,
}, },
sessionStats: customMockSessionStats,
}, },
}); },
);
await waitUntilReady();
expect(lastFrame()).toContain('15%'); expect(lastFrame()).toContain('15%');
expect(lastFrame()).toMatchSnapshot(); expect(normalizeFrame(lastFrame())).toMatchSnapshot();
unmount();
}); });
it('hides the usage indicator when usage is not near limit', () => { it('hides the usage indicator when usage is not near limit', async () => {
const { lastFrame } = renderWithProviders(<Footer />, { const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<Footer />,
{
width: 120, width: 120,
uiState: { uiState: {
sessionStats: mockSessionStats,
quota: { quota: {
stats: { remaining: 85, limit: 100 }, userTier: undefined,
userTier: 'free', stats: {
remaining: 85,
limit: 100,
resetTime: undefined,
},
proQuotaRequest: null, proQuotaRequest: null,
validationRequest: null, validationRequest: null,
}, },
sessionStats: customMockSessionStats,
}, },
}); },
);
await waitUntilReady();
expect(lastFrame()).not.toContain('Usage remaining'); expect(lastFrame()).not.toContain('Usage remaining');
expect(lastFrame()).toMatchSnapshot(); expect(normalizeFrame(lastFrame())).toMatchSnapshot();
unmount();
}); });
it('displays "Limit reached" message when remaining is 0', () => { it('displays "Limit reached" message when remaining is 0', async () => {
const { lastFrame } = renderWithProviders(<Footer />, { const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<Footer />,
{
width: 120, width: 120,
uiState: { uiState: {
sessionStats: mockSessionStats,
quota: { quota: {
stats: { remaining: 0, limit: 100 }, userTier: undefined,
userTier: 'free', stats: {
remaining: 0,
limit: 100,
resetTime: undefined,
},
proQuotaRequest: null, proQuotaRequest: null,
validationRequest: null, validationRequest: null,
}, },
sessionStats: customMockSessionStats,
}, },
}); },
);
await waitUntilReady();
expect(lastFrame()?.toLowerCase()).toContain('limit reached'); expect(lastFrame()?.toLowerCase()).toContain('limit reached');
expect(lastFrame()).toMatchSnapshot(); expect(normalizeFrame(lastFrame())).toMatchSnapshot();
unmount();
}); });
it('displays the model name and abbreviated context percentage', () => { it('displays the model name and abbreviated context percentage', async () => {
const { lastFrame } = renderWithProviders(<Footer />, { const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
width: 80, <Footer />,
uiState: { {
currentModel: 'gemini-pro', width: 99,
sessionStats: { uiState: { sessionStats: mockSessionStats },
...customMockSessionStats,
lastPromptTokenCount: 500,
},
},
settings: createMockSettings({ settings: createMockSettings({
ui: { ui: {
footer: { footer: {
@@ -205,163 +272,203 @@ describe('<Footer />', () => {
}, },
}, },
}), }),
}); },
expect(lastFrame()).toContain('gemini-pro'); );
await waitUntilReady();
expect(lastFrame()).toContain(defaultProps.model);
expect(lastFrame()).toMatch(/\d+%/);
unmount();
}); });
describe('sandbox and trust info', () => { describe('sandbox and trust info', () => {
afterEach(() => { it('should display untrusted when isTrustedFolder is false', async () => {
vi.unstubAllEnvs(); const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
}); <Footer />,
{
it('should display untrusted when isTrustedFolder is false', () => {
const { lastFrame } = renderWithProviders(<Footer />, {
width: 120, width: 120,
uiState: { uiState: { isTrustedFolder: false, sessionStats: mockSessionStats },
isTrustedFolder: false,
sessionStats: customMockSessionStats,
}, },
}); );
await waitUntilReady();
expect(lastFrame()).toContain('untrusted'); expect(lastFrame()).toContain('untrusted');
unmount();
}); });
it('should display custom sandbox info when SANDBOX env is set', () => { it('should display custom sandbox info when SANDBOX env is set', async () => {
vi.stubEnv('SANDBOX', 'gemini-test-sandbox'); vi.stubEnv('SANDBOX', 'gemini-cli-test-sandbox');
const { lastFrame } = renderWithProviders(<Footer />, { const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<Footer />,
{
width: 120, width: 120,
uiState: { uiState: {
isTrustedFolder: true, isTrustedFolder: undefined,
sessionStats: customMockSessionStats, sessionStats: mockSessionStats,
}, },
}); },
expect(lastFrame()).toContain('test-sandbox'); );
await waitUntilReady();
expect(lastFrame()).toContain('test');
vi.unstubAllEnvs();
unmount();
}); });
it('should display macOS Seatbelt info when SANDBOX is sandbox-exec', () => { it('should display macOS Seatbelt info when SANDBOX is sandbox-exec', async () => {
vi.stubEnv('SANDBOX', 'sandbox-exec'); vi.stubEnv('SANDBOX', 'sandbox-exec');
vi.stubEnv('SEATBELT_PROFILE', 'test-profile'); vi.stubEnv('SEATBELT_PROFILE', 'test-profile');
const { lastFrame } = renderWithProviders(<Footer />, { const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<Footer />,
{
width: 120, width: 120,
uiState: { uiState: { isTrustedFolder: true, sessionStats: mockSessionStats },
isTrustedFolder: true,
sessionStats: customMockSessionStats,
}, },
}); );
expect(lastFrame()).toContain('macOS Seatbelt'); await waitUntilReady();
expect(lastFrame()).toContain('test-profile'); expect(lastFrame()).toMatch(/macOS Seatbelt.*\(test-profile\)/s);
vi.unstubAllEnvs();
unmount();
}); });
it('should display "no sandbox" when SANDBOX is not set and folder is trusted', () => { it('should display "no sandbox" when SANDBOX is not set and folder is trusted', async () => {
// Clear any SANDBOX env var that might be set.
vi.stubEnv('SANDBOX', ''); vi.stubEnv('SANDBOX', '');
const { lastFrame } = renderWithProviders(<Footer />, { const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<Footer />,
{
width: 120, width: 120,
uiState: { uiState: { isTrustedFolder: true, sessionStats: mockSessionStats },
isTrustedFolder: true,
sessionStats: customMockSessionStats,
}, },
}); );
await waitUntilReady();
expect(lastFrame()).toContain('no sandbox'); expect(lastFrame()).toContain('no sandbox');
vi.unstubAllEnvs();
unmount();
}); });
it('should prioritize untrusted message over sandbox info', () => { it('should prioritize untrusted message over sandbox info', async () => {
vi.stubEnv('SANDBOX', 'gemini-test-sandbox'); vi.stubEnv('SANDBOX', 'gemini-cli-test-sandbox');
const { lastFrame } = renderWithProviders(<Footer />, { const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<Footer />,
{
width: 120, width: 120,
uiState: { uiState: { isTrustedFolder: false, sessionStats: mockSessionStats },
isTrustedFolder: false,
sessionStats: customMockSessionStats,
}, },
}); );
await waitUntilReady();
expect(lastFrame()).toContain('untrusted'); expect(lastFrame()).toContain('untrusted');
expect(lastFrame()).not.toContain('test-sandbox'); expect(lastFrame()).not.toMatch(/test-sandbox/s);
vi.unstubAllEnvs();
unmount();
}); });
}); });
describe('footer configuration filtering (golden snapshots)', () => { describe('footer configuration filtering (golden snapshots)', () => {
it('renders complete footer with all sections visible (baseline)', () => { beforeEach(() => {
const { lastFrame } = renderWithProviders(<Footer />, { vi.stubEnv('SANDBOX', '');
vi.stubEnv('SEATBELT_PROFILE', '');
});
afterEach(() => {
vi.unstubAllEnvs();
});
it('renders complete footer with all sections visible (baseline)', async () => {
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<Footer />,
{
width: 120, width: 120,
uiState: { uiState: { sessionStats: mockSessionStats },
currentModel: 'gemini-pro',
sessionStats: {
...customMockSessionStats,
lastPromptTokenCount: 0,
},
},
settings: createMockSettings({ settings: createMockSettings({
ui: { ui: {
footer: { footer: {
items: [
'cwd',
'sandbox-status',
'model-name',
'context-remaining',
],
hideContextPercentage: false, hideContextPercentage: false,
}, },
}, },
}), }),
}); },
expect(lastFrame()).toMatchSnapshot('complete-footer-wide'); );
await waitUntilReady();
expect(normalizeFrame(lastFrame())).toMatchSnapshot(
'complete-footer-wide',
);
unmount();
}); });
it('renders footer with all optional sections hidden (minimal footer)', () => { it('renders footer with all optional sections hidden (minimal footer)', async () => {
const { lastFrame } = renderWithProviders(<Footer />, { const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<Footer />,
{
width: 120, width: 120,
uiState: { sessionStats: customMockSessionStats }, uiState: { sessionStats: mockSessionStats },
settings: createMockSettings({ settings: createMockSettings({
ui: { ui: {
footer: { footer: {
items: [], hideCWD: true,
hideSandboxStatus: true,
hideModelInfo: true,
}, },
}, },
}), }),
}); },
expect(lastFrame()).toMatchSnapshot('footer-minimal'); );
await waitUntilReady();
expect(normalizeFrame(lastFrame({ allowEmpty: true }))).toMatchSnapshot(
'footer-minimal',
);
unmount();
}); });
it('renders footer with only model info hidden (partial filtering)', () => { it('renders footer with only model info hidden (partial filtering)', async () => {
const { lastFrame } = renderWithProviders(<Footer />, { const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<Footer />,
{
width: 120, width: 120,
uiState: { uiState: { sessionStats: mockSessionStats },
sessionStats: customMockSessionStats,
},
settings: createMockSettings({ settings: createMockSettings({
ui: { ui: {
footer: { footer: {
items: ['cwd', 'sandbox-status'], hideCWD: false,
hideSandboxStatus: false,
hideModelInfo: true,
}, },
}, },
}), }),
}); },
expect(lastFrame()).toMatchSnapshot('footer-no-model'); );
await waitUntilReady();
expect(normalizeFrame(lastFrame())).toMatchSnapshot('footer-no-model');
unmount();
}); });
it('renders footer with CWD and model info hidden to test alignment (only sandbox visible)', () => { it('renders footer with CWD and model info hidden to test alignment (only sandbox visible)', async () => {
const { lastFrame } = renderWithProviders(<Footer />, { const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<Footer />,
{
width: 120, width: 120,
uiState: { sessionStats: customMockSessionStats }, uiState: { sessionStats: mockSessionStats },
settings: createMockSettings({ settings: createMockSettings({
ui: { ui: {
footer: { footer: {
items: ['sandbox-status'], hideCWD: true,
hideSandboxStatus: false,
hideModelInfo: true,
}, },
}, },
}), }),
}); },
expect(lastFrame()).toMatchSnapshot('footer-only-sandbox'); );
await waitUntilReady();
expect(normalizeFrame(lastFrame())).toMatchSnapshot(
'footer-only-sandbox',
);
unmount();
}); });
it('hides the context percentage when hideContextPercentage is true', () => { it('hides the context percentage when hideContextPercentage is true', async () => {
const { lastFrame } = renderWithProviders(<Footer />, { const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<Footer />,
{
width: 120, width: 120,
uiState: { uiState: { sessionStats: mockSessionStats },
currentModel: 'gemini-pro',
sessionStats: {
...customMockSessionStats,
lastPromptTokenCount: 1000,
},
},
settings: createMockSettings({ settings: createMockSettings({
ui: { ui: {
footer: { footer: {
@@ -369,20 +476,19 @@ describe('<Footer />', () => {
}, },
}, },
}), }),
},
);
await waitUntilReady();
expect(lastFrame()).toContain(defaultProps.model);
expect(lastFrame()).not.toMatch(/\d+% left/);
unmount();
}); });
expect(lastFrame()).not.toContain('left'); it('shows the context percentage when hideContextPercentage is false', async () => {
}); const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<Footer />,
it('shows the context percentage when hideContextPercentage is false', () => { {
const { lastFrame } = renderWithProviders(<Footer />, {
width: 120, width: 120,
uiState: { uiState: { sessionStats: mockSessionStats },
currentModel: 'gemini-pro',
sessionStats: {
...customMockSessionStats,
lastPromptTokenCount: 1000,
},
},
settings: createMockSettings({ settings: createMockSettings({
ui: { ui: {
footer: { footer: {
@@ -390,71 +496,45 @@ describe('<Footer />', () => {
}, },
}, },
}), }),
});
expect(lastFrame()).toContain('left');
});
it('renders complete footer in narrow terminal (baseline narrow)', () => {
const { lastFrame } = renderWithProviders(<Footer />, {
width: 80,
uiState: {
currentModel: 'gemini-pro',
sessionStats: {
...customMockSessionStats,
lastPromptTokenCount: 0,
},
}, },
);
await waitUntilReady();
expect(lastFrame()).toContain(defaultProps.model);
expect(lastFrame()).toMatch(/\d+% left/);
unmount();
});
it('renders complete footer in narrow terminal (baseline narrow)', async () => {
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<Footer />,
{
width: 79,
uiState: { sessionStats: mockSessionStats },
settings: createMockSettings({ settings: createMockSettings({
ui: { ui: {
footer: { footer: {
items: [
'cwd',
'sandbox-status',
'model-name',
'context-remaining',
],
hideContextPercentage: false, hideContextPercentage: false,
}, },
}, },
}), }),
});
expect(lastFrame()).toMatchSnapshot('complete-footer-narrow');
});
});
describe('fallback mode display', () => {
it('should display Flash model when in fallback mode, not the configured Pro model', () => {
const { lastFrame } = renderWithProviders(<Footer />, {
width: 120,
uiState: {
currentModel: 'gemini-1.5-flash',
sessionStats: customMockSessionStats,
}, },
}); );
expect(lastFrame()).toContain('gemini-1.5-flash'); await waitUntilReady();
}); expect(normalizeFrame(lastFrame())).toMatchSnapshot(
'complete-footer-narrow',
it('should display Pro model when NOT in fallback mode', () => { );
const { lastFrame } = renderWithProviders(<Footer />, { unmount();
width: 120,
uiState: {
currentModel: 'gemini-pro',
sessionStats: customMockSessionStats,
},
});
expect(lastFrame()).toContain('gemini-pro');
}); });
}); });
describe('Footer Token Formatting', () => { describe('Footer Token Formatting', () => {
const renderWithTokens = (tokens: number) => const renderWithTokens = async (tokens: number) => {
renderWithProviders(<Footer />, { const result = renderWithProviders(<Footer />, {
width: 120, width: 120,
uiState: { uiState: {
sessionStats: { sessionStats: {
...customMockSessionStats, ...mockSessionStats,
metrics: { metrics: {
...customMockSessionStats.metrics, ...mockSessionStats.metrics,
models: { models: {
'gemini-pro': { 'gemini-pro': {
api: { api: {
@@ -471,6 +551,7 @@ describe('<Footer />', () => {
thoughts: 0, thoughts: 0,
tool: 0, tool: 0,
}, },
roles: {},
}, },
}, },
}, },
@@ -484,35 +565,44 @@ describe('<Footer />', () => {
}, },
}), }),
}); });
await result.waitUntilReady();
return result;
};
it('formats thousands with k', () => { it('formats thousands with k', async () => {
const { lastFrame } = renderWithTokens(1500); const { lastFrame, unmount } = await renderWithTokens(1500);
expect(lastFrame()).toContain('1.5k tokens'); expect(lastFrame()).toContain('1.5k tokens');
unmount();
}); });
it('formats millions with m', () => { it('formats millions with m', async () => {
const { lastFrame } = renderWithTokens(1500000); const { lastFrame, unmount } = await renderWithTokens(1500000);
expect(lastFrame()).toContain('1.5m tokens'); expect(lastFrame()).toContain('1.5m tokens');
unmount();
}); });
it('formats billions with b', () => { it('formats billions with b', async () => {
const { lastFrame } = renderWithTokens(1500000000); const { lastFrame, unmount } = await renderWithTokens(1500000000);
expect(lastFrame()).toContain('1.5b tokens'); expect(lastFrame()).toContain('1.5b tokens');
unmount();
}); });
it('formats small numbers without suffix', () => { it('formats small numbers without suffix', async () => {
const { lastFrame } = renderWithTokens(500); const { lastFrame, unmount } = await renderWithTokens(500);
expect(lastFrame()).toContain('500 tokens'); expect(lastFrame()).toContain('500 tokens');
unmount();
}); });
}); });
describe('Footer Custom Items', () => { describe('Footer Custom Items', () => {
it('renders items in the specified order', () => { it('renders items in the specified order', async () => {
const { lastFrame } = renderWithProviders(<Footer />, { const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<Footer />,
{
width: 120, width: 120,
uiState: { uiState: {
currentModel: 'gemini-pro', currentModel: 'gemini-pro',
sessionStats: customMockSessionStats, sessionStats: mockSessionStats,
}, },
settings: createMockSettings({ settings: createMockSettings({
ui: { ui: {
@@ -521,19 +611,24 @@ describe('<Footer />', () => {
}, },
}, },
}), }),
}); },
);
await waitUntilReady();
const output = lastFrame(); const output = lastFrame();
const modelIdx = output!.indexOf('/model'); const modelIdx = output.indexOf('/model');
const cwdIdx = output!.indexOf('Path'); const cwdIdx = output.indexOf('Path');
expect(modelIdx).toBeLessThan(cwdIdx); expect(modelIdx).toBeLessThan(cwdIdx);
unmount();
}); });
it('renders multiple items with proper alignment', () => { it('renders multiple items with proper alignment', async () => {
const { lastFrame } = renderWithProviders(<Footer />, { const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<Footer />,
{
width: 120, width: 120,
uiState: { uiState: {
sessionStats: customMockSessionStats, sessionStats: mockSessionStats,
branchName: 'main', branchName: 'main',
}, },
settings: createMockSettings({ settings: createMockSettings({
@@ -546,7 +641,9 @@ describe('<Footer />', () => {
}, },
}, },
}), }),
}); },
);
await waitUntilReady();
const output = lastFrame(); const output = lastFrame();
expect(output).toBeDefined(); expect(output).toBeDefined();
@@ -558,12 +655,15 @@ describe('<Footer />', () => {
// Data should be present // Data should be present
expect(output).toContain('main'); expect(output).toContain('main');
expect(output).toContain('gemini-pro'); expect(output).toContain('gemini-pro');
unmount();
}); });
it('handles empty items array', () => { it('handles empty items array', async () => {
const { lastFrame } = renderWithProviders(<Footer />, { const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<Footer />,
{
width: 120, width: 120,
uiState: { sessionStats: customMockSessionStats }, uiState: { sessionStats: mockSessionStats },
settings: createMockSettings({ settings: createMockSettings({
ui: { ui: {
footer: { footer: {
@@ -571,18 +671,23 @@ describe('<Footer />', () => {
}, },
}, },
}), }),
}); },
);
await waitUntilReady();
const output = lastFrame(); const output = lastFrame({ allowEmpty: true });
expect(output).toBeDefined(); expect(output).toBeDefined();
expect(output!.trim()).toBe(''); expect(output.trim()).toBe('');
unmount();
}); });
it('does not render items that are conditionally hidden', () => { it('does not render items that are conditionally hidden', async () => {
const { lastFrame } = renderWithProviders(<Footer />, { const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<Footer />,
{
width: 120, width: 120,
uiState: { uiState: {
sessionStats: customMockSessionStats, sessionStats: mockSessionStats,
branchName: undefined, // No branch branchName: undefined, // No branch
}, },
settings: createMockSettings({ settings: createMockSettings({
@@ -592,13 +697,54 @@ describe('<Footer />', () => {
}, },
}, },
}), }),
}); },
);
await waitUntilReady();
const output = lastFrame(); const output = lastFrame();
expect(output).toBeDefined(); expect(output).toBeDefined();
expect(output).not.toContain('Branch'); expect(output).not.toContain('Branch');
expect(output).toContain('Path'); expect(output).toContain('Path');
expect(output).toContain('/model'); expect(output).toContain('/model');
unmount();
});
});
describe('fallback mode display', () => {
it('should display Flash model when in fallback mode, not the configured Pro model', async () => {
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<Footer />,
{
width: 120,
uiState: {
sessionStats: mockSessionStats,
currentModel: 'gemini-2.5-flash', // Fallback active, showing Flash
},
},
);
await waitUntilReady();
// Footer should show the effective model (Flash), not the config model (Pro)
expect(lastFrame()).toContain('gemini-2.5-flash');
expect(lastFrame()).not.toContain('gemini-2.5-pro');
unmount();
});
it('should display Pro model when NOT in fallback mode', async () => {
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<Footer />,
{
width: 120,
uiState: {
sessionStats: mockSessionStats,
currentModel: 'gemini-2.5-pro', // Normal mode, showing Pro
},
},
);
await waitUntilReady();
expect(lastFrame()).toContain('gemini-2.5-pro');
unmount();
}); });
}); });
}); });