ZDDC/tests/diff.spec.js
ZDDC 9972e6773a feat(browse): markdown version-history viewer with diff + restore
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>
2026-05-28 12:49:00 -05:00

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);
});