- tests/browse.spec.js: expand a .zip in the file tree (offline), drill into a member subdir, preview a text member — exercises shared/zip-source.js and the migrated offline path end to end. - tests/archive.spec.js: a .zip whose name parses as a transmittal folder is scanned like an uncompressed one — members land in the file list with tracking numbers parsed, tied to the zip transmittal's folder. - tests/fixtures/mock-fs-api.js: __setMockDirectoryTree now keeps binary leaf values (Uint8Array/ArrayBuffer/Blob) intact instead of String()-ing them — needed to feed real zip bytes through the mock FS. - tests/data/test-archive.sh: each party gets one transmittal delivered as a single .zip in received/, so the bitnest fixture exercises the zip-as-virtual-directory path. - ARCHITECTURE.md / AGENTS.md: document .zip-as-navigable-directory (server route + ACL model + shared client adapter + the one-level nesting limit). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
150 lines
7.1 KiB
JavaScript
150 lines
7.1 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();
|
|
});
|
|
|
|
test('clicking a .md file mounts the markdown editor with a TOC', async ({ page }) => {
|
|
await page.goto(`file://${HTML_PATH}`, { waitUntil: 'load' });
|
|
await page.waitForSelector('#addDirectoryBtn', { timeout: 15000 });
|
|
|
|
await page.evaluate(() => {
|
|
window.__setMockDirectory('notes', [
|
|
{
|
|
name: 'readme.md',
|
|
content: '# Title\n\nIntro.\n\n## Section One\n\nText.\n\n### Subsection\n\nDeeper.',
|
|
size: 100,
|
|
},
|
|
]);
|
|
});
|
|
await page.locator('#addDirectoryBtn').click();
|
|
await page.waitForSelector('#treeBody .tree-row[data-isdir="false"]', { timeout: 10000 });
|
|
await page.locator('#treeBody .tree-row[data-isdir="false"]').first().click();
|
|
|
|
// Markdown plugin DOM mounts: shell, sidebar (front matter +
|
|
// TOC), content (info header + editor).
|
|
await expect(page.locator('.md-shell')).toBeVisible({ timeout: 15000 });
|
|
await expect(page.locator('.md-shell__sidebar')).toBeVisible();
|
|
await expect(page.locator('.md-shell__infohdr')).toBeVisible();
|
|
await expect(page.locator('.md-shell__editor')).toBeVisible();
|
|
|
|
// Outline lists the three headings.
|
|
await page.waitForSelector('.md-toc__list .md-toc__item', { timeout: 10000 });
|
|
const tocItems = await page.locator('.md-toc__list .md-toc__item').allTextContents();
|
|
expect(tocItems).toEqual(['Title', 'Section One', 'Subsection']);
|
|
|
|
// Source hint reflects local FS-API mode.
|
|
await expect(page.locator('.md-shell__source')).toHaveText(/local/i);
|
|
});
|
|
|
|
test('expands a .zip transmittal folder and previews a member', async ({ page }) => {
|
|
await page.goto(`file://${HTML_PATH}`, { waitUntil: 'load' });
|
|
await page.waitForSelector('#addDirectoryBtn', { timeout: 15000 });
|
|
|
|
// Build a real zip in-browser (JSZip is bundled into browse.html)
|
|
// and hand its bytes to the mock FS as a single .zip file whose
|
|
// name parses as a transmittal folder.
|
|
await page.evaluate(async () => {
|
|
const zip = new window.JSZip();
|
|
zip.file('DOC-001 (IFI) - Spec.pdf', '%PDF-1.4 fixture body');
|
|
zip.file('sub/note.txt', 'a note inside the zip');
|
|
const buf = await zip.generateAsync({ type: 'uint8array' });
|
|
window.__setMockDirectory('PartyA', [
|
|
{ name: '2025-05-12_DOC-001 (IFI) - Title.zip', content: buf, size: buf.length },
|
|
]);
|
|
});
|
|
await page.locator('#addDirectoryBtn').click();
|
|
await page.waitForSelector('#treeBody .tree-row[data-iszip="true"]', { timeout: 10000 });
|
|
|
|
// Expand the .zip — it should list its members like a folder.
|
|
await page.locator('#treeBody .tree-row[data-iszip="true"]').first().click();
|
|
await page.waitForFunction(
|
|
() => document.querySelectorAll('#treeBody .tree-row').length >= 3,
|
|
{ timeout: 10000 }
|
|
);
|
|
const labels = await page.locator('#treeBody .tree-name__label').allTextContents();
|
|
expect(labels.join('|')).toContain('DOC-001 (IFI) - Spec.pdf');
|
|
expect(labels.join('|')).toContain('sub');
|
|
|
|
// Drill into the subdir, then preview the text member.
|
|
await page.locator('#treeBody .tree-row[data-isdir="true"]').last().click();
|
|
await page.waitForFunction(
|
|
() => Array.from(document.querySelectorAll('#treeBody .tree-name__label'))
|
|
.some(el => el.textContent === 'note.txt'),
|
|
{ timeout: 10000 }
|
|
);
|
|
const noteRow = page.locator('#treeBody .tree-row', { has: page.locator('.tree-name__label', { hasText: /^note\.txt$/ }) });
|
|
await noteRow.click();
|
|
await expect(page.locator('#previewTitle')).toHaveText(/note\.txt/);
|
|
await expect(page.locator('#previewBody')).toContainText('a note inside the zip');
|
|
});
|
|
});
|