880 lines
28 KiB
TypeScript
880 lines
28 KiB
TypeScript
import Masonry from 'masonry-layout';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { mock } from 'vitest-mock-extended';
|
|
|
|
import {
|
|
MediaGridController,
|
|
type ExtendedMasonry,
|
|
type MediaGridConstructorOptions,
|
|
} from '../../src/components-lib/media-grid-controller';
|
|
import {
|
|
createSlot,
|
|
createSlotHost,
|
|
MutationObserverMock,
|
|
ResizeObserverMock,
|
|
} from '../test-utils';
|
|
|
|
vi.mock('lodash-es', async () => ({
|
|
...(await vi.importActual('lodash-es')),
|
|
throttle: vi.fn((fn) => {
|
|
const throttled = vi.fn(fn) as unknown as { cancel: () => void };
|
|
throttled.cancel = vi.fn();
|
|
return throttled;
|
|
}),
|
|
}));
|
|
|
|
const masonry = mock<ExtendedMasonry>();
|
|
vi.mock('masonry-layout', () => ({
|
|
default: vi.fn().mockImplementation(() => {
|
|
return masonry;
|
|
}),
|
|
}));
|
|
|
|
const createChildren = (childIDs?: string[], idAttribute?: string): HTMLElement[] => {
|
|
const children: HTMLElement[] = [];
|
|
for (let i = 0; i < (childIDs?.length ?? 3); ++i) {
|
|
const child = document.createElement('div');
|
|
if (childIDs) {
|
|
child.setAttribute(idAttribute ?? 'grid-id', childIDs[i]);
|
|
}
|
|
children.push(child);
|
|
}
|
|
return children;
|
|
};
|
|
|
|
const setElementWidth = (element: HTMLElement, width: number): void => {
|
|
element.getBoundingClientRect = vi.fn().mockReturnValue({
|
|
width: width,
|
|
});
|
|
};
|
|
|
|
const createParent = (options?: {
|
|
children?: HTMLElement[];
|
|
width?: number;
|
|
}): HTMLElement => {
|
|
const host = document.createElement('div');
|
|
if (options?.children) {
|
|
host.append(...options.children);
|
|
}
|
|
// Default Lovelace card width is 492.
|
|
setElementWidth(host, options?.width ?? 492);
|
|
return host;
|
|
};
|
|
|
|
const createController = (host: HTMLElement, options?: MediaGridConstructorOptions) => {
|
|
return new MediaGridController(host, options);
|
|
};
|
|
|
|
const triggerMutationObserver = (
|
|
hostOrCell: 'cell' | 'host',
|
|
attributeName?: string,
|
|
): void => {
|
|
const mutationObserverTrigger = vi.mocked(global.MutationObserver).mock.calls[
|
|
hostOrCell === 'host' ? 0 : 1
|
|
][0];
|
|
mutationObserverTrigger(
|
|
attributeName ? [mock<MutationRecord>({ attributeName: attributeName })] : [],
|
|
mock<MutationObserver>(),
|
|
);
|
|
};
|
|
|
|
const triggerResizeObserver = (hostOrCell: 'cell' | 'host'): void => {
|
|
const resizeObserverTrigger = vi.mocked(global.ResizeObserver).mock.calls[
|
|
hostOrCell === 'host' ? 0 : 1
|
|
][0];
|
|
resizeObserverTrigger([], mock<ResizeObserver>());
|
|
};
|
|
|
|
// @vitest-environment jsdom
|
|
describe('MediaGridController', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.stubGlobal('MutationObserver', MutationObserverMock);
|
|
vi.stubGlobal('ResizeObserver', ResizeObserverMock);
|
|
|
|
masonry.items = [];
|
|
});
|
|
|
|
it('should be constructable', () => {
|
|
const controller = createController(createParent());
|
|
expect(controller).toBeTruthy();
|
|
expect(masonry.layout).toBeCalled();
|
|
});
|
|
|
|
it('should set grid contents correctly from regular elements', () => {
|
|
const children = createChildren();
|
|
const parent = createParent({ children: children });
|
|
const controller = createController(parent);
|
|
expect(controller.getGridContents()).toEqual(
|
|
new Map([
|
|
['0', children[0]],
|
|
['1', children[1]],
|
|
['2', children[2]],
|
|
]),
|
|
);
|
|
expect(controller.getGridSize()).toBe(3);
|
|
expect(masonry.layout).toBeCalled();
|
|
});
|
|
|
|
it('should set grid contents correctly from slotted elements', () => {
|
|
const children = createChildren();
|
|
const slot = createSlot();
|
|
const host = createSlotHost({ slot: slot, children: children });
|
|
const controller = createController(host);
|
|
expect(controller.getGridContents()).toEqual(
|
|
new Map([
|
|
['0', children[0]],
|
|
['1', children[1]],
|
|
['2', children[2]],
|
|
]),
|
|
);
|
|
expect(controller.getGridSize()).toBe(3);
|
|
});
|
|
|
|
it('should select element', () => {
|
|
const children = createChildren();
|
|
const slot = createSlot();
|
|
createSlotHost({ slot: slot, children: children });
|
|
|
|
const controller = createController(slot);
|
|
|
|
// All children should be unselected.
|
|
expect(controller.getSelected()).toBeNull();
|
|
for (const child of children) {
|
|
expect(child.getAttribute('selected')).toBeNull();
|
|
expect(child.getAttribute('unselected')).toEqual('');
|
|
}
|
|
|
|
controller.selectCell('0');
|
|
expect(controller.getSelected()).toBe('0');
|
|
|
|
// 1st child should now be selected.
|
|
expect(children[0].getAttribute('selected')).toEqual('');
|
|
expect(children[0].getAttribute('unselected')).toBeNull();
|
|
|
|
// 2nd and 3rd should be unselected.
|
|
for (const child of children.slice(1)) {
|
|
expect(child.getAttribute('selected')).toBeNull();
|
|
expect(child.getAttribute('unselected')).toEqual('');
|
|
}
|
|
});
|
|
|
|
it('should re-select element', () => {
|
|
const children = createChildren();
|
|
const slot = createSlot();
|
|
createSlotHost({ slot: slot, children: children });
|
|
const controller = createController(slot);
|
|
|
|
// All children should be unselected.
|
|
expect(controller.getSelected()).toBeNull();
|
|
|
|
controller.selectCell('0');
|
|
expect(controller.getSelected()).toBe('0');
|
|
|
|
controller.selectCell('0');
|
|
expect(controller.getSelected()).toBe('0');
|
|
});
|
|
|
|
it('should unselect', () => {
|
|
const children = createChildren();
|
|
const slot = createSlot();
|
|
const host = createSlotHost({ slot: slot, children: children });
|
|
const controller = createController(host);
|
|
|
|
const unselectedHandler = vi.fn();
|
|
host.addEventListener(
|
|
'advanced-camera-card:media-grid:unselected',
|
|
unselectedHandler,
|
|
);
|
|
|
|
controller.selectCell('0');
|
|
expect(controller.getSelected()).toBe('0');
|
|
|
|
// Unselect all elements.
|
|
controller.unselectAll();
|
|
|
|
// Expect selected to now be null.
|
|
expect(controller.getSelected()).toBeNull();
|
|
|
|
// Expect styles to have been updated.
|
|
for (const child of children) {
|
|
expect(child.getAttribute('selected')).toBeNull();
|
|
expect(child.getAttribute('unselected')).toEqual('');
|
|
}
|
|
|
|
// The grid signals its own state change via media-grid:unselected.
|
|
expect(unselectedHandler).toBeCalledTimes(1);
|
|
|
|
// Unselecting a second time should do nothing.
|
|
controller.unselectAll();
|
|
|
|
expect(unselectedHandler).toBeCalledTimes(1);
|
|
});
|
|
|
|
it('should select in constructor', () => {
|
|
const children = createChildren();
|
|
const slot = createSlot();
|
|
const host = createSlotHost({ slot: slot, children: children });
|
|
const controller = createController(host, { selected: '2' });
|
|
|
|
expect(controller.getSelected()).toBe('2');
|
|
});
|
|
|
|
it('should respect grid attribute option', () => {
|
|
const children = createChildren(['one', 'two', 'three'], 'test-id');
|
|
const slot = createSlot();
|
|
const host = createSlotHost({ slot: slot, children: children });
|
|
const controller = createController(host, { idAttribute: 'test-id' });
|
|
expect(controller.getGridContents()).toEqual(
|
|
new Map([
|
|
['one', children[0]],
|
|
['two', children[1]],
|
|
['three', children[2]],
|
|
]),
|
|
);
|
|
});
|
|
|
|
it('should destroy with regular elements', () => {
|
|
const children = createChildren();
|
|
const parent = createParent({ children: children });
|
|
const controller = createController(parent);
|
|
expect(controller.getGridSize()).toBe(3);
|
|
controller.destroy();
|
|
expect(controller.getGridSize()).toBe(0);
|
|
});
|
|
|
|
it('should destroy with slotted elements', () => {
|
|
const children = createChildren();
|
|
const slot = createSlot();
|
|
createSlotHost({ slot: slot, children: children });
|
|
const controller = createController(slot);
|
|
|
|
expect(controller.getGridSize()).toBe(3);
|
|
controller.destroy();
|
|
expect(controller.getGridSize()).toBe(0);
|
|
});
|
|
|
|
it('should replace children when they change', () => {
|
|
const children = createChildren();
|
|
const parent = createParent({ children: children });
|
|
const controller = createController(parent, { selected: '1' });
|
|
|
|
expect(controller.getSelected()).toBe('1');
|
|
expect(controller.getGridSize()).toBe(3);
|
|
|
|
children.forEach((child) => parent.removeChild(child));
|
|
const newChildren = createChildren(['one', 'two', 'three']);
|
|
newChildren.forEach((child) => parent.appendChild(child));
|
|
|
|
triggerMutationObserver('host');
|
|
|
|
expect(controller.getGridContents()).toEqual(
|
|
new Map([
|
|
['one', newChildren[0]],
|
|
['two', newChildren[1]],
|
|
['three', newChildren[2]],
|
|
]),
|
|
);
|
|
expect(controller.getSelected()).toBeNull();
|
|
});
|
|
|
|
it('should re-calculate children when id attribute changes', () => {
|
|
const children = createChildren(['one', 'two', 'three'], 'test-id');
|
|
const parent = createParent({ children: children });
|
|
const controller = createController(parent, {
|
|
selected: 'one',
|
|
idAttribute: 'test-id',
|
|
});
|
|
|
|
expect(controller.getSelected()).toBe('one');
|
|
expect(controller.getGridSize()).toBe(3);
|
|
|
|
children[0].setAttribute('test-id', 'alpha');
|
|
children[1].setAttribute('test-id', 'beta');
|
|
children[2].setAttribute('test-id', 'gamma');
|
|
|
|
triggerMutationObserver('cell', 'test-id');
|
|
|
|
expect(controller.getGridContents()).toEqual(
|
|
new Map([
|
|
['alpha', children[0]],
|
|
['beta', children[1]],
|
|
['gamma', children[2]],
|
|
]),
|
|
);
|
|
expect(controller.getSelected()).toBeNull();
|
|
});
|
|
|
|
it('should replace children of a slot when they change', () => {
|
|
const children = createChildren();
|
|
const slot = createSlot();
|
|
const host = createSlotHost({ slot: slot, children: children });
|
|
|
|
const controller = createController(slot, { selected: '1' });
|
|
|
|
expect(controller.getSelected()).toBe('1');
|
|
expect(controller.getGridSize()).toBe(3);
|
|
|
|
children.forEach((child) => host.removeChild(child));
|
|
const newChildren = createChildren(['one', 'two', 'three']);
|
|
newChildren.forEach((child) => host.append(child));
|
|
|
|
slot.dispatchEvent(new Event('slotchange'));
|
|
|
|
expect(controller.getGridContents()).toEqual(
|
|
new Map([
|
|
['one', newChildren[0]],
|
|
['two', newChildren[1]],
|
|
['three', newChildren[2]],
|
|
]),
|
|
);
|
|
expect(controller.getSelected()).toBeNull();
|
|
});
|
|
|
|
it('should construct masonry correctly', () => {
|
|
const children = createChildren();
|
|
const parent = createParent({ children: children });
|
|
createController(parent);
|
|
expect(Masonry).toBeCalledWith(
|
|
parent,
|
|
expect.objectContaining({
|
|
initLayout: false,
|
|
percentPosition: true,
|
|
transitionDuration: 0,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should set default column size correctly', () => {
|
|
const parent = createParent({ children: createChildren() });
|
|
createController(parent);
|
|
expect(Masonry).toBeCalledWith(
|
|
parent,
|
|
expect.objectContaining({
|
|
columnWidth: 245,
|
|
}),
|
|
);
|
|
expect(
|
|
parent.style.getPropertyValue('--advanced-camera-card-grid-column-size'),
|
|
).toBe('245px');
|
|
});
|
|
|
|
it('should respect exact columns', () => {
|
|
const parent = createParent({ children: createChildren(), width: 3000 });
|
|
const controller = createController(parent);
|
|
controller.setDisplayConfig({ mode: 'grid', grid_columns: 2 });
|
|
|
|
// The cells are unchanged, so the new column width is applied to the
|
|
// existing Masonry instance rather than by constructing a new one.
|
|
expect(Masonry).toBeCalledTimes(1);
|
|
expect(masonry.option).toBeCalledWith(
|
|
expect.objectContaining({
|
|
columnWidth: 1499,
|
|
}),
|
|
);
|
|
expect(
|
|
parent.style.getPropertyValue('--advanced-camera-card-grid-column-size'),
|
|
).toBe('1499px');
|
|
});
|
|
|
|
it('should not rebuild the grid when the cells are unchanged', () => {
|
|
const slot = createSlot();
|
|
createSlotHost({ slot: slot, children: createChildren() });
|
|
createController(slot);
|
|
|
|
expect(Masonry).toBeCalledTimes(1);
|
|
expect(masonry.destroy).not.toBeCalled();
|
|
|
|
slot.dispatchEvent(new Event('slotchange'));
|
|
|
|
expect(Masonry).toBeCalledTimes(1);
|
|
expect(masonry.destroy).not.toBeCalled();
|
|
});
|
|
|
|
it('should rebuild the grid and lay it out when the cells change', () => {
|
|
const slot = createSlot();
|
|
const host = createSlotHost({ slot: slot, children: createChildren() });
|
|
createController(slot);
|
|
|
|
expect(Masonry).toBeCalledTimes(1);
|
|
|
|
host.replaceChildren(...createChildren());
|
|
slot.dispatchEvent(new Event('slotchange'));
|
|
|
|
expect(Masonry).toBeCalledTimes(2);
|
|
expect(masonry.destroy).toBeCalledTimes(1);
|
|
|
|
// A rebuild leaves the cells unpositioned, so the layout must not be left
|
|
// to the throttle.
|
|
expect(masonry.layout).toBeCalled();
|
|
});
|
|
|
|
it('should rebuild the grid when the number of cells changes', () => {
|
|
const slot = createSlot();
|
|
const host = createSlotHost({ slot: slot, children: createChildren() });
|
|
createController(slot);
|
|
|
|
expect(Masonry).toBeCalledTimes(1);
|
|
|
|
host.append(...createChildren(['new-cell']));
|
|
slot.dispatchEvent(new Event('slotchange'));
|
|
|
|
expect(Masonry).toBeCalledTimes(2);
|
|
expect(masonry.destroy).toBeCalledTimes(1);
|
|
});
|
|
|
|
it('should not use more columns than the items ask for', () => {
|
|
const parent = createParent({ children: createChildren(['0']), width: 3000 });
|
|
createController(parent);
|
|
|
|
// The lone item takes the whole grid. Sizing from the width alone would
|
|
// give it 1 of 5 columns, with the other 4 left empty.
|
|
expect(Masonry).toBeCalledWith(
|
|
parent,
|
|
expect.objectContaining({
|
|
columnWidth: 3000,
|
|
}),
|
|
);
|
|
expect(
|
|
parent.style.getPropertyValue('--advanced-camera-card-grid-column-size'),
|
|
).toBe('3000px');
|
|
});
|
|
|
|
it('should not use more columns than the items ask for on a narrow host', () => {
|
|
const parent = createParent({ children: createChildren(['0']) });
|
|
createController(parent);
|
|
|
|
// Sizing from the width alone would give the lone item half of a default
|
|
// width card.
|
|
expect(Masonry).toBeCalledWith(
|
|
parent,
|
|
expect.objectContaining({
|
|
columnWidth: 492,
|
|
}),
|
|
);
|
|
expect(
|
|
parent.style.getPropertyValue('--advanced-camera-card-grid-column-size'),
|
|
).toBe('492px');
|
|
});
|
|
|
|
it('should use a single column when there are no items', () => {
|
|
const parent = createParent({ width: 3000 });
|
|
createController(parent);
|
|
|
|
expect(Masonry).toBeCalledWith(
|
|
parent,
|
|
expect.objectContaining({
|
|
columnWidth: 3000,
|
|
}),
|
|
);
|
|
expect(
|
|
parent.style.getPropertyValue('--advanced-camera-card-grid-column-size'),
|
|
).toBe('3000px');
|
|
});
|
|
|
|
it('should respect exact columns even with fewer items', () => {
|
|
const parent = createParent({ children: createChildren(['0']), width: 3000 });
|
|
const controller = createController(parent);
|
|
controller.setDisplayConfig({ mode: 'grid', grid_columns: 4 });
|
|
|
|
expect(masonry.option).toBeCalledWith(
|
|
expect.objectContaining({
|
|
columnWidth: 749,
|
|
}),
|
|
);
|
|
expect(
|
|
parent.style.getPropertyValue('--advanced-camera-card-grid-column-size'),
|
|
).toBe('749px');
|
|
});
|
|
|
|
it('should size columns so a selected item and its siblings fit a single row', () => {
|
|
const parent = createParent({
|
|
children: createChildren(['0', '1', '2']),
|
|
width: 3000,
|
|
});
|
|
createController(parent, { selected: '1' });
|
|
|
|
// The items ask for 4 columns: 2 for the selection (the default
|
|
// `grid_selected_width_factor`) and 1 for each of its siblings.
|
|
expect(Masonry).toBeCalledWith(
|
|
parent,
|
|
expect.objectContaining({
|
|
columnWidth: 749,
|
|
}),
|
|
);
|
|
|
|
const selectedWidth = 2 * 749;
|
|
expect(selectedWidth + 749 + 749).toBeLessThanOrEqual(3000);
|
|
});
|
|
|
|
it('should give a lone selected item the whole grid', () => {
|
|
const parent = createParent({ children: createChildren(['0']), width: 3000 });
|
|
createController(parent, { selected: '0' });
|
|
|
|
// A selection is normally reserved extra columns, but a lone item cannot be
|
|
// wider than the grid and so cannot use them.
|
|
expect(Masonry).toBeCalledWith(
|
|
parent,
|
|
expect.objectContaining({
|
|
columnWidth: 3000,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should count a custom selected width factor towards the columns asked for', () => {
|
|
const parent = createParent({
|
|
children: createChildren(['0', '1', '2']),
|
|
width: 3000,
|
|
});
|
|
const controller = createController(parent, { selected: '1' });
|
|
controller.setDisplayConfig({ mode: 'grid', grid_selected_width_factor: 3 });
|
|
|
|
// 3 columns for the selection and 1 for each sibling exhausts the 5
|
|
// columns the width allows.
|
|
expect(masonry.option).toBeCalledWith(
|
|
expect.objectContaining({
|
|
columnWidth: 599,
|
|
}),
|
|
);
|
|
expect(
|
|
parent.style.getPropertyValue('--advanced-camera-card-grid-column-size'),
|
|
).toBe('599px');
|
|
});
|
|
|
|
it('should count item width factors towards the columns asked for', () => {
|
|
const children = createChildren(['0', '1', '2']);
|
|
children[0].setAttribute('grid-width-factor', '2');
|
|
const parent = createParent({ children: children, width: 4200 });
|
|
createController(parent);
|
|
|
|
// The items span 4 columns, and the widest needs 2 more when selected.
|
|
// Ignoring the width factor would give 4 columns of 1049px.
|
|
expect(Masonry).toBeCalledWith(
|
|
parent,
|
|
expect.objectContaining({
|
|
columnWidth: 699,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should give an item that is narrower than a column a column of its own', () => {
|
|
const children = createChildren(['0', '1', '2']);
|
|
for (const child of children) {
|
|
child.setAttribute('grid-width-factor', '0.5');
|
|
}
|
|
const parent = createParent({ children: children, width: 1800 });
|
|
createController(parent, { selected: '0' });
|
|
|
|
// Each item asks for one column: the selection fills exactly one at 0.5 x
|
|
// 2, and a half-width sibling still occupies a whole one.
|
|
expect(Masonry).toBeCalledWith(
|
|
parent,
|
|
expect.objectContaining({
|
|
columnWidth: 599,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should not resize columns when a selection is made or removed', () => {
|
|
const children = createChildren(['0', '1', '2']);
|
|
const parent = createParent({ children: children, width: 3000 });
|
|
const controller = createController(parent);
|
|
|
|
// Room for a selection is reserved whether or not there is one, so the
|
|
// three items ask for 4 columns either way.
|
|
expect(Masonry).toBeCalledWith(
|
|
parent,
|
|
expect.objectContaining({
|
|
columnWidth: 749,
|
|
}),
|
|
);
|
|
|
|
vi.mocked(masonry.option)?.mockClear();
|
|
controller.selectCell('1');
|
|
|
|
// Selecting an item would otherwise resize the items the user did not
|
|
// interact with.
|
|
expect(masonry.option).not.toBeCalled();
|
|
expect(
|
|
parent.style.getPropertyValue('--advanced-camera-card-grid-column-size'),
|
|
).toBe('749px');
|
|
|
|
controller.unselectAll();
|
|
|
|
expect(masonry.option).not.toBeCalled();
|
|
expect(
|
|
parent.style.getPropertyValue('--advanced-camera-card-grid-column-size'),
|
|
).toBe('749px');
|
|
});
|
|
|
|
it('should respect selected width factor', () => {
|
|
const parent = createParent({ children: createChildren(), width: 2000 });
|
|
const controller = createController(parent);
|
|
controller.setDisplayConfig({ mode: 'grid', grid_selected_width_factor: 3 });
|
|
expect(
|
|
parent.style.getPropertyValue('--advanced-camera-card-grid-selected-width-factor'),
|
|
).toBe('3');
|
|
|
|
// Setting the same config again should do nothing.
|
|
controller.setDisplayConfig({ mode: 'grid', grid_selected_width_factor: 3 });
|
|
expect(
|
|
parent.style.getPropertyValue('--advanced-camera-card-grid-selected-width-factor'),
|
|
).toBe('3');
|
|
});
|
|
|
|
it('should dispatch a selection request when interacted with', () => {
|
|
const children = createChildren();
|
|
const parent = createParent({ children: children, width: 2000 });
|
|
const controller = createController(parent);
|
|
|
|
expect(controller.getSelected()).toBeNull();
|
|
|
|
const clickHandler = vi.fn();
|
|
parent.addEventListener('click', clickHandler);
|
|
|
|
const selectedHandler = vi.fn();
|
|
parent.addEventListener('advanced-camera-card:media-grid:selected', selectedHandler);
|
|
|
|
children[1].click();
|
|
|
|
// Click is consumed; the controller dispatches the selection request but
|
|
// does NOT mutate local state. The authoritative selection is applied by
|
|
// the parent via `selectCell` once it propagates back.
|
|
expect(clickHandler).not.toBeCalled();
|
|
expect(selectedHandler).toBeCalledTimes(1);
|
|
expect(selectedHandler.mock.calls[0][0].detail).toEqual({ selected: '1' });
|
|
expect(controller.getSelected()).toBeNull();
|
|
});
|
|
|
|
it('should ignore interaction events on already selected cell', () => {
|
|
const children = createChildren();
|
|
const parent = createParent({ children: children, width: 2000 });
|
|
const controller = createController(parent);
|
|
controller.selectCell('1');
|
|
|
|
const clickHandler = vi.fn();
|
|
parent.addEventListener('click', clickHandler);
|
|
children[1].click();
|
|
|
|
// Click will be allowed through.
|
|
expect(clickHandler).toBeCalled();
|
|
expect(controller.getSelected()).toBe('1');
|
|
});
|
|
|
|
it('should re-layout when child size changes', () => {
|
|
createController(createParent({ children: createChildren() }));
|
|
|
|
vi.mocked(masonry.layout)?.mockClear();
|
|
triggerResizeObserver('cell');
|
|
expect(masonry.layout).toBeCalled();
|
|
});
|
|
|
|
it('should update masonry column width when host size changes', () => {
|
|
const children = createChildren();
|
|
const parent = createParent({ children: children });
|
|
createController(parent);
|
|
expect(Masonry).toBeCalledWith(
|
|
parent,
|
|
expect.objectContaining({
|
|
columnWidth: 245,
|
|
}),
|
|
);
|
|
expect(
|
|
parent.style.getPropertyValue('--advanced-camera-card-grid-column-size'),
|
|
).toBe('245px');
|
|
|
|
// Clear mock state.
|
|
vi.mocked(Masonry).mockClear();
|
|
vi.mocked(masonry.layout)?.mockClear();
|
|
vi.mocked(masonry.option)?.mockClear();
|
|
|
|
// Resize the host.
|
|
setElementWidth(parent, 3000);
|
|
triggerResizeObserver('host');
|
|
|
|
// Masonry should not be recreated, but column width should be updated
|
|
// via option() and layout should be called.
|
|
expect(Masonry).not.toBeCalled();
|
|
expect(masonry.option).toBeCalledWith({ columnWidth: 749 });
|
|
expect(
|
|
parent.style.getPropertyValue('--advanced-camera-card-grid-column-size'),
|
|
).toBe('749px');
|
|
expect(masonry.layout).toBeCalled();
|
|
|
|
// Clear mock state.
|
|
vi.mocked(Masonry).mockClear();
|
|
vi.mocked(masonry.layout)?.mockClear();
|
|
vi.mocked(masonry.option)?.mockClear();
|
|
|
|
// Trigger with the same sizes.
|
|
triggerResizeObserver('host');
|
|
expect(Masonry).not.toBeCalled();
|
|
expect(masonry.option).not.toBeCalled();
|
|
expect(masonry.layout).not.toBeCalled();
|
|
});
|
|
|
|
describe('describe should sort grid elements correctly', () => {
|
|
it('should respect placement when grid_selected_position is default', () => {
|
|
const children = createChildren();
|
|
const parent = createParent({ children: children });
|
|
|
|
// Simulate wrapped children in masonry object.
|
|
masonry.items = children.map((child) => ({ element: child }));
|
|
|
|
const controller = createController(parent);
|
|
controller.setDisplayConfig({ mode: 'grid', grid_selected_position: 'default' });
|
|
|
|
controller.selectCell('1');
|
|
|
|
expect(masonry.items).toEqual([
|
|
{ element: children[0] },
|
|
{ element: children[1] },
|
|
{ element: children[2] },
|
|
]);
|
|
});
|
|
|
|
it('should respect placement when grid_selected_position is first', () => {
|
|
const children = createChildren(['0', '1', '2']);
|
|
const parent = createParent({ children: children });
|
|
|
|
// Simulate wrapped children in masonry object.
|
|
masonry.items = children.map((child) => ({ element: child }));
|
|
|
|
const controller = createController(parent);
|
|
controller.setDisplayConfig({ mode: 'grid', grid_selected_position: 'first' });
|
|
|
|
controller.selectCell('1');
|
|
|
|
expect(masonry.items).toEqual([
|
|
{ element: children[1] },
|
|
{ element: children[0] },
|
|
{ element: children[2] },
|
|
]);
|
|
});
|
|
|
|
it('should respect placement when grid_selected_position is last', () => {
|
|
const children = createChildren(['0', '1', '2']);
|
|
const parent = createParent({ children: children });
|
|
|
|
// Simulate wrapped children in masonry object.
|
|
masonry.items = children.map((child) => ({ element: child }));
|
|
|
|
const controller = createController(parent);
|
|
controller.setDisplayConfig({ mode: 'grid', grid_selected_position: 'last' });
|
|
|
|
controller.selectCell('1');
|
|
|
|
expect(masonry.items).toEqual([
|
|
{ element: children[0] },
|
|
{ element: children[2] },
|
|
{ element: children[1] },
|
|
]);
|
|
});
|
|
|
|
it('should order from the grid contents rather than the previous sort', () => {
|
|
const children = createChildren(['0', '1', '2']);
|
|
const parent = createParent({ children: children });
|
|
|
|
// Simulate wrapped children in masonry object.
|
|
masonry.items = children.map((child) => ({ element: child }));
|
|
|
|
const controller = createController(parent);
|
|
controller.setDisplayConfig({ mode: 'grid', grid_selected_position: 'last' });
|
|
|
|
controller.selectCell('0');
|
|
expect(masonry.items).toEqual([
|
|
{ element: children[1] },
|
|
{ element: children[2] },
|
|
{ element: children[0] },
|
|
]);
|
|
|
|
// The unselected cells return to their configured order rather than
|
|
// keeping the order the previous sort left them in.
|
|
controller.selectCell('1');
|
|
expect(masonry.items).toEqual([
|
|
{ element: children[0] },
|
|
{ element: children[2] },
|
|
{ element: children[1] },
|
|
]);
|
|
|
|
// Returning to 'default' restores the configured order entirely.
|
|
controller.setDisplayConfig({ mode: 'grid', grid_selected_position: 'default' });
|
|
expect(masonry.items).toEqual([
|
|
{ element: children[0] },
|
|
{ element: children[1] },
|
|
{ element: children[2] },
|
|
]);
|
|
});
|
|
|
|
it('should not sort a destroyed grid', () => {
|
|
const children = createChildren(['0', '1', '2']);
|
|
const parent = createParent({ children: children });
|
|
const controller = createController(parent);
|
|
|
|
controller.destroy();
|
|
|
|
expect(() =>
|
|
controller.setDisplayConfig({ mode: 'grid', grid_selected_position: 'first' }),
|
|
).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('should set width factor styles correctly', () => {
|
|
it('should apply width factor CSS variable when attribute is present', () => {
|
|
const children = createChildren(['0', '1', '2']);
|
|
children[0].setAttribute('grid-width-factor', '2');
|
|
children[1].setAttribute('grid-width-factor', '3');
|
|
const parent = createParent({ children: children });
|
|
createController(parent);
|
|
|
|
expect(
|
|
children[0].style.getPropertyValue('--advanced-camera-card-grid-width-factor'),
|
|
).toBe('2');
|
|
expect(
|
|
children[1].style.getPropertyValue('--advanced-camera-card-grid-width-factor'),
|
|
).toBe('3');
|
|
expect(
|
|
children[2].style.getPropertyValue('--advanced-camera-card-grid-width-factor'),
|
|
).toBe('');
|
|
});
|
|
|
|
it('should update width factor styles when attribute changes', () => {
|
|
const children = createChildren(['0', '1', '2']);
|
|
const parent = createParent({ children: children });
|
|
createController(parent);
|
|
|
|
// Initially no width factor.
|
|
expect(
|
|
children[0].style.getPropertyValue('--advanced-camera-card-grid-width-factor'),
|
|
).toBe('');
|
|
|
|
// Set the attribute.
|
|
children[0].setAttribute('grid-width-factor', '4');
|
|
triggerMutationObserver('cell', 'grid-width-factor');
|
|
|
|
expect(
|
|
children[0].style.getPropertyValue('--advanced-camera-card-grid-width-factor'),
|
|
).toBe('4');
|
|
});
|
|
|
|
it('should remove width factor style when attribute is removed', () => {
|
|
const children = createChildren(['0', '1', '2']);
|
|
children[0].setAttribute('grid-width-factor', '2');
|
|
const parent = createParent({ children: children });
|
|
createController(parent);
|
|
|
|
expect(
|
|
children[0].style.getPropertyValue('--advanced-camera-card-grid-width-factor'),
|
|
).toBe('2');
|
|
|
|
// Remove the attribute.
|
|
children[0].removeAttribute('grid-width-factor');
|
|
triggerMutationObserver('cell', 'grid-width-factor');
|
|
|
|
expect(
|
|
children[0].style.getPropertyValue('--advanced-camera-card-grid-width-factor'),
|
|
).toBe('');
|
|
});
|
|
});
|
|
});
|