Files
BoosAPI/desktop/libs/markstream-vue/test/playground-test-page.smoke.test.ts
T
boos e0a5f57793 feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as
a monorepo subdirectory under desktop/. Source only — node_modules,
dist, Cargo target and the upstream .git history are excluded.

desktop/ keeps its own independent Cargo workspace (desktop/src-tauri
with crates/*); it is not part of the backend boosapi crate, so a
`cargo check` at the metapi root still compiles only the backend. Build
the desktop app from desktop/src-tauri per desktop/README.md.

Note: app icons are still placeholder assets pending the final BoosAPI
logo. The updater signing keys live outside the repo and are not
included.
2026-06-22 11:32:57 +08:00

602 lines
20 KiB
TypeScript

import { mount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import { buildTestPageHref, decodeMarkdownHashAsync } from '../playground-shared/testPageState'
import TestPage from '../playground/src/pages/test.vue'
import { flushAll } from './setup/flush-all'
vi.mock('@iconify/vue', () => ({
Icon: {
name: 'IconStub',
props: {
icon: {
type: String,
default: '',
},
},
template: '<span data-testid="icon">{{ icon }}</span>',
},
}))
vi.mock('../src/components/NodeRenderer', () => ({
default: {
name: 'MarkdownRenderStub',
props: {
content: {
type: String,
default: '',
},
mermaidProps: {
type: Object,
default: () => ({}),
},
d2Props: {
type: Object,
default: () => ({}),
},
infographicProps: {
type: Object,
default: () => ({}),
},
},
template: '<div data-testid="preview">{{ content }}</div>',
},
}))
vi.mock('../src/components/CodeBlockNode', () => ({
default: { name: 'CodeBlockNodeStub', template: '<div />' },
}))
vi.mock('../src/components/MarkdownCodeBlockNode', () => ({
default: { name: 'MarkdownCodeBlockNodeStub', template: '<div />' },
}))
vi.mock('../src/components/PreCodeNode', () => ({
default: { name: 'PreCodeNodeStub', template: '<div />' },
}))
vi.mock('../src/components/CodeBlockNode/monaco', () => ({
getUseMonaco: vi.fn(),
}))
vi.mock('../src/utils/nodeComponents', () => ({
setCustomComponents: vi.fn(),
}))
let katexEnabled = true
vi.mock('../src/components/MathInlineNode/katex', () => ({
enableKatex: vi.fn(() => {
katexEnabled = true
}),
disableKatex: vi.fn(() => {
katexEnabled = false
}),
isKatexEnabled: vi.fn(() => katexEnabled),
}))
let mermaidEnabled = true
vi.mock('../src/components/MermaidBlockNode/mermaid', () => ({
enableMermaid: vi.fn(() => {
mermaidEnabled = true
}),
disableMermaid: vi.fn(() => {
mermaidEnabled = false
}),
isMermaidEnabled: vi.fn(() => mermaidEnabled),
}))
vi.mock('../src/workers/katexWorkerClient', () => ({
setKaTeXWorker: vi.fn(),
}))
vi.mock('../src/workers/mermaidWorkerClient', () => ({
setMermaidWorker: vi.fn(),
}))
vi.mock('../src/workers/katexRenderer.worker?worker&inline', () => ({
default: class FakeKatexWorker {},
}))
vi.mock('../src/workers/mermaidParser.worker?worker&inline', () => ({
default: class FakeMermaidWorker {},
}))
const originalFullscreenElement = Object.getOwnPropertyDescriptor(document, 'fullscreenElement')
const originalExitFullscreen = Object.getOwnPropertyDescriptor(document, 'exitFullscreen')
const originalRequestFullscreen = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'requestFullscreen')
const originalClipboard = Object.getOwnPropertyDescriptor(navigator, 'clipboard')
const originalCreateSVGPoint = typeof SVGSVGElement === 'undefined'
? undefined
: Object.getOwnPropertyDescriptor(SVGSVGElement.prototype, 'createSVGPoint')
async function mountTestPage() {
const wrapper = mount(TestPage)
await nextTick()
return wrapper
}
async function waitForClipboardWrite(callCount = 1) {
const writeText = navigator.clipboard.writeText as ReturnType<typeof vi.fn>
for (let attempt = 0; attempt < 250 && writeText.mock.calls.length < callCount; attempt++) {
await new Promise(resolve => setTimeout(resolve, 20))
await nextTick()
}
expect(writeText).toHaveBeenCalledTimes(callCount)
return writeText
}
function dispatchPaste(target: HTMLTextAreaElement, text: string) {
const event = new Event('paste', { bubbles: true, cancelable: true })
Object.defineProperty(event, 'clipboardData', {
configurable: true,
value: {
getData: (type: string) => type === 'text/plain' ? text : '',
},
})
target.dispatchEvent(event)
return event
}
function createLongMarkdown() {
return Array.from(
{ length: 600 },
(_, index) => `- row ${index}: ${index.toString(36)}${(index * 13).toString(36)}${(index * 29).toString(36)}${(index * 47).toString(36)}`,
).join('\n')
}
function createOversizedMarkdown() {
let seed = 0x12345678
const nextChunk = () => {
seed = (seed * 1664525 + 1013904223) >>> 0
return `${seed.toString(36).padStart(7, '0')}${((seed >>> 1) ^ 0x5A5A5A5A).toString(36).padStart(7, '0')}`
}
return [
'```text',
...Array.from({ length: 2400 }, (_, index) => `${index.toString(36).padStart(4, '0')} ${nextChunk()}${nextChunk()}${nextChunk()}${nextChunk()}`),
'```',
].join('\n')
}
describe('playground /test smoke', () => {
beforeEach(() => {
katexEnabled = true
mermaidEnabled = true
window.localStorage.clear()
window.history.replaceState({}, '', '/test')
document.documentElement.classList.remove('dark')
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: {
writeText: vi.fn().mockResolvedValue(undefined),
},
})
if (typeof SVGSVGElement !== 'undefined' && !SVGSVGElement.prototype.createSVGPoint) {
Object.defineProperty(SVGSVGElement.prototype, 'createSVGPoint', {
configurable: true,
value: () => ({
x: 0,
y: 0,
matrixTransform: () => ({ x: 0, y: 0 }),
}),
})
}
})
afterEach(() => {
vi.useRealTimers()
window.localStorage.clear()
document.documentElement.classList.remove('dark')
if (originalFullscreenElement)
Object.defineProperty(document, 'fullscreenElement', originalFullscreenElement)
else
Reflect.deleteProperty(document, 'fullscreenElement')
if (originalExitFullscreen)
Object.defineProperty(document, 'exitFullscreen', originalExitFullscreen)
else
Reflect.deleteProperty(document, 'exitFullscreen')
if (originalRequestFullscreen)
Object.defineProperty(HTMLElement.prototype, 'requestFullscreen', originalRequestFullscreen)
else
Reflect.deleteProperty(HTMLElement.prototype, 'requestFullscreen')
if (originalClipboard)
Object.defineProperty(navigator, 'clipboard', originalClipboard)
else
Reflect.deleteProperty(navigator, 'clipboard')
if (typeof SVGSVGElement !== 'undefined') {
if (originalCreateSVGPoint)
Object.defineProperty(SVGSVGElement.prototype, 'createSVGPoint', originalCreateSVGPoint)
else
Reflect.deleteProperty(SVGSVGElement.prototype, 'createSVGPoint')
}
})
it('opens the /test route and renders the lab shell', async () => {
const wrapper = await mountTestPage()
expect(wrapper.text()).toContain('Markstream Diagnostic Studio')
expect(wrapper.text()).toContain('Cross-framework Rendering Studio')
expect(wrapper.text()).toContain('版本沙箱')
expect(wrapper.text()).toContain('Angular')
expect(wrapper.get('iframe').attributes('src')).toContain('/test-sandbox?framework=vue3')
wrapper.unmount()
})
it('switches samples and updates cross-framework links with the current markdown', async () => {
const wrapper = await mountTestPage()
const diffButton = wrapper.findAll('button').find(button => button.text().includes('Diff 与代码流'))
expect(diffButton).toBeTruthy()
await diffButton!.trigger('click')
await nextTick()
const textarea = wrapper.get('textarea')
expect((textarea.element as HTMLTextAreaElement).value).toContain('Diff Regression')
await textarea.setValue('## carried across frameworks')
await nextTick()
const vue2Link = wrapper.findAll('a').find(link => link.text().includes('Vue 2'))
expect(vue2Link).toBeTruthy()
const href = vue2Link!.attributes('href') || ''
const hash = new URL(href, 'https://markstream.local').hash
expect(href).toContain('/test#data=')
expect(await decodeMarkdownHashAsync(hash)).toBe('## carried across frameworks')
wrapper.unmount()
})
it('converts pasted literal \\n sequences into real line breaks', async () => {
const wrapper = await mountTestPage()
const textarea = wrapper.get('textarea').element as HTMLTextAreaElement
textarea.value = ''
textarea.selectionStart = 0
textarea.selectionEnd = 0
dispatchPaste(textarea, 'a\\nbxxx\\n\\nc')
await nextTick()
expect(textarea.value).toBe('a\nbxxx\n\nc')
expect(wrapper.get('[data-testid="preview"]').text()).toBe('a\nbxxx\n\nc')
wrapper.unmount()
})
it('keeps escaped \\\\n sequences unchanged when pasting', async () => {
const wrapper = await mountTestPage()
const textarea = wrapper.get('textarea').element as HTMLTextAreaElement
textarea.value = ''
textarea.selectionStart = 0
textarea.selectionEnd = 0
dispatchPaste(textarea, 'a\\\\nb')
await nextTick()
expect(textarea.value).toBe('a\\\\nb')
expect(wrapper.get('[data-testid="preview"]').text()).toBe('a\\\\nb')
wrapper.unmount()
})
it('copies a preview-only share link from the preview toolbar', async () => {
const wrapper = await mountTestPage()
const textarea = wrapper.get('textarea')
await textarea.setValue('## shared preview')
await nextTick()
await wrapper.get('[data-testid="preview-share-button"]').trigger('click')
const writeText = await waitForClipboardWrite()
const href = writeText.mock.calls[0][0]
const url = new URL(href)
expect(url.pathname).toBe('/test')
expect(url.searchParams.get('view')).toBe('preview')
expect(await decodeMarkdownHashAsync(url.hash)).toBe('## shared preview')
wrapper.unmount()
})
it('keeps preview share links in the url for larger markdown inputs', async () => {
const wrapper = await mountTestPage()
const textarea = wrapper.get('textarea')
const longMarkdown = createLongMarkdown()
await textarea.setValue(longMarkdown)
await nextTick()
await wrapper.get('[data-testid="preview-share-button"]').trigger('click')
const writeText = await waitForClipboardWrite()
const href = writeText.mock.calls[0][0]
const url = new URL(href)
expect(url.pathname).toBe('/test')
expect(url.searchParams.get('view')).toBe('preview')
expect(url.searchParams.get('share')).toBeNull()
expect(await decodeMarkdownHashAsync(url.hash)).toBe(longMarkdown)
wrapper.unmount()
})
it('falls back to storage-backed preview share links for oversized markdown', async () => {
const wrapper = await mountTestPage()
const textarea = wrapper.get('textarea')
const oversizedMarkdown = createOversizedMarkdown()
expect(buildTestPageHref('/test', oversizedMarkdown, 'preview').length).toBeGreaterThan(10000)
await textarea.setValue(oversizedMarkdown)
await nextTick()
await wrapper.get('[data-testid="preview-share-button"]').trigger('click')
const writeText = await waitForClipboardWrite()
const href = writeText.mock.calls[0][0]
const url = new URL(href)
const shareId = url.searchParams.get('share')
expect(url.pathname).toBe('/test')
expect(url.searchParams.get('view')).toBe('preview')
expect(url.hash).toBe('')
expect(shareId).toBeTruthy()
const storedShare = window.localStorage.getItem(`vmr-test-share:${shareId}`)
expect(storedShare).toBeTruthy()
expect(JSON.parse(storedShare!)).toMatchObject({
content: oversizedMarkdown,
})
wrapper.unmount()
})
it('starts streaming and progressively updates the preview', async () => {
vi.useFakeTimers()
const wrapper = await mountTestPage()
const textarea = wrapper.get('textarea')
const fullContent = (textarea.element as HTMLTextAreaElement).value
const streamButton = wrapper.findAll('button').find(button => button.text().includes('开始流式渲染'))
expect(streamButton).toBeTruthy()
await streamButton!.trigger('click')
await nextTick()
const preview = wrapper.get('[data-testid="preview"]')
expect(preview.text().length).toBeLessThan(fullContent.length)
await vi.advanceTimersByTimeAsync(800)
await nextTick()
const initialLength = preview.text().length
expect(initialLength).toBeGreaterThan(0)
expect(initialLength).toBeLessThan(fullContent.length)
await vi.advanceTimersByTimeAsync(600)
await nextTick()
expect(preview.text().length).toBeGreaterThan(initialLength)
expect(preview.text().length).toBeLessThan(fullContent.length)
wrapper.unmount()
})
it('toggles preview fullscreen mode from the preview card', async () => {
let fullscreenElement: Element | null = null
let previewCardElement: Element | null = null
const requestFullscreen = vi.fn(async () => {
fullscreenElement = previewCardElement
document.dispatchEvent(new Event('fullscreenchange'))
})
const exitFullscreen = vi.fn(async () => {
fullscreenElement = null
document.dispatchEvent(new Event('fullscreenchange'))
})
Object.defineProperty(document, 'fullscreenElement', {
configurable: true,
get: () => fullscreenElement,
})
Object.defineProperty(document, 'exitFullscreen', {
configurable: true,
value: exitFullscreen,
})
Object.defineProperty(HTMLElement.prototype, 'requestFullscreen', {
configurable: true,
value: requestFullscreen,
})
const wrapper = await mountTestPage()
previewCardElement = wrapper.get('.workspace-card--preview').element
const previewCard = wrapper.get('.workspace-card--preview')
const button = wrapper.get('[data-testid="preview-fullscreen-button"]')
expect(button.text()).toContain('全屏预览')
expect(previewCard.attributes('data-color-scheme')).toBe('light')
expect(previewCard.classes()).not.toContain('workspace-card--preview-dark')
await button.trigger('click')
await nextTick()
expect(requestFullscreen).toHaveBeenCalledTimes(1)
expect(button.text()).toContain('退出全屏')
expect(wrapper.get('[data-testid="immersive-preview-back-button"]').text()).toContain('返回编辑')
await wrapper.get('[data-testid="immersive-preview-theme-button"]').trigger('click')
await nextTick()
expect(previewCard.attributes('data-color-scheme')).toBe('dark')
expect(previewCard.classes()).toContain('workspace-card--preview-dark')
expect(previewCard.classes()).toContain('dark')
await wrapper.get('[data-testid="immersive-preview-back-button"]').trigger('click')
await nextTick()
expect(exitFullscreen).toHaveBeenCalledTimes(1)
expect(button.text()).toContain('全屏预览')
wrapper.unmount()
})
it('opens preview share links directly in preview-only mode', async () => {
window.history.replaceState({}, '', buildTestPageHref('/test', '## shared only', 'preview'))
const wrapper = await mountTestPage()
expect(wrapper.find('textarea').exists()).toBe(false)
expect(wrapper.text()).not.toContain('Cross-framework regression lab')
expect(wrapper.get('[data-testid="preview"]').text()).toBe('## shared only')
expect(wrapper.get('[data-testid="immersive-preview-back-button"]').text()).toContain('Test Page')
expect(wrapper.get('[data-testid="immersive-preview-star-link"]').attributes('href')).toBe('https://github.com/Simon-He95/markstream-vue')
await wrapper.get('[data-testid="immersive-preview-back-button"]').trigger('click')
await flushAll()
expect(wrapper.find('textarea').exists()).toBe(true)
expect(window.location.search).toBe('')
expect(await decodeMarkdownHashAsync(window.location.hash)).toBe('## shared only')
wrapper.unmount()
})
it('opens storage-backed preview links directly in preview-only mode', async () => {
const longMarkdown = createLongMarkdown()
const shareId = 'stored-preview'
window.localStorage.setItem(`vmr-test-share:${shareId}`, longMarkdown)
window.history.replaceState({}, '', `/test?view=preview&share=${shareId}`)
const wrapper = await mountTestPage()
expect(wrapper.find('textarea').exists()).toBe(false)
expect(wrapper.get('[data-testid="preview"]').text()).toBe(longMarkdown)
await wrapper.get('[data-testid="immersive-preview-back-button"]').trigger('click')
await flushAll()
expect(wrapper.find('textarea').exists()).toBe(true)
expect(window.location.search).toBe('')
expect(await decodeMarkdownHashAsync(window.location.hash)).toBe(longMarkdown)
wrapper.unmount()
})
it('removes infographic height caps in preview fullscreen mode', async () => {
let fullscreenElement: Element | null = null
let previewCardElement: Element | null = null
const requestFullscreen = vi.fn(async () => {
fullscreenElement = previewCardElement
document.dispatchEvent(new Event('fullscreenchange'))
})
Object.defineProperty(document, 'fullscreenElement', {
configurable: true,
get: () => fullscreenElement,
})
Object.defineProperty(HTMLElement.prototype, 'requestFullscreen', {
configurable: true,
value: requestFullscreen,
})
const wrapper = await mountTestPage()
previewCardElement = wrapper.get('.workspace-card--preview').element
const preview = wrapper.getComponent({ name: 'MarkdownRenderStub' })
const button = wrapper.get('[data-testid="preview-fullscreen-button"]')
expect(preview.props('infographicProps')).toEqual({ maxHeight: '500px' })
await button.trigger('click')
await nextTick()
expect(preview.props('infographicProps')).toEqual({ maxHeight: 'none' })
wrapper.unmount()
})
it('keeps d2 preview unconstrained so the outer preview pane owns scrolling', async () => {
const wrapper = await mountTestPage()
const preview = wrapper.getComponent({ name: 'MarkdownRenderStub' })
expect(preview.props('d2Props')).toEqual({ maxHeight: 'none' })
wrapper.unmount()
})
it('toggles dark and light appearance from the icon button', async () => {
const wrapper = await mountTestPage()
const page = wrapper.get('.test-lab')
const button = wrapper.get('[data-testid="theme-toggle-button"]')
expect(page.classes()).not.toContain('test-lab--dark')
expect(button.attributes('aria-label')).toBe('切换到暗色模式')
await button.trigger('click')
await nextTick()
expect(page.classes()).toContain('test-lab--dark')
expect(button.attributes('aria-label')).toBe('切换到浅色模式')
await button.trigger('click')
await nextTick()
expect(page.classes()).not.toContain('test-lab--dark')
wrapper.unmount()
})
it('uses the document dark class as the default /test appearance', async () => {
document.documentElement.classList.add('dark')
const wrapper = await mountTestPage()
const page = wrapper.get('.test-lab')
const previewCard = wrapper.get('.workspace-card--preview')
expect(page.classes()).toContain('test-lab--dark')
expect(previewCard.attributes('data-color-scheme')).toBe('dark')
expect(document.documentElement.classList.contains('dark')).toBe(true)
wrapper.unmount()
})
it('syncs stored light mode back to the document theme class', async () => {
window.localStorage.setItem('vmr-test-dark', 'false')
document.documentElement.classList.add('dark')
const wrapper = await mountTestPage()
const page = wrapper.get('.test-lab')
const previewCard = wrapper.get('.workspace-card--preview')
expect(page.classes()).not.toContain('test-lab--dark')
expect(previewCard.attributes('data-color-scheme')).toBe('light')
expect(document.documentElement.classList.contains('dark')).toBe(false)
wrapper.unmount()
})
it('toggles dark and light appearance from the immersive preview toolbar', async () => {
window.history.replaceState({}, '', buildTestPageHref('/test', '## shared only', 'preview'))
const wrapper = await mountTestPage()
const page = wrapper.get('.test-lab')
const button = wrapper.get('[data-testid="immersive-preview-theme-button"]')
expect(page.classes()).not.toContain('test-lab--dark')
expect(button.attributes('aria-label')).toBe('切换到暗色模式')
await button.trigger('click')
await nextTick()
expect(page.classes()).toContain('test-lab--dark')
expect(button.attributes('aria-label')).toBe('切换到浅色模式')
wrapper.unmount()
})
})