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.
This commit is contained in:
@@ -0,0 +1,392 @@
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
/* eslint-disable antfu/no-import-node-modules-by-path */
|
||||
import React, { act, StrictMode } from '../packages/markstream-react/node_modules/react'
|
||||
import { createRoot } from '../packages/markstream-react/node_modules/react-dom/client'
|
||||
import { LinkNode } from '../packages/markstream-react/src/components/LinkNode/LinkNode'
|
||||
import { NodeRenderer } from '../packages/markstream-react/src/components/NodeRenderer'
|
||||
import { TextNode } from '../packages/markstream-react/src/components/TextNode/TextNode'
|
||||
|
||||
async function flushReact() {
|
||||
await act(async () => {
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
})
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = ''
|
||||
;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = false
|
||||
})
|
||||
|
||||
describe('markstream-react text streaming fade', () => {
|
||||
it('keeps only the latest appended suffix animating during rapid streaming updates, like vue3', async () => {
|
||||
;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true
|
||||
|
||||
const host = document.createElement('div')
|
||||
document.body.appendChild(host)
|
||||
const root = createRoot(host)
|
||||
const textStreamState = new Map<string, string>()
|
||||
const renderText = (content: string) =>
|
||||
React.createElement(StrictMode, null, React.createElement(TextNode as any, {
|
||||
node: {
|
||||
type: 'text',
|
||||
content,
|
||||
},
|
||||
ctx: {
|
||||
typewriter: true,
|
||||
textStreamState,
|
||||
},
|
||||
indexKey: 'stream-0',
|
||||
}))
|
||||
|
||||
await act(async () => {
|
||||
root.render(renderText('Hello'))
|
||||
})
|
||||
await flushReact()
|
||||
|
||||
expect(host.textContent).toBe('Hello')
|
||||
expect(host.querySelectorAll('.text-node-stream-delta')).toHaveLength(0)
|
||||
|
||||
await act(async () => {
|
||||
root.render(renderText('HelloWorld'))
|
||||
})
|
||||
await flushReact()
|
||||
|
||||
let deltas = Array.from(host.querySelectorAll('.text-node-stream-delta'))
|
||||
expect(deltas).toHaveLength(1)
|
||||
expect(deltas.map(delta => delta.textContent)).toEqual(['World'])
|
||||
expect(host.textContent).toBe('HelloWorld')
|
||||
|
||||
await act(async () => {
|
||||
root.render(renderText('HelloWorld'))
|
||||
})
|
||||
await flushReact()
|
||||
|
||||
deltas = Array.from(host.querySelectorAll('.text-node-stream-delta'))
|
||||
expect(deltas).toHaveLength(1)
|
||||
expect(deltas.map(delta => delta.textContent)).toEqual(['World'])
|
||||
|
||||
await act(async () => {
|
||||
root.render(renderText('HelloWorldAgain'))
|
||||
})
|
||||
await flushReact()
|
||||
|
||||
deltas = Array.from(host.querySelectorAll('.text-node-stream-delta'))
|
||||
expect(deltas).toHaveLength(1)
|
||||
expect(deltas.map(delta => delta.textContent)).toEqual(['Again'])
|
||||
expect(host.textContent).toBe('HelloWorldAgain')
|
||||
|
||||
await act(async () => {
|
||||
deltas[0]?.dispatchEvent(new Event('animationend', { bubbles: true }))
|
||||
})
|
||||
await flushReact()
|
||||
|
||||
expect(host.querySelectorAll('.text-node-stream-delta')).toHaveLength(0)
|
||||
expect(host.textContent).toBe('HelloWorldAgain')
|
||||
|
||||
await act(async () => {
|
||||
root.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves appended-text fade spans through the full NodeRenderer streaming path', async () => {
|
||||
;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true
|
||||
|
||||
const host = document.createElement('div')
|
||||
document.body.appendChild(host)
|
||||
const root = createRoot(host)
|
||||
const renderMarkdown = (content: string) =>
|
||||
React.createElement(StrictMode, null, React.createElement(NodeRenderer as any, {
|
||||
content,
|
||||
deferNodesUntilVisible: false,
|
||||
viewportPriority: false,
|
||||
}))
|
||||
|
||||
await act(async () => {
|
||||
root.render(renderMarkdown('Hello'))
|
||||
})
|
||||
await flushReact()
|
||||
|
||||
await act(async () => {
|
||||
root.render(renderMarkdown('HelloWorld'))
|
||||
})
|
||||
await flushReact()
|
||||
|
||||
let deltas = Array.from(host.querySelectorAll('.text-node-stream-delta'))
|
||||
expect(deltas).toHaveLength(1)
|
||||
expect(deltas.map(delta => delta.textContent)).toEqual(['World'])
|
||||
expect(host.textContent).toContain('HelloWorld')
|
||||
|
||||
await act(async () => {
|
||||
root.render(renderMarkdown('HelloWorldAgain'))
|
||||
})
|
||||
await flushReact()
|
||||
|
||||
deltas = Array.from(host.querySelectorAll('.text-node-stream-delta'))
|
||||
expect(deltas).toHaveLength(1)
|
||||
expect(deltas.map(delta => delta.textContent)).toEqual(['Again'])
|
||||
expect(host.textContent).toContain('HelloWorldAgain')
|
||||
|
||||
await act(async () => {
|
||||
root.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
it('settles a finished strong-node delta when following sibling text keeps streaming', async () => {
|
||||
;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true
|
||||
|
||||
const host = document.createElement('div')
|
||||
document.body.appendChild(host)
|
||||
const root = createRoot(host)
|
||||
const renderMarkdown = (content: string) =>
|
||||
React.createElement(StrictMode, null, React.createElement(NodeRenderer as any, {
|
||||
content,
|
||||
batchRendering: false,
|
||||
deferNodesUntilVisible: false,
|
||||
viewportPriority: false,
|
||||
maxLiveNodes: 0,
|
||||
}))
|
||||
|
||||
await act(async () => {
|
||||
root.render(renderMarkdown('1. **记忆化递归(动态规划'))
|
||||
})
|
||||
await flushReact()
|
||||
|
||||
await act(async () => {
|
||||
root.render(renderMarkdown('1. **记忆化递归(动态规划)*'))
|
||||
})
|
||||
await flushReact()
|
||||
|
||||
let strongDelta = host.querySelector('.strong-node .text-node-stream-delta')
|
||||
expect(strongDelta?.textContent).toBe(')')
|
||||
|
||||
await act(async () => {
|
||||
root.render(renderMarkdown('1. **记忆化递归(动态规划)**:'))
|
||||
})
|
||||
await flushReact()
|
||||
|
||||
strongDelta = host.querySelector('.strong-node .text-node-stream-delta')
|
||||
expect(strongDelta).toBeNull()
|
||||
expect(host.querySelector('.strong-node')?.textContent).toBe('记忆化递归(动态规划)')
|
||||
|
||||
await act(async () => {
|
||||
root.render(renderMarkdown('1. **记忆化递归(动态规划)**:使'))
|
||||
})
|
||||
await flushReact()
|
||||
|
||||
expect(host.querySelector('.strong-node .text-node-stream-delta')).toBeNull()
|
||||
expect(host.textContent).toContain('记忆化递归(动态规划):使')
|
||||
|
||||
await act(async () => {
|
||||
root.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
it('replays appended-text fade inside heading nodes without fading the whole heading', async () => {
|
||||
;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true
|
||||
|
||||
const host = document.createElement('div')
|
||||
document.body.appendChild(host)
|
||||
const root = createRoot(host)
|
||||
const renderMarkdown = (content: string) =>
|
||||
React.createElement(StrictMode, null, React.createElement(NodeRenderer as any, {
|
||||
content,
|
||||
deferNodesUntilVisible: false,
|
||||
viewportPriority: false,
|
||||
}))
|
||||
|
||||
await act(async () => {
|
||||
root.render(renderMarkdown('# Hello'))
|
||||
})
|
||||
await flushReact()
|
||||
|
||||
await act(async () => {
|
||||
root.render(renderMarkdown('# HelloWorld'))
|
||||
})
|
||||
await flushReact()
|
||||
|
||||
const heading = host.querySelector('h1')
|
||||
const deltas = Array.from(host.querySelectorAll('.text-node-stream-delta'))
|
||||
|
||||
expect(heading).toBeTruthy()
|
||||
expect(deltas).toHaveLength(1)
|
||||
expect(deltas[0]?.textContent).toBe('World')
|
||||
expect(heading?.textContent).toBe('HelloWorld')
|
||||
|
||||
await act(async () => {
|
||||
root.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
it('replays appended-text fade inside inline code nodes while preserving the code chip shell', async () => {
|
||||
;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true
|
||||
|
||||
const host = document.createElement('div')
|
||||
document.body.appendChild(host)
|
||||
const root = createRoot(host)
|
||||
const renderMarkdown = (content: string) =>
|
||||
React.createElement(StrictMode, null, React.createElement(NodeRenderer as any, {
|
||||
content,
|
||||
deferNodesUntilVisible: false,
|
||||
viewportPriority: false,
|
||||
}))
|
||||
|
||||
await act(async () => {
|
||||
root.render(renderMarkdown('Use `foo` now'))
|
||||
})
|
||||
await flushReact()
|
||||
|
||||
await act(async () => {
|
||||
root.render(renderMarkdown('Use `foobar` now'))
|
||||
})
|
||||
await flushReact()
|
||||
|
||||
const inlineCode = host.querySelector('code.inline-code')
|
||||
const deltas = Array.from(host.querySelectorAll('code.inline-code .text-node-stream-delta'))
|
||||
|
||||
expect(inlineCode).toBeTruthy()
|
||||
expect(deltas).toHaveLength(1)
|
||||
expect(deltas[0]?.textContent).toBe('bar')
|
||||
expect(inlineCode?.textContent).toBe('foobar')
|
||||
|
||||
await act(async () => {
|
||||
root.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
it('replays appended-text fade while a loading link label grows', async () => {
|
||||
;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true
|
||||
|
||||
const host = document.createElement('div')
|
||||
document.body.appendChild(host)
|
||||
const root = createRoot(host)
|
||||
const textStreamState = new Map<string, string>()
|
||||
const renderLink = (text: string) =>
|
||||
React.createElement(StrictMode, null, React.createElement(LinkNode as any, {
|
||||
node: {
|
||||
type: 'link',
|
||||
href: 'https://example.com',
|
||||
title: null,
|
||||
text,
|
||||
loading: true,
|
||||
},
|
||||
ctx: {
|
||||
typewriter: true,
|
||||
textStreamState,
|
||||
},
|
||||
indexKey: 'link-0',
|
||||
}))
|
||||
|
||||
await act(async () => {
|
||||
root.render(renderLink('Exam'))
|
||||
})
|
||||
await flushReact()
|
||||
|
||||
await act(async () => {
|
||||
root.render(renderLink('Example'))
|
||||
})
|
||||
await flushReact()
|
||||
|
||||
const loading = host.querySelector('.link-loading')
|
||||
const deltas = Array.from(host.querySelectorAll('.link-loading .text-node-stream-delta'))
|
||||
|
||||
expect(loading).toBeTruthy()
|
||||
expect(deltas).toHaveLength(1)
|
||||
expect(deltas[0]?.textContent).toBe('ple')
|
||||
expect(loading?.textContent).toContain('Example')
|
||||
|
||||
await act(async () => {
|
||||
root.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
it('replays appended-text fade inside list items', async () => {
|
||||
;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true
|
||||
|
||||
const host = document.createElement('div')
|
||||
document.body.appendChild(host)
|
||||
const root = createRoot(host)
|
||||
const renderMarkdown = (content: string) =>
|
||||
React.createElement(StrictMode, null, React.createElement(NodeRenderer as any, {
|
||||
content,
|
||||
deferNodesUntilVisible: false,
|
||||
viewportPriority: false,
|
||||
}))
|
||||
|
||||
await act(async () => {
|
||||
root.render(renderMarkdown('- Hello'))
|
||||
})
|
||||
await flushReact()
|
||||
|
||||
await act(async () => {
|
||||
root.render(renderMarkdown('- HelloWorld'))
|
||||
})
|
||||
await flushReact()
|
||||
|
||||
const listItem = host.querySelector('li.list-item')
|
||||
const deltas = Array.from(host.querySelectorAll('li.list-item .text-node-stream-delta'))
|
||||
|
||||
expect(listItem).toBeTruthy()
|
||||
expect(deltas).toHaveLength(1)
|
||||
expect(deltas[0]?.textContent).toBe('World')
|
||||
expect(listItem?.textContent).toContain('HelloWorld')
|
||||
|
||||
await act(async () => {
|
||||
root.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps explanatory list text visible while an inline code span is still streaming', async () => {
|
||||
;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true
|
||||
|
||||
const host = document.createElement('div')
|
||||
document.body.appendChild(host)
|
||||
const root = createRoot(host)
|
||||
const initial = `- **计算工具验证**
|
||||
通过数学计算工具确认结果:`
|
||||
const mid = `${initial}
|
||||
\`363 ÷ 15,135 × 100 = 2.39841427...`
|
||||
const final = `${mid}\``
|
||||
const renderMarkdown = (content: string) =>
|
||||
React.createElement(StrictMode, null, React.createElement(NodeRenderer as any, {
|
||||
content,
|
||||
batchRendering: false,
|
||||
deferNodesUntilVisible: false,
|
||||
viewportPriority: false,
|
||||
maxLiveNodes: 0,
|
||||
}))
|
||||
|
||||
await act(async () => {
|
||||
root.render(renderMarkdown(initial))
|
||||
})
|
||||
await flushReact()
|
||||
|
||||
await act(async () => {
|
||||
root.render(renderMarkdown(mid))
|
||||
})
|
||||
await flushReact()
|
||||
|
||||
const midText = String(host.textContent ?? '').replace(/\s*\n\s*/g, '')
|
||||
expect(midText).toContain('计算工具验证通过数学计算工具确认结果:363 ÷ 15,135 × 100 = 2.39841427')
|
||||
expect(host.querySelector('.strong-node')?.textContent).toBe('计算工具验证')
|
||||
expect(host.querySelector('code.inline-code')?.textContent).toContain('363 ÷ 15,135 × 100 = 2.39841427')
|
||||
|
||||
await act(async () => {
|
||||
root.render(renderMarkdown(final))
|
||||
})
|
||||
await flushReact()
|
||||
|
||||
const finalText = String(host.textContent ?? '').replace(/\s*\n\s*/g, '')
|
||||
expect(finalText).toContain('计算工具验证通过数学计算工具确认结果:363 ÷ 15,135 × 100 = 2.39841427...')
|
||||
expect(host.querySelector('.strong-node')?.textContent).toBe('计算工具验证')
|
||||
expect(host.querySelector('code.inline-code')?.textContent).toBe('363 ÷ 15,135 × 100 = 2.39841427...')
|
||||
|
||||
await act(async () => {
|
||||
root.unmount()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user