ZDDC/tests/fixtures/mock-fs-api.js
ZDDC ea385b5366 Initial commit
ZDDC — Zero Day Document Control. A file-naming convention plus five
single-file HTML tools (archive, transmittal, classifier, mdedit,
landing) and an optional Go HTTP server (zddc-server) with ACL and a
virtual archive index. Self-contained, offline-capable, dependency-free.

See README.md for an overview, AGENTS.md and ARCHITECTURE.md for the
build/release/architecture detail, bootstrap/README.md for the
two-level deployment install pattern, and zddc/README.md for the
HTTP server.
2026-04-27 11:05:47 -05:00

196 lines
6.1 KiB
JavaScript

/**
* Reusable File System Access API mock for Playwright tests.
*
* Usage in a Playwright test:
*
* import { MOCK_FS_INIT_SCRIPT, buildMockDirectory } from './fixtures/mock-fs-api.js';
*
* test.beforeEach(async ({ page }) => {
* await page.addInitScript(MOCK_FS_INIT_SCRIPT);
* });
*
* test('loads files', async ({ page }) => {
* const files = [
* { name: '123456-EL-SPC-2623_A (IFR) - Spec.pdf', content: 'pdf-bytes', size: 45000 },
* ];
* await page.evaluate((files) => {
* window.__setMockDirectory('test-project', files);
* }, files);
* await page.goto(`file://${htmlPath}`);
* });
*/
/**
* Init script string to inject via page.addInitScript().
* Defines MockFileHandle, MockDirectoryHandle, and overrides the
* showDirectoryPicker / showOpenFilePicker / showSaveFilePicker APIs.
*/
export const MOCK_FS_INIT_SCRIPT = `
class MockFileHandle {
constructor(name, content, size) {
this.kind = 'file';
this.name = name;
this._content = content || '';
this._size = size || (content ? content.length : 0);
}
async getFile() {
const blob = new File([this._content], this.name, {
lastModified: Date.now(),
type: this._guessMimeType(),
});
if (this._size && this._size !== blob.size) {
Object.defineProperty(blob, 'size', { value: this._size, writable: false });
}
return blob;
}
async createWritable() {
const name = this.name;
const chunks = [];
return {
write(data) { chunks.push(data); },
close() {
window.__writtenFiles = window.__writtenFiles || {};
window.__writtenFiles[name] = chunks;
},
};
}
_guessMimeType() {
const ext = this.name.split('.').pop().toLowerCase();
const types = {
pdf: 'application/pdf',
html: 'text/html',
json: 'application/json',
txt: 'text/plain',
dwg: 'application/acad',
md: 'text/markdown',
};
return types[ext] || 'application/octet-stream';
}
}
class MockDirectoryHandle {
constructor(name, entries) {
this.kind = 'directory';
this.name = name;
this._entries = entries || [];
}
async *values() {
for (const entry of this._entries) {
yield entry;
}
}
async *entries() {
for (const entry of this._entries) {
yield [entry.name, entry];
}
}
async *keys() {
for (const entry of this._entries) {
yield entry.name;
}
}
async getFileHandle(name, opts) {
const found = this._entries.find(e => e.kind === 'file' && e.name === name);
if (found) return found;
if (opts && opts.create) {
const handle = new MockFileHandle(name, '');
this._entries.push(handle);
return handle;
}
throw new DOMException('A requested file or directory could not be found.', 'NotFoundError');
}
async getDirectoryHandle(name, opts) {
const found = this._entries.find(e => e.kind === 'directory' && e.name === name);
if (found) return found;
if (opts && opts.create) {
const handle = new MockDirectoryHandle(name, []);
this._entries.push(handle);
return handle;
}
throw new DOMException('A requested file or directory could not be found.', 'NotFoundError');
}
async queryPermission() { return 'granted'; }
async requestPermission() { return 'granted'; }
async resolve(child) { return child ? [child.name] : null; }
}
// Expose constructors globally for per-test customization
window.__MockFileHandle = MockFileHandle;
window.__MockDirectoryHandle = MockDirectoryHandle;
window.__writtenFiles = {};
/**
* Helper to build a mock directory from a flat file list.
* @param {string} dirName - Root directory name
* @param {Array<{name: string, content?: string, size?: number}>} files - Flat file list
*/
window.__setMockDirectory = function(dirName, files) {
const entries = (files || []).map(f =>
new MockFileHandle(f.name, f.content || '', f.size || 0)
);
window.__mockRootDirectory = new MockDirectoryHandle(dirName, entries);
};
/**
* Helper to build nested mock directories.
* @param {string} dirName - Root directory name
* @param {Object} tree - Nested structure: { 'subdir': { 'file.txt': 'content' }, 'root.pdf': 'content' }
*/
window.__setMockDirectoryTree = function(dirName, tree) {
function buildEntries(obj) {
const entries = [];
for (const [name, value] of Object.entries(obj)) {
if (typeof value === 'object' && value !== null && !ArrayBuffer.isView(value)) {
entries.push(new MockDirectoryHandle(name, buildEntries(value)));
} else {
entries.push(new MockFileHandle(name, String(value), String(value).length));
}
}
return entries;
}
window.__mockRootDirectory = new MockDirectoryHandle(dirName, buildEntries(tree));
};
// Override File System Access API pickers
window.showDirectoryPicker = async function(opts) {
if (!window.__mockRootDirectory) {
throw new DOMException('The user aborted a request.', 'AbortError');
}
return window.__mockRootDirectory;
};
window.showOpenFilePicker = async function(opts) {
if (!window.__mockOpenFiles || window.__mockOpenFiles.length === 0) {
throw new DOMException('The user aborted a request.', 'AbortError');
}
return window.__mockOpenFiles;
};
window.showSaveFilePicker = async function(opts) {
const name = (opts && opts.suggestedName) || 'untitled';
const handle = new MockFileHandle(name, '');
window.__lastSaveHandle = handle;
return handle;
};
`;
/**
* Helper to build a mock directory structure for page.evaluate().
* Call this in test code (Node-side), then pass to page.evaluate().
*
* @param {string} name - Directory name
* @param {Array<{name: string, content?: string, size?: number}>} files
* @returns {Object} Serializable directory description
*/
export function buildMockDirectory(name, files) {
return { name, files };
}