@wordpress/upload-media
Version:
Core media upload logic.
351 lines (290 loc) • 9.95 kB
text/typescript
/**
* External dependencies
*/
// Mock the vips worker module.
// The mock functions must be declared inside the factory to avoid hoisting issues.
jest.mock( '@wordpress/vips/worker', () => ( {
vipsConvertImageFormat: jest.fn(),
vipsCompressImage: jest.fn(),
vipsHasTransparency: jest.fn(),
vipsResizeImage: jest.fn(),
vipsRotateImage: jest.fn(),
vipsCancelOperations: jest.fn(),
} ) );
// Import the mocked module to get access to the mock functions.
import * as vipsWorker from '@wordpress/vips/worker';
/**
* Internal dependencies
*/
import { ImageFile } from '../../image-file';
import type { ImageSizeCrop } from '../types';
// Import after mock is set up.
import {
vipsConvertImageFormat,
vipsCompressImage,
vipsHasTransparency,
vipsResizeImage,
vipsRotateImage,
vipsCancelOperations,
} from '../utils';
// Cast to jest.Mock for type safety.
const mockConvertImageFormat = vipsWorker.vipsConvertImageFormat as jest.Mock;
const mockCompressImage = vipsWorker.vipsCompressImage as jest.Mock;
const mockHasTransparency = vipsWorker.vipsHasTransparency as jest.Mock;
const mockResizeImage = vipsWorker.vipsResizeImage as jest.Mock;
const mockRotateImage = vipsWorker.vipsRotateImage as jest.Mock;
const mockCancelOperations = vipsWorker.vipsCancelOperations as jest.Mock;
const jpegFile = new File( [ 'test-content' ], 'test.jpg', {
type: 'image/jpeg',
lastModified: 1234567890,
} );
const pngFile = new File( [ 'test-content' ], 'image.png', {
type: 'image/png',
lastModified: 1234567890,
} );
describe( 'vips utilities', () => {
beforeEach( () => {
jest.clearAllMocks();
} );
describe( 'vipsConvertImageFormat', () => {
it( 'converts image and returns new File with correct extension', async () => {
mockConvertImageFormat.mockResolvedValue( new ArrayBuffer( 10 ) );
const result = await vipsConvertImageFormat(
'item-1',
jpegFile,
'image/webp',
0.8
);
expect( result.name ).toBe( 'test.webp' );
expect( result.type ).toBe( 'image/webp' );
expect( mockConvertImageFormat ).toHaveBeenCalledTimes( 1 );
expect( mockConvertImageFormat.mock.calls[ 0 ][ 0 ] ).toBe(
'item-1'
);
expect( mockConvertImageFormat.mock.calls[ 0 ][ 2 ] ).toBe(
'image/jpeg'
);
expect( mockConvertImageFormat.mock.calls[ 0 ][ 3 ] ).toBe(
'image/webp'
);
expect( mockConvertImageFormat.mock.calls[ 0 ][ 4 ] ).toBe( 0.8 );
} );
it( 'converts PNG to AVIF with interlacing', async () => {
mockConvertImageFormat.mockResolvedValue( new ArrayBuffer( 5 ) );
const result = await vipsConvertImageFormat(
'item-2',
pngFile,
'image/avif',
0.9,
true
);
expect( result.name ).toBe( 'image.avif' );
expect( result.type ).toBe( 'image/avif' );
expect( mockConvertImageFormat.mock.calls[ 0 ][ 5 ] ).toBe( true );
} );
} );
describe( 'vipsCompressImage', () => {
it( 'compresses image preserving filename and type', async () => {
mockCompressImage.mockResolvedValue( new ArrayBuffer( 5 ) );
const result = await vipsCompressImage( 'item-1', jpegFile, 0.8 );
expect( result.name ).toBe( 'test.jpg' );
expect( result.type ).toBe( 'image/jpeg' );
expect( mockCompressImage ).toHaveBeenCalledTimes( 1 );
expect( mockCompressImage.mock.calls[ 0 ][ 0 ] ).toBe( 'item-1' );
expect( mockCompressImage.mock.calls[ 0 ][ 2 ] ).toBe(
'image/jpeg'
);
expect( mockCompressImage.mock.calls[ 0 ][ 3 ] ).toBe( 0.8 );
} );
it( 'compresses image with interlacing option', async () => {
mockCompressImage.mockResolvedValue( new ArrayBuffer( 5 ) );
const result = await vipsCompressImage(
'item-2',
pngFile,
0.7,
true
);
expect( result.name ).toBe( 'image.png' );
expect( mockCompressImage.mock.calls[ 0 ][ 4 ] ).toBe( true );
} );
} );
describe( 'vipsHasTransparency', () => {
let mockFetch: jest.Mock;
let originalFetch: typeof window.fetch;
beforeEach( () => {
originalFetch = window.fetch;
mockFetch = jest.fn().mockResolvedValue( {
ok: true,
arrayBuffer: () => Promise.resolve( new ArrayBuffer( 0 ) ),
} as Response );
window.fetch = mockFetch;
} );
afterEach( () => {
window.fetch = originalFetch;
} );
it( 'returns true when image has transparency', async () => {
mockHasTransparency.mockResolvedValue( true );
const result = await vipsHasTransparency( 'blob:test-url' );
expect( result ).toBe( true );
expect( mockFetch ).toHaveBeenCalledWith( 'blob:test-url' );
expect( mockHasTransparency ).toHaveBeenCalledTimes( 1 );
} );
it( 'returns false when image has no transparency', async () => {
mockHasTransparency.mockResolvedValue( false );
const result = await vipsHasTransparency(
'https://example.com/img'
);
expect( result ).toBe( false );
} );
it( 'throws when fetch fails', async () => {
mockFetch.mockResolvedValue( {
ok: false,
status: 404,
} as Response );
await expect(
vipsHasTransparency( 'blob:test-url' )
).rejects.toThrow( 'Failed to fetch image: 404' );
} );
it( 'throws when vips check fails', async () => {
mockHasTransparency.mockRejectedValue(
new Error( 'WASM load failed' )
);
await expect(
vipsHasTransparency( 'blob:test-url' )
).rejects.toThrow( 'WASM load failed' );
} );
} );
describe( 'vipsResizeImage', () => {
it( 'resizes image and returns ImageFile with dimensions and suffix', async () => {
mockResizeImage.mockResolvedValue( {
buffer: new ArrayBuffer( 10 ),
width: 150,
height: 150,
originalWidth: 300,
originalHeight: 300,
} );
const resize: ImageSizeCrop = { width: 150, height: 150 };
const result = await vipsResizeImage(
'item-1',
jpegFile,
resize,
false,
true
);
expect( result ).toBeInstanceOf( ImageFile );
// ImageFile extends File, so name/type are direct properties.
expect( result.name ).toBe( 'test-150x150.jpg' );
expect( result.type ).toBe( 'image/jpeg' );
expect( result.width ).toBe( 150 );
expect( result.height ).toBe( 150 );
expect( result.originalWidth ).toBe( 300 );
expect( result.originalHeight ).toBe( 300 );
} );
it( 'does not add suffix when dimensions unchanged', async () => {
mockResizeImage.mockResolvedValue( {
buffer: new ArrayBuffer( 10 ),
width: 300,
height: 300,
originalWidth: 300,
originalHeight: 300,
} );
const resize: ImageSizeCrop = { width: 300, height: 300 };
const result = await vipsResizeImage(
'item-1',
jpegFile,
resize,
false,
true
);
expect( result.name ).toBe( 'test.jpg' );
} );
it( 'does not add suffix when addSuffix is false', async () => {
mockResizeImage.mockResolvedValue( {
buffer: new ArrayBuffer( 10 ),
width: 150,
height: 150,
originalWidth: 300,
originalHeight: 300,
} );
const resize: ImageSizeCrop = { width: 150, height: 150 };
const result = await vipsResizeImage(
'item-1',
jpegFile,
resize,
false,
false
);
expect( result.name ).toBe( 'test.jpg' );
} );
it( 'passes smart crop parameter to worker', async () => {
mockResizeImage.mockResolvedValue( {
buffer: new ArrayBuffer( 10 ),
width: 100,
height: 100,
originalWidth: 200,
originalHeight: 200,
} );
const resize: ImageSizeCrop = {
width: 100,
height: 100,
crop: true,
};
await vipsResizeImage( 'item-1', jpegFile, resize, true, true );
expect( mockResizeImage ).toHaveBeenCalledTimes( 1 );
expect( mockResizeImage.mock.calls[ 0 ][ 0 ] ).toBe( 'item-1' );
expect( mockResizeImage.mock.calls[ 0 ][ 2 ] ).toBe( 'image/jpeg' );
expect( mockResizeImage.mock.calls[ 0 ][ 3 ] ).toEqual( resize );
expect( mockResizeImage.mock.calls[ 0 ][ 4 ] ).toBe( true );
} );
} );
describe( 'vipsRotateImage', () => {
it( 'rotates image and returns ImageFile with -rotated suffix', async () => {
mockRotateImage.mockResolvedValue( {
buffer: new ArrayBuffer( 10 ),
width: 200,
height: 300,
} );
const result = await vipsRotateImage( 'item-1', jpegFile, 6 );
expect( result ).toBeInstanceOf( ImageFile );
expect( result.name ).toBe( 'test-rotated.jpg' );
expect( result.type ).toBe( 'image/jpeg' );
expect( result.width ).toBe( 200 );
expect( result.height ).toBe( 300 );
expect( mockRotateImage ).toHaveBeenCalledTimes( 1 );
expect( mockRotateImage.mock.calls[ 0 ][ 0 ] ).toBe( 'item-1' );
expect( mockRotateImage.mock.calls[ 0 ][ 2 ] ).toBe( 'image/jpeg' );
expect( mockRotateImage.mock.calls[ 0 ][ 3 ] ).toBe( 6 );
} );
it( 'returns original file when orientation is 1 (no rotation needed)', async () => {
const result = await vipsRotateImage( 'item-1', jpegFile, 1 );
expect( result ).toBe( jpegFile );
expect( mockRotateImage ).not.toHaveBeenCalled();
} );
it( 'handles different EXIF orientation values', async () => {
mockRotateImage.mockResolvedValue( {
buffer: new ArrayBuffer( 10 ),
width: 300,
height: 200,
} );
// Test orientation 3 (180° rotation)
await vipsRotateImage( 'item-1', jpegFile, 3 );
expect( mockRotateImage.mock.calls[ 0 ][ 3 ] ).toBe( 3 );
// Test orientation 8 (90° CCW rotation)
await vipsRotateImage( 'item-2', jpegFile, 8 );
expect( mockRotateImage.mock.calls[ 1 ][ 3 ] ).toBe( 8 );
} );
} );
describe( 'vipsCancelOperations', () => {
it( 'calls worker cancelOperations with item ID', async () => {
mockCancelOperations.mockResolvedValue( true );
const result = await vipsCancelOperations( 'item-123' );
expect( mockCancelOperations ).toHaveBeenCalledWith( 'item-123' );
expect( result ).toBe( true );
} );
it( 'returns false when no operations were cancelled', async () => {
mockCancelOperations.mockResolvedValue( false );
const result = await vipsCancelOperations( 'item-456' );
expect( result ).toBe( false );
} );
} );
} );