mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 21:03:05 -07:00
chore: make Windows paths better
This commit is contained in:
committed by
Keith Guerin
parent
450cb19ee9
commit
8d112f55b3
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user