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

@@ -10,7 +10,7 @@ import { renderWithProviders } from '../../test-utils/render.js';
import { LoadedSettings } from '../../config/settings.js';
describe('colorizeCode', () => {
it('renders empty lines correctly when useAlternateBuffer is true', () => {
it('renders empty lines correctly when useAlternateBuffer is true', async () => {
const code = 'line 1\n\nline 3';
const settings = new LoadedSettings(
{ path: '', settings: {}, originalSettings: {} },
@@ -35,7 +35,10 @@ describe('colorizeCode', () => {
hideLineNumbers: true,
});
const { lastFrame } = renderWithProviders(<>{result}</>);
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<>{result}</>,
);
await waitUntilReady();
// We expect the output to preserve the empty line.
// If the bug exists, it might look like "line 1\nline 3"
// If fixed, it should look like "line 1\n \nline 3" (if we use space) or just have the newline.
@@ -45,5 +48,6 @@ describe('colorizeCode', () => {
// But ink-testing-library usually returns the visual representation.
expect(lastFrame()).toMatch(/line 1\s*\n\s*\n\s*line 3/);
unmount();
});
});

View File

@@ -20,19 +20,23 @@ describe('<MarkdownDisplay />', () => {
vi.clearAllMocks();
});
it('renders nothing for empty text', () => {
const { lastFrame } = renderWithProviders(
it('renders nothing for empty text', async () => {
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<MarkdownDisplay {...baseProps} text="" />,
);
expect(lastFrame()).toMatchSnapshot();
await waitUntilReady();
expect(lastFrame({ allowEmpty: true })).toMatchSnapshot();
unmount();
});
it('renders a simple paragraph', () => {
it('renders a simple paragraph', async () => {
const text = 'Hello, world.';
const { lastFrame } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<MarkdownDisplay {...baseProps} text={text} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
const lineEndings = [
@@ -41,82 +45,96 @@ describe('<MarkdownDisplay />', () => {
];
describe.each(lineEndings)('with $name line endings', ({ eol }) => {
it('renders headers with correct levels', () => {
it('renders headers with correct levels', async () => {
const text = `
# Header 1
## Header 2
### Header 3
#### Header 4
`.replace(/\n/g, eol);
const { lastFrame } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<MarkdownDisplay {...baseProps} text={text} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders a fenced code block with a language', () => {
it('renders a fenced code block with a language', async () => {
const text = '```javascript\nconst x = 1;\nconsole.log(x);\n```'.replace(
/\n/g,
eol,
);
const { lastFrame } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<MarkdownDisplay {...baseProps} text={text} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders a fenced code block without a language', () => {
it('renders a fenced code block without a language', async () => {
const text = '```\nplain text\n```'.replace(/\n/g, eol);
const { lastFrame } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<MarkdownDisplay {...baseProps} text={text} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('handles unclosed (pending) code blocks', () => {
it('handles unclosed (pending) code blocks', async () => {
const text = '```typescript\nlet y = 2;'.replace(/\n/g, eol);
const { lastFrame } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<MarkdownDisplay {...baseProps} text={text} isPending={true} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders unordered lists with different markers', () => {
it('renders unordered lists with different markers', async () => {
const text = `
- item A
* item B
+ item C
`.replace(/\n/g, eol);
const { lastFrame } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<MarkdownDisplay {...baseProps} text={text} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders nested unordered lists', () => {
it('renders nested unordered lists', async () => {
const text = `
* Level 1
* Level 2
* Level 3
`.replace(/\n/g, eol);
const { lastFrame } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<MarkdownDisplay {...baseProps} text={text} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders ordered lists', () => {
it('renders ordered lists', async () => {
const text = `
1. First item
2. Second item
`.replace(/\n/g, eol);
const { lastFrame } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<MarkdownDisplay {...baseProps} text={text} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders horizontal rules', () => {
it('renders horizontal rules', async () => {
const text = `
Hello
---
@@ -124,48 +142,56 @@ World
***
Test
`.replace(/\n/g, eol);
const { lastFrame } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<MarkdownDisplay {...baseProps} text={text} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders tables correctly', () => {
it('renders tables correctly', async () => {
const text = `
| Header 1 | Header 2 |
|----------|:--------:|
| Cell 1 | Cell 2 |
| Cell 3 | Cell 4 |
`.replace(/\n/g, eol);
const { lastFrame } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<MarkdownDisplay {...baseProps} text={text} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('handles a table at the end of the input', () => {
it('handles a table at the end of the input', async () => {
const text = `
Some text before.
| A | B |
|---|
| 1 | 2 |`.replace(/\n/g, eol);
const { lastFrame } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<MarkdownDisplay {...baseProps} text={text} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('inserts a single space between paragraphs', () => {
it('inserts a single space between paragraphs', async () => {
const text = `Paragraph 1.
Paragraph 2.`.replace(/\n/g, eol);
const { lastFrame } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<MarkdownDisplay {...baseProps} text={text} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('correctly parses a mix of markdown elements', () => {
it('correctly parses a mix of markdown elements', async () => {
const text = `
# Main Title
@@ -180,13 +206,15 @@ some code
Another paragraph.
`.replace(/\n/g, eol);
const { lastFrame } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<MarkdownDisplay {...baseProps} text={text} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('hides line numbers in code blocks when showLineNumbers is false', () => {
it('hides line numbers in code blocks when showLineNumbers is false', async () => {
const text = '```javascript\nconst x = 1;\n```'.replace(/\n/g, eol);
const settings = new LoadedSettings(
{ path: '', settings: {}, originalSettings: {} },
@@ -201,21 +229,25 @@ Another paragraph.
[],
);
const { lastFrame } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<MarkdownDisplay {...baseProps} text={text} />,
{ settings },
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
expect(lastFrame()).not.toContain(' 1 ');
unmount();
});
it('shows line numbers in code blocks by default', () => {
it('shows line numbers in code blocks by default', async () => {
const text = '```javascript\nconst x = 1;\n```'.replace(/\n/g, eol);
const { lastFrame } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<MarkdownDisplay {...baseProps} text={text} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
expect(lastFrame()).toContain(' 1 ');
unmount();
});
});
});

View File

@@ -8,7 +8,7 @@ import { TableRenderer } from './TableRenderer.js';
import { renderWithProviders } from '../../test-utils/render.js';
describe('TableRenderer', () => {
it('renders a 3x3 table correctly', () => {
it('renders a 3x3 table correctly', async () => {
const headers = ['Header 1', 'Header 2', 'Header 3'];
const rows = [
['Row 1, Col 1', 'Row 1, Col 2', 'Row 1, Col 3'],
@@ -17,22 +17,24 @@ describe('TableRenderer', () => {
];
const terminalWidth = 80;
const { lastFrame } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<TableRenderer
headers={headers}
rows={rows}
terminalWidth={terminalWidth}
/>,
);
await waitUntilReady();
const output = lastFrame();
expect(output).toContain('Header 1');
expect(output).toContain('Row 1, Col 1');
expect(output).toContain('Row 3, Col 3');
expect(output).toMatchSnapshot();
unmount();
});
it('renders a table with long headers and 4 columns correctly', () => {
it('renders a table with long headers and 4 columns correctly', async () => {
const headers = [
'Very Long Column Header One',
'Very Long Column Header Two',
@@ -46,13 +48,14 @@ describe('TableRenderer', () => {
];
const terminalWidth = 80;
const { lastFrame } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<TableRenderer
headers={headers}
rows={rows}
terminalWidth={terminalWidth}
/>,
);
await waitUntilReady();
const output = lastFrame();
// Since terminalWidth is 80 and headers are long, they might be truncated.
@@ -60,9 +63,10 @@ describe('TableRenderer', () => {
expect(output).toContain('Data 1.1');
expect(output).toContain('Data 3.4');
expect(output).toMatchSnapshot();
unmount();
});
it('wraps long cell content correctly', () => {
it('wraps long cell content correctly', async () => {
const headers = ['Col 1', 'Col 2', 'Col 3'];
const rows = [
[
@@ -73,21 +77,23 @@ describe('TableRenderer', () => {
];
const terminalWidth = 50;
const { lastFrame } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<TableRenderer
headers={headers}
rows={rows}
terminalWidth={terminalWidth}
/>,
);
await waitUntilReady();
const output = lastFrame();
expect(output).toContain('This is a very');
expect(output).toContain('long cell');
expect(output).toMatchSnapshot();
unmount();
});
it('wraps all long columns correctly', () => {
it('wraps all long columns correctly', async () => {
const headers = ['Col 1', 'Col 2', 'Col 3'];
const rows = [
[
@@ -98,20 +104,22 @@ describe('TableRenderer', () => {
];
const terminalWidth = 60;
const { lastFrame } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<TableRenderer
headers={headers}
rows={rows}
terminalWidth={terminalWidth}
/>,
);
await waitUntilReady();
const output = lastFrame();
expect(output).toContain('wrapping in');
expect(output).toMatchSnapshot();
unmount();
});
it('wraps mixed long and short columns correctly', () => {
it('wraps mixed long and short columns correctly', async () => {
const headers = ['Short', 'Long', 'Medium'];
const rows = [
[
@@ -122,22 +130,24 @@ describe('TableRenderer', () => {
];
const terminalWidth = 50;
const { lastFrame } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<TableRenderer
headers={headers}
rows={rows}
terminalWidth={terminalWidth}
/>,
);
await waitUntilReady();
const output = lastFrame();
expect(output).toContain('Tiny');
expect(output).toContain('definitely needs');
expect(output).toMatchSnapshot();
unmount();
});
// The snapshot looks weird but checked on VS Code terminal and it looks fine
it('wraps columns with punctuation correctly', () => {
it('wraps columns with punctuation correctly', async () => {
const headers = ['Punctuation 1', 'Punctuation 2', 'Punctuation 3'];
const rows = [
[
@@ -148,40 +158,44 @@ describe('TableRenderer', () => {
];
const terminalWidth = 60;
const { lastFrame } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<TableRenderer
headers={headers}
rows={rows}
terminalWidth={terminalWidth}
/>,
);
await waitUntilReady();
const output = lastFrame();
expect(output).toContain('Start. Stop.');
expect(output).toMatchSnapshot();
unmount();
});
it('strips bold markers from headers and renders them correctly', () => {
it('strips bold markers from headers and renders them correctly', async () => {
const headers = ['**Bold Header**', 'Normal Header', '**Another Bold**'];
const rows = [['Data 1', 'Data 2', 'Data 3']];
const terminalWidth = 50;
const { lastFrame } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<TableRenderer
headers={headers}
rows={rows}
terminalWidth={terminalWidth}
/>,
);
await waitUntilReady();
const output = lastFrame();
// The output should NOT contain the literal '**'
expect(output).not.toContain('**Bold Header**');
expect(output).toContain('Bold Header');
expect(output).toMatchSnapshot();
unmount();
});
it('handles wrapped bold headers without showing markers', () => {
it('handles wrapped bold headers without showing markers', async () => {
const headers = [
'**Very Long Bold Header That Will Wrap**',
'Short',
@@ -190,22 +204,24 @@ describe('TableRenderer', () => {
const rows = [['Data 1', 'Data 2', 'Data 3']];
const terminalWidth = 40;
const { lastFrame } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<TableRenderer
headers={headers}
rows={rows}
terminalWidth={terminalWidth}
/>,
);
await waitUntilReady();
const output = lastFrame();
// Markers should be gone
expect(output).not.toContain('**');
expect(output).toContain('Very Long');
expect(output).toMatchSnapshot();
unmount();
});
it('renders a complex table with mixed content lengths correctly', () => {
it('renders a complex table with mixed content lengths correctly', async () => {
const headers = [
'Comprehensive Architectural Specification for the Distributed Infrastructure Layer',
'Implementation Details for the High-Throughput Asynchronous Message Processing Pipeline with Extended Scalability Features and Redundancy Protocols',
@@ -231,7 +247,7 @@ describe('TableRenderer', () => {
const terminalWidth = 160;
const { lastFrame } = renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<TableRenderer
headers={headers}
rows={rows}
@@ -239,6 +255,7 @@ describe('TableRenderer', () => {
/>,
{ width: terminalWidth },
);
await waitUntilReady();
const output = lastFrame();
@@ -255,6 +272,7 @@ describe('TableRenderer', () => {
expect(output).toContain('Doe');
expect(output).toMatchSnapshot();
unmount();
});
it.each([
@@ -298,8 +316,8 @@ describe('TableRenderer', () => {
terminalWidth: 80,
expected: ['Mixed 😃 中文', '你好 😃', 'こんにちは 🚀'],
},
])('$name', ({ headers, rows, terminalWidth, expected }) => {
const { lastFrame } = renderWithProviders(
])('$name', async ({ headers, rows, terminalWidth, expected }) => {
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<TableRenderer
headers={headers}
rows={rows}
@@ -307,12 +325,14 @@ describe('TableRenderer', () => {
/>,
{ width: terminalWidth },
);
await waitUntilReady();
const output = lastFrame();
expected.forEach((text) => {
expect(output).toContain(text);
});
expect(output).toMatchSnapshot();
unmount();
});
it.each([
@@ -328,16 +348,17 @@ describe('TableRenderer', () => {
rows: [['Data 1', 'Data 2']],
expected: ['Header 1', 'Header 2', 'Header 3', 'Data 1', 'Data 2'],
},
])('$name', ({ headers, rows, expected }) => {
])('$name', async ({ headers, rows, expected }) => {
const terminalWidth = 50;
const { lastFrame } = renderWithProviders(
const { lastFrame, waitUntilReady } = renderWithProviders(
<TableRenderer
headers={headers}
rows={rows}
terminalWidth={terminalWidth}
/>,
);
await waitUntilReady();
const output = lastFrame();
expected.forEach((text) => {

View File

@@ -1,6 +1,9 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<MarkdownDisplay /> > renders a simple paragraph 1`] = `"Hello, world."`;
exports[`<MarkdownDisplay /> > renders a simple paragraph 1`] = `
"Hello, world.
"
`;
exports[`<MarkdownDisplay /> > renders nothing for empty text 1`] = `""`;
@@ -22,25 +25,37 @@ exports[`<MarkdownDisplay /> > with 'Unix' line endings > handles a table at the
"Some text before.
| A | B |
|---|
| 1 | 2 |"
| 1 | 2 |
"
`;
exports[`<MarkdownDisplay /> > with 'Unix' line endings > handles unclosed (pending) code blocks 1`] = `" 1 let y = 2;"`;
exports[`<MarkdownDisplay /> > with 'Unix' line endings > handles unclosed (pending) code blocks 1`] = `
" 1 let y = 2;
"
`;
exports[`<MarkdownDisplay /> > with 'Unix' line endings > hides line numbers in code blocks when showLineNumbers is false 1`] = `" const x = 1;"`;
exports[`<MarkdownDisplay /> > with 'Unix' line endings > hides line numbers in code blocks when showLineNumbers is false 1`] = `
" const x = 1;
"
`;
exports[`<MarkdownDisplay /> > with 'Unix' line endings > inserts a single space between paragraphs 1`] = `
"Paragraph 1.
Paragraph 2."
Paragraph 2.
"
`;
exports[`<MarkdownDisplay /> > with 'Unix' line endings > renders a fenced code block with a language 1`] = `
" 1 const x = 1;
2 console.log(x);"
2 console.log(x);
"
`;
exports[`<MarkdownDisplay /> > with 'Unix' line endings > renders a fenced code block without a language 1`] = `" 1 plain text"`;
exports[`<MarkdownDisplay /> > with 'Unix' line endings > renders a fenced code block without a language 1`] = `
" 1 plain text
"
`;
exports[`<MarkdownDisplay /> > with 'Unix' line endings > renders headers with correct levels 1`] = `
"Header 1
@@ -90,7 +105,10 @@ exports[`<MarkdownDisplay /> > with 'Unix' line endings > renders unordered list
"
`;
exports[`<MarkdownDisplay /> > with 'Unix' line endings > shows line numbers in code blocks by default 1`] = `" 1 const x = 1;"`;
exports[`<MarkdownDisplay /> > with 'Unix' line endings > shows line numbers in code blocks by default 1`] = `
" 1 const x = 1;
"
`;
exports[`<MarkdownDisplay /> > with 'Windows' line endings > correctly parses a mix of markdown elements 1`] = `
"Main Title
@@ -110,25 +128,37 @@ exports[`<MarkdownDisplay /> > with 'Windows' line endings > handles a table at
"Some text before.
| A | B |
|---|
| 1 | 2 |"
| 1 | 2 |
"
`;
exports[`<MarkdownDisplay /> > with 'Windows' line endings > handles unclosed (pending) code blocks 1`] = `" 1 let y = 2;"`;
exports[`<MarkdownDisplay /> > with 'Windows' line endings > handles unclosed (pending) code blocks 1`] = `
" 1 let y = 2;
"
`;
exports[`<MarkdownDisplay /> > with 'Windows' line endings > hides line numbers in code blocks when showLineNumbers is false 1`] = `" const x = 1;"`;
exports[`<MarkdownDisplay /> > with 'Windows' line endings > hides line numbers in code blocks when showLineNumbers is false 1`] = `
" const x = 1;
"
`;
exports[`<MarkdownDisplay /> > with 'Windows' line endings > inserts a single space between paragraphs 1`] = `
"Paragraph 1.
Paragraph 2."
Paragraph 2.
"
`;
exports[`<MarkdownDisplay /> > with 'Windows' line endings > renders a fenced code block with a language 1`] = `
" 1 const x = 1;
2 console.log(x);"
2 console.log(x);
"
`;
exports[`<MarkdownDisplay /> > with 'Windows' line endings > renders a fenced code block without a language 1`] = `" 1 plain text"`;
exports[`<MarkdownDisplay /> > with 'Windows' line endings > renders a fenced code block without a language 1`] = `
" 1 plain text
"
`;
exports[`<MarkdownDisplay /> > with 'Windows' line endings > renders headers with correct levels 1`] = `
"Header 1
@@ -178,4 +208,7 @@ exports[`<MarkdownDisplay /> > with 'Windows' line endings > renders unordered l
"
`;
exports[`<MarkdownDisplay /> > with 'Windows' line endings > shows line numbers in code blocks by default 1`] = `" 1 const x = 1;"`;
exports[`<MarkdownDisplay /> > with 'Windows' line endings > shows line numbers in code blocks by default 1`] = `
" 1 const x = 1;
"
`;