mirror of
https://github.com/stackblitz/bolt.new
synced 2024-11-27 22:42:21 +00:00
208 lines
6.9 KiB
TypeScript
208 lines
6.9 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import { StreamingMessageParser, type ActionCallback, type ArtifactCallback } from './message-parser';
|
|
|
|
interface ExpectedResult {
|
|
output: string;
|
|
callbacks?: {
|
|
onArtifactOpen?: number;
|
|
onArtifactClose?: number;
|
|
onActionOpen?: number;
|
|
onActionClose?: number;
|
|
};
|
|
}
|
|
|
|
describe('StreamingMessageParser', () => {
|
|
it('should pass through normal text', () => {
|
|
const parser = new StreamingMessageParser();
|
|
expect(parser.parse('test_id', 'Hello, world!')).toBe('Hello, world!');
|
|
});
|
|
|
|
it('should allow normal HTML tags', () => {
|
|
const parser = new StreamingMessageParser();
|
|
expect(parser.parse('test_id', 'Hello <strong>world</strong>!')).toBe('Hello <strong>world</strong>!');
|
|
});
|
|
|
|
describe('no artifacts', () => {
|
|
it.each<[string | string[], ExpectedResult | string]>([
|
|
['Foo bar', 'Foo bar'],
|
|
['Foo bar <', 'Foo bar '],
|
|
['Foo bar <p', 'Foo bar <p'],
|
|
[['Foo bar <', 's', 'p', 'an>some text</span>'], 'Foo bar <span>some text</span>'],
|
|
])('should correctly parse chunks and strip out bolt artifacts (%#)', (input, expected) => {
|
|
runTest(input, expected);
|
|
});
|
|
});
|
|
|
|
describe('invalid or incomplete artifacts', () => {
|
|
it.each<[string | string[], ExpectedResult | string]>([
|
|
['Foo bar <b', 'Foo bar '],
|
|
['Foo bar <ba', 'Foo bar <ba'],
|
|
['Foo bar <bol', 'Foo bar '],
|
|
['Foo bar <bolt', 'Foo bar '],
|
|
['Foo bar <bolta', 'Foo bar <bolta'],
|
|
['Foo bar <boltA', 'Foo bar '],
|
|
['Foo bar <boltArtifacs></boltArtifact>', 'Foo bar <boltArtifacs></boltArtifact>'],
|
|
['Before <oltArtfiact>foo</boltArtifact> After', 'Before <oltArtfiact>foo</boltArtifact> After'],
|
|
['Before <boltArtifactt>foo</boltArtifact> After', 'Before <boltArtifactt>foo</boltArtifact> After'],
|
|
])('should correctly parse chunks and strip out bolt artifacts (%#)', (input, expected) => {
|
|
runTest(input, expected);
|
|
});
|
|
});
|
|
|
|
describe('valid artifacts without actions', () => {
|
|
it.each<[string | string[], ExpectedResult | string]>([
|
|
[
|
|
'Some text before <boltArtifact title="Some title" id="artifact_1">foo bar</boltArtifact> Some more text',
|
|
{
|
|
output: 'Some text before Some more text',
|
|
callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 0, onActionClose: 0 },
|
|
},
|
|
],
|
|
[
|
|
['Some text before <boltArti', 'fact', ' title="Some title" id="artifact_1">foo</boltArtifact> Some more text'],
|
|
{
|
|
output: 'Some text before Some more text',
|
|
callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 0, onActionClose: 0 },
|
|
},
|
|
],
|
|
[
|
|
[
|
|
'Some text before <boltArti',
|
|
'fac',
|
|
't title="Some title" id="artifact_1"',
|
|
' ',
|
|
'>',
|
|
'foo</boltArtifact> Some more text',
|
|
],
|
|
{
|
|
output: 'Some text before Some more text',
|
|
callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 0, onActionClose: 0 },
|
|
},
|
|
],
|
|
[
|
|
[
|
|
'Some text before <boltArti',
|
|
'fact',
|
|
' title="Some title" id="artifact_1"',
|
|
' >fo',
|
|
'o</boltArtifact> Some more text',
|
|
],
|
|
{
|
|
output: 'Some text before Some more text',
|
|
callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 0, onActionClose: 0 },
|
|
},
|
|
],
|
|
[
|
|
[
|
|
'Some text before <boltArti',
|
|
'fact tit',
|
|
'le="Some ',
|
|
'title" id="artifact_1">fo',
|
|
'o',
|
|
'<',
|
|
'/boltArtifact> Some more text',
|
|
],
|
|
{
|
|
output: 'Some text before Some more text',
|
|
callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 0, onActionClose: 0 },
|
|
},
|
|
],
|
|
[
|
|
[
|
|
'Some text before <boltArti',
|
|
'fact title="Some title" id="artif',
|
|
'act_1">fo',
|
|
'o<',
|
|
'/boltArtifact> Some more text',
|
|
],
|
|
{
|
|
output: 'Some text before Some more text',
|
|
callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 0, onActionClose: 0 },
|
|
},
|
|
],
|
|
[
|
|
'Before <boltArtifact title="Some title" id="artifact_1">foo</boltArtifact> After',
|
|
{
|
|
output: 'Before After',
|
|
callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 0, onActionClose: 0 },
|
|
},
|
|
],
|
|
])('should correctly parse chunks and strip out bolt artifacts (%#)', (input, expected) => {
|
|
runTest(input, expected);
|
|
});
|
|
});
|
|
|
|
describe('valid artifacts with actions', () => {
|
|
it.each<[string | string[], ExpectedResult | string]>([
|
|
[
|
|
'Before <boltArtifact title="Some title" id="artifact_1"><boltAction type="shell">npm install</boltAction></boltArtifact> After',
|
|
{
|
|
output: 'Before After',
|
|
callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 1, onActionClose: 1 },
|
|
},
|
|
],
|
|
[
|
|
'Before <boltArtifact title="Some title" id="artifact_1"><boltAction type="shell">npm install</boltAction><boltAction type="file" filePath="index.js">some content</boltAction></boltArtifact> After',
|
|
{
|
|
output: 'Before After',
|
|
callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 2, onActionClose: 2 },
|
|
},
|
|
],
|
|
])('should correctly parse chunks and strip out bolt artifacts (%#)', (input, expected) => {
|
|
runTest(input, expected);
|
|
});
|
|
});
|
|
});
|
|
|
|
function runTest(input: string | string[], outputOrExpectedResult: string | ExpectedResult) {
|
|
let expected: ExpectedResult;
|
|
|
|
if (typeof outputOrExpectedResult === 'string') {
|
|
expected = { output: outputOrExpectedResult };
|
|
} else {
|
|
expected = outputOrExpectedResult;
|
|
}
|
|
|
|
const callbacks = {
|
|
onArtifactOpen: vi.fn<ArtifactCallback>((data) => {
|
|
expect(data).toMatchSnapshot('onArtifactOpen');
|
|
}),
|
|
onArtifactClose: vi.fn<ArtifactCallback>((data) => {
|
|
expect(data).toMatchSnapshot('onArtifactClose');
|
|
}),
|
|
onActionOpen: vi.fn<ActionCallback>((data) => {
|
|
expect(data).toMatchSnapshot('onActionOpen');
|
|
}),
|
|
onActionClose: vi.fn<ActionCallback>((data) => {
|
|
expect(data).toMatchSnapshot('onActionClose');
|
|
}),
|
|
};
|
|
|
|
const parser = new StreamingMessageParser({
|
|
artifactElement: () => '',
|
|
callbacks,
|
|
});
|
|
|
|
let message = '';
|
|
|
|
let result = '';
|
|
|
|
const chunks = Array.isArray(input) ? input : input.split('');
|
|
|
|
for (const chunk of chunks) {
|
|
message += chunk;
|
|
|
|
result += parser.parse('message_1', message);
|
|
}
|
|
|
|
for (const name in expected.callbacks) {
|
|
const callbackName = name;
|
|
|
|
expect(callbacks[callbackName as keyof typeof callbacks]).toHaveBeenCalledTimes(
|
|
expected.callbacks[callbackName as keyof typeof expected.callbacks] ?? 0,
|
|
);
|
|
}
|
|
|
|
expect(result).toEqual(expected.output);
|
|
}
|