e0a5f57793
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.
152 lines
4.4 KiB
TypeScript
152 lines
4.4 KiB
TypeScript
import { mount } from '@vue/test-utils'
|
|
import { describe, expect, it } from 'vitest'
|
|
import { nextTick } from 'vue'
|
|
import MarkdownCodeBlockNode from '../src/components/MarkdownCodeBlockNode/MarkdownCodeBlockNode.vue'
|
|
|
|
function makeNode(code = 'console.log(1)') {
|
|
return {
|
|
type: 'code_block' as const,
|
|
language: 'plaintext',
|
|
code,
|
|
raw: `\`\`\`txt\n${code}\n\`\`\``,
|
|
}
|
|
}
|
|
|
|
function makeScrollable(element: HTMLElement) {
|
|
Object.defineProperty(element, 'scrollHeight', {
|
|
configurable: true,
|
|
value: 1000,
|
|
})
|
|
Object.defineProperty(element, 'clientHeight', {
|
|
configurable: true,
|
|
value: 100,
|
|
})
|
|
element.scrollTop = 0
|
|
}
|
|
|
|
async function flushCodeWatchers() {
|
|
await nextTick()
|
|
await Promise.resolve()
|
|
await nextTick()
|
|
}
|
|
|
|
describe('markdownCodeBlockNode props and features', () => {
|
|
it('should support all header control props', () => {
|
|
const expectedProps = [
|
|
'showHeader',
|
|
'showCopyButton',
|
|
'showExpandButton',
|
|
'showPreviewButton',
|
|
'showFontSizeButtons',
|
|
'showTooltips',
|
|
'enableFontSizeControl',
|
|
]
|
|
|
|
// These props should now be supported on MarkdownCodeBlockNode
|
|
expectedProps.forEach((prop) => {
|
|
expect(prop).toBeTruthy()
|
|
})
|
|
})
|
|
|
|
it('should support language detection for preview', () => {
|
|
const htmlLanguages = ['html', 'svg']
|
|
const isPreviewable = (lang: string, isShowPreview: boolean) => {
|
|
return isShowPreview && htmlLanguages.includes(lang.toLowerCase())
|
|
}
|
|
|
|
expect(isPreviewable('html', true)).toBe(true)
|
|
expect(isPreviewable('svg', true)).toBe(true)
|
|
expect(isPreviewable('javascript', true)).toBe(false)
|
|
expect(isPreviewable('html', false)).toBe(false)
|
|
})
|
|
|
|
it('should support font size controls', () => {
|
|
const codeFontMin = 10
|
|
const codeFontMax = 36
|
|
const codeFontStep = 1
|
|
const defaultSize = 14
|
|
|
|
const increaseFont = (current: number) => Math.min(codeFontMax, current + codeFontStep)
|
|
const decreaseFont = (current: number) => Math.max(codeFontMin, current - codeFontStep)
|
|
const resetFont = () => defaultSize
|
|
|
|
expect(increaseFont(14)).toBe(15)
|
|
expect(increaseFont(36)).toBe(36)
|
|
expect(decreaseFont(14)).toBe(13)
|
|
expect(decreaseFont(10)).toBe(10)
|
|
expect(resetFont()).toBe(14)
|
|
})
|
|
|
|
it('should emit proper events', () => {
|
|
const expectedEvents = ['previewCode', 'copy']
|
|
expectedEvents.forEach((event) => {
|
|
expect(event).toBeTruthy()
|
|
})
|
|
})
|
|
|
|
it('should support named slots', () => {
|
|
const expectedSlots = ['header-left', 'header-right']
|
|
expectedSlots.forEach((slot) => {
|
|
expect(slot).toBeTruthy()
|
|
})
|
|
})
|
|
|
|
it('should support auto-scroll to bottom behavior', () => {
|
|
// Test the isAtBottom helper function logic with 50px threshold
|
|
const isAtBottom = (scrollHeight: number, scrollTop: number, clientHeight: number, threshold = 50): boolean => {
|
|
return scrollHeight - scrollTop - clientHeight <= threshold
|
|
}
|
|
|
|
// Test when at bottom
|
|
expect(isAtBottom(1000, 900, 100, 50)).toBe(true) // exactly at bottom
|
|
expect(isAtBottom(1000, 860, 100, 50)).toBe(true) // within threshold (40px from bottom)
|
|
|
|
// Test when not at bottom
|
|
expect(isAtBottom(1000, 840, 100, 50)).toBe(false) // beyond threshold (60px from bottom)
|
|
expect(isAtBottom(1000, 500, 100, 50)).toBe(false) // in middle
|
|
expect(isAtBottom(1000, 0, 100, 50)).toBe(false) // at top
|
|
})
|
|
|
|
it('should not auto-scroll when autoScrollOnUpdate is false', async () => {
|
|
const wrapper = mount(MarkdownCodeBlockNode, {
|
|
props: {
|
|
loading: false,
|
|
node: makeNode(),
|
|
autoScrollOnUpdate: false,
|
|
},
|
|
})
|
|
|
|
const content = wrapper.get('.code-block-content').element as HTMLElement
|
|
makeScrollable(content)
|
|
|
|
await wrapper.setProps({
|
|
node: makeNode('line\n'.repeat(80)),
|
|
})
|
|
await flushCodeWatchers()
|
|
|
|
expect(content.scrollTop).toBe(0)
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('should start with auto-scroll disabled when autoScrollInitial is false', async () => {
|
|
const wrapper = mount(MarkdownCodeBlockNode, {
|
|
props: {
|
|
loading: false,
|
|
node: makeNode(),
|
|
autoScrollInitial: false,
|
|
},
|
|
})
|
|
|
|
const content = wrapper.get('.code-block-content').element as HTMLElement
|
|
makeScrollable(content)
|
|
|
|
await wrapper.setProps({
|
|
node: makeNode('line\n'.repeat(80)),
|
|
})
|
|
await flushCodeWatchers()
|
|
|
|
expect(content.scrollTop).toBe(0)
|
|
wrapper.unmount()
|
|
})
|
|
})
|