refactor: Refactor media loading manager for improved robustness (#2464)
This commit is contained in:
committed by
dermotduffy
parent
47bcce93d3
commit
bc366626f1
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { onAbort } from '../../src/utils/abort-signal';
|
||||
|
||||
describe('onAbort', () => {
|
||||
it('should call the callback when the signal aborts', () => {
|
||||
const ac = new AbortController();
|
||||
const cb = vi.fn();
|
||||
onAbort(ac.signal, cb);
|
||||
|
||||
expect(cb).not.toBeCalled();
|
||||
|
||||
ac.abort();
|
||||
expect(cb).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call the callback synchronously if the signal is already aborted', () => {
|
||||
const ac = new AbortController();
|
||||
ac.abort();
|
||||
|
||||
const cb = vi.fn();
|
||||
onAbort(ac.signal, cb);
|
||||
|
||||
expect(cb).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should fire only once even if the signal aborts repeatedly', () => {
|
||||
const ac = new AbortController();
|
||||
const cb = vi.fn();
|
||||
onAbort(ac.signal, cb);
|
||||
|
||||
ac.abort();
|
||||
// Aborting an AbortController again is a no-op, but verify the listener
|
||||
// is registered with `once: true` so any synthetic re-fire would be a
|
||||
// no-op too.
|
||||
ac.signal.dispatchEvent(new Event('abort'));
|
||||
|
||||
expect(cb).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
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,
|
||||
callMutationHandler,
|
||||
@@ -111,41 +110,23 @@ describe('CarouselController', () => {
|
||||
const children = createTestSlideNodes();
|
||||
const parent = createParent({ children: children });
|
||||
|
||||
const forceSelectListener = vi.fn();
|
||||
parent.addEventListener(
|
||||
'advanced-camera-card:carousel:force-select',
|
||||
forceSelectListener,
|
||||
);
|
||||
|
||||
const carousel = new CarouselController(createRoot(), parent);
|
||||
|
||||
carousel.selectSlide(4);
|
||||
|
||||
expect(getEmblaApi()?.scrollTo).toBeCalledWith(4, false);
|
||||
expect(forceSelectListener).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: { index: 4, element: children[4] },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not select non-existent slide', () => {
|
||||
const children = createTestSlideNodes({ n: 10 });
|
||||
const parent = createParent({ children: children });
|
||||
|
||||
const forceSelectListener = vi.fn();
|
||||
parent.addEventListener(
|
||||
'advanced-camera-card:carousel:force-select',
|
||||
forceSelectListener,
|
||||
);
|
||||
|
||||
const carousel = new CarouselController(createRoot(), parent);
|
||||
|
||||
carousel.selectSlide(11);
|
||||
|
||||
// Should not call scrollTo or fire event because index is out of bounds
|
||||
// Should not call scrollTo because index is out of bounds.
|
||||
expect(getEmblaApi()?.scrollTo).not.toBeCalled();
|
||||
expect(forceSelectListener).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should dispatch select event', () => {
|
||||
@@ -191,7 +172,6 @@ describe('CarouselController', () => {
|
||||
const children = createTestSlideNodes({ n: 1 });
|
||||
const root = createRoot();
|
||||
const parent = createParent({ children: children });
|
||||
const plugins = [AutoMediaLoadedInfo()];
|
||||
|
||||
new CarouselController(root, parent, {
|
||||
direction: 'vertical',
|
||||
@@ -200,7 +180,6 @@ describe('CarouselController', () => {
|
||||
dragFree: true,
|
||||
loop: true,
|
||||
dragEnabled: false,
|
||||
plugins: plugins,
|
||||
textDirection: 'rtl',
|
||||
});
|
||||
|
||||
@@ -219,7 +198,7 @@ describe('CarouselController', () => {
|
||||
watchDrag: false,
|
||||
direction: 'rtl',
|
||||
},
|
||||
plugins,
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
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 {
|
||||
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());
|
||||
});
|
||||
|
||||
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('advanced-camera-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('advanced-camera-card:media:loaded', mediaLoadedHandler);
|
||||
dispatchExistingMediaLoadedInfoAsEvent(children[5], createMediaLoadedInfo());
|
||||
|
||||
vi.mocked(emblaApi.selectedScrollSnap).mockReturnValue(4);
|
||||
emblaApi
|
||||
.containerNode()
|
||||
.dispatchEvent(new Event('advanced-camera-card:carousel:force-select'));
|
||||
expect(mediaLoadedHandler).not.toBeCalled();
|
||||
|
||||
vi.mocked(emblaApi.selectedScrollSnap).mockReturnValue(5);
|
||||
emblaApi
|
||||
.containerNode()
|
||||
.dispatchEvent(new Event('advanced-camera-card:carousel:force-select'));
|
||||
expect(mediaLoadedHandler).toBeCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,25 +1,8 @@
|
||||
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';
|
||||
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;
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
optionsMediaQueries: (_optionsList: LooseOptionsType[]): MediaQueryList[] => [],
|
||||
});
|
||||
|
||||
export const callEmblaHandler = (
|
||||
emblaApi: EmblaCarouselType | null,
|
||||
eventName: EmblaEventType,
|
||||
|
||||
@@ -3,11 +3,8 @@ import { mock } from 'vitest-mock-extended';
|
||||
import { MediaLoadedCapabilities, MediaPlayer } from '../../src/types';
|
||||
import {
|
||||
createMediaLoadedInfo,
|
||||
dispatchExistingMediaLoadedInfoAsEvent,
|
||||
dispatchMediaLoadedEvent,
|
||||
dispatchMediaPauseEvent,
|
||||
dispatchMediaPlayEvent,
|
||||
dispatchMediaUnloadedEvent,
|
||||
dispatchMediaVolumeChangeEvent,
|
||||
isValidMediaLoadedInfo,
|
||||
} from '../../src/utils/media-info';
|
||||
@@ -83,74 +80,6 @@ describe('createMediaLoadedInfo', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('dispatchMediaLoadedEvent', () => {
|
||||
const options = {
|
||||
player: mock<MediaPlayer>(),
|
||||
capabilities: mock<MediaLoadedCapabilities>(),
|
||||
};
|
||||
|
||||
it('should dispatch', () => {
|
||||
const handler = vi.fn();
|
||||
const div = document.createElement('div');
|
||||
div.addEventListener('advanced-camera-card:media:loaded', handler);
|
||||
|
||||
// Need to write readonly properties.
|
||||
const img = document.createElement('img');
|
||||
Object.defineProperty(img, 'naturalWidth', { value: 10 });
|
||||
Object.defineProperty(img, 'naturalHeight', { value: 20 });
|
||||
|
||||
dispatchMediaLoadedEvent(div, img, options);
|
||||
expect(handler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: {
|
||||
width: 10,
|
||||
height: 20,
|
||||
...options,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not dispatch', () => {
|
||||
const handler = vi.fn();
|
||||
const div = document.createElement('div');
|
||||
div.addEventListener('advanced-camera-card:media:loaded', handler);
|
||||
|
||||
dispatchMediaLoadedEvent(div, div, options);
|
||||
expect(handler).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('dispatchExistingMediaLoadedInfoAsEvent', () => {
|
||||
it('should dispatch', () => {
|
||||
const handler = vi.fn();
|
||||
const div = document.createElement('div');
|
||||
div.addEventListener('advanced-camera-card:media:loaded', handler);
|
||||
const info = createTestMediaLoadedInfo();
|
||||
|
||||
dispatchExistingMediaLoadedInfoAsEvent(div, info);
|
||||
expect(handler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: info,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('dispatchMediaUnloadedEvent', () => {
|
||||
it('should dispatch', () => {
|
||||
const handler = vi.fn();
|
||||
const div = document.createElement('div');
|
||||
div.addEventListener('advanced-camera-card:media:unloaded', handler);
|
||||
|
||||
dispatchMediaUnloadedEvent(div);
|
||||
expect(handler).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('dispatchMediaVolumeChangeEvent', () => {
|
||||
it('should dispatch', () => {
|
||||
|
||||
Reference in New Issue
Block a user