Skip to content

Commit c62e3f7

Browse files
Add tests for generateThumbnails scaled sideload
The generateThumbnails function had no test coverage, so the bigImageSizeThreshold dimension check bug was not caught. These tests verify scaled sideload behavior for images above, below, and exactly at the threshold.
1 parent e300637 commit c62e3f7

1 file changed

Lines changed: 299 additions & 0 deletions

File tree

packages/upload-media/src/store/test/actions.ts

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ jest.mock( '@wordpress/blob', () => ( {
2222
jest.mock( '../utils', () => ( {
2323
vipsCancelOperations: jest.fn( () => Promise.resolve( true ) ),
2424
vipsResizeImage: jest.fn(),
25+
vipsRotateImage: jest.fn(),
26+
vipsHasTransparency: jest.fn( () => Promise.resolve( false ) ),
27+
vipsConvertImageFormat: jest.fn(),
2528
terminateVipsWorker: jest.fn(),
2629
} ) );
2730

@@ -411,4 +414,300 @@ describe( 'actions', () => {
411414
expect( onError ).not.toHaveBeenCalled();
412415
} );
413416
} );
417+
418+
describe( 'generateThumbnails', () => {
419+
const mockBitmapClose = jest.fn();
420+
421+
function mockCreateImageBitmap( width: number, height: number ) {
422+
global.createImageBitmap = jest.fn( () =>
423+
Promise.resolve( {
424+
width,
425+
height,
426+
close: mockBitmapClose,
427+
} )
428+
) as unknown as typeof global.createImageBitmap;
429+
}
430+
431+
/**
432+
* Sets up an item in the queue with an attachment and
433+
* ThumbnailGeneration as the current operation, simulating
434+
* the state after upload completes.
435+
*
436+
* @param overrides Optional overrides.
437+
* @param overrides.attachment Attachment field overrides.
438+
*/
439+
async function setupItemForThumbnailGeneration( overrides?: {
440+
attachment?: Record< string, unknown >;
441+
} ) {
442+
// Add an item with ThumbnailGeneration as the only operation.
443+
unlock( registry.dispatch( uploadStore ) ).addItem( {
444+
file: jpegFile,
445+
operations: [ OperationType.ThumbnailGeneration ],
446+
} );
447+
448+
const item = unlock(
449+
registry.select( uploadStore )
450+
).getAllItems()[ 0 ];
451+
452+
// Simulate upload completion by finishing the operation with attachment data.
453+
// finishOperation shifts the operations array, so ThumbnailGeneration
454+
// becomes the next operation to process.
455+
await unlock( registry.dispatch( uploadStore ) ).finishOperation(
456+
item.id,
457+
{
458+
attachment: {
459+
id: 123,
460+
filename: 'example.jpg',
461+
missing_image_sizes: [ 'thumbnail', 'medium' ],
462+
...( overrides?.attachment || {} ),
463+
},
464+
}
465+
);
466+
467+
return unlock( registry.select( uploadStore ) ).getAllItems()[ 0 ];
468+
}
469+
470+
beforeEach( () => {
471+
mockBitmapClose.mockClear();
472+
} );
473+
474+
afterEach( () => {
475+
// Clean up global mock.
476+
// @ts-ignore
477+
delete global.createImageBitmap;
478+
} );
479+
480+
it( 'should not sideload a scaled version when image is below the threshold', async () => {
481+
// Image is 800x600, threshold is 2560.
482+
mockCreateImageBitmap( 800, 600 );
483+
484+
unlock( registry.dispatch( uploadStore ) ).updateSettings( {
485+
bigImageSizeThreshold: 2560,
486+
allImageSizes: {
487+
thumbnail: { width: 150, height: 150 },
488+
medium: { width: 300, height: 300 },
489+
},
490+
} );
491+
492+
const item = await setupItemForThumbnailGeneration();
493+
await unlock( registry.dispatch( uploadStore ) ).generateThumbnails(
494+
item.id
495+
);
496+
497+
const allItems = unlock(
498+
registry.select( uploadStore )
499+
).getAllItems();
500+
501+
// Should have sideload items for thumbnails, but NOT for 'scaled'.
502+
const scaledItems = allItems.filter(
503+
( i ) => i.additionalData?.image_size === 'scaled'
504+
);
505+
expect( scaledItems ).toHaveLength( 0 );
506+
expect( mockBitmapClose ).toHaveBeenCalled();
507+
} );
508+
509+
it( 'should sideload a scaled version when image exceeds the threshold', async () => {
510+
// Image is 4000x3000, threshold is 2560.
511+
mockCreateImageBitmap( 4000, 3000 );
512+
513+
unlock( registry.dispatch( uploadStore ) ).updateSettings( {
514+
bigImageSizeThreshold: 2560,
515+
allImageSizes: {
516+
thumbnail: { width: 150, height: 150 },
517+
medium: { width: 300, height: 300 },
518+
},
519+
} );
520+
521+
const item = await setupItemForThumbnailGeneration();
522+
await unlock( registry.dispatch( uploadStore ) ).generateThumbnails(
523+
item.id
524+
);
525+
526+
const allItems = unlock(
527+
registry.select( uploadStore )
528+
).getAllItems();
529+
530+
const scaledItems = allItems.filter(
531+
( i ) => i.additionalData?.image_size === 'scaled'
532+
);
533+
expect( scaledItems ).toHaveLength( 1 );
534+
expect( scaledItems[ 0 ].additionalData.post ).toBe( 123 );
535+
expect( mockBitmapClose ).toHaveBeenCalled();
536+
} );
537+
538+
it( 'should sideload a scaled version when only height exceeds the threshold', async () => {
539+
// Image is 2000x3000, threshold is 2560 — height exceeds.
540+
mockCreateImageBitmap( 2000, 3000 );
541+
542+
unlock( registry.dispatch( uploadStore ) ).updateSettings( {
543+
bigImageSizeThreshold: 2560,
544+
allImageSizes: {
545+
thumbnail: { width: 150, height: 150 },
546+
medium: { width: 300, height: 300 },
547+
},
548+
} );
549+
550+
const item = await setupItemForThumbnailGeneration();
551+
await unlock( registry.dispatch( uploadStore ) ).generateThumbnails(
552+
item.id
553+
);
554+
555+
const allItems = unlock(
556+
registry.select( uploadStore )
557+
).getAllItems();
558+
559+
const scaledItems = allItems.filter(
560+
( i ) => i.additionalData?.image_size === 'scaled'
561+
);
562+
expect( scaledItems ).toHaveLength( 1 );
563+
} );
564+
565+
it( 'should not sideload a scaled version when bigImageSizeThreshold is not set', async () => {
566+
// No bigImageSizeThreshold in settings — scaling should be skipped entirely.
567+
unlock( registry.dispatch( uploadStore ) ).updateSettings( {
568+
allImageSizes: {
569+
thumbnail: { width: 150, height: 150 },
570+
medium: { width: 300, height: 300 },
571+
},
572+
} );
573+
574+
const item = await setupItemForThumbnailGeneration();
575+
await unlock( registry.dispatch( uploadStore ) ).generateThumbnails(
576+
item.id
577+
);
578+
579+
const allItems = unlock(
580+
registry.select( uploadStore )
581+
).getAllItems();
582+
583+
const scaledItems = allItems.filter(
584+
( i ) => i.additionalData?.image_size === 'scaled'
585+
);
586+
expect( scaledItems ).toHaveLength( 0 );
587+
// createImageBitmap should not have been called since threshold is not set.
588+
expect( global.createImageBitmap ).toBeUndefined();
589+
} );
590+
591+
it( 'should not sideload a scaled version when attachment has no id', async () => {
592+
mockCreateImageBitmap( 4000, 3000 );
593+
594+
unlock( registry.dispatch( uploadStore ) ).updateSettings( {
595+
bigImageSizeThreshold: 2560,
596+
allImageSizes: {
597+
thumbnail: { width: 150, height: 150 },
598+
medium: { width: 300, height: 300 },
599+
},
600+
} );
601+
602+
// Set up item with attachment that has no id.
603+
const item = await setupItemForThumbnailGeneration( {
604+
attachment: { id: undefined },
605+
} );
606+
await unlock( registry.dispatch( uploadStore ) ).generateThumbnails(
607+
item.id
608+
);
609+
610+
const allItems = unlock(
611+
registry.select( uploadStore )
612+
).getAllItems();
613+
614+
const scaledItems = allItems.filter(
615+
( i ) => i.additionalData?.image_size === 'scaled'
616+
);
617+
expect( scaledItems ).toHaveLength( 0 );
618+
} );
619+
620+
it( 'should create sideload items for missing image sizes', async () => {
621+
mockCreateImageBitmap( 800, 600 );
622+
623+
unlock( registry.dispatch( uploadStore ) ).updateSettings( {
624+
bigImageSizeThreshold: 2560,
625+
allImageSizes: {
626+
thumbnail: { width: 150, height: 150 },
627+
medium: { width: 300, height: 300 },
628+
},
629+
} );
630+
631+
const item = await setupItemForThumbnailGeneration();
632+
await unlock( registry.dispatch( uploadStore ) ).generateThumbnails(
633+
item.id
634+
);
635+
636+
const allItems = unlock(
637+
registry.select( uploadStore )
638+
).getAllItems();
639+
640+
// Should have the original item plus 2 sideload items for thumbnail and medium.
641+
const thumbnailItems = allItems.filter(
642+
( i ) => i.additionalData?.image_size === 'thumbnail'
643+
);
644+
const mediumItems = allItems.filter(
645+
( i ) => i.additionalData?.image_size === 'medium'
646+
);
647+
expect( thumbnailItems ).toHaveLength( 1 );
648+
expect( mediumItems ).toHaveLength( 1 );
649+
} );
650+
651+
it( 'should skip thumbnail generation when item has no attachment', async () => {
652+
// Add an item without going through the attachment setup.
653+
unlock( registry.dispatch( uploadStore ) ).addItem( {
654+
file: jpegFile,
655+
operations: [ OperationType.ThumbnailGeneration ],
656+
} );
657+
658+
const item = unlock(
659+
registry.select( uploadStore )
660+
).getAllItems()[ 0 ];
661+
662+
// Clear the attachment to simulate no upload response.
663+
await unlock( registry.dispatch( uploadStore ) ).finishOperation(
664+
item.id,
665+
{
666+
attachment: undefined,
667+
}
668+
);
669+
670+
const updatedItem = unlock(
671+
registry.select( uploadStore )
672+
).getAllItems()[ 0 ];
673+
674+
await unlock( registry.dispatch( uploadStore ) ).generateThumbnails(
675+
updatedItem.id
676+
);
677+
678+
// Should only have the original item — no sideloads created.
679+
const allItems = unlock(
680+
registry.select( uploadStore )
681+
).getAllItems();
682+
expect( allItems ).toHaveLength( 1 );
683+
} );
684+
685+
it( 'should not create scaled version when image is exactly at the threshold', async () => {
686+
// Image is exactly 2560x2560, threshold is 2560 — should NOT scale.
687+
mockCreateImageBitmap( 2560, 2560 );
688+
689+
unlock( registry.dispatch( uploadStore ) ).updateSettings( {
690+
bigImageSizeThreshold: 2560,
691+
allImageSizes: {
692+
thumbnail: { width: 150, height: 150 },
693+
medium: { width: 300, height: 300 },
694+
},
695+
} );
696+
697+
const item = await setupItemForThumbnailGeneration();
698+
await unlock( registry.dispatch( uploadStore ) ).generateThumbnails(
699+
item.id
700+
);
701+
702+
const allItems = unlock(
703+
registry.select( uploadStore )
704+
).getAllItems();
705+
706+
const scaledItems = allItems.filter(
707+
( i ) => i.additionalData?.image_size === 'scaled'
708+
);
709+
// Exactly at threshold means no scaling (condition is > not >=).
710+
expect( scaledItems ).toHaveLength( 0 );
711+
} );
712+
} );
414713
} );

0 commit comments

Comments
 (0)