refactor: Refactor media loading manager for improved robustness (#2464)
This commit is contained in:
committed by
dermotduffy
parent
47bcce93d3
commit
bc366626f1
@@ -1,61 +1,362 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { MediaLoadedInfoManager } from '../../src/card-controller/media-info-manager';
|
||||
import { createCardAPI, createMediaLoadedInfo } from '../test-utils.js';
|
||||
import {
|
||||
createCardAPI,
|
||||
createMediaLoadedInfo,
|
||||
createMediaLoadedInfoEvent,
|
||||
} from '../test-utils.js';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('MediaLoadedInfoManager', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should initialize', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new MediaLoadedInfoManager(api);
|
||||
|
||||
manager.initialize();
|
||||
|
||||
expect(api.getConditionStateManager().setState).toBeCalledWith({
|
||||
mediaLoadedInfo: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should set', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new MediaLoadedInfoManager(api);
|
||||
const mediaInfo = createMediaLoadedInfo();
|
||||
describe('set', () => {
|
||||
it('should surface info and fire condition state for the selected target', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new MediaLoadedInfoManager(api);
|
||||
const owner = document.createElement('div');
|
||||
const info = createMediaLoadedInfo({ targetID: 'target-1' });
|
||||
|
||||
manager.set(mediaInfo);
|
||||
manager.setSelected('target-1');
|
||||
vi.clearAllMocks();
|
||||
manager.set(info, owner);
|
||||
|
||||
expect(manager.has()).toBeTruthy();
|
||||
expect(manager.get()).toBe(mediaInfo);
|
||||
expect(api.getConditionStateManager().setState).toBeCalledWith(
|
||||
expect.objectContaining({ mediaLoadedInfo: mediaInfo }),
|
||||
);
|
||||
expect(api.getStyleManager().setExpandedMode).toBeCalled();
|
||||
expect(api.getCardElementManager().update).toBeCalled();
|
||||
expect(manager.has()).toBeTruthy();
|
||||
expect(manager.get()).toBe(info);
|
||||
expect(api.getConditionStateManager().setState).toBeCalledWith({
|
||||
mediaLoadedInfo: info,
|
||||
});
|
||||
expect(api.getStyleManager().setExpandedMode).toBeCalled();
|
||||
expect(api.getCardElementManager().update).toBeCalled();
|
||||
});
|
||||
|
||||
it('should cache info for non-selected targets without side effects', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new MediaLoadedInfoManager(api);
|
||||
const owner = document.createElement('div');
|
||||
const info = createMediaLoadedInfo({ targetID: 'target-1' });
|
||||
|
||||
manager.set(info, owner);
|
||||
|
||||
expect(manager.has()).toBeFalsy();
|
||||
expect(manager.get()).toBeNull();
|
||||
expect(api.getConditionStateManager().setState).not.toBeCalled();
|
||||
expect(api.getStyleManager().setExpandedMode).not.toBeCalled();
|
||||
expect(api.getCardElementManager().update).not.toBeCalled();
|
||||
|
||||
manager.setSelected('target-1');
|
||||
|
||||
expect(manager.has()).toBeTruthy();
|
||||
expect(manager.get()).toBe(info);
|
||||
expect(api.getConditionStateManager().setState).toBeCalledWith({
|
||||
mediaLoadedInfo: info,
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject info missing dimensions', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new MediaLoadedInfoManager(api);
|
||||
const owner = document.createElement('div');
|
||||
const info = createMediaLoadedInfo({
|
||||
width: 0,
|
||||
height: 0,
|
||||
targetID: 'target-1',
|
||||
});
|
||||
|
||||
manager.setSelected('target-1');
|
||||
vi.clearAllMocks();
|
||||
manager.set(info, owner);
|
||||
|
||||
expect(manager.has()).toBeFalsy();
|
||||
expect(manager.get()).toBeNull();
|
||||
expect(api.getConditionStateManager().setState).not.toBeCalled();
|
||||
expect(api.getStyleManager().setExpandedMode).not.toBeCalled();
|
||||
expect(api.getCardElementManager().update).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should reject info without a targetID', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new MediaLoadedInfoManager(api);
|
||||
const owner = document.createElement('div');
|
||||
const info = createMediaLoadedInfo({ targetID: undefined });
|
||||
|
||||
manager.setSelected('target-1');
|
||||
manager.set(info, owner);
|
||||
|
||||
expect(manager.has()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not set invalid media info', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new MediaLoadedInfoManager(api);
|
||||
const mediaLoadedInfo = createMediaLoadedInfo({ width: 0, height: 0 });
|
||||
describe('setSelected', () => {
|
||||
it('should switch between targets', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new MediaLoadedInfoManager(api);
|
||||
const owner1 = document.createElement('div');
|
||||
const owner2 = document.createElement('div');
|
||||
const info1 = createMediaLoadedInfo({ targetID: 'target-1' });
|
||||
const info2 = createMediaLoadedInfo({ targetID: 'target-2' });
|
||||
|
||||
manager.set(mediaLoadedInfo);
|
||||
manager.set(info1, owner1);
|
||||
manager.set(info2, owner2);
|
||||
|
||||
expect(manager.has()).toBeFalsy();
|
||||
expect(manager.get()).toBeNull();
|
||||
expect(api.getConditionStateManager().setState).not.toBeCalled();
|
||||
manager.setSelected('target-1');
|
||||
expect(manager.get()).toBe(info1);
|
||||
|
||||
manager.setSelected('target-2');
|
||||
expect(manager.get()).toBe(info2);
|
||||
|
||||
manager.setSelected(null);
|
||||
expect(manager.get()).toBeNull();
|
||||
});
|
||||
|
||||
it('should be a no-op when re-selecting the same target', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new MediaLoadedInfoManager(api);
|
||||
|
||||
manager.setSelected('target-1');
|
||||
vi.clearAllMocks();
|
||||
manager.setSelected('target-1');
|
||||
|
||||
expect(api.getConditionStateManager().setState).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should emit null condition state when selecting a target with no info', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new MediaLoadedInfoManager(api);
|
||||
|
||||
manager.setSelected('target-1');
|
||||
|
||||
expect(api.getConditionStateManager().setState).toBeCalledWith({
|
||||
mediaLoadedInfo: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should get last known', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new MediaLoadedInfoManager(api);
|
||||
const mediaLoadedInfo = createMediaLoadedInfo();
|
||||
describe('getLastKnown', () => {
|
||||
it('should return the last known info for the selected target', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new MediaLoadedInfoManager(api);
|
||||
const owner = document.createElement('div');
|
||||
const info = createMediaLoadedInfo({ targetID: 'target-1' });
|
||||
|
||||
manager.set(mediaLoadedInfo);
|
||||
manager.setSelected('target-1');
|
||||
manager.set(info, owner);
|
||||
|
||||
expect(manager.has()).toBeTruthy();
|
||||
expect(manager.getLastKnown()).toBe(info);
|
||||
});
|
||||
|
||||
manager.clear();
|
||||
it('should return null when nothing is selected', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new MediaLoadedInfoManager(api);
|
||||
|
||||
expect(manager.has()).toBeFalsy();
|
||||
expect(manager.getLastKnown()).toBe(mediaLoadedInfo);
|
||||
expect(api.getConditionStateManager().setState).toBeCalledWith(
|
||||
expect.objectContaining({ mediaLoadedInfo }),
|
||||
);
|
||||
expect(manager.getLastKnown()).toBeNull();
|
||||
});
|
||||
|
||||
it('should preserve last known across clear', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new MediaLoadedInfoManager(api);
|
||||
const owner = document.createElement('div');
|
||||
const info = createMediaLoadedInfo({ targetID: 'target-1' });
|
||||
|
||||
manager.setSelected('target-1');
|
||||
manager.set(info, owner);
|
||||
manager.clear();
|
||||
|
||||
expect(manager.has()).toBeFalsy();
|
||||
expect(manager.getLastKnown()).toBe(info);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleLoadEvent', () => {
|
||||
it('should register info from a valid event', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new MediaLoadedInfoManager(api);
|
||||
const source = document.createElement('div');
|
||||
const ac = new AbortController();
|
||||
const info = createMediaLoadedInfo({ targetID: 'target-1' });
|
||||
|
||||
manager.setSelected('target-1');
|
||||
vi.clearAllMocks();
|
||||
manager.handleLoadEvent(
|
||||
createMediaLoadedInfoEvent({ source, info, signal: ac.signal }),
|
||||
);
|
||||
|
||||
expect(manager.get()).toBe(info);
|
||||
expect(api.getConditionStateManager().setState).toBeCalledWith({
|
||||
mediaLoadedInfo: info,
|
||||
});
|
||||
});
|
||||
|
||||
it('should ignore events whose composedPath()[0] is not an HTMLElement', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new MediaLoadedInfoManager(api);
|
||||
const ac = new AbortController();
|
||||
const info = createMediaLoadedInfo({ targetID: 'target-1' });
|
||||
const ev = createMediaLoadedInfoEvent({ info, signal: ac.signal });
|
||||
// Force the path to look like a non-HTMLElement (e.g. a window).
|
||||
Object.defineProperty(ev, 'composedPath', { value: () => [window] });
|
||||
|
||||
manager.setSelected('target-1');
|
||||
manager.handleLoadEvent(ev);
|
||||
|
||||
expect(manager.has()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should ignore events whose info has no targetID', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new MediaLoadedInfoManager(api);
|
||||
const source = document.createElement('div');
|
||||
const ac = new AbortController();
|
||||
const info = createMediaLoadedInfo({ targetID: undefined });
|
||||
|
||||
manager.setSelected('target-1');
|
||||
manager.handleLoadEvent(
|
||||
createMediaLoadedInfoEvent({ source, info, signal: ac.signal }),
|
||||
);
|
||||
|
||||
expect(manager.has()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should clear an entry on signal abort', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new MediaLoadedInfoManager(api);
|
||||
const source = document.createElement('div');
|
||||
const ac = new AbortController();
|
||||
const info = createMediaLoadedInfo({ targetID: 'target-1' });
|
||||
|
||||
manager.setSelected('target-1');
|
||||
manager.handleLoadEvent(
|
||||
createMediaLoadedInfoEvent({ source, info, signal: ac.signal }),
|
||||
);
|
||||
expect(manager.has()).toBeTruthy();
|
||||
|
||||
ac.abort();
|
||||
|
||||
expect(manager.has()).toBeFalsy();
|
||||
expect(api.getConditionStateManager().setState).toHaveBeenLastCalledWith({
|
||||
mediaLoadedInfo: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not clear an entry whose owner has been replaced when an older signal aborts', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new MediaLoadedInfoManager(api);
|
||||
|
||||
const owner1 = document.createElement('div');
|
||||
const owner2 = document.createElement('div');
|
||||
const ac1 = new AbortController();
|
||||
const ac2 = new AbortController();
|
||||
const info1 = createMediaLoadedInfo({ targetID: 'target-1' });
|
||||
const info2 = createMediaLoadedInfo({ targetID: 'target-1' });
|
||||
|
||||
manager.setSelected('target-1');
|
||||
manager.handleLoadEvent(
|
||||
createMediaLoadedInfoEvent({ source: owner1, info: info1, signal: ac1.signal }),
|
||||
);
|
||||
manager.handleLoadEvent(
|
||||
createMediaLoadedInfoEvent({ source: owner2, info: info2, signal: ac2.signal }),
|
||||
);
|
||||
|
||||
expect(manager.get()).toBe(info2);
|
||||
|
||||
// The first owner's signal aborts, but it shouldn't blow away the entry
|
||||
// owner2 has since taken over.
|
||||
ac1.abort();
|
||||
|
||||
expect(manager.get()).toBe(info2);
|
||||
});
|
||||
|
||||
it('should not fire condition state when clearing a non-selected target', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new MediaLoadedInfoManager(api);
|
||||
const source = document.createElement('div');
|
||||
const ac = new AbortController();
|
||||
const info = createMediaLoadedInfo({ targetID: 'target-1' });
|
||||
|
||||
// Select a different target than the one being cleared.
|
||||
manager.setSelected('target-other');
|
||||
manager.handleLoadEvent(
|
||||
createMediaLoadedInfoEvent({ source, info, signal: ac.signal }),
|
||||
);
|
||||
vi.clearAllMocks();
|
||||
|
||||
ac.abort();
|
||||
|
||||
expect(api.getConditionStateManager().setState).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('clear', () => {
|
||||
it('should drop active entries and fire condition state when selected target had info', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new MediaLoadedInfoManager(api);
|
||||
const owner = document.createElement('div');
|
||||
const info = createMediaLoadedInfo({ targetID: 'target-1' });
|
||||
|
||||
manager.setSelected('target-1');
|
||||
manager.set(info, owner);
|
||||
vi.clearAllMocks();
|
||||
|
||||
manager.clear();
|
||||
|
||||
expect(manager.has()).toBeFalsy();
|
||||
expect(api.getConditionStateManager().setState).toBeCalledWith({
|
||||
mediaLoadedInfo: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should be a no-op on condition state when nothing is selected', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new MediaLoadedInfoManager(api);
|
||||
|
||||
manager.clear();
|
||||
|
||||
expect(api.getConditionStateManager().setState).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should not fire condition state when the selected target has no info', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new MediaLoadedInfoManager(api);
|
||||
|
||||
manager.setSelected('target-1');
|
||||
vi.clearAllMocks();
|
||||
manager.clear();
|
||||
|
||||
expect(api.getConditionStateManager().setState).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('initialize', () => {
|
||||
it('should clear active state, last-known and selected', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new MediaLoadedInfoManager(api);
|
||||
const owner = document.createElement('div');
|
||||
const info = createMediaLoadedInfo({ targetID: 'target-1' });
|
||||
|
||||
manager.setSelected('target-1');
|
||||
manager.set(info, owner);
|
||||
|
||||
manager.initialize();
|
||||
|
||||
expect(manager.get()).toBeNull();
|
||||
expect(manager.getLastKnown()).toBeNull();
|
||||
|
||||
// Re-selecting `target-1` after initialize should produce no last-known.
|
||||
manager.setSelected('target-1');
|
||||
expect(manager.getLastKnown()).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user