ZDDC/tests/browse.spec.js
ZDDC 7d4d2dc9a2 feat(browse): two-pane shell + markdown plugin + grid mode (Phases A/B/C/D)
Reshape browse from "tree-as-table with popup preview" into a unified
file-experience tool with three layered behaviors:

  Phase A — Two-pane shell
  Phase B — Markdown plugin (Toast UI inline)
  Phase C — Grid mode (classifier workflow)
  Phase D — Deprecation banners on standalone classifier + mdedit

= Phase A: two-pane shell + lightweight preview plugins =

Browse's table view becomes a tree-pane on the left + preview-pane on
the right with a draggable resizer. Click a folder → expand inline.
Click a file → render in the right pane. The previous popup window
becomes an explicit "⤴ Pop out" button in the right-pane header for
users with a second monitor.

Preview rendering reuses shared/preview-lib.js (PDF iframe, image
<img>, TIFF, ZIP listing, text <pre>). Unknown types show a download
link. browse/js/preview.js refactored into renderInline (default) +
renderInPopup (Pop out button); both share the same plugin
dispatch logic.

Filter rows were already removed earlier this session. Sort columns
likewise — the tree is alphabetical by default; the underlying
setSort API still exists for future re-introduction.

= Phase B: markdown plugin =

New browse/js/preview-markdown.js: when a .md or .markdown file is
clicked, the right pane mounts a Toast UI editor (initial-value =
file contents) with a small toolbar containing Save + dirty indicator
+ status text. Save sends PUT through the file API for server-mode
files; non-server sources are read-only for now (deferred to a
follow-up that wires zddc-source.js writes too). Ctrl+S / Cmd+S
inside the editor saves.

Toast UI Editor (~700 KB JS + ~160 KB CSS) was previously bundled
only in mdedit/vendor/. Moved to shared/vendor/ so browse and mdedit
both pull from one location.

= Phase C: grid mode =

View-mode toggle [Browse | Grid] in the toolbar. Grid mode loads the
classifier tool as an iframe scoped to the current directory (server
mode at working/staging/incoming locations) — classifier's full
bulk-rename workflow without leaving browse. v1 implementation; a
future iteration could bundle classifier's modules directly into
browse for tighter integration. Hostile cases (file:// origin, paths
outside working/staging/incoming) show a friendly explanation
instead of a blank iframe.

new browse/js/grid.js handles the activation logic.

= Phase D: deprecation banners =

mdedit and classifier standalones gain a "this tool is being absorbed
into Browse" advisory banner. Both standalones remain fully
functional and continue to ship — they're useful for offline single-
file editing and air-gapped environments. The banner just points
users toward the unified browse experience.

= Files =

  + browse/js/preview-markdown.js   (markdown plugin)
  + browse/js/grid.js               (grid-mode plugin)
  M browse/template.html            (two-pane layout, view toggle, banners)
  M browse/css/tree.css             (two-pane CSS, replaces table styles)
  M browse/js/init.js               (state additions: selectedId, viewMode)
  M browse/js/tree.js               (rowHtml: <tr>+<td> → <div>)
  M browse/js/preview.js            (renderInline / renderInPopup split)
  M browse/js/events.js             (toggle wiring, resizer, click handlers
                                     adapted from <table> to <div>)
  M browse/build.sh                 (Toast UI vendor + new modules)
  R mdedit/vendor/toastui-*         → shared/vendor/  (one bundle, two tools)
  M mdedit/build.sh                 (paths)
  M mdedit/template.html            (deprecation banner)
  M classifier/template.html        (deprecation banner)
  M tests/browse.spec.js            (selectors updated for new layout +
                                     new "click file → preview" test)

Bundle sizes after this commit:
  browse:     ~1020 KB  (was ~290 KB; added Toast UI ~700 KB)
  classifier: ~1470 KB  (unchanged from prior baseline)
  mdedit:     ~2140 KB  (unchanged; vendor location moved but not added)

What's deferred:
  - TOC + front-matter pane in browse's markdown plugin (mdedit has
    these; browse v1 uses just the editor).
  - FS-API writes from browse's markdown plugin (server PUT works).
  - Classifier modules bundled directly into browse (v1 uses iframe).
  - Sort UI in the new tree (model still supports it; no widget yet).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:46:51 -05:00

75 lines
3.3 KiB
JavaScript

import { test, expect } from '@playwright/test';
import { MOCK_FS_INIT_SCRIPT } from './fixtures/mock-fs-api.js';
import * as path from 'path';
const HTML_PATH = path.resolve('browse/dist/browse.html');
test.describe('Browse', () => {
test.beforeEach(async ({ page }) => {
await page.addInitScript(MOCK_FS_INIT_SCRIPT);
});
test('loads with the empty state visible and add-directory button enabled', async ({ page }) => {
await page.goto(`file://${HTML_PATH}`, { waitUntil: 'load' });
await page.waitForSelector('#addDirectoryBtn', { timeout: 15000 });
// file:// + no auto-server-root → the empty state is shown and the
// browse table stays hidden until the user picks a directory.
await expect(page.locator('#emptyState')).toBeVisible();
await expect(page.locator('#browseRoot')).toBeHidden();
const addDirBtn = page.locator('#addDirectoryBtn');
await expect(addDirBtn).toBeVisible();
await expect(addDirBtn).not.toBeDisabled();
});
test('renders rows from a mock directory after picking', async ({ page }) => {
await page.goto(`file://${HTML_PATH}`, { waitUntil: 'load' });
await page.waitForSelector('#addDirectoryBtn', { timeout: 15000 });
await page.evaluate(() => {
window.__setMockDirectory('docs', [
{ name: 'spec.pdf', content: '%PDF-fixture', size: 1024 },
{ name: 'readme.md', content: '# Hello\n', size: 8 },
{ name: 'notes.txt', content: 'note one\nnote two\n', size: 16 },
]);
});
await page.locator('#addDirectoryBtn').click();
// Browse swaps from empty state to the two-pane layout. The
// tree pane is on the left; rows are <div class="tree-row">.
await page.waitForSelector('#browseRoot:not(.hidden)', { timeout: 10000 });
await page.waitForFunction(
() => document.querySelectorAll('#treeBody .tree-row').length >= 3,
{ timeout: 10000 }
);
const rows = await page.locator('#treeBody .tree-row').count();
expect(rows).toBeGreaterThanOrEqual(3);
// Right preview pane is present and starts in the empty state.
await expect(page.locator('#previewPane')).toBeVisible();
await expect(page.locator('#previewBody')).toContainText(/Click a file/);
});
test('clicking a file shows it in the preview pane', async ({ page }) => {
await page.goto(`file://${HTML_PATH}`, { waitUntil: 'load' });
await page.waitForSelector('#addDirectoryBtn', { timeout: 15000 });
await page.evaluate(() => {
window.__setMockDirectory('docs', [
{ name: 'notes.txt', content: 'hello world', size: 11 },
]);
});
await page.locator('#addDirectoryBtn').click();
await page.waitForSelector('#treeBody .tree-row', { timeout: 10000 });
// Click the file row.
await page.locator('#treeBody .tree-row[data-isdir="false"]').first().click();
// Preview title updates to the file name; pop-out button appears.
await expect(page.locator('#previewTitle')).toHaveText(/notes\.txt/);
await expect(page.locator('#previewPopout')).toBeVisible();
});
});