test: establish storage adapter contracts

This commit is contained in:
vorotamoroz
2026-07-12 08:52:43 +00:00
parent a60932d9e4
commit dbb8d2be22
5 changed files with 186 additions and 10 deletions
@@ -70,6 +70,20 @@ export class FSAPIStorageAdapter implements IStorageAdapter<FSAPIStat> {
}
}
/** Resolve a writable file path after creating its parent directories. */
private async resolveWritablePath(p: string): Promise<{
dirHandle: FileSystemDirectoryHandle;
fileName: string;
} | null> {
const parts = p.split("/").filter((part) => part !== "");
if (parts.length === 0) return null;
const fileName = parts.pop()!;
const parentPath = parts.join("/");
await this.mkdir(parentPath);
const dirHandle = await this.getDirectoryHandle(parentPath);
return dirHandle ? { dirHandle, fileName } : null;
}
async exists(p: string): Promise<boolean> {
const fileHandle = await this.getFileHandle(p);
if (fileHandle) return true;
@@ -146,14 +160,11 @@ export class FSAPIStorageAdapter implements IStorageAdapter<FSAPIStat> {
}
async write(p: string, data: string, options?: UXDataWriteOptions): Promise<void> {
const resolved = await this.resolvePath(p);
const resolved = await this.resolveWritablePath(p);
if (!resolved) {
throw new Error(`Invalid path: ${p}`);
}
// Ensure parent directory exists
await this.mkdir(p.split("/").slice(0, -1).join("/"));
const fileHandle = await resolved.dirHandle.getFileHandle(resolved.fileName, { create: true });
const writable = await fileHandle.createWritable();
await writable.write(data);
@@ -161,14 +172,11 @@ export class FSAPIStorageAdapter implements IStorageAdapter<FSAPIStat> {
}
async writeBinary(p: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void> {
const resolved = await this.resolvePath(p);
const resolved = await this.resolveWritablePath(p);
if (!resolved) {
throw new Error(`Invalid path: ${p}`);
}
// Ensure parent directory exists
await this.mkdir(p.split("/").slice(0, -1).join("/"));
const fileHandle = await resolved.dirHandle.getFileHandle(resolved.fileName, { create: true });
const writable = await fileHandle.createWritable();
await writable.write(data);
@@ -0,0 +1,81 @@
import { describe, it } from "vitest";
import { storageAdapterContractCases } from "@/apps/storageAdapterContract";
import { FSAPIStorageAdapter } from "./FSAPIStorageAdapter";
class MemoryFileHandle {
readonly kind = "file";
private data = new Uint8Array();
constructor(readonly name: string) {}
async getFile(): Promise<File> {
return new File([this.data], this.name, { lastModified: 1 });
}
async createWritable(): Promise<FileSystemWritableFileStream> {
const handle = this;
return {
async write(data: FileSystemWriteChunkType) {
if (typeof data === "string") {
handle.data = new TextEncoder().encode(data);
} else if (data instanceof ArrayBuffer) {
handle.data = new Uint8Array(data.slice(0));
} else if (ArrayBuffer.isView(data)) {
handle.data = new Uint8Array(data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength));
} else {
throw new TypeError("Unsupported in-memory write type");
}
},
async close() {},
} as FileSystemWritableFileStream;
}
}
class MemoryDirectoryHandle {
readonly kind = "directory";
private readonly children = new Map<string, MemoryDirectoryHandle | MemoryFileHandle>();
constructor(readonly name: string) {}
async getDirectoryHandle(name: string, options?: FileSystemGetDirectoryOptions): Promise<FileSystemDirectoryHandle> {
const existing = this.children.get(name);
if (existing instanceof MemoryDirectoryHandle) return existing as unknown as FileSystemDirectoryHandle;
if (existing !== undefined || !options?.create) throw new DOMException("Directory not found", "NotFoundError");
const directory = new MemoryDirectoryHandle(name);
this.children.set(name, directory);
return directory as unknown as FileSystemDirectoryHandle;
}
async getFileHandle(name: string, options?: FileSystemGetFileOptions): Promise<FileSystemFileHandle> {
const existing = this.children.get(name);
if (existing instanceof MemoryFileHandle) return existing as unknown as FileSystemFileHandle;
if (existing !== undefined || !options?.create) throw new DOMException("File not found", "NotFoundError");
const file = new MemoryFileHandle(name);
this.children.set(name, file);
return file as unknown as FileSystemFileHandle;
}
async removeEntry(name: string, options?: FileSystemRemoveOptions): Promise<void> {
const existing = this.children.get(name);
if (existing === undefined) throw new DOMException("Entry not found", "NotFoundError");
if (existing instanceof MemoryDirectoryHandle && !options?.recursive && existing.children.size > 0) {
throw new DOMException("Directory is not empty", "InvalidModificationError");
}
this.children.delete(name);
}
async *entries(): AsyncIterableIterator<[string, FileSystemHandle]> {
for (const [name, entry] of this.children) {
yield [name, entry as unknown as FileSystemHandle];
}
}
}
describe("FSAPIStorageAdapter", () => {
for (const contractCase of storageAdapterContractCases) {
it(contractCase.name, async () => {
const root = new MemoryDirectoryHandle("root") as unknown as FileSystemDirectoryHandle;
await contractCase.run(new FSAPIStorageAdapter(root));
});
}
});