UNPKG

@julesl23/s5js

Version:

Enhanced TypeScript SDK for S5 decentralized storage with path-based API, media processing, and directory utilities

442 lines 18.5 kB
import { describe, it, expect, beforeEach } from 'vitest'; import { FS5 } from '../../src/fs/fs5.js'; import { JSCryptoImplementation } from '../../src/api/crypto/js.js'; // Mock browser APIs for media processing let lastCreatedBlob = null; global.Image = class Image { src = ''; onload = null; onerror = null; width = 800; height = 600; constructor() { setTimeout(() => { if (this.src === 'blob:mock-url' && lastCreatedBlob) { if (lastCreatedBlob.size < 10) { if (this.onerror) this.onerror(); return; } } if (this.onload) this.onload(); }, 0); } }; global.URL = { createObjectURL: (blob) => { lastCreatedBlob = blob; return 'blob:mock-url'; }, revokeObjectURL: (url) => { lastCreatedBlob = null; }, }; global.document = { createElement: (tag) => { if (tag === 'canvas') { const canvas = { _width: 0, _height: 0, get width() { return this._width; }, set width(val) { this._width = val; }, get height() { return this._height; }, set height(val) { this._height = val; }, getContext: () => ({ imageSmoothingEnabled: true, imageSmoothingQuality: 'high', fillStyle: '', drawImage: () => { }, fillRect: () => { }, getImageData: (x, y, w, h) => ({ width: w, height: h, data: new Uint8ClampedArray(w * h * 4), }), }), toBlob: (callback, type, quality) => { const baseSize = Math.max(canvas._width * canvas._height, 100); const qualityFactor = quality !== undefined ? quality : 0.92; const size = Math.floor(baseSize * qualityFactor * 0.5) + 50; const mockBlob = new Blob([new Uint8Array(size)], { type }); setTimeout(() => callback(mockBlob), 0); }, }; return canvas; } return {}; }, }; // Create a minimal mock API similar to path-api-simple.test.ts class SimpleMockAPI { crypto; blobs = new Map(); registry = new Map(); constructor() { this.crypto = new JSCryptoImplementation(); } async uploadBlob(blob) { const data = new Uint8Array(await blob.arrayBuffer()); const hash = await this.crypto.hashBlake3(data); const fullHash = new Uint8Array([0x1e, ...hash]); const key = Buffer.from(hash).toString('hex'); this.blobs.set(key, data); return { hash: fullHash, size: blob.size }; } async downloadBlobAsBytes(hash) { const actualHash = hash[0] === 0x1e ? hash.slice(1) : hash; const key = Buffer.from(actualHash).toString('hex'); const data = this.blobs.get(key); if (!data) throw new Error(`Blob not found: ${key}`); return data; } async registryGet(publicKey) { const key = Buffer.from(publicKey).toString('hex'); return this.registry.get(key); } async registrySet(entry) { const key = Buffer.from(entry.pk).toString('hex'); this.registry.set(key, entry); } } // Simple mock identity class SimpleMockIdentity { fsRootKey = new Uint8Array(32).fill(42); } describe('FS5 Media Extensions', () => { let fs; let api; let identity; let directories; // Helper to create test image blob const createTestImageBlob = () => { const jpegData = new Uint8Array([ 0xFF, 0xD8, 0xFF, 0xE0, // JPEG SOI and APP0 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xD9 // EOI ]); return new Blob([jpegData], { type: 'image/jpeg' }); }; beforeEach(() => { api = new SimpleMockAPI(); identity = new SimpleMockIdentity(); fs = new FS5(api, identity); // Initialize directory structure directories = new Map(); directories.set("", { magic: "S5.pro", header: {}, dirs: new Map(), files: new Map() }); // Mock _loadDirectory to return from our directory map fs._loadDirectory = async (path) => { const dir = directories.get(path || ""); if (!dir) { // Create directory if it doesn't exist const newDir = { magic: "S5.pro", header: {}, dirs: new Map(), files: new Map() }; directories.set(path, newDir); return newDir; } return dir; }; // Mock _updateDirectory to update our directory map fs._updateDirectory = async (path, updater) => { const segments = path.split('/').filter(s => s); // Ensure all parent directories exist for (let i = 0; i < segments.length; i++) { const currentPath = segments.slice(0, i + 1).join('/'); const parentPath = segments.slice(0, i).join('/') || ''; const dirName = segments[i]; if (!directories.has(currentPath)) { const newDir = { magic: "S5.pro", header: {}, dirs: new Map(), files: new Map() }; directories.set(currentPath, newDir); const parent = directories.get(parentPath); if (parent) { parent.dirs.set(dirName, { link: { type: 'fixed_hash_blake3', hash: new Uint8Array(32) } }); } } } const dir = directories.get(path || "") || { magic: "S5.pro", header: {}, dirs: new Map(), files: new Map() }; const result = await updater(dir, new Uint8Array(32)); if (result) { directories.set(path || "", result); } }; }); describe('putImage', () => { it('should upload an image and return reference', async () => { const blob = createTestImageBlob(); const result = await fs.putImage('gallery/photo.jpg', blob); expect(result).toBeDefined(); expect(result.path).toBe('gallery/photo.jpg'); }); it('should generate thumbnail by default', async () => { const blob = createTestImageBlob(); const result = await fs.putImage('gallery/photo.jpg', blob); expect(result.thumbnailPath).toBeDefined(); expect(result.thumbnailPath).toBe('gallery/.thumbnails/photo.jpg'); }); it('should extract metadata by default', async () => { const blob = createTestImageBlob(); const result = await fs.putImage('gallery/photo.jpg', blob); expect(result.metadata).toBeDefined(); expect(result.metadata?.width).toBeGreaterThan(0); expect(result.metadata?.height).toBeGreaterThan(0); expect(result.metadata?.format).toBe('jpeg'); }); it('should skip thumbnail generation when disabled', async () => { const blob = createTestImageBlob(); const options = { generateThumbnail: false }; const result = await fs.putImage('gallery/photo.jpg', blob, options); expect(result.thumbnailPath).toBeUndefined(); }); it('should skip metadata extraction when disabled', async () => { const blob = createTestImageBlob(); const options = { extractMetadata: false }; const result = await fs.putImage('gallery/photo.jpg', blob, options); expect(result.metadata).toBeUndefined(); }); it('should support custom thumbnail options', async () => { const blob = createTestImageBlob(); const options = { thumbnailOptions: { maxWidth: 128, maxHeight: 128, quality: 75 } }; const result = await fs.putImage('gallery/photo.jpg', blob, options); expect(result.thumbnailPath).toBeDefined(); }); it('should handle nested paths', async () => { const blob = createTestImageBlob(); const result = await fs.putImage('photos/2024/vacation/beach.jpg', blob); expect(result.path).toBe('photos/2024/vacation/beach.jpg'); expect(result.thumbnailPath).toBe('photos/2024/vacation/.thumbnails/beach.jpg'); }); it('should handle unicode filenames', async () => { const blob = createTestImageBlob(); const result = await fs.putImage('gallery/照片.jpg', blob); expect(result.path).toBe('gallery/照片.jpg'); }); }); describe('getThumbnail', () => { it('should return pre-generated thumbnail', async () => { const blob = createTestImageBlob(); await fs.putImage('gallery/photo.jpg', blob); const thumbnail = await fs.getThumbnail('gallery/photo.jpg'); expect(thumbnail).toBeInstanceOf(Blob); expect(thumbnail.type).toContain('image'); }); it('should generate thumbnail on-demand if missing', async () => { const blob = createTestImageBlob(); await fs.putImage('gallery/photo.jpg', blob, { generateThumbnail: false }); const thumbnail = await fs.getThumbnail('gallery/photo.jpg'); expect(thumbnail).toBeInstanceOf(Blob); }); it('should cache generated thumbnail by default', async () => { const blob = createTestImageBlob(); await fs.putImage('gallery/photo.jpg', blob, { generateThumbnail: false }); const thumbnail1 = await fs.getThumbnail('gallery/photo.jpg'); const thumbnail2 = await fs.getThumbnail('gallery/photo.jpg'); expect(thumbnail1).toBeInstanceOf(Blob); expect(thumbnail2).toBeInstanceOf(Blob); }); it('should support custom thumbnail options', async () => { const blob = createTestImageBlob(); await fs.putImage('gallery/photo.jpg', blob, { generateThumbnail: false }); const options = { thumbnailOptions: { maxWidth: 64, maxHeight: 64 } }; const thumbnail = await fs.getThumbnail('gallery/photo.jpg', options); expect(thumbnail).toBeInstanceOf(Blob); }); it('should throw error for non-existent image', async () => { await expect(fs.getThumbnail('nonexistent/photo.jpg')).rejects.toThrow(); }); it('should throw error for non-image file', async () => { await fs.put('documents/text.txt', new TextEncoder().encode('hello')); await expect(fs.getThumbnail('documents/text.txt')).rejects.toThrow(); }); }); describe('getImageMetadata', () => { it('should return stored metadata', async () => { const blob = createTestImageBlob(); await fs.putImage('gallery/photo.jpg', blob); const metadata = await fs.getImageMetadata('gallery/photo.jpg'); expect(metadata).toBeDefined(); expect(metadata.width).toBeGreaterThan(0); expect(metadata.height).toBeGreaterThan(0); expect(metadata.format).toBe('jpeg'); }); it('should extract fresh metadata if not stored', async () => { const blob = createTestImageBlob(); await fs.putImage('gallery/photo.jpg', blob, { extractMetadata: false }); const metadata = await fs.getImageMetadata('gallery/photo.jpg'); expect(metadata).toBeDefined(); expect(metadata.width).toBeGreaterThan(0); }); it('should throw error for non-existent image', async () => { await expect(fs.getImageMetadata('nonexistent/photo.jpg')).rejects.toThrow(); }); it('should throw error for non-image file', async () => { await fs.put('documents/text.txt', new TextEncoder().encode('hello')); await expect(fs.getImageMetadata('documents/text.txt')).rejects.toThrow(); }); }); describe('createImageGallery', () => { it('should upload multiple images', async () => { const images = [ { name: 'photo1.jpg', blob: createTestImageBlob() }, { name: 'photo2.jpg', blob: createTestImageBlob() }, { name: 'photo3.jpg', blob: createTestImageBlob() } ]; const results = await fs.createImageGallery('gallery', images); expect(results).toHaveLength(3); expect(results.every(r => r.path)).toBe(true); }); it('should generate thumbnails for all images', async () => { const images = [ { name: 'photo1.jpg', blob: createTestImageBlob() }, { name: 'photo2.jpg', blob: createTestImageBlob() } ]; const results = await fs.createImageGallery('gallery', images); expect(results.every(r => r.thumbnailPath !== undefined)).toBe(true); }); it('should create manifest.json by default', async () => { const images = [ { name: 'photo1.jpg', blob: createTestImageBlob() }, { name: 'photo2.jpg', blob: createTestImageBlob() } ]; await fs.createImageGallery('gallery', images); const manifestData = await fs.get('gallery/manifest.json'); expect(manifestData).toBeDefined(); // FS5.get() auto-decodes JSON files to objects const manifest = typeof manifestData === 'object' && manifestData !== null ? manifestData : (typeof manifestData === 'string' ? JSON.parse(manifestData) : JSON.parse(new TextDecoder().decode(manifestData))); expect(manifest.count).toBe(2); expect(manifest.images).toHaveLength(2); }); it('should skip manifest creation when disabled', async () => { const images = [ { name: 'photo1.jpg', blob: createTestImageBlob() } ]; const options = { createManifest: false }; await fs.createImageGallery('gallery', images, options); const manifestData = await fs.get('gallery/manifest.json'); expect(manifestData).toBeUndefined(); }); it('should call progress callback', async () => { const images = [ { name: 'photo1.jpg', blob: createTestImageBlob() }, { name: 'photo2.jpg', blob: createTestImageBlob() }, { name: 'photo3.jpg', blob: createTestImageBlob() } ]; const progressCalls = []; const options = { onProgress: (completed, total) => { progressCalls.push([completed, total]); } }; await fs.createImageGallery('gallery', images, options); expect(progressCalls.length).toBeGreaterThan(0); expect(progressCalls[progressCalls.length - 1]).toEqual([3, 3]); }); it('should respect concurrency limit', async () => { const images = Array.from({ length: 10 }, (_, i) => ({ name: `photo${i}.jpg`, blob: createTestImageBlob() })); const options = { concurrency: 2 }; const results = await fs.createImageGallery('gallery', images, options); expect(results).toHaveLength(10); }); it('should handle empty image list', async () => { const results = await fs.createImageGallery('gallery', []); expect(results).toHaveLength(0); }); it('should handle metadata in image uploads', async () => { const images = [ { name: 'photo1.jpg', blob: createTestImageBlob(), metadata: { format: 'jpeg' } } ]; const results = await fs.createImageGallery('gallery', images); expect(results[0].metadata).toBeDefined(); }); }); describe('Integration', () => { it('should work with regular FS5 operations', async () => { // Upload image const blob = createTestImageBlob(); await fs.putImage('photos/sunset.jpg', blob); // List directory const entries = []; for await (const entry of fs.list('photos')) { entries.push(entry); } expect(entries.some(e => e.name === 'sunset.jpg')).toBe(true); }); it('should support delete operations', async () => { const blob = createTestImageBlob(); await fs.putImage('temp/photo.jpg', blob); await fs.delete('temp/photo.jpg'); const result = await fs.get('temp/photo.jpg'); expect(result).toBeUndefined(); }); it('should handle thumbnails directory structure', async () => { const blob = createTestImageBlob(); await fs.putImage('gallery/photo.jpg', blob); const entries = []; for await (const entry of fs.list('gallery/.thumbnails')) { entries.push(entry); } expect(entries.some(e => e.name === 'photo.jpg')).toBe(true); }); }); }); //# sourceMappingURL=media-extensions.test.js.map