140 lines
7.2 KiB
JavaScript
140 lines
7.2 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { readFile } from "node:fs/promises";
|
|
|
|
async function importSource(path) {
|
|
const source = await readFile(new URL(path, import.meta.url), "utf8");
|
|
return import(`data:text/javascript;base64,${Buffer.from(source).toString("base64")}`);
|
|
}
|
|
|
|
class FakeEditor {
|
|
constructor(value, start = 0, end = start) {
|
|
this.value = value;
|
|
this.selectionStart = start;
|
|
this.selectionEnd = end;
|
|
this.selectionDirection = "none";
|
|
this.readOnly = false;
|
|
this.inputEvents = 0;
|
|
}
|
|
|
|
setRangeText(replacement, start, end, mode) {
|
|
this.value = `${this.value.slice(0, start)}${replacement}${this.value.slice(end)}`;
|
|
const caret = mode === "end" ? start + replacement.length : start;
|
|
this.selectionStart = caret;
|
|
this.selectionEnd = caret;
|
|
}
|
|
|
|
setSelectionRange(start, end, direction = "none") {
|
|
this.selectionStart = start;
|
|
this.selectionEnd = end;
|
|
this.selectionDirection = direction;
|
|
}
|
|
|
|
dispatchEvent(event) {
|
|
if (event.type === "input") this.inputEvents += 1;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
const { applyIndentation } = await importSource("../static/js/editor-format.js");
|
|
const markdownSource = await readFile(new URL("../static/js/markdown.js", import.meta.url), "utf8");
|
|
const alignSource = markdownSource.match(/export function alignPreviewLineNumbers\(root\) \{[\s\S]*?\n\}/)?.[0];
|
|
assert.ok(alignSource, "alignPreviewLineNumbers source must exist");
|
|
const alignPreviewLineNumbers = new Function(`${alignSource.replace("export ", "")}; return alignPreviewLineNumbers;`)();
|
|
|
|
test("Tab inserts two spaces at the caret", () => {
|
|
const editor = new FakeEditor("abcd", 2);
|
|
assert.equal(applyIndentation(editor), true);
|
|
assert.equal(editor.value, "ab cd");
|
|
assert.deepEqual([editor.selectionStart, editor.selectionEnd], [4, 4]);
|
|
assert.equal(editor.inputEvents, 1);
|
|
});
|
|
|
|
test("Tab indents every selected line", () => {
|
|
const editor = new FakeEditor("alpha\nbeta\ngamma", 0, 10);
|
|
assert.equal(applyIndentation(editor), true);
|
|
assert.equal(editor.value, " alpha\n beta\ngamma");
|
|
assert.deepEqual([editor.selectionStart, editor.selectionEnd], [2, 14]);
|
|
});
|
|
|
|
test("Shift+Tab removes up to two spaces from selected lines", () => {
|
|
const editor = new FakeEditor(" alpha\n beta", 2, 13);
|
|
assert.equal(applyIndentation(editor, { outdent: true }), true);
|
|
assert.equal(editor.value, "alpha\nbeta");
|
|
assert.deepEqual([editor.selectionStart, editor.selectionEnd], [0, 10]);
|
|
});
|
|
|
|
test("Shift+Tab removes a leading tab", () => {
|
|
const editor = new FakeEditor("\talpha", 4);
|
|
assert.equal(applyIndentation(editor, { outdent: true }), true);
|
|
assert.equal(editor.value, "alpha");
|
|
assert.deepEqual([editor.selectionStart, editor.selectionEnd], [3, 3]);
|
|
});
|
|
|
|
test("preview line-number alignment ignores horizontal scroll", () => {
|
|
const previousGetComputedStyle = globalThis.getComputedStyle;
|
|
globalThis.getComputedStyle = () => ({ paddingLeft: "62px" });
|
|
let assigned;
|
|
const line = {
|
|
getBoundingClientRect: () => ({ left: 70 }),
|
|
style: { setProperty: (name, value) => { assigned = [name, value]; } },
|
|
};
|
|
const root = {
|
|
scrollLeft: 80,
|
|
getBoundingClientRect: () => ({ left: 100 }),
|
|
querySelectorAll: () => [line],
|
|
};
|
|
try {
|
|
alignPreviewLineNumbers(root);
|
|
assert.deepEqual(assigned, ["--preview-line-left", "-38px"]);
|
|
} finally {
|
|
globalThis.getComputedStyle = previousGetComputedStyle;
|
|
}
|
|
});
|
|
|
|
test("compact editor layout starts below 1500px", async () => {
|
|
const css = await readFile(new URL("../static/css/styles.css", import.meta.url), "utf8");
|
|
const editorSource = await readFile(new URL("../static/js/note-editor.js", import.meta.url), "utf8");
|
|
assert.equal((css.match(/@media \(max-width: 1499px\)/g) || []).length, 4);
|
|
assert.doesNotMatch(css, /@media \(max-width: 1920px\)/);
|
|
assert.match(editorSource, /compactLayoutQuery = window\.matchMedia\("\(max-width: 1499px\)"\)/);
|
|
assert.match(editorSource, /compactBubbleQuery = matchMedia\("\(max-width: 1499px\)"\)/);
|
|
});
|
|
|
|
test("read-only mode blocks every document write path", async () => {
|
|
const editorHtml = await readFile(new URL("../static/editor.html", import.meta.url), "utf8");
|
|
const editorSource = await readFile(new URL("../static/js/note-editor.js", import.meta.url), "utf8");
|
|
assert.match(editorHtml, /<textarea id="editor"[\s\S]*?readonly><\/textarea>/);
|
|
assert.match(editorSource, /function canEditDocument\(\) \{ return !editor\.readOnly; \}/);
|
|
assert.match(editorSource, /function scheduleDocumentSave\(\) \{[\s\S]*?if \(!canEditDocument\(\)\) \{[\s\S]*?saveState\.textContent = "Read only";[\s\S]*?return;/);
|
|
assert.match(editorSource, /function activatePreviewEdit\(target, offset = null\) \{\s*if \(!target \|\| !canEditDocument\(\)\) return;/);
|
|
assert.match(editorSource, /function commitPreviewEdit\(target,[\s\S]*?if \(!canEditDocument\(\)\) \{ render\(\); return; \}/);
|
|
assert.match(editorSource, /if \(socket && canEditDocument\(\)\) socket\.update/g);
|
|
assert.match(editorSource, /setDocumentReadOnly\(true, "Read only — changes not saved"\);[\s\S]*?applyRemote\(lastServerContent, lastServerOwnerMap\);[\s\S]*?queueMicrotask\(connect\);/);
|
|
});
|
|
|
|
test("narrow editor switches stay compact", async () => {
|
|
const css = await readFile(new URL("../static/css/styles.css", import.meta.url), "utf8");
|
|
assert.match(css, /\.pad-page #mode-toggle \{[\s\S]*?width:\s*auto;[\s\S]*?min-width:\s*0;[\s\S]*?white-space:\s*nowrap;/);
|
|
assert.match(css, /@media \(max-width: 760px\) \{[\s\S]*?\.pad-page \.editor-toolbar \{\s*grid-template-columns:\s*minmax\(0, 1fr\) auto;/);
|
|
assert.match(css, /@media \(max-width: 760px\) \{[\s\S]*?\.pad-page \.view-switch \{[\s\S]*?display:\s*inline-flex;[\s\S]*?width:\s*max-content;[\s\S]*?justify-self:\s*end;/);
|
|
assert.doesNotMatch(css, /\.pad-page \.view-switch \{\s*position:\s*static;\s*display:\s*grid;\s*width:\s*100%;/);
|
|
});
|
|
|
|
test("Full HD scaling keeps Preview visible without compacting the whole editor", async () => {
|
|
const css = await readFile(new URL("../static/css/styles.css", import.meta.url), "utf8");
|
|
assert.match(css, /@media \(min-width: 1500px\) and \(max-width: 1699px\) \{[\s\S]*?\.pad-page \.toolbar-group \{[\s\S]*?flex:\s*1 1 auto;[\s\S]*?overflow-x:\s*auto;/);
|
|
assert.match(css, /@media \(min-width: 1500px\) and \(max-width: 1699px\) \{[\s\S]*?\.pad-page \.toolbar-fill \{\s*display:\s*none;/);
|
|
assert.match(css, /@media \(min-width: 1500px\) and \(max-width: 1699px\) \{[\s\S]*?\.pad-page :is\(\.editor-controls, \.toolbar-action, \.line-toggle, \.view-switch\) \{\s*flex:\s*0 0 auto;/);
|
|
});
|
|
|
|
test("Markdown/Text uses the same toolbar action styling as More", async () => {
|
|
const html = await readFile(new URL("../static/editor.html", import.meta.url), "utf8");
|
|
const css = await readFile(new URL("../static/css/styles.css", import.meta.url), "utf8");
|
|
const editorSource = await readFile(new URL("../static/js/note-editor.js", import.meta.url), "utf8");
|
|
assert.match(html, /id="mode-toggle" class="toolbar-action active"/);
|
|
assert.doesNotMatch(css, /\.markdown-toggle/);
|
|
assert.match(css, /\.markdown-more>summary,\s*\.pad-page \.toolbar-action \{/);
|
|
assert.match(editorSource, /modeToggle\.setAttribute\("aria-pressed", String\(markdown\)\)/);
|
|
});
|