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
33 changes: 13 additions & 20 deletions scripts/checkedReadMigration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';

import { readSourceFiles, sliceBetween } from './sourceTree.js';
import { offsetOf, readSourceFiles, sliceBetween } from './sourceTree.js';

// Runes and the Tauri bridge, shimmed the way truncatedBufferGuard.test.ts
// shims them: the stores are runes modules, and Node's test runner gives every
Expand Down Expand Up @@ -174,22 +174,19 @@ test('the unchecked read command is gone, and nothing calls it', () => {
});

test('entering the editor reads the fidelity and stores it', () => {
const toggle = viewer.slice(viewer.indexOf('async function toggleEdit'), viewer.indexOf('async function saveContent'));
const toggle = sliceBetween(viewer, 'async function toggleEdit', 'async function saveContent');
assert.match(toggle, /\[content, lossy\] = \(await invoke\('read_file_content_checked', \{ path: tab\.path \}\)\)/);
const read = toggle.indexOf('read_file_content_checked');
const flag = toggle.indexOf('setTabDecodedLossy(tab.id, lossy)');
const store = toggle.indexOf('setTabRawContent(tab.id, content)');
assert.notEqual(flag, -1, 'the verdict must reach the tab');
const read = offsetOf(toggle, 'read_file_content_checked');
const flag = offsetOf(toggle, 'setTabDecodedLossy(tab.id, lossy)');
const store = offsetOf(toggle, 'setTabRawContent(tab.id, content)');
assert.ok(read < flag && flag < store, 'flag the tab before the buffer is published');
});

test('entering split view reads the fidelity and stores it', () => {
const split = viewer.slice(viewer.indexOf('async function toggleSplitView'));
const enter = split.slice(0, split.indexOf('} else {'));
const enter = sliceBetween(viewer, 'async function toggleSplitView', '} else {');
assert.match(enter, /\[content, lossy\] = \(await invoke\('read_file_content_checked', \{ path: tab\.path \}\)\)/);
const flag = enter.indexOf('setTabDecodedLossy(tab.id, lossy)');
const store = enter.indexOf('setTabRawContent(tab.id, content)');
assert.notEqual(flag, -1, 'the verdict must reach the tab');
const flag = offsetOf(enter, 'setTabDecodedLossy(tab.id, lossy)');
const store = offsetOf(enter, 'setTabRawContent(tab.id, content)');
assert.ok(flag < store, 'flag the tab before the buffer is published');
});

Expand All @@ -212,22 +209,18 @@ test('the session can tell a refusal from a failure', () => {
});

test('a refused save does not add a generic toast to its own explanation', () => {
const effect = viewer.slice(viewer.indexOf('Auto-save effect.'));
const body = effect.slice(0, effect.indexOf('for (const id of ['));
const check = body.indexOf('if (documentSession.isLossySaveRefused(s.id)) return;');
const toast = body.indexOf("t('toast.autoSaveFailed'");
assert.notEqual(check, -1, 'the refusal must be recognised');
assert.notEqual(toast, -1);
assert.ok(check < toast, 'and recognised before the generic toast is raised');
const body = sliceBetween(viewer, 'Auto-save effect.', 'for (const id of [');
const check = offsetOf(body, 'if (documentSession.isLossySaveRefused(s.id)) return;');
const toast = offsetOf(body, "t('toast.autoSaveFailed'");
assert.ok(check < toast, 'the refusal must be recognised before the generic toast is raised');
});

test('a tab that can only be refused stops re-arming the timer', () => {
// Auto-save re-arms on every keystroke. Without this the guard was reached
// again every 1.5s for as long as the user kept typing — each time
// producing the console warning, the wasted round trip, and (before the
// test above) the toast.
const effect = viewer.slice(viewer.indexOf('Auto-save effect.'));
const body = effect.slice(0, effect.indexOf('for (const id of ['));
const body = sliceBetween(viewer, 'Auto-save effect.', 'for (const id of [');
assert.match(body, /decodedLossily: tab\.hasReplacementChars/);
assert.match(body, /const eligible = [^;]*!\(s\.decodedLossily && documentSession\.isLossySaveRefused\(s\.id\)\)/);
// The FIRST attempt must still happen: it is what produces the explanation.
Expand Down
4 changes: 3 additions & 1 deletion scripts/documentWatcherSession.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';

import { sliceFrom } from './sourceTree.js';

const session = readFileSync('src/lib/sessions/documentSession.svelte.ts', 'utf8');

test('self writes suppress watcher reloads only during their grace period', () => {
const handler = session.slice(session.indexOf('function shouldReloadExternalChange'));
const handler = sliceFrom(session, 'function shouldReloadExternalChange');
assert.match(handler, /if \(Date\.now\(\) < until\) return false;/);
assert.match(handler, /selfWriteUntilByPath\.delete\(path\);/);
assert.match(handler, /return true;/);
Expand Down
16 changes: 5 additions & 11 deletions scripts/editorOptionWiring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';

import { sliceBetween } from './sourceTree.js';

// Editor.svelte translates the settings store into Monaco options and
// keybindings. Every regression locked here came from that translation layer
// being wired to the wrong shape: a string option read as a boolean, a
Expand All @@ -15,14 +17,6 @@ function count(source: string, pattern: RegExp): number {
return source.match(pattern)?.length ?? 0;
}

function sliceBlock(source: string, startMarker: string, endMarker: string): string {
const start = source.indexOf(startMarker);
assert.notEqual(start, -1, `expected to find ${startMarker}`);
const end = source.indexOf(endMarker, start + startMarker.length);
assert.notEqual(end, -1, `expected to find ${endMarker} after ${startMarker}`);
return source.slice(start, end);
}

test('renderLineHighlight is a Monaco string enum, not a boolean flag', () => {
// The store holds 'line' / 'none'. Any non-empty string is truthy, so a
// ternary on it can only ever produce "line" and silently defeats both the
Expand Down Expand Up @@ -54,7 +48,7 @@ test('editor options are applied by a single updateOptions effect', () => {
// without the zoom factor — so the winner depended on effect ordering.
assert.equal(count(editor, /editor\.updateOptions\(\{/g), 1, 'exactly one updateOptions call site');

const block = sliceBlock(editor, 'editor.updateOptions({', '});');
const block = sliceBetween(editor, 'editor.updateOptions({', '});');
assert.match(block, /wordWrapColumn: settings\.editorMaxWidth/, 'wordWrapColumn survived the merge');
assert.match(block, /fontSize: settings\.editorFontSize \* \(zoomLevel \/ 100\)/, 'zoom-aware font size is the surviving one');
for (const option of [
Expand Down Expand Up @@ -110,7 +104,7 @@ test('platform detection reads settings.osType and never writes it', () => {
// the keybindings are registered once, at mount, rather than re-registered
// when settings.osType resolves. Full argument: the comment on
// isMacPlatform() in Editor.svelte.
const helper = sliceBlock(editor, 'function isMacPlatform', '\n\t}');
const helper = sliceBetween(editor, 'function isMacPlatform', '\n\t}');
assert.match(helper, /settings\.osType !== 'unknown'/, 'prefers the resolved Tauri os type');
assert.match(helper, /settings\.osType === 'macos'/);
assert.match(helper, /navigator\.platform/, 'falls back while osType is still resolving');
Expand Down Expand Up @@ -139,7 +133,7 @@ test('custom copy keeps Monaco\'s whole-line copy on an empty selection', () =>
// (editor.emptySelectionClipboard) and Sublime Text behave the same. Since
// custom-copy overrides the native copy action, bailing out on an empty
// selection deleted the behaviour outright.
const copyAction = sliceBlock(editor, 'id: "custom-copy"', 'id: "toggle-minimap"');
const copyAction = sliceBetween(editor, 'id: "custom-copy"', 'id: "toggle-minimap"');

assert.doesNotMatch(
copyAction,
Expand Down
30 changes: 11 additions & 19 deletions scripts/editorPdfExport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import test from 'node:test';

import { compile } from 'svelte/compiler';

import { offsetOf, sliceBetween, sliceFrom } from './sourceTree.js';

/*
* Export PDF from plain edit mode produced a blank page, for two independent
* reasons. This file covers both.
Expand Down Expand Up @@ -317,9 +319,8 @@ test('the reveal does not depend on which sheet the browser applies last', () =>
// is not guaranteed. The resolver above already places the component last,
// which is the losing arrangement for the print rules; assert that the
// winning declaration is `!important` so order cannot decide it either way.
const printBlock = styles.slice(styles.indexOf('@media print'));
const override = printBlock.slice(printBlock.indexOf('#app .pane.viewer-pane'));
const rule = override.slice(0, override.indexOf('}'));
const printBlock = sliceFrom(styles, '@media print');
const rule = sliceBetween(printBlock, '#app .pane.viewer-pane', '}');
for (const property of ['width', 'flex', 'opacity']) {
assert.match(rule, new RegExp(`${property}:[^;]*!important`), `${property} must be !important`);
}
Expand All @@ -333,14 +334,6 @@ test('the reveal does not depend on which sheet the browser applies last', () =>
// the resulting DOM is complete — that is what the awaited `renderRichContent`
// is for, and only a real browser can confirm it.

const slice = (source: string, start: string, end: string) => {
const from = source.indexOf(start);
assert.notEqual(from, -1, `expected to find ${start}`);
const to = source.indexOf(end, from + start.length);
assert.notEqual(to, -1, `expected to find ${end} after ${start}`);
return source.slice(from, to);
};

test('the preview is only kept live while it is on screen', () => {
// The premise of the second half. If this condition ever widens to cover
// plain edit mode the export-time render below becomes a no-op rather than
Expand All @@ -349,25 +342,24 @@ test('the preview is only kept live while it is on screen', () => {
});

test('exporting a PDF renders the buffer before the DOM is printed', () => {
const body_ = slice(viewer, 'async function exportAsPdf', 'function handleNewFile');
const sync = body_.indexOf('await syncPreviewForPrint()');
assert.notEqual(sync, -1, 'the export must refresh the preview first');
const body_ = sliceBetween(viewer, 'async function exportAsPdf', 'function handleNewFile');
const sync = offsetOf(body_, 'await syncPreviewForPrint()');
// Before the diagram pass, which reads the nodes that render produces, and
// before the print itself.
assert.ok(sync < body_.indexOf('renderDiagramsForPrint'), 'refresh before re-theming the diagrams');
assert.ok(sync < body_.indexOf('_exportPdf('), 'refresh before printing');
assert.ok(sync < offsetOf(body_, 'renderDiagramsForPrint'), 'refresh before re-theming the diagrams');
assert.ok(sync < offsetOf(body_, '_exportPdf('), 'refresh before printing');
});

test('the refresh is skipped when the DOM already matches the buffer', () => {
const body_ = slice(viewer, 'async function syncPreviewForPrint', 'async function exportAsPdf');
const body_ = sliceBetween(viewer, 'async function syncPreviewForPrint', 'async function exportAsPdf');
// Reading mode is rendered by loadMarkdown from this same buffer, and
// re-rendering it would discard the scroll, fold and find state on screen.
assert.match(body_, /if \(!tab \|\| !\(tab\.isEditing \|\| tab\.isSplit\)\) return;/);
assert.match(body_, /_lastRenderedRawContent === rawContent\) return;/);
});

test('the refresh actually lands before the print', () => {
const body_ = slice(viewer, 'async function syncPreviewForPrint', 'async function exportAsPdf');
const body_ = sliceBetween(viewer, 'async function syncPreviewForPrint', 'async function exportAsPdf');
// What matters here is WHAT is rendered — this tab's buffer and this tab's
// path, not the disk — and that it is awaited before the content is
// swapped in. The trailing arguments are deliberately not pinned: fold
Expand All @@ -382,7 +374,7 @@ test('the refresh actually lands before the print', () => {
});

test('a failed refresh does not pass off a stale export as a fresh one', () => {
const body_ = slice(viewer, 'async function syncPreviewForPrint', 'async function exportAsPdf');
const body_ = sliceBetween(viewer, 'async function syncPreviewForPrint', 'async function exportAsPdf');
assert.match(body_, /catch \(error\)/);
assert.match(body_, /addToast\(/);
});
14 changes: 6 additions & 8 deletions scripts/externalChangeReload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';

import { sliceBetween } from './sourceTree.js';

// Live Mode watches the open file and reloads it when something else writes
// it — git checkout, a cloud sync, a second Markpad window. The reload path
// replaces rawContent AND originalContent, so a buffer with unsaved edits is
Expand Down Expand Up @@ -135,8 +137,7 @@ test('our own writes still suppress the reload', async () => {
// --- wiring that cannot be executed outside a Svelte runtime ---

test('the watcher listener routes every event through the guarded resolution', () => {
const listener = viewer.slice(viewer.indexOf("listen('file-changed'"));
const body = listener.slice(0, listener.indexOf('}),'));
const body = sliceBetween(viewer, "listen('file-changed'", '}),');
assert.match(body, /resolveExternalChange/);
// The old body called loadMarkdown(currentFile) directly.
assert.doesNotMatch(body, /loadMarkdown\(currentFile\)/);
Expand All @@ -152,24 +153,21 @@ test('the debounced auto-save is held back while a conflict is unanswered', () =
// Otherwise the 1.5s timer fires while the bar is still asking "reload or
// keep mine": the user's edits survive, but the external change is gone
// from disk before either answer can be given.
const effect = viewer.slice(viewer.indexOf('Auto-save effect.'));
const body = effect.slice(0, effect.indexOf('for (const id of ['));
const body = sliceBetween(viewer, 'Auto-save effect.', 'for (const id of [');
assert.match(body, /hasPendingConflict: externalChangeConflicts\[tab\.id\] === true/);
assert.match(body, /const eligible = [^;]*!s\.hasPendingConflict/);
});

test('an explicit save answers the conflict and takes the bar down', () => {
// Pressing Cmd+S is a decision. Leaving the bar up afterwards would ask a
// question the user already answered.
const wrapper = viewer.slice(viewer.indexOf('async function saveContent(tabId?: string)'));
const body = wrapper.slice(0, wrapper.indexOf('async function saveContentAs'));
const body = sliceBetween(viewer, 'async function saveContent(tabId?: string)', 'async function saveContentAs');
assert.match(body, /clearExternalChangeConflict/);
});

test('turning Live Mode on installs the watcher without reloading', () => {
// Enabling a watcher is not a request to discard the buffer, and this was
// the one loadMarkdown call in the app with no canCloseTab in front of it.
const toggle = viewer.slice(viewer.indexOf('function toggleLiveMode'));
const body = toggle.slice(0, toggle.indexOf('\n\t}') + 3);
const body = sliceBetween(viewer, 'function toggleLiveMode', '\n\t}');
assert.doesNotMatch(body, /loadMarkdown/);
});
4 changes: 3 additions & 1 deletion scripts/foldKeys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';

import { sliceFrom } from './sourceTree.js';

// Fold state is keyed by `h.id || textContent`. comrak emits the
// deduplicated heading id on an empty inner <a class="anchor">, not on the
// heading element, so without promotion every fold consumer falls back to
Expand All @@ -11,7 +13,7 @@ import test from 'node:test';
test('processMarkdownHtml promotes the anchor id onto the heading element', () => {
const source = readFileSync('src/lib/utils/markdown.ts', 'utf8');

const headingLoop = source.slice(source.indexOf('querySelectorAll("h1, h2, h3, h4, h5, h6")'));
const headingLoop = sliceFrom(source, 'querySelectorAll("h1, h2, h3, h4, h5, h6")');
assert.match(headingLoop, /querySelector\("a\.anchor"\)/, 'heading loop looks up the comrak anchor');
assert.match(headingLoop, /h\.id = \w+\.id/, 'anchor id is promoted onto the heading');
assert.match(headingLoop, /removeAttribute\("id"\)/, 'anchor id is removed so document ids stay unique');
Expand Down
8 changes: 4 additions & 4 deletions scripts/foldStatePerDocument.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import test from 'node:test';
import ts from 'typescript';

import { installShimDom, parseHtml, type ShimElement } from './renderProtocolDom.ts';
import { offsetOf } from './sourceTree.js';

// ---------------------------------------------------------------- environment

Expand Down Expand Up @@ -98,7 +99,7 @@ function pluckFunction(source: string, name: string, required = true): string {
for (const marker of [`async function ${name}(`, `function ${name}(`]) {
const start = source.indexOf(marker);
if (start === -1) continue;
const open = source.indexOf('{', source.indexOf(')', start));
const open = offsetOf(source, '{', offsetOf(source, ')', start));
return source.slice(start, matchBrace(source, open) + 1);
}
assert.ok(!required, `expected the component to define ${name}`);
Expand Down Expand Up @@ -243,9 +244,8 @@ function buildViewer(): Viewer {
/** The real `visibleItems` out of Toc.svelte, over a caller-supplied fold set. */
function buildTocFilter() {
const marker = 'let visibleItems = $derived.by(() => ';
const start = toc.indexOf(marker);
assert.notEqual(start, -1, 'expected Toc.svelte to derive visibleItems');
const open = toc.indexOf('{', start + marker.length);
const start = offsetOf(toc, marker);
const open = offsetOf(toc, '{', start + marker.length);
const body = toc.slice(open, matchBrace(toc, open) + 1);
const js = ts.transpileModule(body, { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText;
return new Function('items', 'collapsedHeaders', `"use strict";\n${js}`) as (
Expand Down
7 changes: 3 additions & 4 deletions scripts/issue261EditorPdf.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';

import { sliceBetween, sliceFrom } from './sourceTree.js';
import { offsetOf, sliceBetween, sliceFrom } from './sourceTree.js';

const viewer = readFileSync('src/lib/MarkdownViewer.svelte', 'utf8');
const styles = readFileSync('src/styles.css', 'utf8');

test('editor context menu is not intercepted by the document menu', () => {
const handler = sliceBetween(viewer, 'function handleContextMenu(e: MouseEvent)', '\n\tfunction handleMouseOver');
const editorReturn = handler.indexOf('if (isInsideEditor) return;');
const preventDefault = handler.indexOf('e.preventDefault();');
const editorReturn = offsetOf(handler, 'if (isInsideEditor) return;');
const preventDefault = offsetOf(handler, 'e.preventDefault();');

assert.ok(editorReturn !== -1, 'editor context menus must stay with Monaco');
assert.ok(editorReturn < preventDefault, 'Monaco must receive the event before the document menu prevents it');
});

Expand Down
12 changes: 7 additions & 5 deletions scripts/issue281MinimalMacosMenu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';

import { sliceBetween } from './sourceTree.js';

const tauriLib = readFileSync('src-tauri/src/lib.rs', 'utf8');
const viewer = readFileSync('src/lib/MarkdownViewer.svelte', 'utf8');

test('macOS native menu keeps only application-level actions', () => {
const menuStart = tauriLib.indexOf('#[cfg(target_os = "macos")]\n {\n use tauri::menu');
assert.notEqual(menuStart, -1, 'macOS native menu setup must exist');

const menuEnd = tauriLib.indexOf('\n let config_dir', menuStart);
const menuSetup = tauriLib.slice(menuStart, menuEnd);
const menuSetup = sliceBetween(
tauriLib,
'#[cfg(target_os = "macos")]\n {\n use tauri::menu',
'\n let config_dir',
);

assert.match(menuSetup, /MenuItemBuilder::with_id\("menu-app-settings", "Settings…"\)\s*\.accelerator\("CmdOrCtrl\+,"\)/);
assert.match(menuSetup, /MenuItemBuilder::with_id\("check-updates", "Check for Updates…"\)/);
Expand Down
Loading
Loading