Migrate core render util to use xterm.js as part of the rendering loop. (#19044)

This commit is contained in:
Jacob Richman
2026-02-18 16:46:50 -08:00
committed by GitHub
parent 04c52513e7
commit 04f65f3d55
213 changed files with 7065 additions and 3852 deletions

View File

@@ -21,29 +21,36 @@ const mockChats: ChatDetail[] = [
];
describe('<ChatList />', () => {
it('renders correctly with a list of chats', () => {
const { lastFrame, unmount } = render(<ChatList chats={mockChats} />);
it('renders correctly with a list of chats', async () => {
const { lastFrame, unmount, waitUntilReady } = render(
<ChatList chats={mockChats} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders correctly with no chats', () => {
const { lastFrame, unmount } = render(<ChatList chats={[]} />);
it('renders correctly with no chats', async () => {
const { lastFrame, unmount, waitUntilReady } = render(
<ChatList chats={[]} />,
);
await waitUntilReady();
expect(lastFrame()).toContain('No saved conversation checkpoints found.');
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('handles invalid date formats gracefully', () => {
it('handles invalid date formats gracefully', async () => {
const mockChatsWithInvalidDate: ChatDetail[] = [
{
name: 'bad-date-chat',
mtime: 'an-invalid-date-string',
},
];
const { lastFrame, unmount } = render(
const { lastFrame, unmount, waitUntilReady } = render(
<ChatList chats={mockChatsWithInvalidDate} />,
);
await waitUntilReady();
expect(lastFrame()).toContain('(Invalid Date)');
expect(lastFrame()).toMatchSnapshot();
unmount();

View File

@@ -55,18 +55,22 @@ describe('<ExtensionsList />', () => {
} as never);
};
it('should render "No extensions installed." if there are no extensions', () => {
it('should render "No extensions installed." if there are no extensions', async () => {
mockUIState(new Map());
const { lastFrame, unmount } = render(<ExtensionsList extensions={[]} />);
const { lastFrame, waitUntilReady, unmount } = render(
<ExtensionsList extensions={[]} />,
);
await waitUntilReady();
expect(lastFrame()).toContain('No extensions installed.');
unmount();
});
it('should render a list of extensions with their version and status', () => {
it('should render a list of extensions with their version and status', async () => {
mockUIState(new Map());
const { lastFrame, unmount } = render(
const { lastFrame, waitUntilReady, unmount } = render(
<ExtensionsList extensions={mockExtensions} />,
);
await waitUntilReady();
const output = lastFrame();
expect(output).toContain('ext-one (v1.0.0) - active');
expect(output).toContain('ext-two (v2.1.0) - active');
@@ -74,16 +78,17 @@ describe('<ExtensionsList />', () => {
unmount();
});
it('should display "unknown state" if an extension has no update state', () => {
it('should display "unknown state" if an extension has no update state', async () => {
mockUIState(new Map());
const { lastFrame, unmount } = render(
const { lastFrame, waitUntilReady, unmount } = render(
<ExtensionsList extensions={[mockExtensions[0]]} />,
);
await waitUntilReady();
expect(lastFrame()).toContain('(unknown state)');
unmount();
});
const stateTestCases = [
it.each([
{
state: ExtensionUpdateState.CHECKING_FOR_UPDATES,
expectedText: '(checking for updates)',
@@ -112,21 +117,21 @@ describe('<ExtensionsList />', () => {
state: ExtensionUpdateState.UP_TO_DATE,
expectedText: '(up to date)',
},
];
for (const { state, expectedText } of stateTestCases) {
it(`should correctly display the state: ${state}`, () => {
])(
'should correctly display the state: $state',
async ({ state, expectedText }) => {
const updateState = new Map([[mockExtensions[0].name, state]]);
mockUIState(updateState);
const { lastFrame, unmount } = render(
const { lastFrame, waitUntilReady, unmount } = render(
<ExtensionsList extensions={[mockExtensions[0]]} />,
);
await waitUntilReady();
expect(lastFrame()).toContain(expectedText);
unmount();
});
}
},
);
it('should render resolved settings for an extension', () => {
it('should render resolved settings for an extension', async () => {
mockUIState(new Map());
const extensionWithSettings = {
...mockExtensions[0],
@@ -155,9 +160,10 @@ describe('<ExtensionsList />', () => {
},
],
};
const { lastFrame, unmount } = render(
const { lastFrame, waitUntilReady, unmount } = render(
<ExtensionsList extensions={[extensionWithSettings]} />,
);
await waitUntilReady();
const output = lastFrame();
expect(output).toContain('settings:');
expect(output).toContain('- sensitiveApiKey: ***');

View File

@@ -53,35 +53,41 @@ describe('McpStatus', () => {
showSchema: false,
};
it('renders correctly with a connected server', () => {
const { lastFrame, unmount } = render(<McpStatus {...baseProps} />);
it('renders correctly with a connected server', async () => {
const { lastFrame, unmount, waitUntilReady } = render(
<McpStatus {...baseProps} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders correctly with authenticated OAuth status', () => {
const { lastFrame, unmount } = render(
it('renders correctly with authenticated OAuth status', async () => {
const { lastFrame, unmount, waitUntilReady } = render(
<McpStatus {...baseProps} authStatus={{ 'server-1': 'authenticated' }} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders correctly with expired OAuth status', () => {
const { lastFrame, unmount } = render(
it('renders correctly with expired OAuth status', async () => {
const { lastFrame, unmount, waitUntilReady } = render(
<McpStatus {...baseProps} authStatus={{ 'server-1': 'expired' }} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders correctly with unauthenticated OAuth status', () => {
const { lastFrame, unmount } = render(
it('renders correctly with unauthenticated OAuth status', async () => {
const { lastFrame, unmount, waitUntilReady } = render(
<McpStatus
{...baseProps}
authStatus={{ 'server-1': 'unauthenticated' }}
/>,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
@@ -91,29 +97,34 @@ describe('McpStatus', () => {
await import('@google/gemini-cli-core'),
'getMCPServerStatus',
).mockReturnValue(MCPServerStatus.DISCONNECTED);
const { lastFrame, unmount } = render(<McpStatus {...baseProps} />);
const { lastFrame, unmount, waitUntilReady } = render(
<McpStatus {...baseProps} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders correctly when discovery is in progress', () => {
const { lastFrame, unmount } = render(
it('renders correctly when discovery is in progress', async () => {
const { lastFrame, unmount, waitUntilReady } = render(
<McpStatus {...baseProps} discoveryInProgress={true} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders correctly with schema enabled', () => {
const { lastFrame, unmount } = render(
it('renders correctly with schema enabled', async () => {
const { lastFrame, unmount, waitUntilReady } = render(
<McpStatus {...baseProps} showSchema={true} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders correctly with parametersJsonSchema', () => {
const { lastFrame, unmount } = render(
it('renders correctly with parametersJsonSchema', async () => {
const { lastFrame, unmount, waitUntilReady } = render(
<McpStatus
{...baseProps}
tools={[
@@ -134,12 +145,13 @@ describe('McpStatus', () => {
showSchema={true}
/>,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders correctly with prompts', () => {
const { lastFrame, unmount } = render(
it('renders correctly with prompts', async () => {
const { lastFrame, unmount, waitUntilReady } = render(
<McpStatus
{...baseProps}
prompts={[
@@ -151,12 +163,13 @@ describe('McpStatus', () => {
]}
/>,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders correctly with resources', () => {
const { lastFrame, unmount } = render(
it('renders correctly with resources', async () => {
const { lastFrame, unmount, waitUntilReady } = render(
<McpStatus
{...baseProps}
resources={[
@@ -169,39 +182,43 @@ describe('McpStatus', () => {
]}
/>,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders correctly with a blocked server', () => {
const { lastFrame, unmount } = render(
it('renders correctly with a blocked server', async () => {
const { lastFrame, unmount, waitUntilReady } = render(
<McpStatus
{...baseProps}
blockedServers={[{ name: 'server-1', extensionName: 'test-extension' }]}
/>,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders correctly with a connecting server', () => {
const { lastFrame, unmount } = render(
it('renders correctly with a connecting server', async () => {
const { lastFrame, unmount, waitUntilReady } = render(
<McpStatus {...baseProps} connectingServers={['server-1']} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('truncates resources when exceeding limit', () => {
it('truncates resources when exceeding limit', async () => {
const manyResources = Array.from({ length: 25 }, (_, i) => ({
serverName: 'server-1',
name: `resource-${i + 1}`,
uri: `file:///tmp/resource-${i + 1}.txt`,
}));
const { lastFrame, unmount } = render(
const { lastFrame, unmount, waitUntilReady } = render(
<McpStatus {...baseProps} resources={manyResources} />,
);
await waitUntilReady();
expect(lastFrame()).toContain('15 resources hidden');
unmount();
});

View File

@@ -34,10 +34,11 @@ describe('SkillsList Component', () => {
},
];
it('should render enabled and disabled skills separately', () => {
const { lastFrame, unmount } = render(
it('should render enabled and disabled skills separately', async () => {
const { lastFrame, unmount, waitUntilReady } = render(
<SkillsList skills={mockSkills} showDescriptions={true} />,
);
await waitUntilReady();
const output = lastFrame();
expect(output).toContain('Available Agent Skills:');
@@ -53,10 +54,11 @@ describe('SkillsList Component', () => {
unmount();
});
it('should not render descriptions when showDescriptions is false', () => {
const { lastFrame, unmount } = render(
it('should not render descriptions when showDescriptions is false', async () => {
const { lastFrame, unmount, waitUntilReady } = render(
<SkillsList skills={mockSkills} showDescriptions={false} />,
);
await waitUntilReady();
const output = lastFrame();
expect(output).toContain('skill1');
@@ -69,10 +71,11 @@ describe('SkillsList Component', () => {
unmount();
});
it('should render "No skills available" when skills list is empty', () => {
const { lastFrame, unmount } = render(
it('should render "No skills available" when skills list is empty', async () => {
const { lastFrame, unmount, waitUntilReady } = render(
<SkillsList skills={[]} showDescriptions={true} />,
);
await waitUntilReady();
const output = lastFrame();
expect(output).toContain('No skills available');
@@ -80,11 +83,12 @@ describe('SkillsList Component', () => {
unmount();
});
it('should only render Available Agent Skills section when all skills are enabled', () => {
it('should only render Available Agent Skills section when all skills are enabled', async () => {
const enabledOnly = mockSkills.filter((s) => !s.disabled);
const { lastFrame, unmount } = render(
const { lastFrame, unmount, waitUntilReady } = render(
<SkillsList skills={enabledOnly} showDescriptions={true} />,
);
await waitUntilReady();
const output = lastFrame();
expect(output).toContain('Available Agent Skills:');
@@ -93,11 +97,12 @@ describe('SkillsList Component', () => {
unmount();
});
it('should only render Disabled Skills section when all skills are disabled', () => {
it('should only render Disabled Skills section when all skills are disabled', async () => {
const disabledOnly = mockSkills.filter((s) => s.disabled);
const { lastFrame, unmount } = render(
const { lastFrame, unmount, waitUntilReady } = render(
<SkillsList skills={disabledOnly} showDescriptions={true} />,
);
await waitUntilReady();
const output = lastFrame();
expect(output).not.toContain('Available Agent Skills:');
@@ -106,7 +111,7 @@ describe('SkillsList Component', () => {
unmount();
});
it('should render [Built-in] tag for built-in skills', () => {
it('should render [Built-in] tag for built-in skills', async () => {
const builtinSkill: SkillDefinition = {
name: 'builtin-skill',
description: 'A built-in skill',
@@ -116,9 +121,10 @@ describe('SkillsList Component', () => {
isBuiltin: true,
};
const { lastFrame, unmount } = render(
const { lastFrame, unmount, waitUntilReady } = render(
<SkillsList skills={[builtinSkill]} showDescriptions={true} />,
);
await waitUntilReady();
const output = lastFrame();
expect(output).toContain('builtin-skill');

View File

@@ -31,32 +31,35 @@ const mockTools: ToolDefinition[] = [
];
describe('<ToolsList />', () => {
it('renders correctly with descriptions', () => {
const { lastFrame } = renderWithProviders(
it('renders correctly with descriptions', async () => {
const { lastFrame, waitUntilReady } = renderWithProviders(
<ToolsList
tools={mockTools}
showDescriptions={true}
terminalWidth={40}
/>,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
});
it('renders correctly without descriptions', () => {
const { lastFrame } = renderWithProviders(
it('renders correctly without descriptions', async () => {
const { lastFrame, waitUntilReady } = renderWithProviders(
<ToolsList
tools={mockTools}
showDescriptions={false}
terminalWidth={40}
/>,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
});
it('renders correctly with no tools', () => {
const { lastFrame } = renderWithProviders(
it('renders correctly with no tools', async () => {
const { lastFrame, waitUntilReady } = renderWithProviders(
<ToolsList tools={[]} showDescriptions={true} terminalWidth={40} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
});
});

View File

@@ -5,7 +5,8 @@ exports[`<ChatList /> > handles invalid date formats gracefully 1`] = `
- bad-date-chat (Invalid Date)
Note: Newest last, oldest first"
Note: Newest last, oldest first
"
`;
exports[`<ChatList /> > renders correctly with a list of chats 1`] = `
@@ -14,7 +15,11 @@ exports[`<ChatList /> > renders correctly with a list of chats 1`] = `
- chat-1 (2025-10-02 10:00:00)
- another-chat (2025-10-01 12:30:00)
Note: Newest last, oldest first"
Note: Newest last, oldest first
"
`;
exports[`<ChatList /> > renders correctly with no chats 1`] = `"No saved conversation checkpoints found."`;
exports[`<ChatList /> > renders correctly with no chats 1`] = `
"No saved conversation checkpoints found.
"
`;