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,459 @@
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { defineComponent, h } from 'vue'
|
||||
// Import Vue 2 components
|
||||
import HtmlBlockNode from '../packages/markstream-vue2/src/components/HtmlBlockNode/HtmlBlockNode.vue'
|
||||
import HtmlInlineNode from '../packages/markstream-vue2/src/components/HtmlInlineNode/HtmlInlineNode.vue'
|
||||
import { setCustomComponents } from '../packages/markstream-vue2/src/utils/nodeComponents'
|
||||
import { flushAll } from './setup/flush-all'
|
||||
|
||||
// Mock custom components
|
||||
const TestComponent = defineComponent({
|
||||
name: 'TestComponent',
|
||||
props: ['dataType', 'active'],
|
||||
setup(props, { slots }) {
|
||||
return () => h(
|
||||
'div',
|
||||
{
|
||||
'class': 'test-component',
|
||||
'data-type': props.dataType,
|
||||
'data-active': props.active,
|
||||
},
|
||||
slots.default?.() || 'Test Component',
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const NestedComponent = defineComponent({
|
||||
name: 'NestedComponent',
|
||||
setup(_, { slots }) {
|
||||
return () => h('div', { class: 'nested-component' }, slots.default?.())
|
||||
},
|
||||
})
|
||||
|
||||
describe('vue 2 - HtmlBlockNode Custom Components Integration', () => {
|
||||
const testId = 'test-vue2-html-block'
|
||||
|
||||
beforeEach(() => {
|
||||
setCustomComponents(testId, {
|
||||
testcomp: TestComponent,
|
||||
nestedcomp: NestedComponent,
|
||||
})
|
||||
})
|
||||
|
||||
it('should render custom component in HTML block', () => {
|
||||
const wrapper = mount(HtmlBlockNode, {
|
||||
props: {
|
||||
node: {
|
||||
content: '<testcomp data-type="block">Content</testcomp>',
|
||||
loading: false,
|
||||
},
|
||||
customId: testId,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.test-component').exists()).toBe(true)
|
||||
expect(wrapper.find('.test-component').attributes('data-type')).toBe('block')
|
||||
expect(wrapper.html()).toContain('Content')
|
||||
})
|
||||
|
||||
it('should render nested custom components', () => {
|
||||
const wrapper = mount(HtmlBlockNode, {
|
||||
props: {
|
||||
node: {
|
||||
content: '<testcomp data-type="outer">Outer <nestedcomp>Nested</nestedcomp> End</testcomp>',
|
||||
loading: false,
|
||||
},
|
||||
customId: testId,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.test-component').exists()).toBe(true)
|
||||
expect(wrapper.find('.nested-component').exists()).toBe(true)
|
||||
expect(wrapper.html()).toContain('Outer')
|
||||
expect(wrapper.html()).toContain('Nested')
|
||||
expect(wrapper.html()).toContain('End')
|
||||
})
|
||||
|
||||
it('should render deeply nested custom components', () => {
|
||||
const wrapper = mount(HtmlBlockNode, {
|
||||
props: {
|
||||
node: {
|
||||
content: `
|
||||
<testcomp data-type="level1">
|
||||
Level 1
|
||||
<nestedcomp>
|
||||
Level 2
|
||||
<testcomp data-type="level3">Level 3</testcomp>
|
||||
</nestedcomp>
|
||||
</testcomp>
|
||||
`,
|
||||
loading: false,
|
||||
},
|
||||
customId: testId,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.test-component').exists()).toBe(true)
|
||||
expect(wrapper.findAll('.test-component').length).toBeGreaterThan(0)
|
||||
expect(wrapper.findAll('.nested-component').length).toBeGreaterThan(0)
|
||||
expect(wrapper.html()).toContain('Level 1')
|
||||
expect(wrapper.html()).toContain('Level 2')
|
||||
expect(wrapper.html()).toContain('Level 3')
|
||||
})
|
||||
|
||||
it('should render mixed standard HTML and custom components', () => {
|
||||
const wrapper = mount(HtmlBlockNode, {
|
||||
props: {
|
||||
node: {
|
||||
content: '<div class="standard"><testcomp data-type="mixed">Component</testcomp><p>Paragraph</p></div>',
|
||||
loading: false,
|
||||
},
|
||||
customId: testId,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.standard').exists()).toBe(true)
|
||||
expect(wrapper.find('.test-component').exists()).toBe(true)
|
||||
expect(wrapper.find('p').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should use v-html when no custom components are present', () => {
|
||||
const wrapper = mount(HtmlBlockNode, {
|
||||
props: {
|
||||
node: {
|
||||
content: '<div class="standard">Pure HTML</div>',
|
||||
loading: false,
|
||||
},
|
||||
customId: testId,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.standard').exists()).toBe(true)
|
||||
expect(wrapper.html()).toContain('Pure HTML')
|
||||
})
|
||||
|
||||
it('should sanitize raw HTML fallback content in blocks', async () => {
|
||||
const wrapper = mount(HtmlBlockNode, {
|
||||
props: {
|
||||
node: {
|
||||
content: '<div><img src="x" onerror="alert(1)"><a href="javascript:alert(1)" title="ok">Link</a><script>alert(1)</script></div>',
|
||||
loading: false,
|
||||
},
|
||||
customId: testId,
|
||||
},
|
||||
})
|
||||
|
||||
await flushAll()
|
||||
const img = wrapper.find('img')
|
||||
const link = wrapper.find('a')
|
||||
|
||||
expect(img.exists()).toBe(true)
|
||||
expect(img.attributes('onerror')).toBeUndefined()
|
||||
expect(link.exists()).toBe(true)
|
||||
expect(link.attributes('href')).toBeUndefined()
|
||||
expect(link.attributes('title')).toBe('ok')
|
||||
expect(wrapper.html()).not.toContain('<script')
|
||||
expect(wrapper.html()).not.toContain('alert(1)')
|
||||
})
|
||||
|
||||
it('should pass props correctly to custom components', () => {
|
||||
const wrapper = mount(HtmlBlockNode, {
|
||||
props: {
|
||||
node: {
|
||||
content: '<testcomp data-type="test" active="true">With Props</testcomp>',
|
||||
loading: false,
|
||||
},
|
||||
customId: testId,
|
||||
},
|
||||
})
|
||||
|
||||
const comp = wrapper.find('.test-component')
|
||||
expect(comp.attributes('data-type')).toBe('test')
|
||||
expect(comp.attributes('data-active')).toBe('true')
|
||||
})
|
||||
|
||||
it('should sanitize dangerous attributes', () => {
|
||||
const wrapper = mount(HtmlBlockNode, {
|
||||
props: {
|
||||
node: {
|
||||
content: '<testcomp onclick="alert(1)" data-type="safe">Content</testcomp>',
|
||||
loading: false,
|
||||
},
|
||||
customId: testId,
|
||||
},
|
||||
})
|
||||
|
||||
const comp = wrapper.find('.test-component')
|
||||
expect(comp.attributes('onclick')).toBeUndefined()
|
||||
expect(comp.attributes('data-type')).toBe('safe')
|
||||
})
|
||||
|
||||
it('should render placeholder when loading is true', async () => {
|
||||
const wrapper = mount(HtmlBlockNode, {
|
||||
props: {
|
||||
node: {
|
||||
content: '<testcomp>Content</testcomp>',
|
||||
loading: true,
|
||||
},
|
||||
customId: testId,
|
||||
},
|
||||
})
|
||||
|
||||
// Should show placeholder initially
|
||||
expect(wrapper.find('.html-block-node__placeholder').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should render structured markdown children inside standard html wrappers once', async () => {
|
||||
const wrapper = mount(HtmlBlockNode, {
|
||||
props: {
|
||||
node: {
|
||||
tag: 'span',
|
||||
content: '<span style="font-size: 12px;"></span>',
|
||||
attrs: [['style', 'font-size: 12px;']],
|
||||
children: [
|
||||
{
|
||||
type: 'list',
|
||||
raw: '',
|
||||
ordered: false,
|
||||
items: [
|
||||
{
|
||||
type: 'list_item',
|
||||
raw: '',
|
||||
children: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
raw: '',
|
||||
children: [{ type: 'text', raw: '', content: 'alpha' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'list_item',
|
||||
raw: '',
|
||||
children: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
raw: '',
|
||||
children: [{ type: 'text', raw: '', content: 'beta' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
loading: false,
|
||||
},
|
||||
customId: testId,
|
||||
},
|
||||
})
|
||||
|
||||
await flushAll()
|
||||
|
||||
const root = wrapper.find('.html-block-node')
|
||||
expect(root.element.tagName).toBe('SPAN')
|
||||
expect(root.attributes('style')).toContain('font-size: 12px;')
|
||||
expect(wrapper.findAll('ul')).toHaveLength(1)
|
||||
expect(wrapper.findAll('li')).toHaveLength(2)
|
||||
expect((wrapper.text().match(/alpha/g) || []).length).toBe(1)
|
||||
expect((wrapper.text().match(/beta/g) || []).length).toBe(1)
|
||||
})
|
||||
|
||||
it('should sanitize dangerous attrs on structured wrapper roots', async () => {
|
||||
const wrapper = mount(HtmlBlockNode, {
|
||||
props: {
|
||||
node: {
|
||||
tag: 'a',
|
||||
content: '<a href="javascript:alert(1)" onclick="alert(1)" data-safe="ok"></a>',
|
||||
attrs: [
|
||||
['href', 'javascript:alert(1)'],
|
||||
['onclick', 'alert(1)'],
|
||||
['data-safe', 'ok'],
|
||||
],
|
||||
children: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
raw: '',
|
||||
children: [{ type: 'text', raw: '', content: 'safe child' }],
|
||||
},
|
||||
],
|
||||
loading: false,
|
||||
},
|
||||
customId: testId,
|
||||
},
|
||||
})
|
||||
|
||||
await flushAll()
|
||||
|
||||
const root = wrapper.find('.html-block-node')
|
||||
expect(root.element.tagName).toBe('A')
|
||||
expect(root.attributes('data-safe')).toBe('ok')
|
||||
expect(root.attributes('href')).toBeUndefined()
|
||||
expect(root.attributes('onclick')).toBeUndefined()
|
||||
expect(wrapper.text()).toContain('safe child')
|
||||
})
|
||||
})
|
||||
|
||||
describe('vue 2 - HtmlInlineNode Custom Components Integration', () => {
|
||||
const testId = 'test-vue2-html-inline'
|
||||
|
||||
beforeEach(() => {
|
||||
setCustomComponents(testId, {
|
||||
testcomp: TestComponent,
|
||||
nestedcomp: NestedComponent,
|
||||
})
|
||||
})
|
||||
|
||||
it('should render custom component inline', () => {
|
||||
const wrapper = mount(HtmlInlineNode, {
|
||||
props: {
|
||||
node: {
|
||||
type: 'html_inline',
|
||||
content: '<testcomp data-type="inline">Content</testcomp>',
|
||||
loading: false,
|
||||
},
|
||||
customId: testId,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.test-component').exists()).toBe(true)
|
||||
expect(wrapper.html()).toContain('Content')
|
||||
})
|
||||
|
||||
it('should render nested custom components inline', () => {
|
||||
const wrapper = mount(HtmlInlineNode, {
|
||||
props: {
|
||||
node: {
|
||||
type: 'html_inline',
|
||||
content: 'Text <testcomp data-type="outer"><nestedcomp>Nested</nestedcomp></testcomp> more',
|
||||
loading: false,
|
||||
},
|
||||
customId: testId,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.test-component').exists()).toBe(true)
|
||||
expect(wrapper.find('.nested-component').exists()).toBe(true)
|
||||
expect(wrapper.html()).toContain('Text')
|
||||
expect(wrapper.html()).toContain('more')
|
||||
})
|
||||
|
||||
it('should use DOM rendering for pure HTML', () => {
|
||||
const wrapper = mount(HtmlInlineNode, {
|
||||
props: {
|
||||
node: {
|
||||
type: 'html_inline',
|
||||
content: '<span class="standard">Pure HTML</span>',
|
||||
loading: false,
|
||||
},
|
||||
customId: testId,
|
||||
},
|
||||
})
|
||||
|
||||
// HtmlInlineNode uses DOM manipulation, check that the container exists
|
||||
expect(wrapper.find('.html-inline-node').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should sanitize raw HTML fallback content inline', async () => {
|
||||
const wrapper = mount(HtmlInlineNode, {
|
||||
props: {
|
||||
node: {
|
||||
type: 'html_inline',
|
||||
content: 'Before <img src="x" onerror="alert(1)"><a href="javascript:alert(1)" title="ok">Link</a> After',
|
||||
loading: false,
|
||||
},
|
||||
customId: testId,
|
||||
},
|
||||
})
|
||||
|
||||
await flushAll()
|
||||
const img = wrapper.find('img')
|
||||
const link = wrapper.find('a')
|
||||
|
||||
expect(img.exists()).toBe(true)
|
||||
expect(img.attributes('onerror')).toBeUndefined()
|
||||
expect(link.exists()).toBe(true)
|
||||
expect(link.attributes('href')).toBeUndefined()
|
||||
expect(link.attributes('title')).toBe('ok')
|
||||
expect(wrapper.html()).not.toContain('alert(1)')
|
||||
})
|
||||
|
||||
it('should handle mixed inline content', () => {
|
||||
const wrapper = mount(HtmlInlineNode, {
|
||||
props: {
|
||||
node: {
|
||||
type: 'html_inline',
|
||||
content: 'Before <testcomp data-type="test">Component</testcomp> After',
|
||||
loading: false,
|
||||
},
|
||||
customId: testId,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.test-component').exists()).toBe(true)
|
||||
expect(wrapper.html()).toContain('Before')
|
||||
expect(wrapper.html()).toContain('After')
|
||||
})
|
||||
|
||||
it('should handle loading state', () => {
|
||||
const wrapper = mount(HtmlInlineNode, {
|
||||
props: {
|
||||
node: {
|
||||
type: 'html_inline',
|
||||
content: '<testcomp>Loading Content</testcomp>',
|
||||
loading: true,
|
||||
autoClosed: false,
|
||||
},
|
||||
customId: testId,
|
||||
},
|
||||
})
|
||||
|
||||
// Should show loading text
|
||||
expect(wrapper.find('.html-inline-node--loading').exists()).toBe(true)
|
||||
expect(wrapper.html()).toContain('Loading Content')
|
||||
})
|
||||
})
|
||||
|
||||
describe('vue 2 - Component Behavior', () => {
|
||||
const testId = 'test-vue2-behavior'
|
||||
|
||||
beforeEach(() => {
|
||||
setCustomComponents(testId, {
|
||||
testcomp: TestComponent,
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle empty content', () => {
|
||||
const wrapper = mount(HtmlBlockNode, {
|
||||
props: {
|
||||
node: {
|
||||
content: '',
|
||||
loading: false,
|
||||
},
|
||||
customId: testId,
|
||||
},
|
||||
})
|
||||
|
||||
// Empty content should render empty container
|
||||
expect(wrapper.find('.html-block-node').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle content with only whitespace', () => {
|
||||
const wrapper = mount(HtmlBlockNode, {
|
||||
props: {
|
||||
node: {
|
||||
content: ' \n\t ',
|
||||
loading: false,
|
||||
},
|
||||
customId: testId,
|
||||
},
|
||||
})
|
||||
|
||||
// Whitespace-only content should still render container
|
||||
expect(wrapper.find('.html-block-node').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user