diff --git a/packages/host/tests/integration/components/room-message-test.gts b/packages/host/tests/integration/components/room-message-test.gts index c7a698d196..922151a107 100644 --- a/packages/host/tests/integration/components/room-message-test.gts +++ b/packages/host/tests/integration/components/room-message-test.gts @@ -23,11 +23,12 @@ module('Integration | Component | RoomMessage', function (hooks) { isStreaming: boolean, timeAgoForCreated: number, timeAgoForUpdated: number, + messageContent: string, ) { let message = { author: { userId: '@aibot:localhost' }, - message: 'Hello,', - formattedMessage: 'Hello, ', + message: messageContent, + formattedMessage: messageContent, created: new Date(new Date().getTime() - timeAgoForCreated * 60 * 1000), updated: new Date(new Date().getTime() - timeAgoForUpdated * 60 * 1000), }; @@ -67,7 +68,7 @@ module('Integration | Component | RoomMessage', function (hooks) { } test('it shows an error when AI bot message streaming timeouts', async function (assert) { - let testScenario = await setupTestScenario(true, 2, 1); // Streaming, created 2 mins ago, updated 1 min ago + let testScenario = await setupTestScenario(true, 2, 1, 'Hello,'); // Streaming, created 2 mins ago, updated 1 min ago await renderRoomMessageComponent(testScenario); await waitUntil( @@ -85,7 +86,7 @@ module('Integration | Component | RoomMessage', function (hooks) { }); test('it does not show an error when last streaming chunk is still within reasonable time limit', async function (assert) { - let testScenario = await setupTestScenario(true, 2, 0.5); // Streaming, created 2 mins ago, updated 30 seconds ago + let testScenario = await setupTestScenario(true, 2, 0.5, 'Hello,'); // Streaming, created 2 mins ago, updated 30 seconds ago await renderRoomMessageComponent(testScenario); assert @@ -96,4 +97,30 @@ module('Integration | Component | RoomMessage', function (hooks) { .dom('[data-test-ai-message-content] span.streaming-text') .includesText('Hello,'); }); + + test('it escapes html code that is in code tags', async function (assert) { + let testScenario = await setupTestScenario( + true, + 0, + 0, + ` +\`\`\`typescript + +\`\`\` +`, + ); + await renderRoomMessageComponent(testScenario); + + let content = document.querySelector( + '[data-test-ai-message-content]', + )?.innerHTML; + assert.ok( + content?.includes(`<template> + <h1>Hello, world!</h1> +</template>`), + 'rendered code snippet in a streaming message should contain escaped HTML template so that we see code, not rendered html', + ); + }); }); diff --git a/packages/runtime-common/marked-sync.ts b/packages/runtime-common/marked-sync.ts index d672a09e81..1ae85cd661 100644 --- a/packages/runtime-common/marked-sync.ts +++ b/packages/runtime-common/marked-sync.ts @@ -22,7 +22,7 @@ export function markedSync(markdown: string) { // also note that since we are in common, we don't have ember-window-mock // available to us. globalThis.localStorage?.setItem(id, code); - return `
${code}
`; + return `
${escapeHtmlInPreTags(code)}
`; }, }, }) @@ -32,3 +32,11 @@ export function markedSync(markdown: string) { export function markdownToHtml(markdown: string | null | undefined): string { return markdown ? sanitizeHtml(markedSync(markdown)) : ''; } + +function escapeHtmlInPreTags(html: string) { + // For example, html can be

Hello

+ // We want to escape the

Hello

so that it is rendered as + //
<h1>Hello</h1>
, otherwise the h1 will + // be rendered as a real header, not code (same applies for other html tags, such as