UNPKG

@wordpress/upload-media

Version:
429 lines (355 loc) 12.4 kB
/** * Internal dependencies */ import { detectClientSideMediaSupport, isClientSideMediaSupported, isHeicCanvasSupported, clearFeatureDetectionCache, exceedsClientProcessingMemory, } from '../feature-detection'; describe( 'feature-detection', () => { const originalWebAssembly = global.WebAssembly; const originalSharedArrayBuffer = global.SharedArrayBuffer; const originalWorker = global.Worker; const originalCreateObjectURL = global.URL.createObjectURL; const originalRevokeObjectURL = global.URL.revokeObjectURL; // Store original property descriptors for navigator properties. const originalDeviceMemoryDescriptor = Object.getOwnPropertyDescriptor( navigator, 'deviceMemory' ); const originalConnectionDescriptor = Object.getOwnPropertyDescriptor( navigator, 'connection' ); const originalHardwareConcurrencyDescriptor = Object.getOwnPropertyDescriptor( navigator, 'hardwareConcurrency' ); beforeEach( () => { // Clear the cache before each test. clearFeatureDetectionCache(); // Restore WebAssembly and SharedArrayBuffer before each test. global.WebAssembly = originalWebAssembly; global.SharedArrayBuffer = originalSharedArrayBuffer; // By default, provide a mock Worker that does not throw (CSP allows blob workers). global.Worker = class MockWorker { terminate() {} } as unknown as typeof Worker; // jsdom does not implement URL.createObjectURL/revokeObjectURL. global.URL.createObjectURL = jest.fn( () => 'blob:http://localhost/test' ); global.URL.revokeObjectURL = jest.fn(); // Remove navigator.deviceMemory and navigator.connection by default // so they don't interfere with unrelated tests. if ( 'deviceMemory' in navigator ) { // @ts-ignore delete navigator.deviceMemory; } if ( 'connection' in navigator ) { // @ts-ignore delete navigator.connection; } if ( 'hardwareConcurrency' in navigator ) { // @ts-ignore delete navigator.hardwareConcurrency; } } ); afterEach( () => { // Restore original values. global.WebAssembly = originalWebAssembly; global.SharedArrayBuffer = originalSharedArrayBuffer; global.Worker = originalWorker; global.URL.createObjectURL = originalCreateObjectURL; global.URL.revokeObjectURL = originalRevokeObjectURL; // Restore navigator.deviceMemory. if ( originalDeviceMemoryDescriptor ) { Object.defineProperty( navigator, 'deviceMemory', originalDeviceMemoryDescriptor ); } else if ( 'deviceMemory' in navigator ) { // @ts-ignore delete navigator.deviceMemory; } // Restore navigator.connection. if ( originalConnectionDescriptor ) { Object.defineProperty( navigator, 'connection', originalConnectionDescriptor ); } else if ( 'connection' in navigator ) { // @ts-ignore delete navigator.connection; } // Restore navigator.hardwareConcurrency. if ( originalHardwareConcurrencyDescriptor ) { Object.defineProperty( navigator, 'hardwareConcurrency', originalHardwareConcurrencyDescriptor ); } else if ( 'hardwareConcurrency' in navigator ) { // @ts-ignore delete navigator.hardwareConcurrency; } } ); describe( 'detectClientSideMediaSupport', () => { it( 'returns supported when all features are available', () => { const result = detectClientSideMediaSupport(); expect( result.supported ).toBe( true ); expect( result.reason ).toBeUndefined(); } ); it( 'returns not supported when WebAssembly is unavailable', () => { // @ts-ignore - Intentionally setting WebAssembly to undefined for testing. global.WebAssembly = undefined; const result = detectClientSideMediaSupport(); expect( result.supported ).toBe( false ); expect( result.reason ).toBe( 'WebAssembly is not supported in this browser.' ); } ); it( 'returns not supported when SharedArrayBuffer is unavailable', () => { // @ts-ignore - Intentionally setting SharedArrayBuffer to undefined for testing. global.SharedArrayBuffer = undefined; const result = detectClientSideMediaSupport(); expect( result.supported ).toBe( false ); expect( result.reason ).toContain( 'SharedArrayBuffer' ); } ); it( 'returns not supported when Worker is unavailable', () => { // @ts-ignore - Intentionally setting Worker to undefined for testing. global.Worker = undefined; const result = detectClientSideMediaSupport(); expect( result.supported ).toBe( false ); expect( result.reason ).toBe( 'Web Workers are not supported in this browser.' ); } ); it( 'returns not supported when device memory is 2 GB or less', () => { Object.defineProperty( navigator, 'deviceMemory', { value: 2, configurable: true, } ); const result = detectClientSideMediaSupport(); expect( result.supported ).toBe( false ); expect( result.reason ).toContain( 'insufficient memory' ); } ); it( 'returns supported when device memory is greater than 2 GB', () => { Object.defineProperty( navigator, 'deviceMemory', { value: 4, configurable: true, } ); const result = detectClientSideMediaSupport(); expect( result.supported ).toBe( true ); } ); it( 'returns not supported when hardware concurrency is less than 2', () => { Object.defineProperty( navigator, 'hardwareConcurrency', { value: 1, configurable: true, } ); const result = detectClientSideMediaSupport(); expect( result.supported ).toBe( false ); expect( result.reason ).toContain( 'insufficient CPU cores' ); } ); it( 'returns supported when hardware concurrency is 2 or more', () => { Object.defineProperty( navigator, 'hardwareConcurrency', { value: 2, configurable: true, } ); const result = detectClientSideMediaSupport(); expect( result.supported ).toBe( true ); } ); it( 'returns not supported when data saver is enabled', () => { Object.defineProperty( navigator, 'connection', { value: { saveData: true, effectiveType: '4g' }, configurable: true, } ); const result = detectClientSideMediaSupport(); expect( result.supported ).toBe( false ); expect( result.reason ).toBe( 'Data saver mode is enabled.' ); } ); it( 'returns not supported when connection is 2g', () => { Object.defineProperty( navigator, 'connection', { value: { saveData: false, effectiveType: '2g' }, configurable: true, } ); const result = detectClientSideMediaSupport(); expect( result.supported ).toBe( false ); expect( result.reason ).toContain( 'too slow' ); } ); it( 'returns supported when connection is 3g', () => { Object.defineProperty( navigator, 'connection', { value: { saveData: false, effectiveType: '3g' }, configurable: true, } ); const result = detectClientSideMediaSupport(); expect( result.supported ).toBe( true ); } ); it( 'returns not supported when connection is slow-2g', () => { Object.defineProperty( navigator, 'connection', { value: { saveData: false, effectiveType: 'slow-2g', }, configurable: true, } ); const result = detectClientSideMediaSupport(); expect( result.supported ).toBe( false ); expect( result.reason ).toContain( 'too slow' ); } ); it( 'returns not supported when CSP blocks blob workers', () => { // Simulate CSP blocking blob URL workers by throwing a SecurityError. global.Worker = class ThrowingWorker { constructor() { throw new DOMException( "Refused to create a worker from 'blob:...' because it violates the Content Security Policy directive: \"worker-src 'self'\".", 'SecurityError' ); } } as unknown as typeof Worker; const result = detectClientSideMediaSupport(); expect( result.supported ).toBe( false ); expect( result.reason ).toContain( 'Content Security Policy' ); expect( result.reason ).toContain( 'worker-src' ); } ); it( 'caches the result', () => { const result1 = detectClientSideMediaSupport(); expect( result1.supported ).toBe( true ); // Now set WebAssembly to undefined - cached result should still be returned. // @ts-ignore - Intentionally setting WebAssembly to undefined for testing. global.WebAssembly = undefined; const result2 = detectClientSideMediaSupport(); expect( result2.supported ).toBe( true ); expect( result2 ).toBe( result1 ); // Same object reference. } ); } ); describe( 'isClientSideMediaSupported', () => { it( 'returns true when all features are available', () => { expect( isClientSideMediaSupported() ).toBe( true ); } ); it( 'returns false when features are unavailable', () => { // @ts-ignore - Intentionally setting WebAssembly to undefined for testing. global.WebAssembly = undefined; expect( isClientSideMediaSupported() ).toBe( false ); } ); } ); describe( 'isHeicCanvasSupported', () => { const originalCreateImageBitmap = global.createImageBitmap as typeof createImageBitmap; const originalOffscreenCanvas = global.OffscreenCanvas as typeof OffscreenCanvas; afterEach( () => { // Restore globals after each test. if ( originalCreateImageBitmap !== undefined ) { global.createImageBitmap = originalCreateImageBitmap as typeof createImageBitmap; } else { // @ts-ignore delete global.createImageBitmap; } if ( originalOffscreenCanvas !== undefined ) { global.OffscreenCanvas = originalOffscreenCanvas as typeof OffscreenCanvas; } else { // @ts-ignore delete global.OffscreenCanvas; } } ); it( 'returns true when both createImageBitmap and OffscreenCanvas are available', () => { global.createImageBitmap = jest.fn() as unknown as typeof createImageBitmap; global.OffscreenCanvas = jest.fn() as unknown as typeof OffscreenCanvas; expect( isHeicCanvasSupported() ).toBe( true ); } ); it( 'returns false when createImageBitmap is unavailable', () => { // @ts-ignore delete global.createImageBitmap; global.OffscreenCanvas = jest.fn() as unknown as typeof OffscreenCanvas; expect( isHeicCanvasSupported() ).toBe( false ); } ); it( 'returns false when OffscreenCanvas is unavailable', () => { global.createImageBitmap = jest.fn() as unknown as typeof createImageBitmap; // @ts-ignore delete global.OffscreenCanvas; expect( isHeicCanvasSupported() ).toBe( false ); } ); it( 'returns false when both are unavailable', () => { // @ts-ignore delete global.createImageBitmap; // @ts-ignore delete global.OffscreenCanvas; expect( isHeicCanvasSupported() ).toBe( false ); } ); } ); describe( 'clearFeatureDetectionCache', () => { it( 'clears the cached result', () => { const result1 = detectClientSideMediaSupport(); expect( result1.supported ).toBe( true ); // Clear cache and set WebAssembly to undefined. clearFeatureDetectionCache(); // @ts-ignore - Intentionally setting WebAssembly to undefined for testing. global.WebAssembly = undefined; const result2 = detectClientSideMediaSupport(); expect( result2.supported ).toBe( false ); } ); } ); describe( 'exceedsClientProcessingMemory', () => { it( 'allows typical images', () => { expect( exceedsClientProcessingMemory( { width: 4000, height: 3000, interlaced: false, } ) ).toBe( false ); expect( exceedsClientProcessingMemory( { width: 4000, height: 3000, interlaced: true, } ) ).toBe( false ); } ); it( 'gates the reported interlaced flower.jpg (20000x11857)', () => { expect( exceedsClientProcessingMemory( { width: 20000, height: 11857, interlaced: true, } ) ).toBe( true ); } ); it( 'applies a tighter budget to interlaced images', () => { // ~150 MP: over the ~0.5 GiB interlaced budget but under the // ~0.9 GiB baseline budget. const dimensions = { width: 15000, height: 10000 }; expect( exceedsClientProcessingMemory( { ...dimensions, interlaced: true, } ) ).toBe( true ); expect( exceedsClientProcessingMemory( { ...dimensions, interlaced: false, } ) ).toBe( false ); } ); it( 'gates extremely large baseline images', () => { // ~300 MP exceeds even the generous baseline budget. expect( exceedsClientProcessingMemory( { width: 20000, height: 15000, interlaced: false, } ) ).toBe( true ); } ); } ); } );