UNPKG

@julesl23/s5js

Version:

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

339 lines 14.8 kB
import { describe, it, expect } from 'vitest'; import { ThumbnailGenerator } from '../../src/media/thumbnail/generator.js'; // Mock browser APIs for Node.js environment let lastCreatedBlob = null; global.Image = class Image { src = ''; onload = null; onerror = null; width = 100; height = 100; constructor() { // Simulate image loading setTimeout(async () => { // Check if this is a corrupted blob (very small size indicates corruption) if (this.src === 'blob:mock-url' && lastCreatedBlob) { // For corrupted images (less than 10 bytes), trigger error 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; }, }; // Mock document and canvas 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: (type, options) => ({ imageSmoothingEnabled: true, imageSmoothingQuality: 'high', fillStyle: '', drawImage: () => { }, fillRect: () => { }, // Add fillRect for test helper getImageData: (x, y, w, h) => ({ width: w, height: h, data: new Uint8ClampedArray(w * h * 4), }), }), toBlob: (callback, type, quality) => { // Create a mock blob with realistic size based on dimensions and quality // Ensure minimum size for valid images const baseSize = Math.max(canvas._width * canvas._height, 100); const qualityFactor = quality !== undefined ? quality : 0.92; // default quality const size = Math.floor(baseSize * qualityFactor * 0.5) + 50; // Rough estimate of compressed size const mockBlob = new Blob([new Uint8Array(size)], { type }); setTimeout(() => callback(mockBlob), 0); }, }; return canvas; } return {}; }, }; describe('ThumbnailGenerator', () => { // Helper to create a simple test image blob (1x1 red pixel PNG) const createTestImageBlob = () => { // 1x1 red pixel PNG (base64 decoded) const pngData = new Uint8Array([ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, // IHDR chunk 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, // 1x1 dimensions 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xDE, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, // IDAT chunk 0x54, 0x08, 0xD7, 0x63, 0xF8, 0xCF, 0xC0, 0x00, 0x00, 0x03, 0x01, 0x01, 0x00, 0x18, 0xDD, 0x8D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, // IEND chunk 0x44, 0xAE, 0x42, 0x60, 0x82 ]); return new Blob([pngData], { type: 'image/png' }); }; // Helper to create a larger test image (100x100 checkerboard pattern) const createLargeTestImageBlob = async () => { // In Node.js environment, we'll create a simple colored PNG // For browser environment, we could use Canvas API if (typeof document !== 'undefined') { const canvas = document.createElement('canvas'); canvas.width = 100; canvas.height = 100; const ctx = canvas.getContext('2d'); if (!ctx) { throw new Error('Failed to get canvas context'); } // Draw checkerboard pattern for (let y = 0; y < 100; y += 10) { for (let x = 0; x < 100; x += 10) { ctx.fillStyle = (x + y) % 20 === 0 ? '#000' : '#FFF'; ctx.fillRect(x, y, 10, 10); } } return new Promise((resolve, reject) => { canvas.toBlob((blob) => blob ? resolve(blob) : reject(new Error('Failed to create blob')), 'image/png'); }); } else { // For Node.js, return a simple test blob return createTestImageBlob(); } }; describe('Basic thumbnail generation', () => { it('should generate a thumbnail with default options', async () => { const blob = createTestImageBlob(); const result = await ThumbnailGenerator.generateThumbnail(blob); expect(result).toBeDefined(); expect(result.blob).toBeInstanceOf(Blob); expect(result.width).toBeGreaterThan(0); expect(result.height).toBeGreaterThan(0); expect(result.format).toBe('jpeg'); expect(result.quality).toBe(85); // default expect(result.processingTime).toBeGreaterThanOrEqual(0); }); it('should respect maxWidth and maxHeight options', async () => { const blob = await createLargeTestImageBlob(); const options = { maxWidth: 50, maxHeight: 50 }; const result = await ThumbnailGenerator.generateThumbnail(blob, options); expect(result.width).toBeLessThanOrEqual(50); expect(result.height).toBeLessThanOrEqual(50); }); it('should maintain aspect ratio by default', async () => { const blob = await createLargeTestImageBlob(); const options = { maxWidth: 50, maxHeight: 100 }; const result = await ThumbnailGenerator.generateThumbnail(blob, options); // Original is 100x100 (1:1 ratio), so thumbnail should also be 1:1 // Given max 50x100, it should be 50x50 to maintain ratio expect(result.width).toBe(50); expect(result.height).toBe(50); }); it('should allow disabling aspect ratio maintenance', async () => { const blob = await createLargeTestImageBlob(); const options = { maxWidth: 50, maxHeight: 100, maintainAspectRatio: false }; const result = await ThumbnailGenerator.generateThumbnail(blob, options); expect(result.width).toBe(50); expect(result.height).toBe(100); }); it('should support custom quality setting', async () => { const blob = createTestImageBlob(); const options = { quality: 50 }; const result = await ThumbnailGenerator.generateThumbnail(blob, options); expect(result.quality).toBe(50); }); }); describe('Format support', () => { it('should generate JPEG thumbnails', async () => { const blob = createTestImageBlob(); const options = { format: 'jpeg' }; const result = await ThumbnailGenerator.generateThumbnail(blob, options); expect(result.format).toBe('jpeg'); expect(result.blob.type).toContain('jpeg'); }); it('should generate PNG thumbnails', async () => { const blob = createTestImageBlob(); const options = { format: 'png' }; const result = await ThumbnailGenerator.generateThumbnail(blob, options); expect(result.format).toBe('png'); expect(result.blob.type).toContain('png'); }); it('should generate WebP thumbnails', async () => { const blob = createTestImageBlob(); const options = { format: 'webp' }; const result = await ThumbnailGenerator.generateThumbnail(blob, options); expect(result.format).toBe('webp'); expect(result.blob.type).toContain('webp'); }); }); describe('Target size optimization', () => { it('should adjust quality to meet target size', async () => { const blob = await createLargeTestImageBlob(); const targetSize = 2048; // 2KB target const options = { targetSize, quality: 95 // Start high, should be reduced }; const result = await ThumbnailGenerator.generateThumbnail(blob, options); expect(result.blob.size).toBeLessThanOrEqual(targetSize); expect(result.quality).toBeLessThan(95); // Quality should be reduced }); it('should not increase quality above requested value', async () => { const blob = createTestImageBlob(); const options = { targetSize: 1024 * 1024, // 1MB - very large target quality: 50 }; const result = await ThumbnailGenerator.generateThumbnail(blob, options); expect(result.quality).toBeLessThanOrEqual(50); }); it('should handle target size larger than result', async () => { const blob = createTestImageBlob(); const options = { targetSize: 1024 * 1024, // 1MB quality: 85 }; const result = await ThumbnailGenerator.generateThumbnail(blob, options); expect(result.blob.size).toBeLessThanOrEqual(1024 * 1024); expect(result.quality).toBe(85); // Should keep original quality }); }); describe('Smart cropping', () => { it('should support smart crop option', async () => { const blob = await createLargeTestImageBlob(); const options = { maxWidth: 50, maxHeight: 50, maintainAspectRatio: false, smartCrop: true }; const result = await ThumbnailGenerator.generateThumbnail(blob, options); expect(result.width).toBe(50); expect(result.height).toBe(50); }); it('should work without smart crop', async () => { const blob = await createLargeTestImageBlob(); const options = { maxWidth: 50, maxHeight: 50, maintainAspectRatio: false, smartCrop: false }; const result = await ThumbnailGenerator.generateThumbnail(blob, options); expect(result.width).toBe(50); expect(result.height).toBe(50); }); }); describe('Performance', () => { it('should complete processing within reasonable time', async () => { const blob = await createLargeTestImageBlob(); const startTime = performance.now(); const result = await ThumbnailGenerator.generateThumbnail(blob); const duration = performance.now() - startTime; expect(result.processingTime).toBeGreaterThanOrEqual(0); expect(duration).toBeLessThan(5000); // 5 seconds max }); it('should handle concurrent thumbnail generation', async () => { const blobs = await Promise.all([ createLargeTestImageBlob(), createLargeTestImageBlob(), createLargeTestImageBlob() ]); const startTime = performance.now(); const results = await Promise.all(blobs.map(blob => ThumbnailGenerator.generateThumbnail(blob, { maxWidth: 128, maxHeight: 128 }))); const duration = performance.now() - startTime; expect(results).toHaveLength(3); expect(results.every(r => r.blob.size > 0)).toBe(true); expect(duration).toBeLessThan(10000); // 10 seconds for 3 images }); }); describe('Error handling', () => { it('should handle invalid blob gracefully', async () => { const invalidBlob = new Blob(['not an image'], { type: 'text/plain' }); await expect(ThumbnailGenerator.generateThumbnail(invalidBlob)).rejects.toThrow(); }); it('should handle empty blob', async () => { const emptyBlob = new Blob([], { type: 'image/png' }); await expect(ThumbnailGenerator.generateThumbnail(emptyBlob)).rejects.toThrow(); }); it('should handle corrupted image data', async () => { // Create a blob that looks like an image but has corrupted data const corruptedData = new Uint8Array([ 0x89, 0x50, 0x4E, 0x47, // PNG signature 0x00, 0x00, 0x00, 0x00 // Invalid data ]); const corruptedBlob = new Blob([corruptedData], { type: 'image/png' }); await expect(ThumbnailGenerator.generateThumbnail(corruptedBlob)).rejects.toThrow(); }); }); describe('Edge cases', () => { it('should handle very small images', async () => { const blob = createTestImageBlob(); // 1x1 image const options = { maxWidth: 256, maxHeight: 256 }; const result = await ThumbnailGenerator.generateThumbnail(blob, options); expect(result.width).toBeGreaterThan(0); expect(result.height).toBeGreaterThan(0); }); it('should handle quality at minimum (1)', async () => { const blob = createTestImageBlob(); const options = { quality: 1 }; const result = await ThumbnailGenerator.generateThumbnail(blob, options); expect(result.quality).toBe(1); expect(result.blob.size).toBeGreaterThan(0); }); it('should handle quality at maximum (100)', async () => { const blob = createTestImageBlob(); const options = { quality: 100 }; const result = await ThumbnailGenerator.generateThumbnail(blob, options); expect(result.quality).toBe(100); expect(result.blob.size).toBeGreaterThan(0); }); }); }); //# sourceMappingURL=thumbnail-generator.test.js.map