mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-02-04 14:32:59 -05:00
feat: utils/classes unit tests
This commit is contained in:
92
frontend/tests/utils/classes/BrowserCache.test.ts
Normal file
92
frontend/tests/utils/classes/BrowserCache.test.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { BrowserCache } from '../../../src/static/js/utils/classes/BrowserCache';
|
||||||
|
|
||||||
|
// Mocks for helpers used by BrowserCache
|
||||||
|
jest.mock('../../../src/static/js/utils/helpers/', () => ({
|
||||||
|
logErrorAndReturnError: jest.fn((args: any[]) => ({ error: true, args })),
|
||||||
|
logWarningAndReturnError: jest.fn((args: any[]) => ({ warning: true, args })),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { logErrorAndReturnError } = jest.requireMock('../../../src/static/js/utils/helpers/');
|
||||||
|
|
||||||
|
describe('utils/classes', () => {
|
||||||
|
describe('BrowserCache', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Returns error when prefix is missing', () => {
|
||||||
|
const cache = BrowserCache(undefined, 3600);
|
||||||
|
expect(cache).toEqual(expect.objectContaining({ error: true }));
|
||||||
|
expect(logErrorAndReturnError).toHaveBeenCalledWith(['Cache object prefix is required']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Set and get returns stored primitive value before expiration', () => {
|
||||||
|
const cache = BrowserCache('prefix', 3600);
|
||||||
|
|
||||||
|
if (cache instanceof Error) {
|
||||||
|
expect(cache instanceof Error).toBe(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(cache.set('foo', 'bar')).toBe(true);
|
||||||
|
expect(cache.get('foo')).toBe('bar');
|
||||||
|
|
||||||
|
// Ensure value serialized in localStorage with namespaced key
|
||||||
|
const raw = localStorage.getItem('prefix[foo]') as string;
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
expect(parsed.value).toBe('bar');
|
||||||
|
expect(typeof parsed.expire).toBe('number');
|
||||||
|
expect(parsed.expire).toBeGreaterThan(Date.now());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Get returns null when expired', () => {
|
||||||
|
const cache = BrowserCache('prefix', 1);
|
||||||
|
|
||||||
|
if (cache instanceof Error) {
|
||||||
|
expect(cache instanceof Error).toBe(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.set('exp', { a: 1 });
|
||||||
|
|
||||||
|
jest.useFakeTimers();
|
||||||
|
jest.advanceTimersByTime(1_000);
|
||||||
|
|
||||||
|
expect(cache.get('exp')).toBeNull();
|
||||||
|
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Clear removes only keys for its prefix', () => {
|
||||||
|
const cacheA = BrowserCache('A', 3600);
|
||||||
|
const cacheB = BrowserCache('B', 3600);
|
||||||
|
|
||||||
|
if (cacheA instanceof Error) {
|
||||||
|
expect(cacheA instanceof Error).toBe(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cacheB instanceof Error) {
|
||||||
|
expect(cacheB instanceof Error).toBe(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheA.set('x', 1);
|
||||||
|
cacheB.set('x', 2);
|
||||||
|
|
||||||
|
expect(localStorage.getItem('A[x]')).toBeTruthy();
|
||||||
|
expect(localStorage.getItem('B[x]')).toBeTruthy();
|
||||||
|
|
||||||
|
cacheA.clear();
|
||||||
|
|
||||||
|
expect(localStorage.getItem('A[x]')).toBeNull();
|
||||||
|
expect(localStorage.getItem('B[x]')).toBeTruthy();
|
||||||
|
|
||||||
|
cacheB.clear();
|
||||||
|
|
||||||
|
expect(localStorage.getItem('A[x]')).toBeNull();
|
||||||
|
expect(localStorage.getItem('B[x]')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
101
frontend/tests/utils/classes/MediaDurationInfo.test.ts
Normal file
101
frontend/tests/utils/classes/MediaDurationInfo.test.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import { MediaDurationInfo } from '../../../src/static/js/utils/classes/MediaDurationInfo';
|
||||||
|
|
||||||
|
describe('utils/classes', () => {
|
||||||
|
describe('MediaDurationInfo', () => {
|
||||||
|
test('Initializes via constructor when seconds is a positive integer (<= 59)', () => {
|
||||||
|
const mdi = new MediaDurationInfo(42);
|
||||||
|
expect(mdi.toString()).toBe('0:42');
|
||||||
|
expect(mdi.ariaLabel()).toBe('42 seconds');
|
||||||
|
expect(mdi.ISO8601()).toBe('P0Y0M0DT0H0M42S');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Formats minutes and zero-pads seconds; no hours prefix under 60 minutes', () => {
|
||||||
|
const mdi = new MediaDurationInfo();
|
||||||
|
mdi.update(5 * 60 + 7);
|
||||||
|
expect(mdi.toString()).toBe('5:07');
|
||||||
|
expect(mdi.ariaLabel()).toBe('5 minutes, 7 seconds');
|
||||||
|
expect(mdi.ISO8601()).toBe('P0Y0M0DT0H5M7S');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Includes hours when duration >= 1 hour and zero-pads minutes when needed', () => {
|
||||||
|
const mdi = new MediaDurationInfo();
|
||||||
|
mdi.update(1 * 3600 + 2 * 60 + 3);
|
||||||
|
expect(mdi.toString()).toBe('1:02:03');
|
||||||
|
expect(mdi.ariaLabel()).toBe('1 hours, 2 minutes, 3 seconds');
|
||||||
|
expect(mdi.ISO8601()).toBe('P0Y0M0DT1H2M3S');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Accumulates hours when days are present (e.g., 1 day + 2:03:04 => 26:03:04)', () => {
|
||||||
|
const mdi = new MediaDurationInfo();
|
||||||
|
const seconds = 1 * 86400 + 2 * 3600 + 3 * 60 + 4; // 1d 2:03:04 => 26:03:04
|
||||||
|
mdi.update(seconds);
|
||||||
|
expect(mdi.toString()).toBe('26:03:04');
|
||||||
|
expect(mdi.ariaLabel()).toBe('26 hours, 3 minutes, 4 seconds');
|
||||||
|
expect(mdi.ISO8601()).toBe('P0Y0M0DT26H3M4S');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Large durations: multiple days correctly mapped into hours', () => {
|
||||||
|
const mdi = new MediaDurationInfo();
|
||||||
|
const seconds = 3 * 86400 + 10 * 3600 + 15 * 60 + 9; // 3d 10:15:09 => 82:15:09
|
||||||
|
mdi.update(seconds);
|
||||||
|
expect(mdi.toString()).toBe('82:15:09');
|
||||||
|
expect(mdi.ariaLabel()).toBe('82 hours, 15 minutes, 9 seconds');
|
||||||
|
expect(mdi.ISO8601()).toBe('P0Y0M0DT82H15M9S');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Caching: toString and ariaLabel recompute only after update()', () => {
|
||||||
|
const mdi = new MediaDurationInfo(59);
|
||||||
|
const firstToString = mdi.toString();
|
||||||
|
const firstAria = mdi.ariaLabel();
|
||||||
|
expect(firstToString).toBe('0:59');
|
||||||
|
expect(firstAria).toBe('59 seconds');
|
||||||
|
|
||||||
|
// Call again to hit cached path
|
||||||
|
expect(mdi.toString()).toBe(firstToString);
|
||||||
|
expect(mdi.ariaLabel()).toBe(firstAria);
|
||||||
|
|
||||||
|
// Update and ensure cache invalidates
|
||||||
|
mdi.update(60);
|
||||||
|
expect(mdi.toString()).toBe('1:00');
|
||||||
|
expect(mdi.ariaLabel()).toBe('1 minutes');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Ignores invalid (non-positive integer or zero) updates, retaining previous value', () => {
|
||||||
|
const mdi = new MediaDurationInfo(10);
|
||||||
|
expect(mdi.toString()).toBe('0:10');
|
||||||
|
|
||||||
|
mdi.update(1.23);
|
||||||
|
expect(mdi.toString()).toBe('0:10');
|
||||||
|
|
||||||
|
mdi.update(-5);
|
||||||
|
expect(mdi.toString()).toBe('0:10');
|
||||||
|
|
||||||
|
mdi.update('x');
|
||||||
|
expect(mdi.toString()).toBe('0:10');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Boundary conditions around a minute and an hour', () => {
|
||||||
|
const mdi = new MediaDurationInfo();
|
||||||
|
|
||||||
|
mdi.update(59);
|
||||||
|
expect(mdi.toString()).toBe('0:59');
|
||||||
|
|
||||||
|
mdi.update(60);
|
||||||
|
expect(mdi.toString()).toBe('1:00');
|
||||||
|
|
||||||
|
mdi.update(3599);
|
||||||
|
expect(mdi.toString()).toBe('59:59');
|
||||||
|
|
||||||
|
mdi.update(3600);
|
||||||
|
expect(mdi.toString()).toBe('1:00:00');
|
||||||
|
});
|
||||||
|
|
||||||
|
// @todo: Revisit this behavior
|
||||||
|
test('Constructs without initial seconds', () => {
|
||||||
|
const mdi = new MediaDurationInfo();
|
||||||
|
expect(typeof mdi.toString()).toBe('function');
|
||||||
|
expect(mdi.ariaLabel()).toBe('');
|
||||||
|
expect(mdi.ISO8601()).toBe('P0Y0M0DTundefinedHundefinedMundefinedS');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
102
frontend/tests/utils/classes/UpNextLoaderView.test.ts
Normal file
102
frontend/tests/utils/classes/UpNextLoaderView.test.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { UpNextLoaderView } from '../../../src/static/js/utils/classes/UpNextLoaderView';
|
||||||
|
|
||||||
|
// Minimal helpers mocks used by UpNextLoaderView
|
||||||
|
jest.mock('../../../src/static/js/utils/helpers/', () => ({
|
||||||
|
addClassname: jest.fn((el: any, cn: string) => el && el.classList && el.classList.add(cn)),
|
||||||
|
removeClassname: jest.fn((el: any, cn: string) => el && el.classList && el.classList.remove(cn)),
|
||||||
|
translateString: (s: string) => s,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { addClassname, removeClassname } = jest.requireMock('../../../src/static/js/utils/helpers/');
|
||||||
|
|
||||||
|
const makeNextItem = () => ({
|
||||||
|
url: '/next-url',
|
||||||
|
title: 'Next title',
|
||||||
|
author_name: 'Jane Doe',
|
||||||
|
thumbnail_url: 'https://example.com/thumb.jpg',
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('utils/classes', () => {
|
||||||
|
describe('UpNextLoaderView', () => {
|
||||||
|
test('html() builds structure with expected classes and content', () => {
|
||||||
|
const v = new UpNextLoaderView(makeNextItem());
|
||||||
|
|
||||||
|
const root = v.html();
|
||||||
|
|
||||||
|
expect(root).toBeInstanceOf(HTMLElement);
|
||||||
|
expect(root.querySelector('.up-next-loader-inner')).not.toBeNull();
|
||||||
|
expect(root.querySelector('.up-next-label')!.textContent).toBe('Up Next');
|
||||||
|
expect(root.querySelector('.next-media-title')!.textContent).toBe('Next title');
|
||||||
|
expect(root.querySelector('.next-media-author')!.textContent).toBe('Jane Doe');
|
||||||
|
|
||||||
|
// poster background
|
||||||
|
const poster = root.querySelector('.next-media-poster') as HTMLElement;
|
||||||
|
expect(poster.style.backgroundImage).toContain('thumb.jpg');
|
||||||
|
|
||||||
|
// go-next link points to next url
|
||||||
|
const link = root.querySelector('.go-next a') as HTMLAnchorElement;
|
||||||
|
expect(link.getAttribute('href')).toBe('/next-url');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setVideoJsPlayerElem marks player with vjs-mediacms-has-up-next-view class', () => {
|
||||||
|
const v = new UpNextLoaderView(makeNextItem());
|
||||||
|
const player = document.createElement('div');
|
||||||
|
|
||||||
|
v.setVideoJsPlayerElem(player);
|
||||||
|
|
||||||
|
expect(addClassname).toHaveBeenCalledWith(player, 'vjs-mediacms-has-up-next-view');
|
||||||
|
expect(v.vjsPlayerElem).toBe(player);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('startTimer shows view, registers scroll, and navigates after 10s', () => {
|
||||||
|
const next = makeNextItem();
|
||||||
|
const v = new UpNextLoaderView(next);
|
||||||
|
const player = document.createElement('div');
|
||||||
|
|
||||||
|
v.setVideoJsPlayerElem(player);
|
||||||
|
v.startTimer();
|
||||||
|
|
||||||
|
expect(removeClassname).toHaveBeenCalledWith(player, 'vjs-mediacms-up-next-hidden');
|
||||||
|
expect(removeClassname).toHaveBeenCalledWith(player, 'vjs-mediacms-canceled-next');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cancelTimer clears timeout, stops scroll, and marks canceled', () => {
|
||||||
|
const v = new UpNextLoaderView(makeNextItem());
|
||||||
|
const player = document.createElement('div');
|
||||||
|
|
||||||
|
v.setVideoJsPlayerElem(player);
|
||||||
|
|
||||||
|
v.startTimer();
|
||||||
|
v.cancelTimer();
|
||||||
|
|
||||||
|
expect(addClassname).toHaveBeenCalledWith(player, 'vjs-mediacms-canceled-next');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Cancel button click hides the view and cancels timer', () => {
|
||||||
|
const v = new UpNextLoaderView(makeNextItem());
|
||||||
|
const player = document.createElement('div');
|
||||||
|
v.setVideoJsPlayerElem(player);
|
||||||
|
|
||||||
|
v.startTimer();
|
||||||
|
const root = v.html();
|
||||||
|
const cancelBtn = root.querySelector('.up-next-cancel button') as HTMLButtonElement;
|
||||||
|
cancelBtn.click();
|
||||||
|
|
||||||
|
expect(addClassname).toHaveBeenCalledWith(player, 'vjs-mediacms-canceled-next');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('showTimerView shows or starts timer based on flag', () => {
|
||||||
|
const v = new UpNextLoaderView(makeNextItem());
|
||||||
|
const player = document.createElement('div');
|
||||||
|
v.setVideoJsPlayerElem(player);
|
||||||
|
|
||||||
|
// beginTimer=false -> just show view
|
||||||
|
v.showTimerView(false);
|
||||||
|
expect(removeClassname).toHaveBeenCalledWith(player, 'vjs-mediacms-up-next-hidden');
|
||||||
|
|
||||||
|
// beginTimer=true -> starts timer
|
||||||
|
v.showTimerView(true);
|
||||||
|
expect(removeClassname).toHaveBeenCalledWith(player, 'vjs-mediacms-canceled-next');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user