mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-01-20 15:22:58 -05:00
221 lines
9.1 KiB
TypeScript
221 lines
9.1 KiB
TypeScript
import {
|
|
supportsSvgAsImg,
|
|
removeClassname,
|
|
addClassname,
|
|
hasClassname,
|
|
BrowserEvents,
|
|
} from '../../../src/static/js/utils/helpers/dom';
|
|
|
|
declare global {
|
|
interface Window {
|
|
mozRequestAnimationFrame?: Window['requestAnimationFrame'];
|
|
webkitRequestAnimationFrame?: Window['requestAnimationFrame'];
|
|
msRequestAnimationFrame?: Window['requestAnimationFrame'];
|
|
mozCancelAnimationFrame?: Window['cancelAnimationFrame'];
|
|
}
|
|
}
|
|
|
|
describe('js/utils/helpers', () => {
|
|
describe('dom', () => {
|
|
describe('supportsSvgAsImg', () => {
|
|
test('Delegates to document.implementation.hasFeature', () => {
|
|
const spy = jest.spyOn(document.implementation as any, 'hasFeature').mockReturnValueOnce(true);
|
|
expect(supportsSvgAsImg()).toBe(true);
|
|
expect(spy).toHaveBeenCalledWith('http://www.w3.org/TR/SVG11/feature#Image', '1.1');
|
|
spy.mockRestore();
|
|
});
|
|
|
|
test('Returns false when feature detection fails', () => {
|
|
const spy = jest.spyOn(document.implementation as any, 'hasFeature').mockReturnValueOnce(false);
|
|
expect(supportsSvgAsImg()).toBe(false);
|
|
spy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe('BrowserEvents', () => {
|
|
beforeEach(() => {
|
|
jest.spyOn(document, 'addEventListener').mockClear();
|
|
jest.spyOn(window, 'addEventListener').mockClear();
|
|
document.addEventListener = jest.fn();
|
|
window.addEventListener = jest.fn();
|
|
});
|
|
|
|
test('Registers global listeners on construction and invokes callbacks on events', () => {
|
|
const be = BrowserEvents();
|
|
|
|
const visCb = jest.fn();
|
|
const resizeCb = jest.fn();
|
|
const scrollCb = jest.fn();
|
|
|
|
// Register callbacks
|
|
be.doc(visCb);
|
|
be.win(resizeCb, scrollCb);
|
|
|
|
// Capture the callback passed to addEventListener for each event
|
|
const docHandler = (document.addEventListener as jest.Mock).mock.calls.find(
|
|
(c) => c[0] === 'visibilitychange'
|
|
)?.[1] as Function;
|
|
|
|
const resizeHandler = (window.addEventListener as jest.Mock).mock.calls.find(
|
|
(c) => c[0] === 'resize'
|
|
)?.[1] as Function;
|
|
|
|
const scrollHandler = (window.addEventListener as jest.Mock).mock.calls.find(
|
|
(c) => c[0] === 'scroll'
|
|
)?.[1] as Function;
|
|
|
|
// Fire handlers to simulate events
|
|
docHandler();
|
|
resizeHandler();
|
|
scrollHandler();
|
|
|
|
expect(visCb).toHaveBeenCalledTimes(1);
|
|
expect(resizeCb).toHaveBeenCalledTimes(1);
|
|
expect(scrollCb).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
// @todo: Revisit this behavior
|
|
test('Does not register non-function callbacks', () => {
|
|
const be = BrowserEvents();
|
|
|
|
be.win('not-a-fn', null);
|
|
be.doc(undefined);
|
|
|
|
// Should still have registered the listeners on construction
|
|
expect(
|
|
(document.addEventListener as jest.Mock).mock.calls.some((c) => c[0] === 'visibilitychange')
|
|
).toBe(true);
|
|
expect((window.addEventListener as jest.Mock).mock.calls.some((c) => c[0] === 'resize')).toBe(true);
|
|
expect((window.addEventListener as jest.Mock).mock.calls.some((c) => c[0] === 'scroll')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('BrowserEvents (edge cases)', () => {
|
|
beforeEach(() => {
|
|
(document.addEventListener as jest.Mock).mockClear();
|
|
(window.addEventListener as jest.Mock).mockClear();
|
|
document.addEventListener = jest.fn();
|
|
window.addEventListener = jest.fn();
|
|
});
|
|
|
|
test('Multiple callbacks are invoked in order for each event type', () => {
|
|
const be = BrowserEvents();
|
|
|
|
const v1 = jest.fn();
|
|
const v2 = jest.fn();
|
|
const r1 = jest.fn();
|
|
const r2 = jest.fn();
|
|
const s1 = jest.fn();
|
|
const s2 = jest.fn();
|
|
|
|
be.doc(v1);
|
|
be.doc(v2);
|
|
be.win(r1, s1);
|
|
be.win(r2, s2);
|
|
|
|
const docHandler = (document.addEventListener as jest.Mock).mock.calls.find(
|
|
(c) => c[0] === 'visibilitychange'
|
|
)?.[1] as Function;
|
|
|
|
const resizeHandler = (window.addEventListener as jest.Mock).mock.calls.find(
|
|
(c) => c[0] === 'resize'
|
|
)?.[1] as Function;
|
|
|
|
const scrollHandler = (window.addEventListener as jest.Mock).mock.calls.find(
|
|
(c) => c[0] === 'scroll'
|
|
)?.[1] as Function;
|
|
|
|
// Fire events twice to ensure each call triggers callbacks once per firing
|
|
docHandler();
|
|
resizeHandler();
|
|
scrollHandler();
|
|
|
|
docHandler();
|
|
resizeHandler();
|
|
scrollHandler();
|
|
|
|
expect(v1).toHaveBeenCalledTimes(2);
|
|
expect(v2).toHaveBeenCalledTimes(2);
|
|
expect(r1).toHaveBeenCalledTimes(2);
|
|
expect(r2).toHaveBeenCalledTimes(2);
|
|
expect(s1).toHaveBeenCalledTimes(2);
|
|
expect(s2).toHaveBeenCalledTimes(2);
|
|
|
|
// Ensure order of invocation within each firing respects registration order
|
|
// Jest mock call order grows monotonically; validate the first calls were in the expected sequence
|
|
expect(v1.mock.invocationCallOrder[0]).toBeLessThan(v2.mock.invocationCallOrder[0]);
|
|
expect(r1.mock.invocationCallOrder[0]).toBeLessThan(r2.mock.invocationCallOrder[0]);
|
|
expect(s1.mock.invocationCallOrder[0]).toBeLessThan(s2.mock.invocationCallOrder[0]);
|
|
});
|
|
|
|
// @todo: Check again this behavior
|
|
test('Ignores non-function values without throwing and still registers listeners once', () => {
|
|
const be = BrowserEvents();
|
|
|
|
be.doc('noop');
|
|
be.win(null, undefined);
|
|
|
|
const docCount = (document.addEventListener as jest.Mock).mock.calls.filter(
|
|
(c) => c[0] === 'visibilitychange'
|
|
).length;
|
|
const resizeCount = (window.addEventListener as jest.Mock).mock.calls.filter(
|
|
(c) => c[0] === 'resize'
|
|
).length;
|
|
const scrollCount = (window.addEventListener as jest.Mock).mock.calls.filter(
|
|
(c) => c[0] === 'scroll'
|
|
).length;
|
|
|
|
expect(docCount).toBe(1);
|
|
expect(resizeCount).toBe(1);
|
|
expect(scrollCount).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('classname helpers', () => {
|
|
test('addClassname uses classList.add when available', () => {
|
|
const el = document.createElement('div') as any;
|
|
const mockAdd = jest.fn();
|
|
el.classList.add = mockAdd;
|
|
|
|
addClassname(el, 'active');
|
|
expect(mockAdd).toHaveBeenCalledWith('active');
|
|
});
|
|
|
|
test('removeClassname uses classList.remove when available', () => {
|
|
const el = document.createElement('div') as any;
|
|
const mockRemove = jest.fn();
|
|
el.classList.remove = mockRemove;
|
|
removeClassname(el, 'active');
|
|
expect(mockRemove).toHaveBeenCalledWith('active');
|
|
});
|
|
|
|
test('addClassname fallback appends class to className', () => {
|
|
const el = document.createElement('div') as any;
|
|
el.className = 'one';
|
|
// Remove classList to test fallback behavior
|
|
delete el.classList;
|
|
addClassname(el, 'two');
|
|
expect(el.className).toBe('one two');
|
|
});
|
|
|
|
test('removeClassname fallback removes class via regex', () => {
|
|
const el = document.createElement('div') as any;
|
|
el.className = 'one two three two';
|
|
// Remove classList to test fallback behavior
|
|
delete el.classList;
|
|
removeClassname(el, 'two');
|
|
// The regex replacement may leave extra spaces
|
|
expect(el.className.replaceAll(/\s+/g, ' ').trim()).toBe('one three');
|
|
});
|
|
|
|
test('hasClassname checks for exact class match boundaries', () => {
|
|
const el = document.createElement('div');
|
|
el.className = 'one two-three';
|
|
expect(hasClassname(el, 'one')).toBe(true);
|
|
expect(hasClassname(el, 'two')).toBe(false); // Should not match within two-three
|
|
expect(hasClassname(el, 'two-three')).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
});
|