Complete carousel refactor.
Reduces one layer of DOM nesting for simplication, uses the latest Embla version, unittests for everything.
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
import EmblaCarousel, { EmblaCarouselType } from 'embla-carousel';
|
||||
import { MockedObject, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { CarouselController } from '../../../src/utils/embla/carousel-controller';
|
||||
import AutoMediaLoadedInfo from '../../../src/utils/embla/plugins/auto-media-loaded-info/auto-media-loaded-info';
|
||||
import {
|
||||
MutationObserverMock,
|
||||
createParent,
|
||||
createSlot,
|
||||
createSlotHost,
|
||||
} from '../../test-utils';
|
||||
import {
|
||||
callEmblaHandler,
|
||||
callMutationHandler,
|
||||
createEmblaApiInstance,
|
||||
createTestSlideNodes,
|
||||
} from './test-utils';
|
||||
|
||||
vi.mock('embla-carousel', () => ({
|
||||
default: vi.fn().mockImplementation(() => {
|
||||
return createEmblaApiInstance();
|
||||
}),
|
||||
}));
|
||||
|
||||
// Get the nth most recently constructed EmblaAPI instance.
|
||||
const getEmblaApi = (n = 0): MockedObject<EmblaCarouselType> | null => {
|
||||
const constructions = vi.mocked(EmblaCarousel).mock.results;
|
||||
const mostRecentResult = constructions[constructions.length - 1 - n] ?? null;
|
||||
if (mostRecentResult && mostRecentResult.type === 'return') {
|
||||
return vi.mocked(mostRecentResult.value);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const createRoot = (): HTMLElement => {
|
||||
return document.createElement('div');
|
||||
};
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('CarouselController', () => {
|
||||
beforeAll(() => {
|
||||
vi.stubGlobal('MutationObserver', MutationObserverMock);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should construct', () => {
|
||||
const children = createTestSlideNodes();
|
||||
const parent = createParent({ children: children });
|
||||
const carousel = new CarouselController(createRoot(), parent);
|
||||
expect(carousel).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should construct with slot parent', () => {
|
||||
const slot = createSlot();
|
||||
const host = createSlotHost({ slot: slot, children: createTestSlideNodes() });
|
||||
const carousel = new CarouselController(host, slot);
|
||||
expect(carousel).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should destroy', () => {
|
||||
const children = createTestSlideNodes();
|
||||
const parent = createParent({ children: children });
|
||||
const carousel = new CarouselController(createRoot(), parent);
|
||||
|
||||
carousel.destroy();
|
||||
|
||||
expect(getEmblaApi()?.destroy).toBeCalled();
|
||||
});
|
||||
|
||||
it('should destroy with slot', () => {
|
||||
const slot = createSlot();
|
||||
const host = createSlotHost({ slot: slot, children: createTestSlideNodes() });
|
||||
const carousel = new CarouselController(host, slot);
|
||||
|
||||
carousel.destroy();
|
||||
|
||||
expect(getEmblaApi()?.destroy).toBeCalled();
|
||||
});
|
||||
|
||||
it('should get slide by index', () => {
|
||||
const children = createTestSlideNodes();
|
||||
const parent = createParent({ children: children });
|
||||
const carousel = new CarouselController(createRoot(), parent);
|
||||
|
||||
getEmblaApi()?.slideNodes.mockReturnValue(children);
|
||||
expect(carousel.getSlide(2)).toBe(children[2]);
|
||||
});
|
||||
|
||||
it('should get slide by index when index is invalid', () => {
|
||||
const children = createTestSlideNodes();
|
||||
const parent = createParent({ children: children });
|
||||
const carousel = new CarouselController(createRoot(), parent);
|
||||
|
||||
expect(carousel.getSlide(1000)).toBeNull();
|
||||
});
|
||||
|
||||
it('should get selected slide', () => {
|
||||
const children = createTestSlideNodes();
|
||||
const parent = createParent({ children: children });
|
||||
const carousel = new CarouselController(createRoot(), parent);
|
||||
|
||||
getEmblaApi()?.slideNodes.mockReturnValue(children);
|
||||
getEmblaApi()?.selectedScrollSnap.mockReturnValue(3);
|
||||
expect(carousel.getSelectedIndex()).toBe(3);
|
||||
expect(carousel.getSelectedSlide()).toBe(children[3]);
|
||||
});
|
||||
|
||||
it('should select given slide', () => {
|
||||
const children = createTestSlideNodes();
|
||||
const parent = createParent({ children: children });
|
||||
const carousel = new CarouselController(createRoot(), parent);
|
||||
|
||||
carousel.selectSlide(4);
|
||||
|
||||
expect(getEmblaApi()?.scrollTo).toBeCalledWith(4, false);
|
||||
});
|
||||
|
||||
it('should dispatch settle event', () => {
|
||||
const children = createTestSlideNodes();
|
||||
const parent = createParent({ children: children });
|
||||
new CarouselController(createRoot(), parent);
|
||||
|
||||
const settleHandler = vi.fn();
|
||||
parent.addEventListener('frigate-card:carousel:settle', settleHandler);
|
||||
|
||||
callEmblaHandler(getEmblaApi(), 'settle');
|
||||
|
||||
expect(settleHandler).toBeCalled();
|
||||
});
|
||||
|
||||
describe('should dispatch select event on', () => {
|
||||
it.each([['init' as const], ['select' as const]])(
|
||||
'%s',
|
||||
(emblaApiEvt: 'init' | 'select') => {
|
||||
const children = createTestSlideNodes();
|
||||
const parent = createParent({ children: children });
|
||||
new CarouselController(createRoot(), parent);
|
||||
|
||||
const selectHandler = vi.fn();
|
||||
parent.addEventListener('frigate-card:carousel:select', selectHandler);
|
||||
|
||||
getEmblaApi()?.selectedScrollSnap.mockReturnValue(6);
|
||||
getEmblaApi()?.slideNodes.mockReturnValue(children);
|
||||
callEmblaHandler(getEmblaApi(), emblaApiEvt);
|
||||
|
||||
expect(selectHandler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: {
|
||||
index: 6,
|
||||
element: children[6],
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should not dispatch anything with an invalid scroll snap', () => {
|
||||
const children = createTestSlideNodes();
|
||||
const parent = createParent({ children: children });
|
||||
new CarouselController(createRoot(), parent);
|
||||
|
||||
const selectHandler = vi.fn();
|
||||
parent.addEventListener('frigate-card:carousel:select', selectHandler);
|
||||
|
||||
getEmblaApi()?.selectedScrollSnap.mockReturnValue(1000);
|
||||
getEmblaApi()?.slideNodes.mockReturnValue(children);
|
||||
callEmblaHandler(getEmblaApi(), 'init');
|
||||
callEmblaHandler(getEmblaApi(), 'select');
|
||||
callEmblaHandler(getEmblaApi(), 'settle');
|
||||
|
||||
expect(selectHandler).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should honor creation options', () => {
|
||||
const children = createTestSlideNodes({ n: 1 });
|
||||
const root = createRoot();
|
||||
const parent = createParent({ children: children });
|
||||
const plugins = [AutoMediaLoadedInfo()];
|
||||
|
||||
new CarouselController(root, parent, {
|
||||
direction: 'vertical',
|
||||
transitionEffect: 'none',
|
||||
startIndex: 7,
|
||||
dragFree: true,
|
||||
loop: true,
|
||||
dragEnabled: false,
|
||||
plugins: plugins,
|
||||
});
|
||||
|
||||
expect(EmblaCarousel).toBeCalledWith(
|
||||
root,
|
||||
{
|
||||
slides: children,
|
||||
axis: 'y',
|
||||
duration: 20,
|
||||
startIndex: 7,
|
||||
dragFree: true,
|
||||
loop: true,
|
||||
containScroll: 'trimSnaps',
|
||||
watchSlides: false,
|
||||
watchResize: false,
|
||||
watchDrag: false,
|
||||
},
|
||||
plugins,
|
||||
);
|
||||
});
|
||||
|
||||
it('should include wheel plugin when slides > 1', () => {
|
||||
const children = createTestSlideNodes();
|
||||
const root = createRoot();
|
||||
const parent = createParent({ children: children });
|
||||
new CarouselController(root, parent);
|
||||
|
||||
expect(EmblaCarousel).toBeCalledWith(
|
||||
root,
|
||||
expect.anything(),
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: 'wheelGestures',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should recreate carousel when children are added', () => {
|
||||
const children = createTestSlideNodes();
|
||||
const root = createRoot();
|
||||
const parent = createParent({ children: children });
|
||||
new CarouselController(root, parent);
|
||||
|
||||
expect(EmblaCarousel).toBeCalledTimes(1);
|
||||
|
||||
const originalEmblaApi = getEmblaApi();
|
||||
expect(originalEmblaApi).toBeTruthy();
|
||||
|
||||
originalEmblaApi?.slideNodes.mockReturnValue(children);
|
||||
parent.appendChild(document.createElement('div'));
|
||||
callMutationHandler();
|
||||
|
||||
expect(originalEmblaApi?.destroy).toBeCalled();
|
||||
expect(getEmblaApi()).not.toBe(originalEmblaApi);
|
||||
|
||||
expect(EmblaCarousel).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should not recreate carousel when children have not changed', () => {
|
||||
const children = createTestSlideNodes();
|
||||
const root = createRoot();
|
||||
const parent = createParent({ children: children });
|
||||
new CarouselController(root, parent);
|
||||
|
||||
expect(EmblaCarousel).toBeCalledTimes(1);
|
||||
|
||||
const originalEmblaApi = getEmblaApi();
|
||||
expect(originalEmblaApi).toBeTruthy();
|
||||
|
||||
originalEmblaApi?.slideNodes.mockReturnValue(children);
|
||||
callMutationHandler();
|
||||
|
||||
expect(originalEmblaApi?.destroy).not.toBeCalled();
|
||||
expect(getEmblaApi()).toBe(originalEmblaApi);
|
||||
|
||||
expect(EmblaCarousel).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should recreate carousel when children are added to slot', () => {
|
||||
const children = createTestSlideNodes();
|
||||
const slot = createSlot();
|
||||
const host = createSlotHost({ slot: slot, children: children });
|
||||
|
||||
new CarouselController(host, slot);
|
||||
|
||||
expect(EmblaCarousel).toBeCalledTimes(1);
|
||||
|
||||
const originalEmblaApi = getEmblaApi();
|
||||
expect(originalEmblaApi).toBeTruthy();
|
||||
|
||||
originalEmblaApi?.slideNodes.mockReturnValue(children);
|
||||
|
||||
host.appendChild(document.createElement('div'));
|
||||
slot.dispatchEvent(new Event('slotchange'));
|
||||
|
||||
expect(originalEmblaApi?.destroy).toBeCalled();
|
||||
expect(getEmblaApi()).not.toBe(originalEmblaApi);
|
||||
|
||||
expect(EmblaCarousel).toBeCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,219 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { AutoLazyLoad } from '../../../../../src/utils/embla/plugins/auto-lazy-load/auto-lazy-load';
|
||||
import {
|
||||
callEmblaHandler,
|
||||
callVisibilityHandler,
|
||||
createEmblaApiInstance,
|
||||
createTestEmblaOptionHandler,
|
||||
createTestSlideNodes,
|
||||
} from '../../test-utils';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('AutoLazyLoad', () => {
|
||||
it('should construct', () => {
|
||||
const plugin = AutoLazyLoad();
|
||||
expect(plugin.name).toBe('autoLazyLoad');
|
||||
});
|
||||
|
||||
it('should destroy', () => {
|
||||
const plugin = AutoLazyLoad({
|
||||
lazyLoadCallback: vi.fn(),
|
||||
lazyUnloadCallback: vi.fn(),
|
||||
});
|
||||
const emblaApi = createEmblaApiInstance();
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
plugin.destroy();
|
||||
|
||||
expect(emblaApi.off).toBeCalledWith('init', expect.anything());
|
||||
expect(emblaApi.off).toBeCalledWith('select', expect.anything());
|
||||
});
|
||||
|
||||
it('should do nothing without callbacks', () => {
|
||||
const plugin = AutoLazyLoad({
|
||||
// No callbacks provided.
|
||||
});
|
||||
|
||||
const children = createTestSlideNodes();
|
||||
const emblaApi = createEmblaApiInstance({ slideNodes: children });
|
||||
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
expect(emblaApi.on).not.toBeCalled();
|
||||
|
||||
plugin.destroy();
|
||||
expect(emblaApi.off).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should lazy load single slide on select', () => {
|
||||
const lazyLoadCallback = vi.fn();
|
||||
const plugin = AutoLazyLoad({
|
||||
lazyLoadCallback: lazyLoadCallback,
|
||||
});
|
||||
|
||||
const children = createTestSlideNodes();
|
||||
const emblaApi = createEmblaApiInstance({ slideNodes: children });
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
|
||||
expect(emblaApi.on).toBeCalledWith('init', expect.anything());
|
||||
expect(emblaApi.on).toBeCalledWith('select', expect.anything());
|
||||
|
||||
callEmblaHandler(emblaApi, 'init');
|
||||
expect(lazyLoadCallback).toBeCalledWith(0, children[0]);
|
||||
|
||||
callEmblaHandler(emblaApi, 'select');
|
||||
|
||||
// The select call will not re-lazyload the same slide.
|
||||
expect(lazyLoadCallback).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should lazy load multiple slides on select', () => {
|
||||
const lazyLoadCallback = vi.fn();
|
||||
const plugin = AutoLazyLoad({
|
||||
lazyLoadCallback: lazyLoadCallback,
|
||||
lazyLoadCount: 3,
|
||||
});
|
||||
|
||||
const children = createTestSlideNodes();
|
||||
const emblaApi = createEmblaApiInstance({
|
||||
slideNodes: children,
|
||||
selectedScrollSnap: 5,
|
||||
});
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
|
||||
callEmblaHandler(emblaApi, 'select');
|
||||
for (let i = 3; i <= 8; ++i) {
|
||||
expect(lazyLoadCallback).toBeCalledWith(i, children[i]);
|
||||
}
|
||||
});
|
||||
|
||||
it('should lazy unload on select', () => {
|
||||
const lazyUnloadCallback = vi.fn();
|
||||
const plugin = AutoLazyLoad({
|
||||
lazyLoadCallback: vi.fn(),
|
||||
lazyLoadCount: 3,
|
||||
lazyUnloadCallback: lazyUnloadCallback,
|
||||
lazyUnloadCondition: 'all',
|
||||
});
|
||||
|
||||
const children = createTestSlideNodes();
|
||||
const emblaApi = createEmblaApiInstance({
|
||||
selectedScrollSnap: 5,
|
||||
slideNodes: children,
|
||||
});
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
|
||||
callEmblaHandler(emblaApi, 'select');
|
||||
|
||||
// First call will not unload anything, since it was not lazy loaded.
|
||||
expect(lazyUnloadCallback).not.toBeCalled();
|
||||
|
||||
vi.mocked(emblaApi.previousScrollSnap).mockReturnValue(5);
|
||||
callEmblaHandler(emblaApi, 'select');
|
||||
|
||||
// Second call should lazy unload the previous slide.
|
||||
expect(lazyUnloadCallback).toBeCalledWith(5, children[5]);
|
||||
});
|
||||
|
||||
it('should lazy load on visibility', () => {
|
||||
vi.spyOn(global.document, 'addEventListener');
|
||||
|
||||
const lazyLoadCallback = vi.fn();
|
||||
const plugin = AutoLazyLoad({
|
||||
lazyLoadCallback: lazyLoadCallback,
|
||||
});
|
||||
|
||||
const children = createTestSlideNodes();
|
||||
const emblaApi = createEmblaApiInstance({
|
||||
slideNodes: children,
|
||||
});
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
|
||||
Object.defineProperty(document, 'visibilityState', {
|
||||
value: 'visible',
|
||||
writable: true,
|
||||
});
|
||||
callVisibilityHandler();
|
||||
expect(lazyLoadCallback).toBeCalledWith(0, children[0]);
|
||||
});
|
||||
|
||||
it('should lazy unload on visibility', () => {
|
||||
vi.spyOn(global.document, 'addEventListener');
|
||||
|
||||
const lazyUnloadCallback = vi.fn();
|
||||
|
||||
const plugin = AutoLazyLoad({
|
||||
lazyLoadCallback: vi.fn(),
|
||||
lazyUnloadCallback: lazyUnloadCallback,
|
||||
lazyUnloadCondition: 'all',
|
||||
});
|
||||
|
||||
const children = createTestSlideNodes();
|
||||
const emblaApi = createEmblaApiInstance({
|
||||
slideNodes: children,
|
||||
});
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
|
||||
Object.defineProperty(document, 'visibilityState', {
|
||||
value: 'visible',
|
||||
writable: true,
|
||||
});
|
||||
callVisibilityHandler();
|
||||
|
||||
Object.defineProperty(document, 'visibilityState', {
|
||||
value: 'hidden',
|
||||
writable: true,
|
||||
});
|
||||
callVisibilityHandler();
|
||||
expect(lazyUnloadCallback).toBeCalledWith(0, children[0]);
|
||||
});
|
||||
|
||||
it('should not lazy unload on visibility without a callback', () => {
|
||||
vi.spyOn(global.document, 'addEventListener');
|
||||
|
||||
const lazyLoadCallback = vi.fn();
|
||||
const plugin = AutoLazyLoad({
|
||||
lazyLoadCallback: lazyLoadCallback,
|
||||
lazyUnloadCondition: 'all',
|
||||
// No lazy unload callback.
|
||||
});
|
||||
const children = createTestSlideNodes();
|
||||
const emblaApi = createEmblaApiInstance({
|
||||
slideNodes: children,
|
||||
});
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
|
||||
Object.defineProperty(document, 'visibilityState', {
|
||||
value: 'visible',
|
||||
writable: true,
|
||||
});
|
||||
callVisibilityHandler();
|
||||
expect(lazyLoadCallback).toBeCalledTimes(1);
|
||||
|
||||
Object.defineProperty(document, 'visibilityState', {
|
||||
value: 'hidden',
|
||||
writable: true,
|
||||
});
|
||||
callVisibilityHandler();
|
||||
expect(lazyLoadCallback).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not lazy load or unload on visibility when no callback provided', () => {
|
||||
vi.spyOn(global.document, 'addEventListener');
|
||||
|
||||
const plugin = AutoLazyLoad({
|
||||
lazyUnloadCondition: 'all',
|
||||
// No callbacks provided.
|
||||
});
|
||||
|
||||
const children = createTestSlideNodes();
|
||||
const emblaApi = createEmblaApiInstance({
|
||||
slideNodes: children,
|
||||
});
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
|
||||
Object.defineProperty(document, 'visibilityState', {
|
||||
value: 'visible',
|
||||
writable: true,
|
||||
});
|
||||
callVisibilityHandler();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,304 @@
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { FrigateCardMediaPlayer } from '../../../../../src/types';
|
||||
import {
|
||||
AutoMediaActions,
|
||||
AutoMediaActionsOptionsType,
|
||||
AutoMediaActionsType,
|
||||
} from '../../../../../src/utils/embla/plugins/auto-media-actions/auto-media-actions';
|
||||
import { dispatchExistingMediaLoadedInfoAsEvent } from '../../../../../src/utils/media-info';
|
||||
import {
|
||||
IntersectionObserverMock,
|
||||
createMediaLoadedInfo,
|
||||
createParent,
|
||||
} from '../../../../test-utils';
|
||||
import {
|
||||
callEmblaHandler,
|
||||
callIntersectionHandler,
|
||||
callVisibilityHandler,
|
||||
createEmblaApiInstance,
|
||||
createTestEmblaOptionHandler,
|
||||
createTestSlideNodes,
|
||||
} from '../../test-utils';
|
||||
|
||||
const getPlayer = (
|
||||
element: HTMLElement,
|
||||
selector: string,
|
||||
): (HTMLElement & FrigateCardMediaPlayer) | null => {
|
||||
return element.querySelector(selector);
|
||||
};
|
||||
|
||||
const createPlayerSlideNodes = (n = 10): HTMLElement[] => {
|
||||
const slides = createTestSlideNodes({ n: n });
|
||||
for (const slide of slides) {
|
||||
const player = document.createElement('video');
|
||||
|
||||
player['play'] = vi.fn();
|
||||
player['pause'] = vi.fn();
|
||||
player['mute'] = vi.fn();
|
||||
player['unmute'] = vi.fn();
|
||||
player['isMuted'] = vi.fn().mockReturnValue(true);
|
||||
player['seek'] = vi.fn();
|
||||
player['getScreenshotURL'] = vi.fn();
|
||||
player['setControls'] = vi.fn();
|
||||
player['isPaused'] = vi.fn();
|
||||
|
||||
slide.appendChild(player);
|
||||
}
|
||||
return slides;
|
||||
};
|
||||
|
||||
const createPlugin = (options?: AutoMediaActionsOptionsType): AutoMediaActionsType => {
|
||||
return AutoMediaActions({
|
||||
playerSelector: 'video',
|
||||
autoPlayCondition: 'all',
|
||||
autoUnmuteCondition: 'all',
|
||||
autoPauseCondition: 'all',
|
||||
autoMuteCondition: 'all',
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('AutoMediaActions', () => {
|
||||
beforeAll(() => {
|
||||
vi.stubGlobal('IntersectionObserver', IntersectionObserverMock);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should construct', () => {
|
||||
const plugin = AutoMediaActions();
|
||||
expect(plugin.name).toBe('autoMediaActions');
|
||||
});
|
||||
|
||||
it('should init without any conditions', () => {
|
||||
const plugin = AutoMediaActions();
|
||||
|
||||
const parent = createParent();
|
||||
const addEventListener = vi.fn();
|
||||
parent.addEventListener = addEventListener;
|
||||
const emblaApi = createEmblaApiInstance({ containerNode: parent });
|
||||
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
|
||||
expect(emblaApi.on).toBeCalledWith('destroy', expect.anything());
|
||||
expect(emblaApi.on).not.toBeCalledWith('select', expect.anything());
|
||||
expect(addEventListener).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should destroy', () => {
|
||||
const plugin = createPlugin();
|
||||
const parent = createParent();
|
||||
const removeEventListener = vi.fn();
|
||||
parent.removeEventListener = removeEventListener;
|
||||
const emblaApi = createEmblaApiInstance({ containerNode: parent });
|
||||
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
|
||||
plugin.destroy();
|
||||
|
||||
expect(emblaApi.off).toBeCalledWith('destroy', expect.anything());
|
||||
expect(emblaApi.off).toBeCalledWith('select', expect.anything());
|
||||
expect(removeEventListener).toBeCalled();
|
||||
});
|
||||
|
||||
it('should destroy without any conditions', () => {
|
||||
const plugin = AutoMediaActions();
|
||||
const parent = createParent();
|
||||
const removeEventListener = vi.fn();
|
||||
parent.removeEventListener = removeEventListener;
|
||||
const emblaApi = createEmblaApiInstance({ containerNode: parent });
|
||||
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
|
||||
plugin.destroy();
|
||||
|
||||
expect(emblaApi.off).toBeCalledWith('destroy', expect.anything());
|
||||
expect(emblaApi.off).not.toBeCalledWith('select', expect.anything());
|
||||
expect(removeEventListener).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should mute and pause on destroy', () => {
|
||||
const plugin = createPlugin();
|
||||
const children = createPlayerSlideNodes();
|
||||
const parent = createParent({ children: children });
|
||||
const emblaApi = createEmblaApiInstance({
|
||||
slideNodes: children,
|
||||
containerNode: parent,
|
||||
selectedScrollSnap: 5,
|
||||
});
|
||||
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
callEmblaHandler(emblaApi, 'destroy');
|
||||
|
||||
expect(getPlayer(children[5], 'video')?.pause).toBeCalled();
|
||||
expect(getPlayer(children[5], 'video')?.mute).toBeCalled();
|
||||
});
|
||||
|
||||
it('should play and unmute on media load', () => {
|
||||
const plugin = createPlugin();
|
||||
const children = createPlayerSlideNodes();
|
||||
const parent = createParent({ children: children });
|
||||
const emblaApi = createEmblaApiInstance({
|
||||
slideNodes: children,
|
||||
containerNode: parent,
|
||||
selectedScrollSnap: 5,
|
||||
});
|
||||
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
dispatchExistingMediaLoadedInfoAsEvent(parent, createMediaLoadedInfo());
|
||||
|
||||
expect(getPlayer(children[5], 'video')?.play).toBeCalled();
|
||||
expect(getPlayer(children[5], 'video')?.unmute).toBeCalled();
|
||||
});
|
||||
|
||||
it('should not play or unmute on media load when player selecter not provided', () => {
|
||||
const plugin = createPlugin({ playerSelector: undefined });
|
||||
const children = createPlayerSlideNodes();
|
||||
const parent = createParent({ children: children });
|
||||
const emblaApi = createEmblaApiInstance({
|
||||
slideNodes: children,
|
||||
containerNode: parent,
|
||||
selectedScrollSnap: 5,
|
||||
});
|
||||
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
dispatchExistingMediaLoadedInfoAsEvent(parent, createMediaLoadedInfo());
|
||||
|
||||
expect(getPlayer(children[5], 'video')?.play).not.toBeCalled();
|
||||
expect(getPlayer(children[5], 'video')?.unmute).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should pause and mute previous on select', () => {
|
||||
const plugin = createPlugin();
|
||||
const children = createPlayerSlideNodes();
|
||||
const emblaApi = createEmblaApiInstance({
|
||||
slideNodes: children,
|
||||
previousScrollSnap: 4,
|
||||
});
|
||||
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
callEmblaHandler(emblaApi, 'select');
|
||||
|
||||
expect(getPlayer(children[4], 'video')?.pause).toBeCalled();
|
||||
expect(getPlayer(children[4], 'video')?.mute).toBeCalled();
|
||||
});
|
||||
|
||||
it('should play and unmute on visibility change to visible', () => {
|
||||
vi.spyOn(global.document, 'addEventListener');
|
||||
|
||||
const plugin = createPlugin();
|
||||
const children = createPlayerSlideNodes();
|
||||
const emblaApi = createEmblaApiInstance({
|
||||
slideNodes: children,
|
||||
selectedScrollSnap: 5,
|
||||
});
|
||||
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
|
||||
Object.defineProperty(document, 'visibilityState', {
|
||||
value: 'visible',
|
||||
writable: true,
|
||||
});
|
||||
callVisibilityHandler();
|
||||
|
||||
expect(getPlayer(children[5], 'video')?.play).toBeCalled();
|
||||
expect(getPlayer(children[5], 'video')?.unmute).toBeCalled();
|
||||
});
|
||||
|
||||
it('should pause and unmute on visibility change to hidden', () => {
|
||||
vi.spyOn(global.document, 'addEventListener');
|
||||
|
||||
const plugin = createPlugin();
|
||||
const children = createPlayerSlideNodes();
|
||||
const emblaApi = createEmblaApiInstance({
|
||||
slideNodes: children,
|
||||
});
|
||||
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
|
||||
Object.defineProperty(document, 'visibilityState', {
|
||||
value: 'hidden',
|
||||
writable: true,
|
||||
});
|
||||
callVisibilityHandler();
|
||||
|
||||
for (const child of children) {
|
||||
expect(getPlayer(child, 'video')?.pause).toBeCalled();
|
||||
expect(getPlayer(child, 'video')?.mute).toBeCalled();
|
||||
}
|
||||
});
|
||||
|
||||
describe('should take no action on visibility change without callbacks', () => {
|
||||
it.each([['visible' as const], ['hidden' as const]])(
|
||||
'%s',
|
||||
(visibilityState: 'visible' | 'hidden') => {
|
||||
vi.spyOn(global.document, 'addEventListener');
|
||||
|
||||
const plugin = AutoMediaActions();
|
||||
const children = createPlayerSlideNodes();
|
||||
const emblaApi = createEmblaApiInstance({
|
||||
slideNodes: children,
|
||||
});
|
||||
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
|
||||
Object.defineProperty(document, 'visibilityState', {
|
||||
value: visibilityState,
|
||||
writable: true,
|
||||
});
|
||||
callVisibilityHandler();
|
||||
|
||||
for (const child of children) {
|
||||
expect(getPlayer(child, 'video')?.play).not.toBeCalled();
|
||||
expect(getPlayer(child, 'video')?.pause).not.toBeCalled();
|
||||
expect(getPlayer(child, 'video')?.mute).not.toBeCalled();
|
||||
expect(getPlayer(child, 'video')?.unmute).not.toBeCalled();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should play and unmute on intersection', () => {
|
||||
const plugin = createPlugin();
|
||||
const children = createPlayerSlideNodes();
|
||||
const emblaApi = createEmblaApiInstance({
|
||||
slideNodes: children,
|
||||
selectedScrollSnap: 5,
|
||||
});
|
||||
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
|
||||
// Intersection observer always calls handler on creation (and we ignore
|
||||
// these first calls).
|
||||
callIntersectionHandler(true);
|
||||
callIntersectionHandler(true);
|
||||
|
||||
expect(getPlayer(children[5], 'video')?.play).toBeCalled();
|
||||
expect(getPlayer(children[5], 'video')?.unmute).toBeCalled();
|
||||
});
|
||||
|
||||
it('should pause and mute on intersection', () => {
|
||||
vi.spyOn(global.document, 'addEventListener');
|
||||
|
||||
const plugin = createPlugin();
|
||||
const children = createPlayerSlideNodes();
|
||||
const emblaApi = createEmblaApiInstance({
|
||||
slideNodes: children,
|
||||
});
|
||||
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
|
||||
// Intersection observer always calls handler on creation (and we ignore
|
||||
// these first calls).
|
||||
callIntersectionHandler(true);
|
||||
callIntersectionHandler(false);
|
||||
|
||||
for (const child of children) {
|
||||
expect(getPlayer(child, 'video')?.pause).toBeCalled();
|
||||
expect(getPlayer(child, 'video')?.mute).toBeCalled();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import AutoMediaLoadedInfo from '../../../../../src/utils/embla/plugins/auto-media-loaded-info/auto-media-loaded-info';
|
||||
import {
|
||||
dispatchExistingMediaLoadedInfoAsEvent,
|
||||
dispatchMediaUnloadedEvent,
|
||||
} from '../../../../../src/utils/media-info';
|
||||
import { createMediaLoadedInfo, createParent } from '../../../../test-utils';
|
||||
import {
|
||||
callEmblaHandler,
|
||||
createEmblaApiInstance,
|
||||
createTestEmblaOptionHandler,
|
||||
createTestSlideNodes,
|
||||
} from '../../test-utils';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('AutoMediaLoadedInfo', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should construct', () => {
|
||||
const plugin = AutoMediaLoadedInfo();
|
||||
expect(plugin.name).toBe('autoMediaLoadedInfo');
|
||||
});
|
||||
|
||||
it('should destroy', () => {
|
||||
const plugin = AutoMediaLoadedInfo();
|
||||
const emblaApi = createEmblaApiInstance();
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
plugin.destroy();
|
||||
|
||||
expect(emblaApi.off).toBeCalledWith('init', expect.anything());
|
||||
expect(emblaApi.off).toBeCalledWith('select', expect.anything());
|
||||
});
|
||||
|
||||
describe('should correctly propogate media load/unload depending on whether media is currently selected', () => {
|
||||
it.each([
|
||||
['loaded' as const, true],
|
||||
['unloaded' as const, true],
|
||||
['loaded' as const, false],
|
||||
['unloaded' as const, false],
|
||||
])('%s', (type: string, selected: boolean) => {
|
||||
const plugin = AutoMediaLoadedInfo();
|
||||
const children = createTestSlideNodes();
|
||||
const parent = createParent({ children: children });
|
||||
|
||||
const emblaApi = createEmblaApiInstance({
|
||||
containerNode: parent,
|
||||
slideNodes: children,
|
||||
selectedScrollSnap: selected ? 5 : 4,
|
||||
});
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
|
||||
const mediaLoadedHandler = vi.fn();
|
||||
parent.addEventListener('frigate-card:media:' + type, mediaLoadedHandler);
|
||||
if (type === 'loaded') {
|
||||
dispatchExistingMediaLoadedInfoAsEvent(children[5], createMediaLoadedInfo());
|
||||
} else if (type === 'unloaded') {
|
||||
dispatchMediaUnloadedEvent(children[5]);
|
||||
}
|
||||
|
||||
if (selected) {
|
||||
expect(mediaLoadedHandler).toBeCalled();
|
||||
} else {
|
||||
expect(mediaLoadedHandler).not.toBeCalled();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('selecting a slide should dispatch a previously saved media loaded info if present', () => {
|
||||
const plugin = AutoMediaLoadedInfo();
|
||||
const children = createTestSlideNodes();
|
||||
const parent = createParent({ children: children });
|
||||
const emblaApi = createEmblaApiInstance({
|
||||
containerNode: parent,
|
||||
slideNodes: children,
|
||||
});
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
|
||||
const mediaLoadedHandler = vi.fn();
|
||||
parent.addEventListener('frigate-card:media:loaded', mediaLoadedHandler);
|
||||
dispatchExistingMediaLoadedInfoAsEvent(children[5], createMediaLoadedInfo());
|
||||
|
||||
vi.mocked(emblaApi.selectedScrollSnap).mockReturnValue(4);
|
||||
callEmblaHandler(emblaApi, 'select');
|
||||
expect(mediaLoadedHandler).not.toBeCalled();
|
||||
|
||||
vi.mocked(emblaApi.selectedScrollSnap).mockReturnValue(5);
|
||||
callEmblaHandler(emblaApi, 'select');
|
||||
expect(mediaLoadedHandler).toBeCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,161 @@
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import AutoSize from '../../../../../src/utils/embla/plugins/auto-size/auto-size';
|
||||
import {
|
||||
IntersectionObserverMock,
|
||||
ResizeObserverMock,
|
||||
createParent,
|
||||
requestAnimationFrameMock,
|
||||
} from '../../../../test-utils';
|
||||
import {
|
||||
callEmblaHandler,
|
||||
callIntersectionHandler,
|
||||
callResizeHandler,
|
||||
createEmblaApiInstance,
|
||||
createTestEmblaOptionHandler,
|
||||
createTestSlideNodes,
|
||||
} from '../../test-utils';
|
||||
|
||||
// Mock out debouncing (used in the reinit controller).
|
||||
vi.mock('lodash-es/debounce', () => ({
|
||||
default: vi.fn((fn) => fn),
|
||||
}));
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('AutoSize', () => {
|
||||
beforeAll(() => {
|
||||
// Mock out requestAnimationFrame (used in the reinit controller).
|
||||
window.requestAnimationFrame = requestAnimationFrameMock;
|
||||
vi.stubGlobal('IntersectionObserver', IntersectionObserverMock);
|
||||
vi.stubGlobal('ResizeObserver', ResizeObserverMock);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should construct', () => {
|
||||
const plugin = AutoSize();
|
||||
expect(plugin.name).toBe('autoSize');
|
||||
});
|
||||
|
||||
it('should destroy', () => {
|
||||
const plugin = AutoSize();
|
||||
const emblaApi = createEmblaApiInstance();
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
plugin.destroy();
|
||||
|
||||
expect(emblaApi.off).toBeCalledWith('settle', expect.anything());
|
||||
|
||||
expect(
|
||||
vi.mocked(IntersectionObserver).mock.results[0].value.disconnect,
|
||||
).toBeCalled();
|
||||
expect(vi.mocked(ResizeObserver).mock.results[0].value.disconnect).toBeCalled();
|
||||
});
|
||||
|
||||
it('should correctly handle intersection', () => {
|
||||
const plugin = AutoSize();
|
||||
const emblaApi = createEmblaApiInstance();
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
|
||||
// First intersection handler call sets the state only.
|
||||
callIntersectionHandler(true);
|
||||
|
||||
callIntersectionHandler(false);
|
||||
callIntersectionHandler(false);
|
||||
callIntersectionHandler(false);
|
||||
|
||||
expect(emblaApi.reInit).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should correctly handle resize', () => {
|
||||
const plugin = AutoSize();
|
||||
const parent = createParent();
|
||||
const emblaApi = createEmblaApiInstance({ containerNode: parent });
|
||||
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
|
||||
callResizeHandler([{ target: parent, width: 10, height: 20 }]);
|
||||
callResizeHandler([{ target: parent, width: 10, height: 20 }]);
|
||||
callResizeHandler([{ target: parent, width: 10, height: 20 }]);
|
||||
|
||||
expect(emblaApi.reInit).toBeCalledTimes(1);
|
||||
|
||||
callResizeHandler([{ target: parent, width: 20, height: 40 }]);
|
||||
|
||||
expect(emblaApi.reInit).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should set container height on slide settle', () => {
|
||||
const plugin = AutoSize();
|
||||
const parent = createParent();
|
||||
const children = createTestSlideNodes();
|
||||
const emblaApi = createEmblaApiInstance({
|
||||
containerNode: parent,
|
||||
selectedScrollSnap: 0,
|
||||
slideNodes: children,
|
||||
// 0th scroll snap shows the 0th slide only.
|
||||
slideRegistry: [[0]],
|
||||
});
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
|
||||
children[0].getBoundingClientRect = vi.fn().mockReturnValue({
|
||||
width: 200,
|
||||
height: 800,
|
||||
});
|
||||
|
||||
// select should not do anything, we wait for it to have settled for
|
||||
// smoothness.
|
||||
callEmblaHandler(emblaApi, 'select');
|
||||
expect(parent.style.maxHeight).toBeFalsy();
|
||||
|
||||
callEmblaHandler(emblaApi, 'settle');
|
||||
expect(parent.style.maxHeight).toBe('800px');
|
||||
});
|
||||
|
||||
it('should not set container height on horizontal carousel', () => {
|
||||
const plugin = AutoSize();
|
||||
const parent = createParent();
|
||||
const children = createTestSlideNodes();
|
||||
const emblaApi = createEmblaApiInstance({
|
||||
containerNode: parent,
|
||||
selectedScrollSnap: 0,
|
||||
slideNodes: children,
|
||||
axis: 'y',
|
||||
// 0th scroll snap shows the 0th slide only.
|
||||
slideRegistry: [[0]],
|
||||
});
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
|
||||
children[0].getBoundingClientRect = vi.fn().mockReturnValue({
|
||||
width: 200,
|
||||
height: 800,
|
||||
});
|
||||
|
||||
callEmblaHandler(emblaApi, 'settle');
|
||||
|
||||
expect(parent.style.maxHeight).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should not set container height when slide dimensions are invalid', () => {
|
||||
const plugin = AutoSize();
|
||||
const parent = createParent();
|
||||
const children = createTestSlideNodes();
|
||||
const emblaApi = createEmblaApiInstance({
|
||||
containerNode: parent,
|
||||
selectedScrollSnap: 0,
|
||||
slideNodes: children,
|
||||
axis: 'x',
|
||||
// 0th scroll snap shows the 0th slide only.
|
||||
slideRegistry: [[0]],
|
||||
});
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
|
||||
children[0].getBoundingClientRect = vi.fn().mockReturnValue(NaN);
|
||||
callEmblaHandler(emblaApi, 'settle');
|
||||
|
||||
children[0].getBoundingClientRect = vi.fn().mockReturnValue(0);
|
||||
callEmblaHandler(emblaApi, 'settle');
|
||||
|
||||
expect(parent.style.maxHeight).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { EmblaReInitController } from '../../../src/utils/embla/reinit-controller';
|
||||
import { requestAnimationFrameMock } from '../../test-utils';
|
||||
import { callEmblaHandler, createEmblaApiInstance } from './test-utils';
|
||||
|
||||
vi.mock('lodash-es/debounce', () => ({
|
||||
default: vi.fn((fn) => fn),
|
||||
}));
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('EmblaReInitController', () => {
|
||||
beforeAll(() => {
|
||||
window.requestAnimationFrame = requestAnimationFrameMock;
|
||||
});
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should construct', () => {
|
||||
const emblaApi = createEmblaApiInstance();
|
||||
new EmblaReInitController(emblaApi);
|
||||
expect(emblaApi.on).toBeCalledWith('scroll', expect.anything());
|
||||
expect(emblaApi.on).toBeCalledWith('settle', expect.anything());
|
||||
expect(emblaApi.on).toBeCalledWith('destroy', expect.anything());
|
||||
});
|
||||
|
||||
it('should destroy', () => {
|
||||
const emblaApi = createEmblaApiInstance();
|
||||
const controller = new EmblaReInitController(emblaApi);
|
||||
|
||||
controller.destroy();
|
||||
|
||||
expect(emblaApi.off).toBeCalledWith('scroll', expect.anything());
|
||||
expect(emblaApi.off).toBeCalledWith('settle', expect.anything());
|
||||
expect(emblaApi.off).toBeCalledWith('destroy', expect.anything());
|
||||
});
|
||||
|
||||
it('should reinit when not scrolling', () => {
|
||||
const emblaApi = createEmblaApiInstance();
|
||||
const controller = new EmblaReInitController(emblaApi);
|
||||
|
||||
controller.reinit();
|
||||
|
||||
expect(emblaApi.reInit).toBeCalled();
|
||||
});
|
||||
|
||||
it('should carefully reinit when scrolling', () => {
|
||||
const emblaApi = createEmblaApiInstance();
|
||||
const controller = new EmblaReInitController(emblaApi);
|
||||
|
||||
callEmblaHandler(emblaApi, 'scroll');
|
||||
|
||||
controller.reinit();
|
||||
expect(emblaApi.reInit).not.toBeCalled();
|
||||
|
||||
callEmblaHandler(emblaApi, 'settle');
|
||||
expect(emblaApi.reInit).toBeCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
import { EmblaCarouselType, EmblaEventType } from 'embla-carousel';
|
||||
import { EngineType } from 'embla-carousel/components/Engine';
|
||||
import { LooseOptionsType } from 'embla-carousel/components/Options';
|
||||
import { OptionsHandlerType } from 'embla-carousel/components/OptionsHandler';
|
||||
import merge from 'lodash-es/merge';
|
||||
import { vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
export const createTestEmblaOptionHandler = (): OptionsHandlerType => ({
|
||||
mergeOptions: <TypeA extends LooseOptionsType, TypeB extends LooseOptionsType>(
|
||||
optionsA: TypeA,
|
||||
optionsB?: TypeB,
|
||||
): TypeA => {
|
||||
return merge({}, optionsA, optionsB);
|
||||
},
|
||||
optionsAtMedia: <Type extends LooseOptionsType>(options: Type): Type => {
|
||||
return options;
|
||||
},
|
||||
optionsMediaQueries: (_optionsList: LooseOptionsType[]): MediaQueryList[] => [],
|
||||
});
|
||||
|
||||
export const callEmblaHandler = (
|
||||
emblaApi: EmblaCarouselType | null,
|
||||
eventName: EmblaEventType,
|
||||
): void => {
|
||||
if (!emblaApi) {
|
||||
return;
|
||||
}
|
||||
const mock = vi.mocked(emblaApi.on).mock;
|
||||
for (const [evt, cb] of mock.calls) {
|
||||
if (evt === eventName) {
|
||||
cb(emblaApi, evt);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const callVisibilityHandler = (): void => {
|
||||
const mock = vi.mocked(global.document.addEventListener).mock;
|
||||
for (const [evt, cb] of mock.calls) {
|
||||
if (evt === 'visibilitychange' && typeof cb === 'function') {
|
||||
cb(new Event('foo'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const callIntersectionHandler = (intersecting = true, n = 0): void => {
|
||||
const mockResult = vi.mocked(IntersectionObserver).mock.results[n];
|
||||
if (mockResult.type !== 'return') {
|
||||
return;
|
||||
}
|
||||
const observer = mockResult.value;
|
||||
vi.mocked(IntersectionObserver).mock.calls[n][0](
|
||||
// Note this is a very incomplete / invalid IntersectionObserverEntry that
|
||||
// just provides the bare basics current implementation uses.
|
||||
intersecting ? [{ isIntersecting: true } as IntersectionObserverEntry] : [],
|
||||
observer,
|
||||
);
|
||||
};
|
||||
|
||||
export const callMutationHandler = (n = 0): void => {
|
||||
const mockResult = vi.mocked(MutationObserver).mock.results[n];
|
||||
if (mockResult.type !== 'return') {
|
||||
return;
|
||||
}
|
||||
const observer = mockResult.value;
|
||||
vi.mocked(MutationObserver).mock.calls[n][0](
|
||||
// Note this is a very incomplete / invalid IntersectionObserverEntry that
|
||||
// just provides the bare basics current implementation uses.
|
||||
[],
|
||||
observer,
|
||||
);
|
||||
};
|
||||
|
||||
export const callResizeHandler = (
|
||||
entries: {
|
||||
target: HTMLElement;
|
||||
width: number;
|
||||
height: number;
|
||||
}[],
|
||||
n = 0,
|
||||
): void => {
|
||||
const mockResult = vi.mocked(ResizeObserver).mock.results[n];
|
||||
if (mockResult.type !== 'return') {
|
||||
return;
|
||||
}
|
||||
const observer = mockResult.value;
|
||||
vi.mocked(ResizeObserver).mock.calls[n][0](
|
||||
// Note this is a very incomplete / invalid ResizeObserverEntry that
|
||||
// just provides the bare basics current implementation uses.
|
||||
entries.map(
|
||||
(entry) =>
|
||||
({
|
||||
target: entry.target,
|
||||
contentRect: {
|
||||
height: entry.height,
|
||||
width: entry.width,
|
||||
},
|
||||
} as unknown as ResizeObserverEntry),
|
||||
),
|
||||
observer,
|
||||
);
|
||||
};
|
||||
|
||||
export const createEmblaApiInstance = (options?: {
|
||||
slideNodes?: HTMLElement[];
|
||||
selectedScrollSnap?: number;
|
||||
previousScrollSnap?: number;
|
||||
containerNode?: HTMLElement;
|
||||
axis?: 'x' | 'y';
|
||||
slideRegistry?: number[][];
|
||||
}): EmblaCarouselType => {
|
||||
const emblaApi = mock<EmblaCarouselType>();
|
||||
emblaApi.slideNodes.mockReturnValue(options?.slideNodes ?? createTestSlideNodes());
|
||||
emblaApi.selectedScrollSnap.mockReturnValue(options?.selectedScrollSnap ?? 0);
|
||||
emblaApi.previousScrollSnap.mockReturnValue(options?.previousScrollSnap ?? 0);
|
||||
emblaApi.containerNode.mockReturnValue(
|
||||
options?.containerNode ?? document.createElement('div'),
|
||||
);
|
||||
emblaApi.internalEngine.mockReturnValue({
|
||||
options: { axis: options?.axis ?? 'x' },
|
||||
...(options?.slideRegistry && { slideRegistry: options.slideRegistry }),
|
||||
} as EngineType);
|
||||
return emblaApi;
|
||||
};
|
||||
|
||||
export const createTestSlideNodes = (options?: {
|
||||
n?: number;
|
||||
}): HTMLElement[] => {
|
||||
return [...Array(options?.n ?? 10).keys()].map((_) =>
|
||||
document.createElement('div'),
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user