Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 132 additions & 1 deletion src/__tests__/plugins/xmind.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import JSZip from 'jszip'
import { describe, expect, it } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import xmind from '../../plugins/xmind.js'

describe('xmind plugin', () => {
Expand Down Expand Up @@ -127,9 +127,140 @@ describe('xmind plugin', () => {
xmind.processJsonTopic({ title: 'Leaf' }, obj)
expect(obj.children).toBeUndefined()
})

it('takes over theme and topic styles, notes, labels and images', () => {
const obj = {}
const context = {
images: [],
theme: {
centralTopic: { properties: { 'svg:fill': '#1F3864', 'fo:font-size': '28pt', 'fo:font-weight': '600' } },
mainTopic: { properties: { 'fo:color': '#333333', 'fo:font-size': '18pt' } },
},
}
xmind.processJsonTopic({
title: 'Root',
style: { properties: { 'fo:color': 'inherited' } },
children: {
attached: [{
title: 'Child',
style: { properties: { 'fo:font-style': 'italic', 'fo:font-size': 'inherited' } },
notes: { plain: { content: 'A note' } },
labels: ['one', 'two'],
image: { src: 'xap:resources/picture.png' },
}],
},
}, obj, context)
expect(obj.data).toEqual({ text: 'Root', background: '#1F3864', color: '#FFFFFF', 'font-size': 28, 'font-weight': 'bold' })
expect(obj.children[0].data).toEqual({ text: 'Child', color: '#333333', 'font-size': 18, 'font-style': 'italic', note: 'A note', resource: ['one', 'two'] })
expect(context.images).toEqual([{ data: obj.children[0].data, path: 'resources/picture.png' }])
})
})

describe('parseStyles', () => {
it('reads styles by id and the style of each family by theme', () => {
const { styles, themes } = xmind.parseStyles(`<xmap-styles xmlns="urn:xmind:xmap:xmlns:style:2.0" xmlns:fo="http://www.w3.org/1999/XSL/Format" xmlns:svg="http://www.w3.org/2000/svg">
<automatic-styles><style id="central" type="topic"><topic-properties svg:fill="#DCE6F2" fo:color="#376092"/></style></automatic-styles>
<master-styles><style id="theme" type="theme"><theme-properties><default-style style-family="centralTopic" style-id="central"/></theme-properties></style></master-styles>
<styles><style id="orange" type="topic"><topic-properties svg:fill="#E46C0A"/></style></styles>
</xmap-styles>`)
expect(styles.orange).toEqual({ 'svg:fill': '#E46C0A' })
expect(themes.theme.centralTopic).toEqual({ 'svg:fill': '#DCE6F2', 'fo:color': '#376092' })
})
})

describe('applyFormatting', () => {
it('replaces links to attached images with the image', () => {
const data = {}
const context = { images: [] }
xmind.applyFormatting(data, { style: {}, href: 'xap:attachments/photo.JPG' }, context)
expect(data.hyperlink).toBeUndefined()
expect(context.images).toEqual([{ data, path: 'attachments/photo.JPG' }])
})

it('drops links to other attached files', () => {
const data = {}
const context = { images: [] }
xmind.applyFormatting(data, { style: {}, href: 'xap:attachments/report.pdf' }, context)
expect(data.hyperlink).toBeUndefined()
expect(context.images).toEqual([])
})
})

describe('folded branches', () => {
it('collapses folded branches of XMind 8 maps', () => {
const obj = {}
xmind.processTopic({ title: 'Folded', branch: 'folded', children: { topics: { topic: { title: 'Child' } } } }, obj)
expect(obj.data.expandState).toBe('collapse')
expect(obj.children[0].data.expandState).toBeUndefined()
})

it('collapses folded branches of XMind 2020 maps', () => {
const obj = {}
xmind.processJsonTopic({ title: 'Folded', branch: 'folded', children: { attached: [{ title: 'Child' }] } }, obj)
expect(obj.data.expandState).toBe('collapse')
expect(obj.children[0].data.expandState).toBeUndefined()
})
})

describe('contrastColor', () => {
it('picks black on light and white on dark fills', () => {
expect(xmind.contrastColor('#FBF09C')).toBe('#000000')
expect(xmind.contrastColor('#4D86DB')).toBe('#FFFFFF')
expect(xmind.contrastColor('red')).toBe('#000000')
})
})

describe('fitImageSize', () => {
it('scales images down to 200 px keeping the aspect ratio', () => {
expect(xmind.fitImageSize(1024, 768)).toEqual({ width: 200, height: 150 })
expect(xmind.fitImageSize(300, 600)).toEqual({ width: 100, height: 200 })
expect(xmind.fitImageSize(80, 60)).toEqual({ width: 80, height: 60 })
})
})

describe('readDocument', () => {
afterEach(() => {
vi.unstubAllGlobals()
})

it('takes over the styles and attached images of XMind 8 maps', async () => {
vi.stubGlobal('createImageBitmap', vi.fn().mockResolvedValue({ width: 400, height: 100 }))
const zip = new JSZip()
zip.file('content.xml', `<xmap-content xmlns="urn:xmind:xmap:xmlns:content:2.0" xmlns:xlink="http://www.w3.org/1999/xlink"><sheet theme="theme"><topic><title>Root</title><children><topics type="attached">
<topic style-id="orange" xlink:href="xap:attachments/photo.png"><title>photo.png</title><labels><label>Tag</label></labels><notes><plain>A note</plain></notes></topic>
</topics></children></topic></sheet></xmap-content>`)
zip.file('styles.xml', `<xmap-styles xmlns="urn:xmind:xmap:xmlns:style:2.0" xmlns:fo="http://www.w3.org/1999/XSL/Format" xmlns:svg="http://www.w3.org/2000/svg">
<automatic-styles><style id="central" type="topic"><topic-properties svg:fill="#DCE6F2" fo:color="#376092"/></style></automatic-styles>
<master-styles><style id="theme" type="theme"><theme-properties><default-style style-family="centralTopic" style-id="central"/></theme-properties></style></master-styles>
<styles><style id="orange" type="topic"><topic-properties svg:fill="#E46C0A"/></style></styles>
</xmap-styles>`)
zip.file('attachments/photo.png', new Uint8Array([137, 80, 78, 71]))
const data = await zip.generateAsync({ type: 'uint8array' })

const result = await xmind.readDocument(data)
expect(result.data).toEqual({ text: 'Root', background: '#DCE6F2', color: '#376092' })
expect(result.children[0].data).toEqual({
text: 'photo.png',
background: '#E46C0A',
color: '#FFFFFF',
note: 'A note',
resource: ['Tag'],
image: 'data:image/png;base64,iVBORw==',
imageSize: { width: 200, height: 50 },
})
})

it('leaves out images the browser cannot read', async () => {
vi.stubGlobal('createImageBitmap', vi.fn().mockRejectedValue(new Error('broken')))
const zip = new JSZip()
zip.file('content.json', JSON.stringify([{ rootTopic: { title: 'Root', image: { src: 'xap:resources/broken.png' } } }]))
zip.file('resources/broken.png', 'not an image')
const data = await zip.generateAsync({ type: 'uint8array' })

const result = await xmind.readDocument(data)
expect(result.data).toEqual({ text: 'Root' })
})

it('rejects when content.xml is missing in the zip', async () => {
await expect(xmind.readDocument('not a zip')).rejects.toBeDefined()
})
Expand Down
Loading
Loading