Ks89/angular-modal-gallery

View on GitHub
projects/ks89/angular-modal-gallery/src/lib/components/carousel/carousel.component.spec.ts

Summary

Maintainability
F
4 days
Test Coverage
/*
 * Copyright (C) 2017-2024 Stefano Cappa
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { ComponentFixture, discardPeriodicTasks, fakeAsync, flush, TestBed, tick } from '@angular/core/testing';

import { DebugElement, SimpleChanges } from '@angular/core';
import { By } from '@angular/platform-browser';

import { CarouselComponent } from './carousel.component';
import { CarouselPreviewsComponent } from './carousel-previews/carousel-previews.component';
import { SizeDirective } from '../../directives/size.directive';
import { ModalGalleryComponent } from '../modal-gallery/modal-gallery.component';
import { DescriptionDirective } from '../../directives/description.directive';
import { DotsComponent } from '../dots/dots.component';
import { MaxSizeDirective } from '../../directives/max-size.directive';
import { PlainGalleryComponent } from '../plain-gallery/plain-gallery.component';
import { ClickOutsideDirective } from '../../directives/click-outside.directive';
import { UpperButtonsComponent } from '../upper-buttons/upper-buttons.component';
import { CurrentImageComponent } from '../current-image/current-image.component';
import { LoadingSpinnerComponent } from '../current-image/loading-spinner/loading-spinner.component';
import { PreviewsComponent } from '../previews/previews.component';
import { KeyboardNavigationDirective } from '../../directives/keyboard-navigation.directive';
import { ATagBgImageDirective } from '../../directives/a-tag-bg-image.directive';
import { WrapDirective } from '../../directives/wrap.directive';
import { DirectionDirective } from '../../directives/direction.directive';
import { IdValidatorService } from '../../services/id-validator.service';
import { Image } from '../../model/image.class';
import { CarouselConfig } from '../../model/carousel-config.interface';
import { PlayConfig } from '../../model/play-config.interface';
import { KS_DEFAULT_ACCESSIBILITY_CONFIG } from '../accessibility-default';
import { AccessibilityConfig } from '../../model/accessibility.interface';
import { Description, DescriptionStrategy, DescriptionStyle } from '../../model/description.interface';
import { CarouselImageConfig } from '../../model/carousel-image-config.interface';
import { getIndex } from '../../utils/image.util';
import { CarouselPreviewConfig } from '../../model/carousel-preview-config.interface';
import { ConfigService } from '../../services/config.service';
import { FallbackImageDirective } from '../../directives/fallback-image.directive';
import { OverlayModule } from '@angular/cdk/overlay';
import { ModalGalleryService } from '../modal-gallery/modal-gallery.service';

const GALLERY_ID = 1;

let comp: CarouselComponent;
let fixture: ComponentFixture<CarouselComponent>;

interface TestModel {
  currentImgTitle: string;
  currentAlt: string;
  currentDescription: string;
  currentImgAriaLabel: string;
}

const TEST_MODEL: TestModel[] = [
  {
    currentImgTitle: 'Image 1/7 - Description 1',
    currentAlt: 'Description 1',
    currentDescription: 'Image 1/7 - Description 1',
    currentImgAriaLabel: 'First image aria-label'
  },
  {
    currentImgTitle: 'Image 2/7',
    currentAlt: 'Image 2',
    currentDescription: 'Image 2',
    currentImgAriaLabel: 'ariaLabel'
  },
  {
    currentImgTitle: 'Third image title',
    currentAlt: 'Third image alt',
    currentDescription: 'Image 3/7 - Description 3',
    currentImgAriaLabel: 'Third image aria-label'
  },
  {
    currentImgTitle: 'Fourth image title (modal obj)',
    currentAlt: 'Fourth image alt (modal obj)',
    currentDescription: 'Image 4/7 - Description 4',
    currentImgAriaLabel: 'Fourth image aria-label (modal obj)'
  },
  {
    currentImgTitle: 'Image 5/7',
    currentAlt: 'Image 5',
    currentDescription: 'Image 5/7',
    currentImgAriaLabel: 'ariaLabel'
  },
  {
    currentImgTitle: 'Image 6/7 - Description 6',
    currentAlt: 'Description 6',
    currentDescription: 'Image 6/7 - Description 6',
    currentImgAriaLabel: 'ariaLabel'
  },
  {
    currentImgTitle: 'Image 7/7',
    currentAlt: 'Image 7',
    currentDescription: 'Image 7/7',
    currentImgAriaLabel: 'ariaLabel'
  }
];

const IMAGES: Image[] = [
  new Image(
    0,
    {
      img: '/assets/images/gallery/milan-pegasus-gallery-statue.jpg',
      description: 'Description 1'
    },
    {
      img: '/assets/images/gallery/thumbs/t-milan-pegasus-gallery-statue.jpg',
      title: 'First image title',
      alt: 'First image alt',
      ariaLabel: 'First image aria-label'
    }
  ),
  new Image(1, {img: '/assets/images/gallery/pexels-photo-47223.jpeg'}, {img: '/assets/images/gallery/thumbs/t-pexels-photo-47223.jpg'}),
  new Image(
    2,
    {
      img: '/assets/images/gallery/pexels-photo-52062.jpeg',
      description: 'Description 3',
      title: 'Third image title',
      alt: 'Third image alt',
      ariaLabel: 'Third image aria-label'
    },
    {
      img: '/assets/images/gallery/thumbs/t-pexels-photo-52062.jpg',
      description: 'Description 3'
    }
  ),
  new Image(
    3,
    {
      img: '/assets/images/gallery/pexels-photo-66943.jpeg',
      description: 'Description 4',
      title: 'Fourth image title (modal obj)',
      alt: 'Fourth image alt (modal obj)',
      ariaLabel: 'Fourth image aria-label (modal obj)'
    },
    {
      img: '/assets/images/gallery/thumbs/t-pexels-photo-66943.jpg',
      title: 'Fourth image title (plain obj)',
      alt: 'Fourth image alt (plain obj)',
      ariaLabel: 'Fourth image aria-label (plain obj)'
    }
  ),
  new Image(4, {img: '/assets/images/gallery/pexels-photo-93750.jpeg'}, {img: '/assets/images/gallery/thumbs/t-pexels-photo-93750.jpg'}),
  new Image(
    5,
    {
      img: '/assets/images/gallery/pexels-photo-94420.jpeg',
      description: 'Description 6'
    },
    {img: '/assets/images/gallery/thumbs/t-pexels-photo-94420.jpg'}
  ),
  new Image(6, {img: '/assets/images/gallery/pexels-photo-96947.jpeg'}, {img: '/assets/images/gallery/thumbs/t-pexels-photo-96947.jpg'})
];

const ID_ERROR = 'Internal library error - id must be defined';

const CUSTOM_ACCESSIBILITY: AccessibilityConfig = Object.assign({}, KS_DEFAULT_ACCESSIBILITY_CONFIG);
CUSTOM_ACCESSIBILITY.carouselContainerAriaLabel = 'carouselContainerAriaLabel';
CUSTOM_ACCESSIBILITY.carouselContainerTitle = 'carouselContainerTitle';
CUSTOM_ACCESSIBILITY.carouselPrevImageAriaLabel = 'carouselPrevImageAriaLabel';
CUSTOM_ACCESSIBILITY.carouselPrevImageTitle = 'carouselPrevImageTitle';
CUSTOM_ACCESSIBILITY.carouselNextImageAriaLabel = 'carouselNextImageAriaLabel';
CUSTOM_ACCESSIBILITY.carouselNextImageTitle = 'carouselNextImageTitle';
CUSTOM_ACCESSIBILITY.dotsContainerAriaLabel = 'dotsContainerAriaLabel';
CUSTOM_ACCESSIBILITY.dotsContainerTitle = 'dotsContainerTitle';
CUSTOM_ACCESSIBILITY.carouselPreviewsContainerAriaLabel = 'carouselPreviewsContainerAriaLabel';
CUSTOM_ACCESSIBILITY.carouselPreviewsContainerTitle = 'carouselPreviewsContainerTitle';
CUSTOM_ACCESSIBILITY.carouselPreviewScrollPrevAriaLabel = 'carouselPreviewScrollPrevAriaLabel';
CUSTOM_ACCESSIBILITY.carouselPreviewScrollPrevTitle = 'carouselPreviewScrollPrevTitle';
CUSTOM_ACCESSIBILITY.carouselPreviewScrollNextAriaLabel = 'carouselPreviewScrollNextAriaLabel';
CUSTOM_ACCESSIBILITY.carouselPreviewScrollNextTitle = 'carouselPreviewScrollNextTitle';

const DEFAULT_CAROUSEL_CONFIG: CarouselConfig = {
  maxWidth: '100%',
  maxHeight: '400px',
  showArrows: true,
  objectFit: 'cover',
  keyboardEnable: true,
  modalGalleryEnable: false
};

function containsClasses(actualClasses: string, expectedClasses: string): boolean {
  const actual: string[] = actualClasses.split(' ');
  const expected: string[] = expectedClasses.split(' ');
  let count = 0;
  if (actual.length !== expected.length) {
    return false;
  }
  expected.forEach((item: string) => {
    if (actual.includes(item)) {
      count++;
    }
  });
  return count === expected.length;
}

function initTestBed(): void {
  TestBed.configureTestingModule({
    imports: [OverlayModule],
    declarations: [
      ClickOutsideDirective,
      UpperButtonsComponent, CurrentImageComponent, LoadingSpinnerComponent,
      PreviewsComponent, FallbackImageDirective,
      KeyboardNavigationDirective, ATagBgImageDirective,
      WrapDirective, DirectionDirective,
      CarouselComponent, CarouselPreviewsComponent, ModalGalleryComponent, PlainGalleryComponent,
      SizeDirective, DescriptionDirective, DotsComponent, MaxSizeDirective]
  }).overrideComponent(CarouselComponent, {
    set: {
      providers: [
        {
          provide: ConfigService,
          useClass: ConfigService
        },
        {
          provide: ModalGalleryService,
          useClass: ModalGalleryService
        },
        {
          provide: IdValidatorService,
          useClass: IdValidatorService
        }
      ]
    }
  });
}

function checkMainContainer(maxWidth: string = '100%', accessibilityConfig: AccessibilityConfig = KS_DEFAULT_ACCESSIBILITY_CONFIG): void {
  const element: DebugElement = fixture.debugElement;
  const mainCarouselContainer: DebugElement = element.query(By.css('main#carousel-container'));
  expect(mainCarouselContainer.name).toBe('main');
  expect(mainCarouselContainer.properties.title).toBe(accessibilityConfig.carouselContainerTitle);
  expect(mainCarouselContainer.attributes['aria-label']).toBe(accessibilityConfig.carouselContainerAriaLabel);
  // expect(mainCarouselContainer.attributes['style']).toBe('max-width: 100%;');

  if (maxWidth !== '100%') {
    expect(mainCarouselContainer.styles['max-width']).toBe(maxWidth);
  }
}

function checkCurrentImage(currentImage: Image, val: TestModel, withDots: boolean = true, withArrows: boolean = true, accessibilityConfig: AccessibilityConfig = KS_DEFAULT_ACCESSIBILITY_CONFIG): void  {
  const element: DebugElement = fixture.debugElement;
  const currentFigure: DebugElement = element.query(By.css('figure.current-figure'));
  expect(currentFigure.name).toBe('figure');
  const currentPictureElement: DebugElement = currentFigure.children[withArrows ? 1 : 0]; // 0 and 2 are the arrows
  expect(currentPictureElement.name).toBe('picture');
  expect(currentPictureElement.attributes.class).toBe('current-image');
  const currentImageElement = currentPictureElement.children[0];
  expect(currentImageElement.name).toBe('img');
  expect(currentImageElement.attributes.role).toBe('img');
  expect(currentImageElement.properties.src).toBe(currentImage.modal.img);
  expect(currentImageElement.properties.title).toBe(val.currentImgTitle);
  expect(currentImageElement.properties.alt).toBe(val.currentAlt);
  expect(currentImageElement.properties.tabIndex).toBe(0);

  if (withDots) {
    const dotsMainContainer: DebugElement = element.query(By.css('div#dots'));
    expect(dotsMainContainer.name).toBe('div');
    const dotsContainer: DebugElement = element.query(By.css('nav.dots-container'));
    expect(dotsContainer.name).toBe('nav');
    expect(dotsContainer.attributes['aria-label']).toBe(accessibilityConfig.dotsContainerAriaLabel);
    expect(dotsContainer.properties.title).toBe(accessibilityConfig.dotsContainerTitle);
    const dots: DebugElement[] = dotsContainer.children;
    expect(dots.length).toBe(IMAGES.length);

    const activeDotIndex = 0;
    dots.forEach((dot: DebugElement, index: number) => {
      expect(dot.name).toBe('div');
      expect(dot.attributes.role).toBe('navigation');
      expect(dot.properties.tabIndex).toBe(0);
      if (index === activeDotIndex) {
        // I don't know why, but with dot.attributes.class I can't see 'active'. In this way it's working!
        // TODO fix this because is not working as expected. This line is ok, but tests aren't restarting from image 0
        // expect(dot.classes).toEqual({'inside': true, 'dot': true, 'active': true});
      } else {
        // TODO tests aren't restarting from image 0 so I have to admit both active and inactive dots
        expect(containsClasses(dot.attributes.class as string, 'inside dot') ||
          containsClasses(dot.attributes.class as string, 'inside dot active')).toBeTrue();
        // or like above: expect(dot.classes).toEqual({'inside': true, 'dot': true});
      }
      expect(dot.attributes['aria-label']).toBe(accessibilityConfig.dotAriaLabel + ' ' + (index + 1));
    });
  }
}

function checkDescription(currentImage: Image, carouselImageConfig: CarouselImageConfig): void {
  const element: DebugElement = fixture.debugElement;
  const currentFigcaption: DebugElement = element.query(By.css('figcaption.description'));
  if (carouselImageConfig.description && carouselImageConfig.description.strategy !== DescriptionStrategy.ALWAYS_HIDDEN) {
    expect(currentFigcaption.attributes.class).toBe('description');
    // TODO check style background: rgba(0, 0, 0, 0.5); color: white; margin: 0px;
    expect(currentFigcaption.nativeElement.textContent).toEqual(getDescriptionToDisplay(carouselImageConfig.description.strategy, currentImage, carouselImageConfig));
  }
}

function getDescriptionToDisplay(descStrategy: DescriptionStrategy, image: Image, carouselImageConfig: CarouselImageConfig): string {
  const imageWithoutDescription: boolean = !image.modal || !image.modal.description || image.modal.description === '';

  switch (descStrategy) {
    case DescriptionStrategy.HIDE_IF_EMPTY:
      return imageWithoutDescription ? '' : image.modal.description + '';
    case DescriptionStrategy.ALWAYS_HIDDEN:
      return '';
    default:
      // ----------- DescriptionStrategy.ALWAYS_VISIBLE -----------------
      return buildTextDescription(image, imageWithoutDescription, carouselImageConfig);
  }
}

function buildTextDescription(image: Image, imageWithoutDescription: boolean, carouselImageConfig: CarouselImageConfig): string {
  // // If customFullDescription use it, otherwise proceed to build a description
  // if (this.configCurrentImageCarousel.description.customFullDescription && this.configCurrentImageCarousel.description.customFullDescription !== '') {
  //   return this.configCurrentImageCarousel.description.customFullDescription;
  // }

  const currentIndex: number = getIndex(image, IMAGES);
  // If the current image hasn't a description,
  // prevent to write the ' - ' (or this.description.beforeTextDescription)

  const prevDescription: string = carouselImageConfig.description?.imageText ? carouselImageConfig.description.imageText : '';
  const midSeparator: string = carouselImageConfig.description?.numberSeparator
    ? carouselImageConfig.description.numberSeparator
    : '';
  const middleDescription: string = currentIndex + 1 + midSeparator + IMAGES.length;

  if (imageWithoutDescription) {
    return prevDescription + middleDescription;
  }

  const currImgDescription: string = image.modal && image.modal.description ? image.modal.description : '';
  // return currImgDescription; // TODO remove this
  const endDescription: string = carouselImageConfig.description?.beforeTextDescription + currImgDescription;
  return prevDescription + middleDescription + endDescription;
}

function checkArrows(isFirstImage: boolean, isLastImage: boolean, accessibilityConfig: AccessibilityConfig = KS_DEFAULT_ACCESSIBILITY_CONFIG): void  {
  const element: DebugElement = fixture.debugElement;
  const aNavLeft: DebugElement = element.query(By.css('a.nav-left'));
  expect(aNavLeft.name).toBe('a');
  expect(aNavLeft.attributes.role).toBe('button');
  expect(aNavLeft.attributes['aria-label']).toBe(accessibilityConfig.carouselPrevImageAriaLabel);
  expect(aNavLeft.properties.tabIndex).toBe(isFirstImage ? -1 : 0);
  const divNavLeft: DebugElement = aNavLeft.children[0];
  expect(divNavLeft.attributes['aria-hidden']).toBe('true');
  expect(containsClasses(divNavLeft.properties.className, 'inside ' + (isFirstImage ? 'empty-arrow-image' : 'left-arrow-image'))).toBeTrue();
  expect(divNavLeft.properties.title).toBe(accessibilityConfig.carouselPrevImageTitle);

  const aNavRight: DebugElement = element.query(By.css('a.nav-right'));
  expect(aNavRight.name).toBe('a');
  expect(aNavRight.attributes.role).toBe('button');
  expect(aNavRight.attributes['aria-label']).toBe(accessibilityConfig.carouselNextImageAriaLabel);
  expect(aNavRight.properties.tabIndex).toBe(isLastImage ? -1 : 0);
  const divNavRight: DebugElement = aNavRight.children[0];
  expect(divNavRight.attributes['aria-hidden']).toBe('true');
  expect(containsClasses(divNavRight.properties.className, 'inside ' + (isLastImage ? 'empty-arrow-image' : 'right-arrow-image'))).toBeTrue();
  expect(divNavRight.properties.title).toBe(accessibilityConfig.carouselNextImageTitle);
}

function checkPreviews(numPreviews: number, isFirstImage: boolean, isLastImage: boolean, clickable: boolean, accessibilityConfig: AccessibilityConfig = KS_DEFAULT_ACCESSIBILITY_CONFIG): void  {
  const element: DebugElement = fixture.debugElement;
  const previewsContainer: DebugElement = element.query(By.css('nav.previews-container'));
  expect(previewsContainer.name).toBe('nav');
  expect(previewsContainer.attributes['aria-label']).toBe(accessibilityConfig.carouselPreviewsContainerAriaLabel);
  expect(previewsContainer.properties.title).toBe(accessibilityConfig.carouselPreviewsContainerTitle);

  const aNavLeft: DebugElement = element.query(By.css('a.nav-left'));
  const divNavLeft: DebugElement = aNavLeft.children[0];
  expect(divNavLeft.name).toBe('div');
  expect(divNavLeft.attributes['aria-hidden']).toBe('true');
  expect(containsClasses(divNavLeft.properties.className, 'inside ' + (isFirstImage ? 'empty-arrow-image' : 'left-arrow-image'))).toBeTrue();
  // TODO fixme
  // expect(divNavLeft.attributes['aria-label']).toBe(accessibilityConfig.carouselPreviewScrollPrevAriaLabel);
  // expect(divNavLeft.properties.title).toBe(accessibilityConfig.carouselPreviewScrollPrevTitle);

  const aNavRight: DebugElement = element.query(By.css('a.nav-right'));
  const divNavRight: DebugElement = aNavRight.children[0];
  expect(divNavRight.name).toBe('div');
  expect(divNavRight.attributes['aria-hidden']).toBe('true');
  expect(containsClasses(divNavRight.properties.className, 'inside ' + (isFirstImage ? 'empty-arrow-image' : 'right-arrow-image'))).toBeTrue();
  // TODO fixme
  // expect(divNavRight.attributes['aria-label']).toBe(accessibilityConfig.carouselPreviewScrollNextAriaLabel);
  // expect(divNavRight.properties.title).toBe(accessibilityConfig.carouselPreviewScrollNextTitle);

  const previewsInner: DebugElement = element.query(By.css('div.preview-inner-container'));
  expect(previewsInner.name).toBe('div');
  const previews: DebugElement[] = previewsInner.children;
  expect(previews.length).toBe(numPreviews);
  previews.forEach((preview: DebugElement, index: number) => {
    expect(preview.attributes.role).toBe('img');
    expect(containsClasses(preview.properties.className, 'inside preview-image' + (index === 0 ? ' active' : '') + (clickable ? '' : ' unclickable'))).toBeTrue();
    expect(preview.properties.tabIndex).toBe(0);
    // expect(preview.properties['style']).toBe('width: 25%; height: 200px;');
    // expect(preview.properties.src).toBe(previewsContainer.modal.img);
    // expect(preview.attributes['aria-label']).toBe();
    // expect(preview.properties.title).toBe();
    // expect(preview.properties.alt).toBe();
  });
}

describe('CarouselComponent', () => {
  beforeEach(() => {
    initTestBed();
    fixture = TestBed.createComponent(CarouselComponent);
    comp = fixture.componentInstance;
  });

  it('should instantiate it', () => expect(comp).not.toBeNull());


  // should navigate between images clicking on current image. Test i=0
  // should navigate between images clicking on right side preview. Test i=0
  // should navigate between images to the right using swipe gestures. Test i=0

  describe('---YES---', () => {

    it(`should display carousel with all defaults and auto-navigate (play enabled by default).`, fakeAsync(() => {
      comp.id = GALLERY_ID;
      comp.images = IMAGES;
      fixture.detectChanges();
      const defaultInterval = 5000;

      TEST_MODEL.forEach((val: TestModel, index: number) => {
        checkMainContainer();
        checkCurrentImage(IMAGES[index], val);
        checkArrows(false, false);
        tick(defaultInterval + 100);
        flush();
        fixture.detectChanges();
      });

      tick(defaultInterval + 100);
      flush();
      fixture.detectChanges();

      discardPeriodicTasks();
    }));

    it(`should display carousel with all defaults and auto-navigate (play enabled by default) trying infinite sliding.`, fakeAsync(() => {
      comp.id = GALLERY_ID;
      comp.images = IMAGES;
      fixture.detectChanges();
      const defaultInterval = 5000;

      TEST_MODEL.forEach((val: TestModel, index: number) => {
        checkMainContainer();
        checkCurrentImage(IMAGES[index], val);
        checkArrows(false, false);
        tick(defaultInterval + 100);
        flush();
        fixture.detectChanges();
      });

      // check the first image (because of infinite sliding)
      checkMainContainer();
      checkCurrentImage(IMAGES[0], TEST_MODEL[0]);
      checkArrows(false, false);
      tick(defaultInterval + 100);
      flush();
      fixture.detectChanges();

      discardPeriodicTasks();
    }));

    it(`should display carousel no infinite and auto-navigate.`, fakeAsync(() => {
      const configService = fixture.debugElement.injector.get(ConfigService);

      configService.setConfig(GALLERY_ID, { carouselSlideInfinite: false });
      comp.id = GALLERY_ID;
      comp.images = IMAGES;
      fixture.detectChanges();

      const defaultInterval = 5000;

      TEST_MODEL.forEach((val: TestModel, index: number) => {
        checkMainContainer();
        checkCurrentImage(IMAGES[index], val);
        // checkArrows(index === 0, index === 6);
        tick(defaultInterval + 100);
        flush();
        fixture.detectChanges();
      });

      // infinite sliding is disabled, so after the next interval, current image must be still the last one
      checkMainContainer();
      checkCurrentImage(IMAGES[6], TEST_MODEL[6]);
      // checkArrows(false, true);
      tick(defaultInterval + 100);
      flush();
      fixture.detectChanges();

      discardPeriodicTasks();
    }));

    it(`should display carousel without dots.`, () => {
      const configService = fixture.debugElement.injector.get(ConfigService);

      configService.setConfig(GALLERY_ID, { carouselDotsConfig: {visible: false} });
      comp.id = GALLERY_ID;
      comp.images = IMAGES;
      fixture.detectChanges();

      checkMainContainer();
      checkCurrentImage(IMAGES[0], TEST_MODEL[0], false, true);
      checkArrows(false, false);
    });

    it(`should display carousel without arrows.`, fakeAsync(() => {
      const configService = fixture.debugElement.injector.get(ConfigService);

      configService.setConfig(GALLERY_ID, { carouselConfig: Object.assign({}, DEFAULT_CAROUSEL_CONFIG, {showArrows: false}) });
      comp.id = GALLERY_ID;
      comp.images = IMAGES;
      comp.ngOnInit();
      fixture.detectChanges();

      checkMainContainer();
      checkCurrentImage(IMAGES[0], TEST_MODEL[0], false, false);

      discardPeriodicTasks();
    }));

    const PLAY_CONFIG_AUTOPLAY: PlayConfig[] = [
      {autoPlay: true, interval: 5000, pauseOnHover: true},
      {autoPlay: true, interval: 1000, pauseOnHover: true},
      {autoPlay: true, interval: 10000, pauseOnHover: true},
      {autoPlay: true, interval: 10, pauseOnHover: true},
      {autoPlay: true, interval: 1000, pauseOnHover: false}
    ];

    PLAY_CONFIG_AUTOPLAY.forEach((val: PlayConfig, index: number) => {
      it(`should display carousel with autoplay enabled, but with different combinations of interval and pauseOnHover. Test i=${index}`, fakeAsync(() => {
        const configService = fixture.debugElement.injector.get(ConfigService);

        configService.setConfig(GALLERY_ID, { carouselPlayConfig: val });
        comp.id = GALLERY_ID;
        comp.images = IMAGES;
        fixture.detectChanges();

        TEST_MODEL.forEach((model: TestModel, modelIndex: number) => {
          checkMainContainer();
          checkCurrentImage(IMAGES[modelIndex], model);
          checkArrows(false, false);
          tick(val.interval + 1);
          flush();
          fixture.detectChanges();
        });

        tick(val.interval + 1);
        flush();
        fixture.detectChanges();

        discardPeriodicTasks();
      }));
    });

    const PLAY_CONFIG_NO_AUTOPLAY: PlayConfig[] = [
      {autoPlay: false, interval: 1000, pauseOnHover: true},
      {autoPlay: false, interval: 10000, pauseOnHover: false}
    ];

    PLAY_CONFIG_NO_AUTOPLAY.forEach((val: PlayConfig, index: number) => {
      it(`should display carousel without autoplay. Test i=${index}`, fakeAsync(() => {
        const configService = fixture.debugElement.injector.get(ConfigService);

        configService.setConfig(GALLERY_ID, { carouselPlayConfig: val });
        comp.id = GALLERY_ID;
        comp.images = IMAGES;
        fixture.detectChanges();

        checkMainContainer();
        checkCurrentImage(IMAGES[0], TEST_MODEL[0]);
        checkArrows(false, false);
        tick(val.interval + 100);
        flush();
        fixture.detectChanges();

        // after interval, the current image must be again the first image (because autoplay is disabled)
        checkMainContainer();
        // TODO uncomment this
        // checkCurrentImage(IMAGES[0], TEST_MODEL[0]);
        checkArrows(false, false);
        tick(val.interval + 100);
        flush();
        fixture.detectChanges();

        discardPeriodicTasks();
      }));
    });

    const PREVIEW_CONFIGS: CarouselPreviewConfig[] = [
      {visible: true},
      {visible: true, clickable: true},
      {visible: true, clickable: false},
      {visible: true, number: 7},
      // {visible: true, number: 0},
      // {visible: true, number: -1},
      {visible: true, number: 4, width: 'auto', maxHeight: '100px'},
      {visible: true, number: 4, width: 'auto', maxHeight: '100px'},
      {visible: true, number: 4, width: '100px', maxHeight: '100px'}
    ];

    PREVIEW_CONFIGS.forEach((val: CarouselPreviewConfig, index: number) => {
      it(`should display carousel with previews. Test i=${index}`, () => {
        const configService = fixture.debugElement.injector.get(ConfigService);

        configService.setConfig(GALLERY_ID, { carouselPreviewsConfig: val });
        comp.id = GALLERY_ID;
        comp.images = IMAGES;
        fixture.detectChanges();

        checkPreviews(val.number || 4, false, false, val.clickable === true || val.clickable === undefined);
      });
    });

    it(`should display carousel with fixed width.`, () => {
      const configService = fixture.debugElement.injector.get(ConfigService);

      configService.setConfig(GALLERY_ID, {
        carouselConfig: Object.assign({}, DEFAULT_CAROUSEL_CONFIG, {maxWidth: '766px'}),
        carouselPreviewsConfig: {number: 5, width: 'auto', maxHeight: '100px', visible: true}
      });
      comp.id = GALLERY_ID;
      comp.images = IMAGES;
      comp.ngOnInit();
      fixture.detectChanges();

      checkMainContainer(comp.carouselConfig?.maxWidth);
      checkCurrentImage(IMAGES[0], TEST_MODEL[0]);
      checkArrows(false, false);
    });

    const defaultDescriptionStyle: DescriptionStyle = {
      bgColor: 'rgba(0, 0, 0, .5)',
      textColor: 'white',
      marginTop: '0px',
      marginBottom: '0px',
      marginLeft: '0px',
      marginRight: '0px'
    };

    const carouselImageConfigs: CarouselImageConfig[] = [
      {
        description: {
          strategy: DescriptionStrategy.ALWAYS_VISIBLE,
          imageText: 'Image ',
          numberSeparator: '/',
          beforeTextDescription: ' - ',
          style: defaultDescriptionStyle
        } as Description
      },
      {
        description: {
          strategy: DescriptionStrategy.ALWAYS_HIDDEN,
          imageText: 'Image ',
          numberSeparator: '/',
          beforeTextDescription: ' - ',
          style: defaultDescriptionStyle
        } as Description
      },
      {
        description: {
          strategy: DescriptionStrategy.HIDE_IF_EMPTY,
          imageText: 'Image ',
          numberSeparator: '/',
          beforeTextDescription: ' - ',
          style: defaultDescriptionStyle
        } as Description
      }
    ];

    carouselImageConfigs.forEach((val: CarouselImageConfig, index: number) => {
      it(`should display carousel with description. Test i=${index}`, () => {
        const configService = fixture.debugElement.injector.get(ConfigService);

        configService.setConfig(GALLERY_ID, { carouselImageConfig: val });
        comp.id = GALLERY_ID;
        comp.images = IMAGES;
        fixture.detectChanges();

        checkMainContainer();
        checkCurrentImage(IMAGES[0], TEST_MODEL[0]);
        checkDescription(IMAGES[0], val);
        checkArrows(false, false);
      });
    });

    const PREVIEW_CONFIGS_BREAKPOINT: CarouselPreviewConfig[] = [
      {visible: true, breakpoints: {xSmall: 50, small: 60, medium: 80, large: 150, xLarge: 180}},
      {visible: true, breakpoints: {xSmall: 100, small: 120, medium: 150, large: 200, xLarge: 220}},
      {visible: true, breakpoints: {xSmall: 200, small: 220, medium: 10, large: 50, xLarge: 100}},
      // zero or negative numbers are admitted in the code, but they are completely useless for the user
      // however, I should improve this
      {visible: true, breakpoints: {xSmall: 0, small: 0, medium: 0, large: 0, xLarge: 0}},
      {visible: true, breakpoints: {xSmall: -1, small: -2, medium: -10, large: -10, xLarge: -1}}
    ];

    PREVIEW_CONFIGS_BREAKPOINT.forEach((previewConfig: CarouselPreviewConfig, index: number) => {
      it(`should display carousel with custom breakpoints.`, () => {
        const configService = fixture.debugElement.injector.get(ConfigService);

        configService.setConfig(GALLERY_ID, { carouselPreviewsConfig: previewConfig });
        comp.id = GALLERY_ID;
        comp.images = IMAGES;
        fixture.detectChanges();

        checkMainContainer();
        checkCurrentImage(IMAGES[0], TEST_MODEL[0]);
        checkArrows(false, false);
      });
    });

    it(`should display carousel with custom accessibility.`, () => {
      const configService = fixture.debugElement.injector.get(ConfigService);

      configService.setConfig(GALLERY_ID, { accessibilityConfig: CUSTOM_ACCESSIBILITY });
      comp.id = GALLERY_ID;
      comp.images = IMAGES;
      comp.ngOnInit();
      fixture.detectChanges();

      checkMainContainer('100%', CUSTOM_ACCESSIBILITY);
      checkCurrentImage(IMAGES[0], TEST_MODEL[0], true, true, CUSTOM_ACCESSIBILITY);
      checkArrows(false, false, CUSTOM_ACCESSIBILITY);
    });

    // // TODO not working, why???
    // it(`should display carousel and open modal-gallery clicking on the current image.`, () => {
    //   comp.id = GALLERY_ID;
    //   comp.images = IMAGES;
    //   comp.carouselConfig = <CarouselConfig>{
    //     maxWidth: '100%',
    //     maxHeight: '200px',
    //     showArrows: true,
    //     objectFit: 'cover',
    //     keyboardEnable: true,
    //     modalGalleryEnable: true, // modalGalleryEnable is required in this test
    //     legacyIE11Mode: false
    //   };
    //   comp.ngOnInit();
    //   fixture.detectChanges();
    //
    //   checkMainContainer();
    //   checkCurrentImage(IMAGES[0], TEST_MODEL[0]);
    //   checkArrows(false, false);
    //   // tick(defaultInterval + 100);
    //   // fixture.detectChanges();
    //
    //   // click on the current image
    //   const element: DebugElement = fixture.debugElement;
    //   const currentImage: DebugElement = element.query(By.css('picture.current-image>img'));
    //   console.log('currentImage', currentImage);
    //
    //   currentImage.nativeElement.click();
    //
    //
    //   fixture.detectChanges();
    //
    //   const modalGalleryWrapper: DebugElement = element.query(By.css('#modal-gallery-wrapper'));
    //   console.log('modalGalleryWrapper', modalGalleryWrapper);
    //   expect(modalGalleryWrapper).not.toBeNull();
    // });

    // it(`should display carousel without autoplay and navigate between images clicking on arrows.`, fakeAsync(() => {
    //   comp.id = GALLERY_ID;
    //   comp.images = IMAGES;
    //   comp.ngOnInit();
    //   fixture.detectChanges();
    //
    //   comp.show.subscribe((res: ImageModalEvent) => {
    //     console.log('^^^^^^^^^^^^^^^^^^^^^^ show called res', res);
    //     // checkMainContainer();
    //     // checkCurrentImage(IMAGES[1], TEST_MODEL[1]);
    //     // checkArrows(false, false);
    //     // comp.nextImage(Action.CLICK);
    //     // tick(defaultInterval + 100);
    //     fixture.detectChanges();
    //   });
    //
    //   // TEST_MODEL.forEach((val: TestModel, index: number) => {
    //   checkMainContainer();
    //   checkCurrentImage(IMAGES[0], TEST_MODEL[0]);
    //   checkArrows(false, false);
    //   // comp.nextImage(Action.CLICK);
    //   // tick(defaultInterval + 100);
    //   fixture.detectChanges();
    //   discardPeriodicTasks();
    //   // });
    // }));

    // TODO improve this adding more cases to cover all lines of code
    it(`should open carousel calling also ngOnChanges.`, fakeAsync(() => {
      comp.id = GALLERY_ID;
      comp.images = IMAGES;
      comp.ngOnChanges({
        currentImage: {
          previousValue: IMAGES[0],
          currentValue: IMAGES[0],
          firstChange: false,
          isFirstChange: () => false
        }
      } as SimpleChanges);
      fixture.detectChanges();
      const defaultInterval = 5000;

      TEST_MODEL.forEach((val: TestModel, index: number) => {
        checkMainContainer();
        checkCurrentImage(IMAGES[index], val);
        checkArrows(false, false);
        tick(defaultInterval + 100);
        flush();
        fixture.detectChanges();
      });

      tick(defaultInterval + 100);
      flush();
      fixture.detectChanges();

      discardPeriodicTasks();
    }));
  });

  describe('---ERRORS---', () => {
    it(`should throw an error, because id is not valid.`, () => {
      const configService = fixture.debugElement.injector.get(ConfigService);

      configService.setConfig(GALLERY_ID, { carouselConfig: Object.assign({}, DEFAULT_CAROUSEL_CONFIG, {modalGalleryEnable: true}) });
      comp.id = undefined;
      comp.images = IMAGES;
      expect(() => comp.ngOnInit()).toThrow(new Error(ID_ERROR));
    });

    const PLAY_CONFIG_AUTOPLAY: PlayConfig[] = [
      {autoPlay: true, interval: 0, pauseOnHover: true},
      {autoPlay: true, interval: -10, pauseOnHover: false},
      {autoPlay: true, interval: -1000, pauseOnHover: true},
      {autoPlay: false, interval: 0, pauseOnHover: false},
      {autoPlay: false, interval: -10, pauseOnHover: true},
      {autoPlay: false, interval: -1000, pauseOnHover: false}
    ];

    PLAY_CONFIG_AUTOPLAY.forEach((val: PlayConfig, index: number) => {
      it(`should throw an error because playConfig.interval is <=0. Test i=${index}`, fakeAsync(() => {
        const configService = fixture.debugElement.injector.get(ConfigService);

        expect(() => configService.setConfig(GALLERY_ID, { carouselPlayConfig: val })).toThrow(new Error(`Carousel's interval must be a number >= 0`));
        discardPeriodicTasks();
      }));
    });
  });
});