mirror of
https://gitlab.com/ArkHost/HelixNotes.git
synced 2026-09-19 09:27:29 +02:00
Revert "chore(release): prepare v1.3.5"
This reverts commit ade146f9f3.
This commit is contained in:
@@ -1,164 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { Schema } from '@tiptap/pm/model';
|
||||
import { EditorState, TextSelection } from '@tiptap/pm/state';
|
||||
import { applyClearFormatting } from '../src/lib/editor/clearFormatting.ts';
|
||||
|
||||
const schema = new Schema({
|
||||
nodes: {
|
||||
doc: { content: 'block+' },
|
||||
paragraph: { content: 'text*', group: 'block' },
|
||||
heading: { attrs: { level: { default: 1 } }, content: 'text*', group: 'block' },
|
||||
codeBlock: { content: 'text*', group: 'block', code: true },
|
||||
blockquote: { content: 'block+', group: 'block' },
|
||||
bulletList: { content: 'listItem+', group: 'block' },
|
||||
listItem: { content: 'paragraph block*' },
|
||||
taskList: { content: 'taskItem+', group: 'block' },
|
||||
taskItem: { attrs: { checked: { default: false } }, content: 'paragraph block*' },
|
||||
text: { group: 'inline' },
|
||||
},
|
||||
marks: {
|
||||
bold: {},
|
||||
italic: {},
|
||||
strike: {},
|
||||
underline: {},
|
||||
highlight: { attrs: { color: { default: null } } },
|
||||
code: {},
|
||||
link: { attrs: { href: {} } },
|
||||
wikiLink: { attrs: { path: { default: null } } },
|
||||
},
|
||||
});
|
||||
|
||||
const mark = (name, attrs = null) => schema.mark(name, attrs);
|
||||
const text = (value, marks = []) => schema.text(value, marks);
|
||||
const para = (...children) => schema.node('paragraph', null, children);
|
||||
|
||||
function clearedDoc(doc, from, to = from) {
|
||||
const state = EditorState.create({ schema, doc, selection: TextSelection.create(doc, from, to) });
|
||||
const tr = state.tr;
|
||||
applyClearFormatting(schema, tr);
|
||||
return state.apply(tr).doc;
|
||||
}
|
||||
|
||||
function markSummary(doc) {
|
||||
const summary = [];
|
||||
doc.descendants((node) => {
|
||||
if (node.isText) summary.push(`${node.text}→${node.marks.map((m) => m.type.name).join('+') || 'none'}`);
|
||||
return true;
|
||||
});
|
||||
return summary;
|
||||
}
|
||||
|
||||
test('strips formatting marks from a selection but keeps link and wikiLink', () => {
|
||||
// "bold"[1,5) "link"[5,9) "wiki"[9,13)
|
||||
const doc = schema.node('doc', null, [
|
||||
para(
|
||||
text('bold', [mark('bold')]),
|
||||
text('link', [mark('link', { href: 'https://example.com' })]),
|
||||
text('wiki', [mark('wikiLink', { path: 'notes/wiki.md' })]),
|
||||
),
|
||||
]);
|
||||
|
||||
const result = clearedDoc(doc, 1, 13);
|
||||
const nodes = [];
|
||||
result.firstChild.forEach((node) => nodes.push(node));
|
||||
|
||||
assert.deepEqual(nodes[0].marks, []);
|
||||
assert.deepEqual(nodes[1].marks.map((m) => m.type.name), ['link']);
|
||||
assert.equal(nodes[1].marks[0].attrs.href, 'https://example.com');
|
||||
assert.deepEqual(nodes[2].marks.map((m) => m.type.name), ['wikiLink']);
|
||||
assert.equal(nodes[2].marks[0].attrs.path, 'notes/wiki.md');
|
||||
});
|
||||
|
||||
test('empty selection clears the entire current line', () => {
|
||||
// "one "[1,5) "two"[5,8) " three"[8,14), cursor inside "one" at 2
|
||||
const doc = schema.node('doc', null, [
|
||||
para(text('one '), text('two', [mark('bold')]), text(' three', [mark('italic')])),
|
||||
]);
|
||||
|
||||
const result = clearedDoc(doc, 2);
|
||||
assert.deepEqual(markSummary(result), ['one two three→none']);
|
||||
});
|
||||
|
||||
test('cursor in a heading demotes it to a plain paragraph', () => {
|
||||
const doc = schema.node('doc', null, [
|
||||
schema.node('heading', { level: 2 }, [text('Title', [mark('bold')])]),
|
||||
]);
|
||||
|
||||
const result = clearedDoc(doc, 3);
|
||||
assert.equal(result.firstChild.type.name, 'paragraph');
|
||||
assert.equal(result.firstChild.textContent, 'Title');
|
||||
assert.deepEqual(markSummary(result), ['Title→none']);
|
||||
});
|
||||
|
||||
test('cursor in a code block converts it to a paragraph', () => {
|
||||
const doc = schema.node('doc', null, [schema.node('codeBlock', null, [text('const x = 1;')])]);
|
||||
|
||||
const result = clearedDoc(doc, 4);
|
||||
assert.equal(result.firstChild.type.name, 'paragraph');
|
||||
assert.equal(result.firstChild.textContent, 'const x = 1;');
|
||||
});
|
||||
|
||||
test('cursor in a blockquote lifts the content out of the wrapper', () => {
|
||||
const doc = schema.node('doc', null, [
|
||||
schema.node('blockquote', null, [para(text('quoted', [mark('italic')]))]),
|
||||
]);
|
||||
|
||||
const result = clearedDoc(doc, 3);
|
||||
assert.equal(result.childCount, 1);
|
||||
assert.equal(result.firstChild.type.name, 'paragraph');
|
||||
assert.deepEqual(markSummary(result), ['quoted→none']);
|
||||
});
|
||||
|
||||
test('nested blockquotes are fully unwrapped', () => {
|
||||
const doc = schema.node('doc', null, [
|
||||
schema.node('blockquote', null, [
|
||||
schema.node('blockquote', null, [para(text('deep', [mark('bold')]))]),
|
||||
]),
|
||||
]);
|
||||
|
||||
const result = clearedDoc(doc, 3);
|
||||
assert.equal(result.childCount, 1);
|
||||
assert.equal(result.firstChild.type.name, 'paragraph');
|
||||
assert.deepEqual(markSummary(result), ['deep→none']);
|
||||
});
|
||||
|
||||
test('task list structure and checked state survive clearing', () => {
|
||||
const doc = schema.node('doc', null, [
|
||||
schema.node('taskList', null, [
|
||||
schema.node('taskItem', { checked: true }, [para(text('done task', [mark('strike')]))]),
|
||||
schema.node('taskItem', { checked: false }, [para(text('open task'))]),
|
||||
]),
|
||||
]);
|
||||
// cursor inside "done task": taskList at 0, taskItem at 1, paragraph at 2, text from 3
|
||||
const result = clearedDoc(doc, 5);
|
||||
|
||||
const list = result.firstChild;
|
||||
assert.equal(list.type.name, 'taskList');
|
||||
assert.equal(list.childCount, 2);
|
||||
assert.equal(list.firstChild.type.name, 'taskItem');
|
||||
assert.equal(list.firstChild.attrs.checked, true);
|
||||
assert.equal(list.lastChild.attrs.checked, false);
|
||||
assert.deepEqual(markSummary(result), ['done task→none', 'open task→none']);
|
||||
});
|
||||
|
||||
test('partial selection keeps formatting outside the selection', () => {
|
||||
// "abcd" all bold; clear only [2,4) = "bc"
|
||||
const doc = schema.node('doc', null, [para(text('abcd', [mark('bold')]))]);
|
||||
|
||||
const result = clearedDoc(doc, 2, 4);
|
||||
assert.deepEqual(markSummary(result), ['a→bold', 'bc→none', 'd→bold']);
|
||||
});
|
||||
|
||||
test('multi-block selection demotes headings and strips marks in range', () => {
|
||||
// heading "H1" [0,4), paragraph at 4 with "body" bold [5,9)
|
||||
const doc = schema.node('doc', null, [
|
||||
schema.node('heading', { level: 1 }, [text('H1')]),
|
||||
para(text('body', [mark('bold')])),
|
||||
]);
|
||||
|
||||
const result = clearedDoc(doc, 2, 7);
|
||||
assert.equal(result.firstChild.type.name, 'paragraph');
|
||||
// marks removed only up to position 7: "bo" cleared, "dy" still bold
|
||||
assert.deepEqual(markSummary(result), ['H1→none', 'bo→none', 'dy→bold']);
|
||||
});
|
||||
@@ -1,95 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { Schema } from '@tiptap/pm/model';
|
||||
import { EditorState, Plugin, TextSelection } from '@tiptap/pm/state';
|
||||
import { createCodeBlockInputScrollPlugin } from '../src/lib/editor/extensions/codeBlockInputScroll.ts';
|
||||
|
||||
const schema = new Schema({
|
||||
nodes: {
|
||||
doc: { content: 'block+' },
|
||||
paragraph: { content: 'text*', group: 'block' },
|
||||
codeBlock: {
|
||||
attrs: { language: { default: null } },
|
||||
content: 'text*',
|
||||
group: 'block',
|
||||
code: true,
|
||||
},
|
||||
text: { group: 'inline' },
|
||||
},
|
||||
});
|
||||
|
||||
const inputRulesPlugin = new Plugin({ isInputRules: true });
|
||||
|
||||
function textBlock(type, text, attrs = null) {
|
||||
return schema.node(type, attrs, text ? [schema.text(text)] : undefined);
|
||||
}
|
||||
|
||||
function stateWithSelection(type, text, options = {}) {
|
||||
const { attrs = null, from = text.length + 1, to = from } = options;
|
||||
const doc = schema.node('doc', null, [textBlock(type, text, attrs)]);
|
||||
|
||||
return EditorState.create({
|
||||
doc,
|
||||
selection: TextSelection.create(doc, from, to),
|
||||
plugins: [inputRulesPlugin, createCodeBlockInputScrollPlugin()],
|
||||
});
|
||||
}
|
||||
|
||||
function convertToCodeBlock(state, language = null, { inputRule = true } = {}) {
|
||||
const codeBlock = textBlock('codeBlock', '', { language });
|
||||
const tr = state.tr.replaceWith(0, state.doc.content.size, codeBlock);
|
||||
tr.setSelection(TextSelection.create(tr.doc, 1));
|
||||
if (inputRule) {
|
||||
tr.setMeta(inputRulesPlugin, {
|
||||
transform: tr,
|
||||
from: state.selection.from,
|
||||
to: state.selection.to,
|
||||
text: '\n',
|
||||
});
|
||||
}
|
||||
|
||||
return state.applyTransaction(tr);
|
||||
}
|
||||
|
||||
function assertAppendedScroll(result) {
|
||||
assert.equal(result.transactions.length, 2);
|
||||
assert.equal(result.transactions[0].scrolledIntoView, false);
|
||||
assert.equal(result.transactions[1].scrolledIntoView, true);
|
||||
assert.equal(result.state.selection.$from.parent.type.name, 'codeBlock');
|
||||
}
|
||||
|
||||
function assertNoAppendedScroll(result) {
|
||||
assert.equal(result.transactions.length, 1);
|
||||
assert.equal(result.transactions[0].scrolledIntoView, false);
|
||||
}
|
||||
|
||||
test('appends a scroll request for backtick and tilde input-rule transitions', () => {
|
||||
assertAppendedScroll(convertToCodeBlock(stateWithSelection('paragraph', '```')));
|
||||
assertAppendedScroll(convertToCodeBlock(stateWithSelection('paragraph', '~~~rust'), 'rust'));
|
||||
});
|
||||
|
||||
test('does not append a scroll request for non-fence paragraph conversions', () => {
|
||||
assertNoAppendedScroll(convertToCodeBlock(stateWithSelection('paragraph', 'ordinary text')));
|
||||
assertNoAppendedScroll(convertToCodeBlock(stateWithSelection('paragraph', '```TypeScript')));
|
||||
assertNoAppendedScroll(convertToCodeBlock(stateWithSelection('paragraph', '````')));
|
||||
});
|
||||
|
||||
test('does not append a scroll request for toolbar conversions or note loads', () => {
|
||||
assertNoAppendedScroll(
|
||||
convertToCodeBlock(stateWithSelection('paragraph', '```'), null, { inputRule: false })
|
||||
);
|
||||
assertNoAppendedScroll(
|
||||
convertToCodeBlock(stateWithSelection('paragraph', '~~~rust'), 'rust', { inputRule: false })
|
||||
);
|
||||
});
|
||||
|
||||
test('does not append a scroll request for a non-empty fence selection', () => {
|
||||
const state = stateWithSelection('paragraph', '```', { from: 1, to: 4 });
|
||||
assertNoAppendedScroll(convertToCodeBlock(state));
|
||||
});
|
||||
|
||||
test('does not append a scroll request for a transaction already in a code block', () => {
|
||||
const state = stateWithSelection('codeBlock', '```');
|
||||
const tr = state.tr.insertText('x');
|
||||
assertNoAppendedScroll(state.applyTransaction(tr));
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import test from 'node:test';
|
||||
|
||||
const editor = await readFile(
|
||||
new URL('../src/lib/components/Editor.svelte', import.meta.url),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
test('Note Info owns the only open-note trash action', () => {
|
||||
assert.doesNotMatch(editor, /editor-trash-btn/);
|
||||
assert.match(editor, /class="info-trash-btn"[\s\S]{0,300}onclick=\{moveOpenNoteToTrash\}/);
|
||||
assert.match(editor, /class="info-trash-btn"[\s\S]{0,500}Move to Trash/);
|
||||
assert.equal(editor.match(/onclick=\{moveOpenNoteToTrash\}/g)?.length, 1);
|
||||
});
|
||||
|
||||
test('Note Info hides the trash action when moving the note is unavailable', () => {
|
||||
assert.match(
|
||||
editor,
|
||||
/\{#if onMoveToTrash && \$viewMode !== 'trash'\}[\s\S]{0,200}class="info-section info-actions"/
|
||||
);
|
||||
});
|
||||
|
||||
test('Note Info stays open when clicking another note', () => {
|
||||
assert.doesNotMatch(editor, /onInfoClickAway/);
|
||||
assert.match(editor, /class="info-close-btn" onclick=\{\(\) => showInfo = false\}/);
|
||||
});
|
||||
@@ -1,80 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import test from 'node:test';
|
||||
import { Schema } from '@tiptap/pm/model';
|
||||
import { transformWithEsbuild } from 'vite';
|
||||
|
||||
const source = await readFile(
|
||||
new URL('../src/lib/editor/mixedLists.ts', import.meta.url),
|
||||
'utf8'
|
||||
);
|
||||
const { code } = await transformWithEsbuild(source, 'mixedLists.ts', {
|
||||
loader: 'ts',
|
||||
format: 'esm',
|
||||
target: 'esnext'
|
||||
});
|
||||
const { convertListNode } = await import(
|
||||
`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`
|
||||
);
|
||||
|
||||
const schema = new Schema({
|
||||
nodes: {
|
||||
doc: { content: 'block+' },
|
||||
paragraph: { content: 'text*', group: 'block' },
|
||||
text: { group: 'inline' },
|
||||
bulletList: { content: 'listItem+', group: 'block' },
|
||||
listItem: { content: 'paragraph block*' },
|
||||
taskList: { content: 'taskItem+', group: 'block' },
|
||||
taskItem: {
|
||||
attrs: { checked: { default: false } },
|
||||
content: 'paragraph block*'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function paragraph(text) {
|
||||
return schema.node('paragraph', null, [schema.text(text)]);
|
||||
}
|
||||
|
||||
test('converts every task item in a list to a bullet item', () => {
|
||||
const sourceList = schema.node('taskList', null, [
|
||||
schema.node('taskItem', { checked: true }, [paragraph('First')]),
|
||||
schema.node('taskItem', { checked: false }, [paragraph('Second')])
|
||||
]);
|
||||
|
||||
const converted = convertListNode(schema, sourceList, 'bulletList');
|
||||
|
||||
assert.equal(converted.type.name, 'bulletList');
|
||||
assert.deepEqual(
|
||||
converted.content.content.map((item) => item.type.name),
|
||||
['listItem', 'listItem']
|
||||
);
|
||||
assert.equal(converted.textContent, 'FirstSecond');
|
||||
assert.equal(sourceList.type.name, 'taskList');
|
||||
});
|
||||
|
||||
test('converts bullet items to unchecked tasks without dropping nested blocks', () => {
|
||||
const nestedBullets = schema.node('bulletList', null, [
|
||||
schema.node('listItem', null, [paragraph('Nested note')])
|
||||
]);
|
||||
const sourceList = schema.node('bulletList', null, [
|
||||
schema.node('listItem', null, [paragraph('Parent'), nestedBullets])
|
||||
]);
|
||||
|
||||
const converted = convertListNode(schema, sourceList, 'taskList');
|
||||
const taskItem = converted.firstChild;
|
||||
|
||||
assert.equal(converted.type.name, 'taskList');
|
||||
assert.equal(taskItem.type.name, 'taskItem');
|
||||
assert.equal(taskItem.attrs.checked, false);
|
||||
assert.equal(taskItem.lastChild.type.name, 'bulletList');
|
||||
assert.equal(taskItem.textContent, 'ParentNested note');
|
||||
});
|
||||
|
||||
test('leaves a list alone when it already has the requested type', () => {
|
||||
const bullets = schema.node('bulletList', null, [
|
||||
schema.node('listItem', null, [paragraph('Unchanged')])
|
||||
]);
|
||||
|
||||
assert.equal(convertListNode(schema, bullets, 'bulletList'), null);
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import test from 'node:test';
|
||||
import { transformWithEsbuild } from 'vite';
|
||||
|
||||
const source = await readFile(
|
||||
new URL('../src/lib/utils/note-drag.ts', import.meta.url),
|
||||
'utf8'
|
||||
);
|
||||
const { code } = await transformWithEsbuild(source, 'note-drag.ts', {
|
||||
loader: 'ts',
|
||||
format: 'esm',
|
||||
target: 'esnext'
|
||||
});
|
||||
const noteDrag = await import(`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`);
|
||||
|
||||
test('round-trips every selected note path in a drag payload', () => {
|
||||
const paths = ['/vault/Alpha.md', '/vault/Projects/Beta.md'];
|
||||
assert.deepEqual(noteDrag.decodeNoteDragPaths(noteDrag.encodeNoteDragPaths(paths)), paths);
|
||||
});
|
||||
|
||||
test('keeps single-note and Windows path payloads compatible', () => {
|
||||
assert.deepEqual(noteDrag.decodeNoteDragPaths('/vault/Alpha.md'), ['/vault/Alpha.md']);
|
||||
assert.deepEqual(
|
||||
noteDrag.decodeNoteDragPaths('C:\\Vault\\Alpha.md\r\nC:\\Vault\\Beta.md'),
|
||||
['C:\\Vault\\Alpha.md', 'C:\\Vault\\Beta.md']
|
||||
);
|
||||
});
|
||||
@@ -1,117 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import test from 'node:test';
|
||||
import { transformWithEsbuild } from 'vite';
|
||||
|
||||
const source = await readFile(
|
||||
new URL('../src/lib/utils/paths.ts', import.meta.url),
|
||||
'utf8'
|
||||
);
|
||||
const { code } = await transformWithEsbuild(source, 'paths.ts', {
|
||||
loader: 'ts',
|
||||
format: 'esm',
|
||||
target: 'esnext'
|
||||
});
|
||||
const {
|
||||
assetSourceToMarkdown,
|
||||
assetUrlToLocalPath,
|
||||
normalizeLocalAssetPath,
|
||||
resolvePathFromFile,
|
||||
resolveVaultFilePath
|
||||
} = await import(
|
||||
`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`
|
||||
);
|
||||
|
||||
test('resolves note links relative to Windows note paths', () => {
|
||||
assert.equal(
|
||||
resolvePathFromFile('C:\\Vault\\Current.md', './Target.md'),
|
||||
'C:/Vault/Target.md'
|
||||
);
|
||||
assert.equal(
|
||||
resolvePathFromFile('C:\\Vault\\Folder\\Current.md', '../Target.md'),
|
||||
'C:/Vault/Target.md'
|
||||
);
|
||||
assert.equal(
|
||||
resolvePathFromFile('\\\\server\\share\\Folder\\Current.md', '../Target.md'),
|
||||
'//server/share/Target.md'
|
||||
);
|
||||
});
|
||||
|
||||
test('preserves Linux relative note-link resolution', () => {
|
||||
assert.equal(
|
||||
resolvePathFromFile('/vault/folder/Current.md', '../Target.md'),
|
||||
'/vault/Target.md'
|
||||
);
|
||||
});
|
||||
|
||||
test('converts Windows asset URLs back to portable attachment paths', () => {
|
||||
assert.equal(
|
||||
normalizeLocalAssetPath('/C:\\Users\\user\\Vault\\image.png'),
|
||||
'C:/Users/user/Vault/image.png'
|
||||
);
|
||||
assert.equal(
|
||||
assetSourceToMarkdown(
|
||||
'http://asset.localhost/C%3A%5CUsers%5Cuser%5CVault%5C.helixnotes%5Cattachments%5Cimage.png',
|
||||
'C:\\Users\\user\\Vault\\Note.md',
|
||||
'C:\\Users\\user\\Vault'
|
||||
),
|
||||
'.helixnotes/attachments/image.png'
|
||||
);
|
||||
});
|
||||
|
||||
test('preserves portable asset paths on Linux and macOS', () => {
|
||||
assert.equal(
|
||||
assetSourceToMarkdown(
|
||||
'asset://localhost/%2Fhome%2Fuser%2FVault%2F.helixnotes%2Fattachments%2Fimage.png',
|
||||
'/home/user/Vault/Note.md',
|
||||
'/home/user/Vault'
|
||||
),
|
||||
'.helixnotes/attachments/image.png'
|
||||
);
|
||||
assert.equal(
|
||||
assetSourceToMarkdown(
|
||||
'asset://localhost/%2FUsers%2Fuser%2FVault%2Fassets%2Fimage.png',
|
||||
'/Users/user/Vault/notes/Note.md',
|
||||
'/Users/user/Vault'
|
||||
),
|
||||
'../assets/image.png'
|
||||
);
|
||||
assert.equal(
|
||||
assetSourceToMarkdown('../assets/image.png', '/vault/notes/Note.md', '/vault'),
|
||||
'../assets/image.png'
|
||||
);
|
||||
});
|
||||
|
||||
test('decodes local asset URLs without damaging platform roots', () => {
|
||||
assert.equal(
|
||||
assetUrlToLocalPath('http://asset.localhost/C%3A%5CUsers%5Cuser%5CVault%5Cimage.png'),
|
||||
'C:/Users/user/Vault/image.png'
|
||||
);
|
||||
assert.equal(
|
||||
assetUrlToLocalPath('asset://localhost/%2Fhome%2Fuser%2FVault%2Fimage.png'),
|
||||
'/home/user/Vault/image.png'
|
||||
);
|
||||
assert.equal(
|
||||
assetUrlToLocalPath('http://asset.localhost/%5C%5Cserver%5Cshare%5CVault%5Cimage.png'),
|
||||
'//server/share/Vault/image.png'
|
||||
);
|
||||
});
|
||||
|
||||
test('resolves vault files consistently across platforms', () => {
|
||||
assert.equal(
|
||||
resolveVaultFilePath('../assets/image.png', 'C:\\Vault\\notes\\Note.md', 'C:\\Vault'),
|
||||
'C:/Vault/assets/image.png'
|
||||
);
|
||||
assert.equal(
|
||||
resolveVaultFilePath('.helixnotes/attachments/image.png', 'C:\\Vault\\notes\\Note.md', 'C:\\Vault'),
|
||||
'C:/Vault/.helixnotes/attachments/image.png'
|
||||
);
|
||||
assert.equal(
|
||||
resolveVaultFilePath('../assets/image.png', '/vault/notes/Note.md', '/vault'),
|
||||
'/vault/assets/image.png'
|
||||
);
|
||||
assert.equal(
|
||||
resolveVaultFilePath('../assets/image.png', '\\\\server\\share\\Vault\\notes\\Note.md', '\\\\server\\share\\Vault'),
|
||||
'//server/share/Vault/assets/image.png'
|
||||
);
|
||||
});
|
||||
@@ -1,120 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import test from 'node:test';
|
||||
import { transformWithEsbuild } from 'vite';
|
||||
|
||||
const source = await readFile(
|
||||
new URL('../src/lib/utils/note-switcher.ts', import.meta.url),
|
||||
'utf8'
|
||||
);
|
||||
const { code } = await transformWithEsbuild(source, 'note-switcher.ts', {
|
||||
loader: 'ts',
|
||||
format: 'esm',
|
||||
target: 'esnext'
|
||||
});
|
||||
const { buildNoteSwitcherRequestPaths, buildNoteSwitcherSections } = await import(
|
||||
`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`
|
||||
);
|
||||
|
||||
const note = (path, title, relativePath) => ({ path, title, relativePath });
|
||||
|
||||
test('requests the current note first, then unique history from newest to oldest', () => {
|
||||
const current = '/vault/Current.md';
|
||||
const recent = '/vault/Recent.md';
|
||||
const older = '/vault/Older.md';
|
||||
|
||||
assert.deepEqual(
|
||||
buildNoteSwitcherRequestPaths(current, [older, recent, recent, current]),
|
||||
[current, recent, older]
|
||||
);
|
||||
});
|
||||
|
||||
test('puts the current note first, then unique history from newest to oldest up to the recent limit', () => {
|
||||
const current = note('/vault/Current.md', 'Current', 'Current.md');
|
||||
const alpha = note('/vault/Projects/Alpha.md', 'Alpha', 'Projects/Alpha.md');
|
||||
const beta = note('/vault/Projects/Beta.md', 'Beta', 'Projects/Beta.md');
|
||||
|
||||
const sections = buildNoteSwitcherSections({
|
||||
currentPath: current.path,
|
||||
historyPaths: [beta.path, alpha.path, alpha.path, current.path],
|
||||
knownNotes: [current, alpha, beta],
|
||||
quickAccessNotes: [],
|
||||
recentLimit: 3
|
||||
});
|
||||
|
||||
assert.deepEqual(sections.recent, [
|
||||
{ path: current.path, title: 'Current', folder: 'Unfiled', current: true },
|
||||
{ path: alpha.path, title: 'Alpha', folder: 'Projects', current: false },
|
||||
{ path: beta.path, title: 'Beta', folder: 'Projects', current: false }
|
||||
]);
|
||||
});
|
||||
|
||||
test('excludes stale, unknown, and external paths that are absent from the known-note inventory', () => {
|
||||
const current = note('/vault/Current.md', 'Current', 'Current.md');
|
||||
const known = note('/vault/Notes/Known.md', 'Known', 'Notes/Known.md');
|
||||
|
||||
const sections = buildNoteSwitcherSections({
|
||||
currentPath: current.path,
|
||||
historyPaths: [
|
||||
'/vault/Deleted.md',
|
||||
known.path,
|
||||
'/home/user/Downloads/External.md'
|
||||
],
|
||||
knownNotes: [current, known],
|
||||
quickAccessNotes: [
|
||||
note('/vault/Missing-Quick.md', 'Missing Quick', 'Missing-Quick.md'),
|
||||
note('/home/user/Downloads/Pinned.md', 'External Quick', 'Pinned.md')
|
||||
]
|
||||
});
|
||||
|
||||
assert.deepEqual(sections, {
|
||||
recent: [
|
||||
{ path: current.path, title: 'Current', folder: 'Unfiled', current: true },
|
||||
{ path: known.path, title: 'Known', folder: 'Notes', current: false }
|
||||
],
|
||||
quickAccess: []
|
||||
});
|
||||
});
|
||||
|
||||
test('keeps Quick Access order, removes Recent overlap, and derives root and nested folder labels', () => {
|
||||
const root = note('/vault/Root.md', 'Root', 'Root.md');
|
||||
const recentNested = note(
|
||||
'/vault/Projects/Helix/Plan.md',
|
||||
'Plan',
|
||||
'Projects/Helix/Plan.md'
|
||||
);
|
||||
const quickRoot = note('/vault/Scratch.md', 'Scratch', 'Scratch.md');
|
||||
const quickNested = note(
|
||||
'/vault/Areas/Reading/Queue.md',
|
||||
'Reading Queue',
|
||||
'Areas/Reading/Queue.md'
|
||||
);
|
||||
|
||||
const sections = buildNoteSwitcherSections({
|
||||
currentPath: root.path,
|
||||
historyPaths: [recentNested.path],
|
||||
knownNotes: [root, recentNested, quickRoot, quickNested],
|
||||
quickAccessNotes: [quickRoot, root, quickNested, recentNested]
|
||||
});
|
||||
|
||||
assert.deepEqual(sections, {
|
||||
recent: [
|
||||
{ path: root.path, title: 'Root', folder: 'Unfiled', current: true },
|
||||
{
|
||||
path: recentNested.path,
|
||||
title: 'Plan',
|
||||
folder: 'Projects/Helix',
|
||||
current: false
|
||||
}
|
||||
],
|
||||
quickAccess: [
|
||||
{ path: quickRoot.path, title: 'Scratch', folder: 'Unfiled', current: false },
|
||||
{
|
||||
path: quickNested.path,
|
||||
title: 'Reading Queue',
|
||||
folder: 'Areas/Reading',
|
||||
current: false
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
const source = await import(new URL('../src/lib/utils/notebook-icons.ts', import.meta.url));
|
||||
|
||||
const {
|
||||
NOTEBOOK_ICON_OPTIONS,
|
||||
decodeBuiltinNotebookIcon,
|
||||
encodeBuiltinNotebookIcon,
|
||||
normalizeNotebookIconKey
|
||||
} = source;
|
||||
|
||||
test('built-in notebook icons have stable unique storage values', () => {
|
||||
assert.equal(NOTEBOOK_ICON_OPTIONS.length, 16);
|
||||
assert.equal(new Set(NOTEBOOK_ICON_OPTIONS.map(({ id }) => id)).size, NOTEBOOK_ICON_OPTIONS.length);
|
||||
|
||||
for (const { id } of NOTEBOOK_ICON_OPTIONS) {
|
||||
const stored = encodeBuiltinNotebookIcon(id);
|
||||
assert.equal(stored, `builtin:${id}`);
|
||||
assert.equal(decodeBuiltinNotebookIcon(stored), id);
|
||||
}
|
||||
});
|
||||
|
||||
test('custom paths and unknown built-in values remain outside the icon codec', () => {
|
||||
assert.equal(decodeBuiltinNotebookIcon('.helixnotes/attachments/notebook-icon.png'), null);
|
||||
assert.equal(decodeBuiltinNotebookIcon('builtin:unknown'), null);
|
||||
assert.equal(decodeBuiltinNotebookIcon(null), null);
|
||||
});
|
||||
|
||||
test('normalizes notebook icon keys to portable vault-relative paths', () => {
|
||||
assert.equal(normalizeNotebookIconKey('Projects/Client'), 'Projects/Client');
|
||||
assert.equal(normalizeNotebookIconKey(String.raw`Projects\Client`), 'Projects/Client');
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import test from 'node:test';
|
||||
|
||||
const sidebar = await readFile(
|
||||
new URL('../src/lib/components/Sidebar.svelte', import.meta.url),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
test('mobile notebook rows expose an accessible actions button', () => {
|
||||
assert.match(sidebar, /\{#if isMobile\}[\s\S]{0,500}class="notebook-actions-btn"/);
|
||||
assert.match(sidebar, /aria-label=\{`Actions for \$\{nb\.name\}`\}/);
|
||||
assert.match(sidebar, /onclick=\{\(e\) => openNotebookMenu\(e, nb\)\}/);
|
||||
});
|
||||
|
||||
test('the notebook context menu receives its mobile styling outside the sidebar', () => {
|
||||
assert.match(sidebar, /class="context-menu" class:mobile=\{isMobile\}/);
|
||||
assert.match(sidebar, /\.context-menu\.mobile\s*\{/);
|
||||
});
|
||||
|
||||
test('mobile action sheets stay inside every safe area and prevent tap-through', () => {
|
||||
assert.match(sidebar, /class="context-menu-backdrop"/);
|
||||
assert.match(sidebar, /safe-area-inset-left/);
|
||||
assert.match(sidebar, /safe-area-inset-right/);
|
||||
assert.match(sidebar, /safe-area-inset-top/);
|
||||
assert.match(sidebar, /safe-area-inset-bottom/);
|
||||
});
|
||||
|
||||
test('the actions button remains inside the notebook manual-sort drop target', () => {
|
||||
assert.match(sidebar, /<div class="notebook-row" data-nb-path=\{nb\.path\}>/);
|
||||
});
|
||||
@@ -1,60 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import test from 'node:test';
|
||||
import { transformWithEsbuild } from 'vite';
|
||||
|
||||
const source = await readFile(
|
||||
new URL('../src/lib/utils/startup-view.ts', import.meta.url),
|
||||
'utf8'
|
||||
);
|
||||
const { code } = await transformWithEsbuild(source, 'startup-view.ts', {
|
||||
loader: 'ts',
|
||||
format: 'esm',
|
||||
target: 'esnext'
|
||||
});
|
||||
const startup = await import(`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`);
|
||||
|
||||
test('normalizes every supported default view and falls back to All Notes', () => {
|
||||
for (const view of ['all', 'quickaccess', 'tasks', 'daily']) {
|
||||
assert.equal(startup.normalizeStartupView(view), view);
|
||||
}
|
||||
assert.equal(startup.normalizeStartupView('notebook'), 'all');
|
||||
assert.equal(startup.normalizeStartupView(undefined), 'all');
|
||||
});
|
||||
|
||||
test('uses the configured default when session restoration is disabled', () => {
|
||||
assert.deepEqual(startup.resolveStartupTarget({
|
||||
startupView: 'tasks',
|
||||
restoreLastSession: false,
|
||||
lastViewMode: 'daily',
|
||||
lastNotebook: null,
|
||||
lastTag: null
|
||||
}), { mode: 'tasks' });
|
||||
});
|
||||
|
||||
test('restores a supported previous list when restoration is enabled', () => {
|
||||
assert.deepEqual(startup.resolveStartupTarget({
|
||||
startupView: 'daily',
|
||||
restoreLastSession: true,
|
||||
lastViewMode: 'quickaccess',
|
||||
lastNotebook: null,
|
||||
lastTag: null
|
||||
}), { mode: 'quickaccess' });
|
||||
});
|
||||
|
||||
test('restores notebook and tag identifiers, otherwise uses the configured default', () => {
|
||||
assert.deepEqual(startup.resolveStartupTarget({
|
||||
startupView: 'all',
|
||||
restoreLastSession: true,
|
||||
lastViewMode: 'notebook',
|
||||
lastNotebook: 'Projects',
|
||||
lastTag: null
|
||||
}), { mode: 'notebook', notebookPath: 'Projects' });
|
||||
assert.deepEqual(startup.resolveStartupTarget({
|
||||
startupView: 'tasks',
|
||||
restoreLastSession: true,
|
||||
lastViewMode: 'tag',
|
||||
lastNotebook: null,
|
||||
lastTag: null
|
||||
}), { mode: 'tasks' });
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import test from 'node:test';
|
||||
|
||||
const appLayout = await readFile(
|
||||
new URL('../src/lib/components/AppLayout.svelte', import.meta.url),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
test('the theme shortcut recognizes a resolved custom dark theme', () => {
|
||||
assert.match(
|
||||
appLayout,
|
||||
/\$customThemes\.find\(theme => theme\.id === \$resolvedTheme\)/
|
||||
);
|
||||
assert.match(
|
||||
appLayout,
|
||||
/darkThemes\.includes\(\$resolvedTheme\) \|\| \(customTheme\?\.is_dark \?\? false\)/
|
||||
);
|
||||
});
|
||||
|
||||
test('AppLayout leaves root theme application to the root layout', () => {
|
||||
assert.doesNotMatch(appLayout, /function applyTheme\(/);
|
||||
assert.doesNotMatch(appLayout, /applyTheme\(\$theme\)/);
|
||||
});
|
||||
Reference in New Issue
Block a user