@@ -27,7 +27,7 @@ import {
2727import { Observable , timer } from 'rxjs' ;
2828import { map } from 'rxjs/operators' ;
2929import { KbqDropzoneData , KbqFullScreenDropzoneService , KbqLocalDropzone } from './dropzone' ;
30- import { KbqFileItem } from './file-upload' ;
30+ import { KbqFileItem , KbqFileUploadAddStrategy , KbqFileUploadAddStrategyValues } from './file-upload' ;
3131import { KbqFileUploadModule } from './file-upload.module' ;
3232import { KbqInputFileMultipleLabel , KbqMultipleFileUploadComponent } from './multiple-file-upload.component' ;
3333import { KbqFileDropDirective } from './primitives/file-drop' ;
@@ -329,6 +329,69 @@ describe(KbqMultipleFileUploadComponent.name, () => {
329329 } ) ;
330330 } ) ;
331331
332+ describe ( 'with addStrategy input' , ( ) => {
333+ // Same object reused across dispatches so name/size/type/lastModified are guaranteed
334+ // identical, instead of relying on two `new File(...)` calls landing in the same millisecond.
335+ const duplicateFile : Partial < File > = { name : FILE_NAME , size : 4 , type : '' , lastModified : 1700000000000 } ;
336+ const otherFile : Partial < File > = { name : 'other.file' , size : 4 , type : '' , lastModified : 1700000000000 } ;
337+
338+ // The native input is inside the `@if (!files.length) {...} @else {...}` branch of the
339+ // template, so it gets destroyed/recreated when the file count crosses zero — must be
340+ // re-queried before every dispatch rather than cached once.
341+ const dispatchChange = ( file : Partial < File > ) => {
342+ dispatchEvent ( component . fileUpload ( ) . input ! . nativeElement , getMockedChangeEvent ( file ) ) ;
343+ fixture . detectChanges ( ) ;
344+ } ;
345+
346+ it ( 'should default to concat strategy' , ( ) => {
347+ expect ( component . fileUpload ( ) . addStrategy ( ) ) . toBe ( KbqFileUploadAddStrategy . Concat ) ;
348+ } ) ;
349+
350+ describe ( 'concat strategy (default)' , ( ) => {
351+ it ( 'should skip a file that duplicates one already in the list' , ( ) => {
352+ dispatchChange ( duplicateFile ) ;
353+ dispatchChange ( duplicateFile ) ;
354+
355+ expect ( component . files ) . toHaveLength ( 1 ) ;
356+ } ) ;
357+
358+ it ( 'should still add files that are genuinely different' , ( ) => {
359+ dispatchChange ( duplicateFile ) ;
360+ dispatchChange ( otherFile ) ;
361+
362+ expect ( component . files ) . toHaveLength ( 2 ) ;
363+ } ) ;
364+
365+ it ( 'should emit filesAdded with an empty array for a skipped duplicate' , ( ) => {
366+ const filesAddedSpy = jest . fn ( ) ;
367+ const subscription = component . fileUpload ( ) . filesAdded . subscribe ( filesAddedSpy ) ;
368+
369+ dispatchChange ( duplicateFile ) ;
370+ dispatchChange ( duplicateFile ) ;
371+
372+ subscription . unsubscribe ( ) ;
373+
374+ expect ( filesAddedSpy ) . toHaveBeenCalledTimes ( 2 ) ;
375+ expect ( filesAddedSpy . mock . calls [ 1 ] [ 0 ] ) . toHaveLength ( 0 ) ;
376+ } ) ;
377+ } ) ;
378+
379+ describe ( 'replace strategy' , ( ) => {
380+ beforeEach ( ( ) => {
381+ component . addStrategy . set ( KbqFileUploadAddStrategy . Replace ) ;
382+ fixture . detectChanges ( ) ;
383+ } ) ;
384+
385+ it ( 'should replace the list instead of appending on a new selection' , ( ) => {
386+ dispatchChange ( duplicateFile ) ;
387+ dispatchChange ( otherFile ) ;
388+
389+ expect ( component . files ) . toHaveLength ( 1 ) ;
390+ expect ( component . files [ 0 ] . file . name ) . toBe ( 'other.file' ) ;
391+ } ) ;
392+ } ) ;
393+ } ) ;
394+
332395 describe ( 'with ControlValueAccessor' , ( ) => {
333396 let fixture : ComponentFixture < ControlValueAccessorMultipleFileUpload > ;
334397 let component : ControlValueAccessorMultipleFileUpload ;
@@ -1764,6 +1827,7 @@ class ControlValueAccessorSingleFileUpload {
17641827 [disabled]="disabled"
17651828 [fullScreenDropZone]="fullScreenDropZone()"
17661829 [localeConfig]="localeConfig()"
1830+ [addStrategy]="addStrategy()"
17671831 (fileQueueChanged)="onChange($event)"
17681832 />
17691833 </div>
@@ -1777,6 +1841,7 @@ class BasicMultipleFileUpload {
17771841 disabled : boolean ;
17781842 files : KbqFileItem [ ] ;
17791843 fullScreenDropZone = signal < KbqDropzoneData | boolean | undefined > ( undefined ) ;
1844+ addStrategy = signal < KbqFileUploadAddStrategyValues > ( KbqFileUploadAddStrategy . Concat ) ;
17801845
17811846 localeConfig = signal < Partial < KbqBaseFileUploadLocaleConfig > > ( { } ) ;
17821847
0 commit comments