@@ -22,6 +22,9 @@ jest.mock( '@wordpress/blob', () => ( {
2222jest . 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