201 lines
6.4 KiB
JavaScript
201 lines
6.4 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 (ArrayBuffer.isView(value) || value instanceof ArrayBuffer || value instanceof Blob) {
|
|
// Binary file content (e.g. a zip's bytes) — keep it intact;
|
|
// String()-ing a Uint8Array would corrupt it.
|
|
const len = value.byteLength != null ? value.byteLength : (value.size || 0);
|
|
entries.push(new MockFileHandle(name, value, len));
|
|
} else if (typeof value === 'object' && value !== null) {
|
|
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 };
|
|
}
|