文件
mynode/packages/lib/services/interop/InteropService_Exporter_Md_frontmatter.test.ts
chen 301194963b
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

222 行
9.1 KiB
TypeScript

import InteropService from '../../services/interop/InteropService';
import { setupDatabaseAndSynchronizer, switchClient, exportDir } from '../../testing/test-utils';
import Folder from '../../models/Folder';
import Note from '../../models/Note';
import Tag from '../../models/Tag';
import time from '../../time';
import * as fs from 'fs-extra';
import { ExportModuleOutputFormat } from './types';
import { fieldOrder } from '../../utils/frontMatter';
async function recreateExportDir() {
const dir = exportDir();
await fs.remove(dir);
await fs.mkdirp(dir);
}
describe('interop/InteropService_Exporter_Md_frontmatter', () => {
async function exportAndLoad(path: string): Promise<string> {
const service = InteropService.instance();
await service.export({
path: exportDir(),
format: ExportModuleOutputFormat.MarkdownFrontMatter,
});
return await fs.readFile(path, 'utf8');
}
beforeEach(async () => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
await recreateExportDir();
});
test('should export MD file with YAML header', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
await Note.save({ title: 'ma', latitude: 58.2222, user_updated_time: 1, user_created_time: 1, body: '**ma note**', parent_id: folder1.id });
const content = await exportAndLoad(`${exportDir()}/folder1/ma.md`);
expect(content.startsWith('---')).toBe(true);
expect(content).toContain('title: ma');
expect(content).toContain('updated:'); // Will be current time of test run
expect(content).toContain(`created: ${time.unixMsToRfc3339Sec(1)}`);
expect(content).toContain('latitude: 58.22220000');
expect(content).toContain('longitude: 0.00000000');
expect(content).toContain('altitude: 0.0000');
expect(content).toContain('**ma note**');
expect(content).not.toContain('completed?');
expect(content).not.toContain('author');
expect(content).not.toContain('source');
expect(content).not.toContain('due');
}));
test('should warn when a note title is truncated in the exported filename', async () => {
const longTitle = 'a'.repeat(51);
const longFolderTitle = 'b'.repeat(51);
const folder = await Folder.save({ title: longFolderTitle });
const note = await Note.save({ title: longTitle, parent_id: folder.id });
const result = await InteropService.instance().export({
path: exportDir(),
format: ExportModuleOutputFormat.MarkdownFrontMatter,
});
expect(result.warnings).toEqual([
`The notebook title "${longFolderTitle}" was truncated to "${'b'.repeat(50)}" in the exported folder name. Notebook ID: ${folder.id}`,
`The note title "${longTitle}" was truncated to "${'a'.repeat(50)}.md" in the exported file name. Note ID: ${note.id}`,
]);
expect(await fs.pathExists(`${exportDir()}/${'b'.repeat(50)}/${'a'.repeat(50)}.md`)).toBe(true);
});
test('should export without additional quotes', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
await Note.save({ title: '-60', body: '**ma note**', parent_id: folder1.id });
const content = await exportAndLoad(`${exportDir()}/folder1/-60.md`);
expect(content).toContain('title: -60');
}));
test('should export tags', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
const note = await Note.save({ title: 'Title', body: '**ma note**', parent_id: folder1.id });
await Tag.addNoteTagByTitle(note.id, 'lamp');
await Tag.addNoteTagByTitle(note.id, 'moth');
await Tag.addNoteTagByTitle(note.id, 'godzilla');
const content = await exportAndLoad(`${exportDir()}/folder1/Title.md`);
expect(content).toContain('tags:\n - godzilla\n - lamp\n - moth');
}));
test('should export todo', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
await Note.save({ title: 'Todo', is_todo: 1, todo_due: 1, body: '**ma note**', parent_id: folder1.id });
const content = await exportAndLoad(`${exportDir()}/folder1/Todo.md`);
expect(content).toContain(`due: ${time.unixMsToRfc3339Sec(1)}`);
expect(content).toContain('completed?: no');
}));
test('should export author', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
await Note.save({ title: 'Author', author: 'Scott Joplin', body: '**ma note**', parent_id: folder1.id });
const content = await exportAndLoad(`${exportDir()}/folder1/Author.md`);
expect(content).toContain('author: Scott Joplin');
}));
test('should export source', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
await Note.save({ title: 'Source', source_url: 'https://joplinapp.org', body: '**ma note**', parent_id: folder1.id });
const content = await exportAndLoad(`${exportDir()}/folder1/Source.md`);
expect(content).toContain('source: https://joplinapp.org');
}));
test('should export fields in the correct order', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
const note = await Note.save({
title: 'Fields',
is_todo: 1,
todo_due: 1,
author: 'Scott Joplin',
source_url: 'https://joplinapp.org',
body: '**ma note**',
parent_id: folder1.id,
});
await Tag.addNoteTagByTitle(note.id, 'piano');
await Tag.addNoteTagByTitle(note.id, 'greatness');
const content = await exportAndLoad(`${exportDir()}/folder1/Fields.md`);
const fieldIndices = fieldOrder.map(field => content.indexOf(field));
expect(fieldIndices).toBe(fieldIndices.sort());
}));
test('should export title with a newline encoded', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
await Note.save({ title: 'Source\ntitle', body: '**ma note**', parent_id: folder1.id });
const content = await exportAndLoad(`${exportDir()}/folder1/Source_title.md`);
expect(content).toContain('title: |-\n Source\n title');
}));
test('should not export coordinates if they\'re not available', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
await Note.save({ title: 'Coordinates', body: '**ma note**', parent_id: folder1.id });
const content = await exportAndLoad(`${exportDir()}/folder1/Coordinates.md`);
expect(content).not.toContain('latitude');
expect(content).not.toContain('longitude');
expect(content).not.toContain('altitude');
}));
test('should export note without tag keyword if the tag has been deleted', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
const note = await Note.save({ title: 'NoTag', body: '**ma note**', parent_id: folder1.id });
const tag = await Tag.save({ title: 'tag' });
await Tag.setNoteTagsByIds(note.id, [tag.id]);
await Tag.delete(tag.id);
const content = await exportAndLoad(`${exportDir()}/folder1/NoTag.md`);
expect(content).not.toContain('tag');
}));
test('should export a valid file when the title starts with a dash', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
await Note.save({ title: '- title with dash', body: '**ma note**', parent_id: folder1.id });
const content = await exportAndLoad(`${exportDir()}/folder1/- title with dash.md`);
expect(content).toContain('title: \'- title with dash\'');
}));
test('should export _folder.yml with emoji icon', (async () => {
const icon = JSON.stringify({ type: 1, emoji: 'notebook', name: '', dataUrl: '' });
const folder1 = await Folder.save({ title: 'folder1', icon });
await Note.save({ title: 'IconNote', body: '**my note**', parent_id: folder1.id });
await exportAndLoad(`${exportDir()}/folder1/IconNote.md`);
const metadataPath = `${exportDir()}/folder1/_folder.yml`;
expect(await fs.pathExists(metadataPath)).toBe(true);
const metadataContent = await fs.readFile(metadataPath, 'utf8');
expect(metadataContent).toContain('type: emoji');
expect(metadataContent).toContain('emoji: notebook');
}));
test('should export _folder.yml with FontAwesome icon', (async () => {
const icon = JSON.stringify({ type: 3, emoji: '', name: 'fas fa-music', dataUrl: '' });
const folder1 = await Folder.save({ title: 'folder1', icon });
await Note.save({ title: 'FANote', body: '**my note**', parent_id: folder1.id });
await exportAndLoad(`${exportDir()}/folder1/FANote.md`);
const metadataPath = `${exportDir()}/folder1/_folder.yml`;
expect(await fs.pathExists(metadataPath)).toBe(true);
const metadataContent = await fs.readFile(metadataPath, 'utf8');
expect(metadataContent).toContain('type: fontawesome');
expect(metadataContent).toContain('name: fas fa-music');
}));
test('should not export _folder.yml when folder has no icon', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
await Note.save({ title: 'NoIcon', body: '**my note**', parent_id: folder1.id });
await exportAndLoad(`${exportDir()}/folder1/NoIcon.md`);
const metadataPath = `${exportDir()}/folder1/_folder.yml`;
expect(await fs.pathExists(metadataPath)).toBe(false);
}));
test('should not include folder_icon in note frontmatter', (async () => {
const icon = JSON.stringify({ type: 1, emoji: 'notebook', name: '', dataUrl: '' });
const folder1 = await Folder.save({ title: 'folder1', icon });
await Note.save({ title: 'TestNote', body: '**my note**', parent_id: folder1.id });
const content = await exportAndLoad(`${exportDir()}/folder1/TestNote.md`);
expect(content).not.toContain('folder_icon');
}));
});