@frsource/tiny-carousel-core
Version:
Core module for @frsource/tiny-carousel, based on CSS scroll snap feature 🚀
464 lines (394 loc) • 14.7 kB
text/typescript
import { on, findXSnapIndex, off } from '@frsource/tiny-carousel-utils';
import { TinyCarousel } from '.';
jest.mock('@frsource/tiny-carousel-utils');
const {
findXSnapIndex: findXSnapIndexActual,
} = jest.requireActual('@frsource/tiny-carousel-utils');
const findXSnapIndexMock = findXSnapIndex as jest.Mock;
let carousel: TinyCarousel;
let element: HTMLElement;
let templateHTML: string;
let config: ConstructorParameters<typeof TinyCarousel>[1];
const initializeCarousel = () => {
element = document.createElement('ul');
element.innerHTML = templateHTML || `<li>item1</li><li>item2</li><li>item3</li>`;
document.body.append(element);
// mock offsets
Object.defineProperty(element.children[1], 'offsetLeft', { value: 150 });
Object.defineProperty(element.children[2], 'offsetLeft', { value: 300 });
carousel = new TinyCarousel(element, config);
};
beforeAll(() => {
findXSnapIndexMock.mockImplementation(findXSnapIndexActual);
});
afterEach(() => {
document.body.innerHTML = '';
jest.clearAllMocks();
});
const defaultConfiguration = {
active: 0,
className: 'frs-tc',
classNameOverflow: 'frs-tc--o',
hideScrollClassName: 'frs-hide-scroll',
itemClassName: 'frs-tc-item',
items: [],
overflow: true,
};
describe('::defaultConfig::get', () => {
it('should return with a copy of a default configuration object', () => {
expect(TinyCarousel.defaultConfig).toEqual(defaultConfiguration);
// if the copy is returned, changing current object's "active" property shouldn't affect next expect assumption
TinyCarousel.defaultConfig.active = 1;
expect(TinyCarousel.defaultConfig).toEqual(defaultConfiguration);
});
});
describe('::updateDefaultConfig', () => {
it('should merge passed config into the default one (not replace it completely)', () => {
expect(TinyCarousel.defaultConfig).toEqual(defaultConfiguration);
TinyCarousel.updateDefaultConfig({ active: 1 });
expect(TinyCarousel.defaultConfig).toEqual({
...defaultConfiguration,
active: 1
});
TinyCarousel.updateDefaultConfig({ active: 0 });
});
});
describe('constructor', () => {
describe('when no config passed', () => {
it('should provide defaults', () => {
initializeCarousel();
expect(carousel.config).toEqual({
active: 0,
className: 'frs-tc',
classNameOverflow: 'frs-tc--o',
hideScrollClassName: 'frs-hide-scroll',
itemClassName: 'frs-tc-item',
items: [],
overflow: true,
});
});
});
describe('when config passed in as a parameter', () => {
it('should merge passed in config with defaults', () => {
config = {active: 1};
initializeCarousel();
expect(carousel.config).toEqual({
active: 1,
className: 'frs-tc',
classNameOverflow: 'frs-tc--o',
hideScrollClassName: 'frs-hide-scroll',
itemClassName: 'frs-tc-item',
items: [],
overflow: true,
});
config = undefined;
});
});
});
describe('use', () => {
const install = jest.fn();
const plugin = { install };
beforeEach(initializeCarousel);
it('should call plugin install method with the carousel context as a first argument', () => {
carousel.use(plugin);
expect(install).toHaveBeenCalledTimes(1);
expect(install).toHaveBeenCalledWith(carousel);
});
it('should pass all of the arguments through to plugin install method', () => {
const args = ['arg1', {}, true];
carousel.use(plugin, ...args);
expect(install).toHaveBeenCalledTimes(1);
expect(install).toHaveBeenCalledWith(carousel, ...args);
});
it('should return instance context for easier chaining', () => {
expect(carousel.use(plugin)).toBe(carousel);
});
});
describe('init', () => {
let goToSpy: jest.SpyInstance;
let findPossibleItemsSpy: jest.SpyInstance;
beforeAll(() => {
findPossibleItemsSpy = jest.spyOn(TinyCarousel.prototype, 'findPossibleItems').mockReturnValue([
element.children[0] as HTMLElement, element.children[1] as HTMLElement
]);
config = {className: 'something', active: 2};
goToSpy = jest.spyOn(TinyCarousel.prototype, 'goTo').mockReturnValue(carousel);
});
beforeEach(initializeCarousel);
afterAll(() => {
config = undefined;
goToSpy.mockRestore();
findPossibleItemsSpy.mockRestore();
});
it('should try to find possible items', () => {
carousel.init();
expect(findPossibleItemsSpy).toHaveBeenCalledTimes(1);
});
it('should bind handler to the "scroll" event', () => {
carousel.init();
expect(on).toHaveBeenCalledWith(element, 'scroll', expect.any(Function), { passive: true });
});
it('should add classnames from the config', () => {
carousel.init();
expect(element.className).toBe('something frs-hide-scroll frs-tc--o');
});
it('should add item classnames from the config to the items', () => {
carousel.init();
carousel.config.items
.forEach(child => expect(child.className).toBe('frs-tc-item'));
});
it('shoud call goTo method', () => {
carousel.init();
expect(goToSpy).toHaveBeenCalledTimes(1);
expect(goToSpy).toHaveBeenCalledWith(2);
});
it('should return instance context for easier chaining', () => {
expect(carousel.init()).toBe(carousel);
});
describe('when `config.items` is set', () => {
beforeEach(() => carousel.config.items = [ document.body /* any HTMLElement */ ]);
it('shouldn`t update `config.items` with element children', () => {
carousel.init();
expect(carousel.config).toEqual({
active: 2,
className: 'something',
classNameOverflow: 'frs-tc--o',
hideScrollClassName: 'frs-hide-scroll',
itemClassName: 'frs-tc-item',
items: carousel.config.items,
overflow: true,
});
config = undefined;
});
});
describe('when `config.overflow` = false', () => {
beforeEach(() => carousel.config.overflow = false);
it('shouldn`t add overflow css class to the carouselElement', () => {
carousel.init();
expect(carousel.carouselElement.className).toBe('frs-tc frs-hide-scroll');
});
});
});
describe('destroy', () => {
it('should unbind the "scroll" handler which was bound by `init` method', () => {
carousel.init();
carousel.destroy();
expect(off).toHaveBeenCalledWith(element, 'scroll', expect.any(Function));
expect((on as jest.Mock).mock.calls[0][2]).toBe((off as jest.Mock).mock.calls[0][2]);
});
it('should return TinyCarousel instance', () =>{
expect(carousel.destroy()).toBe(carousel);
})
});
describe('goTo', () => {
beforeEach(initializeCarousel);
it('should return carousel instance', () => {
expect(carousel.goTo(0)).toBe(carousel);
});
describe('when items are set', () => {
beforeEach(() => carousel.init());
it('should move carousel to the correct position', () => {
carousel.goTo(1);
expect(element.scrollLeft).toBe(150);
});
describe('when config.overflow = false', () => {
it('should move carousel to the correct position', () => {
carousel.config.overflow = false;
carousel.goTo(2);
expect(element.scrollLeft).toBe(300);
});
});
describe('when value is bigger than items count', () => {
it('should handle the overflow gracefully and move carousel to the correct position', () => {
carousel.goTo(4);
expect(element.scrollLeft).toBe(150);
});
describe('when config.overflow = false', () => {
it('should stay at the last item', () => {
carousel.config.overflow = false;
carousel.goTo(4);
expect(element.scrollLeft).toBe(300);
});
});
});
describe('when value is < 0', () => {
it('should count items starting from the end', () => {
carousel.goTo(-2);
expect(element.scrollLeft).toBe(150);
});
describe('when config.overflow = false', () => {
it('should stay at the first item', () => {
carousel.config.overflow = false;
carousel.goTo(-2);
expect(element.scrollLeft).toBe(0);
});
});
});
describe('when value is smaller than -items.length', () => {
it('should repeat the counting with value + items.length until it gets a valid item number', () => {
carousel.goTo(-20); // 20 % 3 == 2
expect(element.scrollLeft).toBe(150);
});
describe('when config.overflow = false', () => {
it('should stay at the first item', () => {
carousel.config.overflow = false;
carousel.goTo(-20);
expect(element.scrollLeft).toBe(0);
});
});
});
});
});
describe('next', () => {
let goToSpy: jest.SpyInstance;
beforeAll(() => {
goToSpy = jest.spyOn(TinyCarousel.prototype, 'goTo').mockReturnValue(carousel);
});
beforeEach(initializeCarousel);
afterAll(() => goToSpy.mockRestore());
it('should call goTo with active + 1', () => {
carousel.next();
expect(goToSpy).toHaveBeenCalledTimes(1);
expect(goToSpy).toHaveBeenCalledWith(1);
});
});
describe('prev', () => {
let goToSpy: jest.SpyInstance;
const initializeCarouselAtActiveItem = (n: number) => {
initializeCarousel();
carousel.init();
carousel.goTo(n);
goToSpy = jest.spyOn(carousel, 'goTo').mockReturnValue(carousel);
};
afterEach(() => goToSpy.mockRestore());
it('should call goTo with active - 1', () => {
initializeCarouselAtActiveItem(1);
carousel.prev();
expect(goToSpy).toHaveBeenCalledTimes(1);
expect(goToSpy).toHaveBeenCalledWith(0);
});
});
describe('resetActive', () => {
it('should run without any errors', () => {
initializeCarousel();
expect(carousel.resetActive()).toBeUndefined();
});
});
describe('handleOverflow', () => {
let goToSpy: jest.SpyInstance;
beforeEach(() => {
initializeCarousel();
carousel.init();
goToSpy = jest.spyOn(carousel, 'goTo').mockReturnValue(carousel);
});
afterEach(() => goToSpy.mockRestore());
describe('when config.overflow = false', () => {
beforeEach(() => carousel.config.overflow = false);
it('shouldn\'t call goTo', () => {
(carousel as unknown as {handleOverflow: ()=> void}).handleOverflow();
expect(goToSpy).not.toHaveBeenCalled();
});
});
describe('when config.overflow = true', () => {
describe('when config.items.length === 0', () => {
it('shouldn\'t call goTo', () => {
carousel.config.items = [];
(carousel as unknown as {handleOverflow: ()=> void}).handleOverflow();
expect(goToSpy).not.toHaveBeenCalled();
});
});
describe('when active === -1', () => {
beforeAll(() => findXSnapIndexMock.mockReturnValue(-1));
afterAll(() => findXSnapIndexMock.mockImplementation(findXSnapIndexActual));
beforeEach(() => {
carousel.resetActive();
(carousel as unknown as {handleOverflow: ()=> void}).handleOverflow();
});
it('should call goTo with this.active', () => {
expect(goToSpy).toHaveBeenCalledWith(-1);
});
});
describe('when active === config.items.length', () => {
beforeAll(() => findXSnapIndexMock.mockReturnValue(3));
afterAll(() => findXSnapIndexMock.mockImplementation(findXSnapIndexActual));
beforeEach(() => {
carousel.resetActive();
(carousel as unknown as {handleOverflow: ()=> void}).handleOverflow();
});
it('should call goTo with this.active', () => {
expect(goToSpy).toHaveBeenCalledWith(carousel.config.items.length);
});
});
describe('when active !== -1 or config.items.length', () => {
beforeAll(() => findXSnapIndexMock.mockReturnValue(2));
afterAll(() => findXSnapIndexMock.mockImplementation(findXSnapIndexActual));
beforeEach(() => {
carousel.resetActive();
(carousel as unknown as {handleOverflow: ()=> void}).handleOverflow();
});
it('shouldn\'t call goTo', () => {
expect(goToSpy).not.toHaveBeenCalled();
});
});
});
});
describe('findPossibleItems', () => {
describe('when none of the children has itemClassName', () => {
beforeEach(initializeCarousel);
it('should return an array of all carousel element`s children', () => {
expect(carousel.findPossibleItems()).toEqual(Array.from(element.children));
expect(carousel.findPossibleItems() instanceof Array).toBeTruthy();
});
describe('after init, when new child is added', () => {
beforeEach(() => {
carousel.init();
carousel.carouselElement.append(document.createElement('li'));
});
it('should return it together with all of the other carousel element`s children', () => {
expect(carousel.findPossibleItems()).toEqual(Array.from(element.children));
});
});
});
describe('when a subset of the children has itemClassName', () => {
it('should return an array of all children with itemClassName set, ignoring all of the others', () => {
templateHTML = `<li class="frs-tc-item">item1</li><li>item 2</li><li class="frs-tc-item">item 3</li><li>item 4</li>`;
initializeCarousel();
expect(carousel.findPossibleItems()).toEqual([element.children[0], element.children[2]]);
expect(carousel.findPossibleItems() instanceof Array).toBeTruthy();
templateHTML = '';
});
});
});
describe('active::get', () => {
beforeEach(() => {
initializeCarousel();
carousel.init();
});
it('should return proper active index no matter how many times is being run', () => {
Object.defineProperty(element, 'scrollLeft', { value: 160 });
Object.defineProperty(element, 'clientWidth', { value: 5 });
expect(carousel.active).toBe(1);
expect(carousel.active).toBe(1);
});
describe('when carouselElement.scrollLeft = 0', () => {
it('should return proper 0', () => {
Object.defineProperty(element, 'scrollLeft', { value: 0 });
Object.defineProperty(element, 'clientWidth', { value: 5 });
expect(carousel.active).toBe(0);
});
});
describe('when item`s offsetParent = carouselElement', () => {
it('should return proper 1', () => {
Object.defineProperty(element, 'scrollLeft', { value: 150 });
Object.defineProperty(element, 'clientWidth', { value: 5 });
Object.defineProperty(carousel.config.items[0], 'offsetParent', { value: carousel.carouselElement });
expect(carousel.active).toBe(1);
});
});
describe('when items array is empty', () => {
it('should return 0', () => {
carousel.config.items = [];
expect(carousel.active).toBe(0);
});
});
});