Adds a "History…" context-menu item on markdown files in a history:true subtree (server mode only — the audit is server-stamped). It opens a modal that lists every saved version newest-first (timestamp + author + size, current flagged), lets you View any version, Diff any two, and Restore one (a forward PUT — non-destructive). - shared/diff.js: dependency-free line/word LCS diff (window.zddc.diff), prefix/suffix trimming + a cell cap so large files don't stall the UI. - browse/js/history.js: the modal (list / view / diff / restore), talking to GET <url>?history=1 and ?history=<sha>. - loader.js carries the per-file history flag; events.js adds the menu item. - Wired diff.js + history.js + history.css into browse/build.sh; diff.js into the zddc-test.html shim. tests/diff.spec.js covers the diff algorithm. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
71 lines
2.6 KiB
JavaScript
71 lines
2.6 KiB
JavaScript
/**
|
|
* Tests for shared/diff.js — the dependency-free text diff used by the
|
|
* browse tool's markdown version-history viewer.
|
|
*
|
|
* Runs against the same shim as zddc.spec.js (shared/zddc-test.html,
|
|
* which loads shared/diff.js and exposes window.zddc.diff).
|
|
*/
|
|
|
|
import { test, expect } from '@playwright/test';
|
|
import * as path from 'path';
|
|
|
|
const SHIM_PATH = 'file://' + path.resolve('shared/zddc-test.html');
|
|
|
|
async function diff(page, fn, ...args) {
|
|
return page.evaluate(
|
|
([fn, args]) => window.zddc.diff[fn](...args),
|
|
[fn, args]
|
|
);
|
|
}
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto(SHIM_PATH, { waitUntil: 'load' });
|
|
});
|
|
|
|
test('diff module is attached to window.zddc', async ({ page }) => {
|
|
const present = await page.evaluate(() =>
|
|
!!(window.zddc && window.zddc.diff &&
|
|
typeof window.zddc.diff.lines === 'function' &&
|
|
typeof window.zddc.diff.words === 'function'));
|
|
expect(present).toBe(true);
|
|
});
|
|
|
|
test('identical text produces only eq ops', async ({ page }) => {
|
|
const ops = await diff(page, 'lines', 'a\nb\nc', 'a\nb\nc');
|
|
expect(ops.every(o => o.type === 'eq')).toBe(true);
|
|
});
|
|
|
|
test('a changed middle line shows del then add', async ({ page }) => {
|
|
const ops = await diff(page, 'lines', 'a\nb\nc', 'a\nB\nc');
|
|
const compact = ops.map(o => `${o.type}:${o.text}`).join('|');
|
|
expect(compact).toContain('eq:a');
|
|
expect(compact).toContain('del:b');
|
|
expect(compact).toContain('add:B');
|
|
expect(compact).toContain('eq:c');
|
|
});
|
|
|
|
test('stats count added and removed lines', async ({ page }) => {
|
|
const addStats = await diff(page, 'stats', await diff(page, 'lines', 'a\nb', 'a\nx\nb'));
|
|
expect(addStats).toEqual({ added: 1, removed: 0 });
|
|
|
|
const delStats = await diff(page, 'stats', await diff(page, 'lines', 'a\nb\nc', 'a\nc'));
|
|
expect(delStats).toEqual({ added: 0, removed: 1 });
|
|
});
|
|
|
|
test('pure insertion at end', async ({ page }) => {
|
|
const ops = await diff(page, 'lines', 'one\ntwo', 'one\ntwo\nthree');
|
|
const added = ops.filter(o => o.type === 'add').map(o => o.text);
|
|
expect(added).toEqual(['three']);
|
|
expect(ops.filter(o => o.type === 'del')).toHaveLength(0);
|
|
});
|
|
|
|
test('word diff aligns on word boundaries, preserving spaces', async ({ page }) => {
|
|
const ops = await diff(page, 'words', 'the quick fox', 'the slow fox');
|
|
const changed = ops.filter(o => o.type !== 'eq').map(o => `${o.type}:${o.text}`);
|
|
expect(changed).toEqual(['del:quick', 'add:slow']);
|
|
});
|
|
|
|
test('empty inputs do not throw', async ({ page }) => {
|
|
const ops = await diff(page, 'lines', '', '');
|
|
expect(Array.isArray(ops)).toBe(true);
|
|
});
|