fix(chat): animate inserted Markdown lists and code lines

This commit is contained in:
Danila Poyarkov 2026-09-10 19:49:37 +03:00
parent 3d74f52344
commit 608cbc9960
2 changed files with 72 additions and 0 deletions

View file

@ -7,6 +7,14 @@
.chat-markdown {
--shiki-dark-bg: var(--color-input);
&[data-chat-markdown-mode='streaming'] {
[data-stream-markdown='li'],
[data-stream-markdown='code-block'],
[data-stream-markdown='code-line'] {
@apply animate-in fade-in duration-200 motion-reduce:animate-none;
}
}
@apply text-[11px] leading-4;
[data-stream-markdown='blockquote'],

View file

@ -0,0 +1,64 @@
import { expect, test } from '@playwright/test'
import { CanvasHelper } from '#tests/helpers/canvas'
test('streaming lists and code lines animate only when inserted', async ({ page }) => {
await page.goto('/?test')
await new CanvasHelper(page).waitForInit()
const fixture = await page.evaluateHandle(async () => {
const vuePath = '/node_modules/.vite/deps/vue.js'
const markdownPath = '/src/components/chat/ChatMarkdown.vue'
const { createApp, h, ref, nextTick } = await import(vuePath)
const { default: ChatMarkdown } = await import(markdownPath)
const content = ref('A paragraph.\n\n- First item\n')
const mode = ref('streaming')
const host = document.createElement('div')
host.dataset.slot = 'streaming-animation-fixture'
document.body.append(host)
const app = createApp(() => h(ChatMarkdown, { content: content.value, mode: mode.value }))
app.mount(host)
return {
async append(text: string) {
content.value += text
await nextTick()
},
async settle() {
mode.value = 'static'
await nextTick()
},
dispose() {
app.unmount()
host.remove()
}
}
})
try {
const markdown = page.locator('[data-slot="streaming-animation-fixture"]')
const first = markdown.locator('[data-stream-markdown="li"]').first()
await expect(first).toHaveCSS('animation-name', 'enter')
await fixture.evaluate((state) =>
state.append('\n- Second item\n\n```typescript\nconst a = 1\n')
)
await expect(markdown.locator('[data-stream-markdown="li"]')).toHaveCount(2)
const code = markdown.locator('[data-stream-markdown="code-block"]')
await expect(code).toBeVisible()
await expect(code).toHaveCSS('animation-name', 'enter')
const line = markdown.locator('[data-stream-markdown="code-line"]').first()
await expect(line).toHaveCSS('animation-name', 'enter')
await fixture.evaluate((state) => state.append('const b = 2\n```'))
await expect(code).toContainText('const b = 2')
await page.emulateMedia({ reducedMotion: 'reduce' })
await expect(first).toHaveCSS('animation-name', 'none')
await expect(line).toHaveCSS('animation-name', 'none')
await fixture.evaluate((state) => state.settle())
await page.emulateMedia({ reducedMotion: 'no-preference' })
await expect(first).toHaveCSS('animation-name', 'none')
await expect(markdown.locator('[data-stream-markdown="code-block"]')).toHaveCSS(
'animation-name',
'none'
)
} finally {
await fixture.evaluate((state) => state.dispose())
await fixture.dispose()
}
})