refactor(frontend): replace legacy utils JS files with typed TS equivalents

This commit is contained in:
Yiannis
2026-03-11 01:51:53 +02:00
parent df4b0422d5
commit 1b8e8aae6a
48 changed files with 711 additions and 502 deletions

View File

@@ -6,6 +6,8 @@ import {
BrowserEvents,
} from '../../../src/static/js/utils/helpers/dom';
const domModulePath = '../../../src/static/js/utils/helpers/dom';
declare global {
interface Window {
mozRequestAnimationFrame?: Window['requestAnimationFrame'];
@@ -15,7 +17,7 @@ declare global {
}
}
describe('js/utils/helpers', () => {
describe('utils/helpers', () => {
describe('dom', () => {
describe('supportsSvgAsImg', () => {
test('Delegates to document.implementation.hasFeature', () => {
@@ -78,7 +80,7 @@ describe('js/utils/helpers', () => {
test('Does not register non-function callbacks', () => {
const be = BrowserEvents();
be.win('not-a-fn', null);
be.win('not-a-fn' as unknown as Function, null as unknown as Function);
be.doc(undefined);
// Should still have registered the listeners on construction
@@ -152,8 +154,8 @@ describe('js/utils/helpers', () => {
test('Ignores non-function values without throwing and still registers listeners once', () => {
const be = BrowserEvents();
be.doc('noop');
be.win(null, undefined);
be.doc('noop' as unknown as Function);
be.win(null as unknown as Function, undefined);
const docCount = (document.addEventListener as jest.Mock).mock.calls.filter(
(c) => c[0] === 'visibilitychange'
@@ -216,5 +218,64 @@ describe('js/utils/helpers', () => {
expect(hasClassname(el, 'two-three')).toBe(true);
});
});
describe('Animation frame helpers', () => {
const requestAnimationFrameDescriptor = Object.getOwnPropertyDescriptor(window, 'requestAnimationFrame');
const cancelAnimationFrameDescriptor = Object.getOwnPropertyDescriptor(window, 'cancelAnimationFrame');
afterEach(() => {
if (requestAnimationFrameDescriptor) {
Object.defineProperty(window, 'requestAnimationFrame', requestAnimationFrameDescriptor);
} else {
delete (window as Partial<Window>).requestAnimationFrame;
}
if (cancelAnimationFrameDescriptor) {
Object.defineProperty(window, 'cancelAnimationFrame', cancelAnimationFrameDescriptor);
} else {
delete (window as Partial<Window>).cancelAnimationFrame;
}
jest.resetModules();
});
test('requestAnimationFrame export is directly callable and delegates to window API', () => {
const requestAnimationFrameMock = jest.fn((callback: FrameRequestCallback) => {
callback(123);
return 7;
});
Object.defineProperty(window, 'requestAnimationFrame', {
configurable: true,
value: requestAnimationFrameMock,
writable: true,
});
jest.resetModules();
const domHelpers = require(domModulePath) as typeof import('../../../src/static/js/utils/helpers/dom');
const callback = jest.fn();
const frameId = domHelpers.requestAnimationFrame(callback);
expect(frameId).toBe(7);
expect(requestAnimationFrameMock).toHaveBeenCalledWith(callback);
expect(callback).toHaveBeenCalledWith(123);
});
test('cancelAnimationFrame export is directly callable and delegates to window API', () => {
const cancelAnimationFrameMock = jest.fn();
Object.defineProperty(window, 'cancelAnimationFrame', {
configurable: true,
value: cancelAnimationFrameMock,
writable: true,
});
jest.resetModules();
const domHelpers = require(domModulePath) as typeof import('../../../src/static/js/utils/helpers/dom');
domHelpers.cancelAnimationFrame(42);
expect(cancelAnimationFrameMock).toHaveBeenCalledWith(42);
});
});
});
});

View File

@@ -1,47 +1,49 @@
// Mock the './log' module used by errors.ts to capture calls without console side effects
jest.mock('../../../src/static/js/utils/helpers/log', () => ({ error: jest.fn(), warn: jest.fn() }));
import { logErrorAndReturnError, logWarningAndReturnError } from '../../../src/static/js/utils/helpers';
// Mock the './log' module used by errors.ts to capture calls without console side effects
jest.mock('../../../src/static/js/utils/helpers/log', () => ({
error: jest.fn(),
warn: jest.fn(),
}));
import { logErrorAndReturnError, logWarningAndReturnError } from '../../../src/static/js/utils/helpers/errors';
import { error as mockedError, warn as mockedWarn } from '../../../src/static/js/utils/helpers/log';
describe('js/utils/helpers', () => {
describe('utils/helpers', () => {
describe('errors', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('logErrorAndReturnError returns Error with first message and logs with error', () => {
const messages = ['Primary msg', 'details', 'more'];
const messages = ['Primary error', 'details', 'more'];
const err = logErrorAndReturnError(messages);
expect(err).toBeInstanceOf(Error);
expect(err.message).toBe('Primary msg');
expect(err.message).toBe('Primary error');
expect(mockedError).toHaveBeenCalledTimes(1);
expect(mockedError).toHaveBeenCalledWith(...messages);
});
test('logWarningAndReturnError returns Error with first message and logs with warn', () => {
const messages = ['Primary msg', 'details', 'more'];
const messages = ['Warn msg', 'context'];
const err = logWarningAndReturnError(messages);
expect(err).toBeInstanceOf(Error);
expect(err.message).toBe('Primary msg');
expect(err.message).toBe('Warn msg');
expect(mockedWarn).toHaveBeenCalledTimes(1);
expect(mockedWarn).toHaveBeenCalledWith(...messages);
});
test('Handles empty array creating an Error with undefined message and logs called with no args', () => {
test('handles empty array creating an Error with undefined message and logs called with no args', () => {
const messages: string[] = [];
const err1 = logErrorAndReturnError(messages);
expect(err1).toBeInstanceOf(Error);
expect(err1.message).toBe('');
expect(mockedError).toHaveBeenCalledWith('');
expect(mockedError).toHaveBeenCalledWith(...messages);
jest.clearAllMocks();
const err2 = logWarningAndReturnError(messages);
expect(err2).toBeInstanceOf(Error);
expect(err2.message).toBe('');
expect(mockedWarn).toHaveBeenCalledWith('');
expect(mockedWarn).toHaveBeenCalledWith(...messages);
});
});
});

View File

@@ -1,44 +1,96 @@
// Mock the dispatcher module used by exportStore
jest.mock('../../../src/static/js/utils/dispatcher', () => ({ register: jest.fn() }));
import EventEmitter from 'events';
import { exportStore } from '../../../src/static/js/utils/helpers';
import exportStore from '../../../src/static/js/utils/helpers/exportStore';
// The dispatcher is an external module dependency used by exportStore; mock it to observe registrations
jest.mock('../../../src/static/js/utils/dispatcher', () => ({
dispatcher: {
register: jest.fn(),
},
}));
// Re-import the mocked dispatcher for assertions
import * as dispatcher from '../../../src/static/js/utils/dispatcher';
import { dispatcher } from '../../../src/static/js/utils/dispatcher';
describe('js/utils/helpers', () => {
/**
* Behaviors covered:
* 1. Binds the provided handler method to store instance context.
* 2. Registers the bound callback exactly once with the dispatcher.
* 3. Returns the same store instance that was provided.
* 4. Invoking the registered callback forwards payload to the handler with correct this.
* 5. Type-safety assumption: only function keys are accepted as handler (runtime sanity via mock class).
*/
describe('utils/helpers', () => {
describe('exportStore', () => {
class TestStore extends (EventEmitter as { new (): EventEmitter }) {
public calls: unknown[] = [];
public handler(payload: unknown) {
// Assert `this` is the store instance when called via bound function
this.calls.push({ self: this, payload });
}
public otherHandler(payload: unknown) {
this.calls.push({ self: this, payload, type: 'other' });
}
}
beforeEach(() => {
jest.clearAllMocks();
});
test('Registers store handler with dispatcher and binds context', () => {
const ctx: { value: number; inc?: () => void } = { value: 0 };
const handlerName = 'inc';
const handler = function (this: typeof ctx) {
this.value += 1;
};
ctx[handlerName] = handler as any;
const result = exportStore(ctx, handlerName);
// returns the same store instance
expect(result).toBe(ctx);
// Ensure dispatcher.register was called once with a bound function
expect((dispatcher as any).register).toHaveBeenCalledTimes(1);
const registeredFn = (dispatcher as any).register.mock.calls[0][0] as Function;
expect(typeof registeredFn).toBe('function');
// Verify the registered function is bound to the store context
registeredFn();
expect(ctx.value).toBe(1);
test('Returns the same store instance and registers exactly once', () => {
const store = new TestStore();
const returned = exportStore(store as unknown as EventEmitter, 'handler' as any);
expect(returned).toBe(store);
expect(dispatcher.register).toHaveBeenCalledTimes(1);
expect(typeof (dispatcher.register as jest.Mock).mock.calls[0][0]).toBe('function');
});
test('Throws if handler name does not exist on store', () => {
const store: any = {};
// Accessing store[handler] would be undefined; calling .bind on undefined would throw
expect(() => exportStore(store, 'missing')).toThrow();
test('Binds the handler to the store instance context', () => {
const store = new TestStore();
exportStore(store as unknown as EventEmitter, 'handler' as any);
const registered = (dispatcher.register as jest.Mock).mock.calls[0][0] as (p: unknown) => void;
const payload = { a: 1 };
registered(payload);
expect(store.calls).toHaveLength(1);
const { self, payload: received } = store.calls[0] as any;
expect(self).toBe(store);
expect(received).toBe(payload);
});
test('Forwards any payload through the registered callback to the handler', () => {
const store = new TestStore();
exportStore(store as unknown as EventEmitter, 'otherHandler' as any);
const registered = (dispatcher.register as jest.Mock).mock.calls[0][0] as (p: unknown) => void;
registered(null);
registered(42);
registered({ x: 'y' });
expect(store.calls.map((c: any) => c.payload)).toEqual([null, 42, { x: 'y' }]);
});
test('Supports different handler keys of the same store', () => {
const store = new TestStore();
exportStore(store as unknown as EventEmitter, 'handler' as any);
exportStore(store as unknown as EventEmitter, 'otherHandler' as any);
expect(dispatcher.register).toHaveBeenCalledTimes(2);
const cb1 = (dispatcher.register as jest.Mock).mock.calls[0][0] as (p: unknown) => void;
const cb2 = (dispatcher.register as jest.Mock).mock.calls[1][0] as (p: unknown) => void;
cb1('a');
cb2('b');
expect(store.calls).toHaveLength(2);
expect(store.calls[0]).toMatchObject({ payload: 'a' });
expect(store.calls[1]).toMatchObject({ payload: 'b', type: 'other' });
});
test('Runtime guard scenario: non-existing handler results in TypeError on bind access', () => {
const store = new TestStore();
// @ts-expect-error intentionally passing wrong key to simulate runtime misuse
expect(() => exportStore(store as unknown as EventEmitter, 'notAHandler')).toThrow();
});
});
});

View File

@@ -1,4 +1,4 @@
import formatViewsNumber from '../../../src/static/js/utils/helpers/formatViewsNumber';
import { formatViewsNumber } from '../../../src/static/js/utils/helpers';
describe('js/utils/helpers', () => {
describe('formatViewsNumber', () => {

View File

@@ -1,4 +1,4 @@
import publishedOnDate from '../../../src/static/js/utils/helpers/publishedOnDate';
import { publishedOnDate } from '../../../src/static/js/utils/helpers';
// Helper to create Date in UTC to avoid timezone issues in CI environments
const makeDate = (y: number, mZeroBased: number, d: number) => new Date(Date.UTC(y, mZeroBased, d));

View File

@@ -5,7 +5,7 @@ jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;
describe('js/utils/helpers', () => {
describe('utils/helpers', () => {
describe('requests', () => {
beforeEach(() => {
jest.clearAllMocks();
@@ -21,8 +21,8 @@ describe('js/utils/helpers', () => {
getRequest(url, false, cb, undefined);
expect(mockedAxios.get).toHaveBeenCalledWith(url, {
timeout: null,
maxContentLength: null,
timeout: undefined,
maxContentLength: undefined,
});
});
@@ -117,7 +117,7 @@ describe('js/utils/helpers', () => {
await postRequest(url, undefined as any, undefined as any, true, cb, undefined);
expect(mockedAxios.post).toHaveBeenCalledWith(url, {}, null);
expect(mockedAxios.post).toHaveBeenCalledWith(url, {}, undefined);
expect(cb).toHaveBeenCalled();
});
@@ -150,7 +150,7 @@ describe('js/utils/helpers', () => {
await putRequest(url, undefined as any, undefined as any, true, undefined, undefined);
expect(mockedAxios.put).toHaveBeenCalledWith(url, {}, null);
expect(mockedAxios.put).toHaveBeenCalledWith(url, {}, undefined);
});
test('Invokes errorCallback on error', async () => {