From 1b8e8aae6aec41910b28ed9d391a636d4ba5a3e9 Mon Sep 17 00:00:00 2001 From: Yiannis <1515939+styiannis@users.noreply.github.com> Date: Wed, 11 Mar 2026 01:51:53 +0200 Subject: [PATCH] refactor(frontend): replace legacy utils JS files with typed TS equivalents --- .../src/static/js/utils/constants/index.js | 2 - .../src/static/js/utils/constants/index.ts | 2 + .../src/static/js/utils/constants/months.js | 14 -- .../src/static/js/utils/constants/months.ts | 14 ++ .../src/static/js/utils/constants/weekdays.js | 1 - .../src/static/js/utils/constants/weekdays.ts | 1 + frontend/src/static/js/utils/dispatcher.js | 2 - frontend/src/static/js/utils/dispatcher.ts | 3 + .../src/static/js/utils/helpers/csrfToken.js | 19 -- .../src/static/js/utils/helpers/csrfToken.ts | 18 ++ frontend/src/static/js/utils/helpers/dom.js | 79 -------- frontend/src/static/js/utils/helpers/dom.ts | 95 ++++++++++ .../static/js/utils/helpers/embeddedApp.ts | 2 +- .../src/static/js/utils/helpers/errors.js | 27 --- .../src/static/js/utils/helpers/errors.ts | 15 ++ .../static/js/utils/helpers/exportStore.js | 5 - .../static/js/utils/helpers/exportStore.ts | 28 +++ .../js/utils/helpers/formatInnerLink.js | 11 -- .../js/utils/helpers/formatInnerLink.ts | 11 ++ .../helpers/formatManagementTableDate.js | 15 -- .../helpers/formatManagementTableDate.ts | 15 ++ .../js/utils/helpers/formatViewsNumber.js | 18 -- .../js/utils/helpers/formatViewsNumber.ts | 17 ++ .../static/js/utils/helpers/imageExtension.js | 7 - .../static/js/utils/helpers/imageExtension.ts | 5 + frontend/src/static/js/utils/helpers/index.js | 17 -- frontend/src/static/js/utils/helpers/index.ts | 17 ++ frontend/src/static/js/utils/helpers/log.js | 4 - frontend/src/static/js/utils/helpers/log.ts | 9 + frontend/src/static/js/utils/helpers/math.js | 10 -- frontend/src/static/js/utils/helpers/math.ts | 10 ++ ...{propTypeFilters.js => propTypeFilters.ts} | 15 +- .../js/utils/helpers/publishedOnDate.js | 17 -- .../js/utils/helpers/publishedOnDate.ts | 17 ++ .../src/static/js/utils/helpers/quickSort.js | 35 ---- .../src/static/js/utils/helpers/quickSort.ts | 29 +++ .../js/utils/helpers/replacementStrings.js | 15 -- .../js/utils/helpers/replacementStrings.ts | 47 +++++ .../src/static/js/utils/helpers/requests.js | 135 -------------- .../src/static/js/utils/helpers/requests.ts | 169 ++++++++++++++++++ .../src/static/js/utils/helpers/translate.js | 5 - .../src/static/js/utils/helpers/translate.ts | 11 ++ frontend/tests/utils/helpers/dom.test.ts | 69 ++++++- frontend/tests/utils/helpers/errors.test.ts | 28 +-- .../tests/utils/helpers/exportStore.test.ts | 114 ++++++++---- .../utils/helpers/formatViewsNumber.test.ts | 2 +- .../utils/helpers/publishedOnDate.test.ts | 2 +- frontend/tests/utils/helpers/requests.test.ts | 10 +- 48 files changed, 711 insertions(+), 502 deletions(-) delete mode 100644 frontend/src/static/js/utils/constants/index.js create mode 100644 frontend/src/static/js/utils/constants/index.ts delete mode 100755 frontend/src/static/js/utils/constants/months.js create mode 100755 frontend/src/static/js/utils/constants/months.ts delete mode 100755 frontend/src/static/js/utils/constants/weekdays.js create mode 100755 frontend/src/static/js/utils/constants/weekdays.ts delete mode 100755 frontend/src/static/js/utils/dispatcher.js create mode 100755 frontend/src/static/js/utils/dispatcher.ts delete mode 100755 frontend/src/static/js/utils/helpers/csrfToken.js create mode 100755 frontend/src/static/js/utils/helpers/csrfToken.ts delete mode 100755 frontend/src/static/js/utils/helpers/dom.js create mode 100755 frontend/src/static/js/utils/helpers/dom.ts delete mode 100755 frontend/src/static/js/utils/helpers/errors.js create mode 100755 frontend/src/static/js/utils/helpers/errors.ts delete mode 100755 frontend/src/static/js/utils/helpers/exportStore.js create mode 100755 frontend/src/static/js/utils/helpers/exportStore.ts delete mode 100755 frontend/src/static/js/utils/helpers/formatInnerLink.js create mode 100755 frontend/src/static/js/utils/helpers/formatInnerLink.ts delete mode 100755 frontend/src/static/js/utils/helpers/formatManagementTableDate.js create mode 100755 frontend/src/static/js/utils/helpers/formatManagementTableDate.ts delete mode 100755 frontend/src/static/js/utils/helpers/formatViewsNumber.js create mode 100755 frontend/src/static/js/utils/helpers/formatViewsNumber.ts delete mode 100755 frontend/src/static/js/utils/helpers/imageExtension.js create mode 100755 frontend/src/static/js/utils/helpers/imageExtension.ts delete mode 100644 frontend/src/static/js/utils/helpers/index.js create mode 100644 frontend/src/static/js/utils/helpers/index.ts delete mode 100755 frontend/src/static/js/utils/helpers/log.js create mode 100755 frontend/src/static/js/utils/helpers/log.ts delete mode 100755 frontend/src/static/js/utils/helpers/math.js create mode 100755 frontend/src/static/js/utils/helpers/math.ts rename frontend/src/static/js/utils/helpers/{propTypeFilters.js => propTypeFilters.ts} (71%) delete mode 100755 frontend/src/static/js/utils/helpers/publishedOnDate.js create mode 100755 frontend/src/static/js/utils/helpers/publishedOnDate.ts delete mode 100755 frontend/src/static/js/utils/helpers/quickSort.js create mode 100755 frontend/src/static/js/utils/helpers/quickSort.ts delete mode 100644 frontend/src/static/js/utils/helpers/replacementStrings.js create mode 100644 frontend/src/static/js/utils/helpers/replacementStrings.ts delete mode 100644 frontend/src/static/js/utils/helpers/requests.js create mode 100644 frontend/src/static/js/utils/helpers/requests.ts delete mode 100644 frontend/src/static/js/utils/helpers/translate.js create mode 100644 frontend/src/static/js/utils/helpers/translate.ts diff --git a/frontend/src/static/js/utils/constants/index.js b/frontend/src/static/js/utils/constants/index.js deleted file mode 100644 index 637b06a4..00000000 --- a/frontend/src/static/js/utils/constants/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default as months } from './months'; -export { default as weekdays } from './weekdays'; diff --git a/frontend/src/static/js/utils/constants/index.ts b/frontend/src/static/js/utils/constants/index.ts new file mode 100644 index 00000000..933204fb --- /dev/null +++ b/frontend/src/static/js/utils/constants/index.ts @@ -0,0 +1,2 @@ +export * from './months'; +export * from './weekdays'; diff --git a/frontend/src/static/js/utils/constants/months.js b/frontend/src/static/js/utils/constants/months.js deleted file mode 100755 index 0391eb3f..00000000 --- a/frontend/src/static/js/utils/constants/months.js +++ /dev/null @@ -1,14 +0,0 @@ -export default [ - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'October', - 'November', - 'December', -]; diff --git a/frontend/src/static/js/utils/constants/months.ts b/frontend/src/static/js/utils/constants/months.ts new file mode 100755 index 00000000..15c248b8 --- /dev/null +++ b/frontend/src/static/js/utils/constants/months.ts @@ -0,0 +1,14 @@ +export const months = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', +] as const; diff --git a/frontend/src/static/js/utils/constants/weekdays.js b/frontend/src/static/js/utils/constants/weekdays.js deleted file mode 100755 index ae301354..00000000 --- a/frontend/src/static/js/utils/constants/weekdays.js +++ /dev/null @@ -1 +0,0 @@ -export default ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; diff --git a/frontend/src/static/js/utils/constants/weekdays.ts b/frontend/src/static/js/utils/constants/weekdays.ts new file mode 100755 index 00000000..fed3a6a8 --- /dev/null +++ b/frontend/src/static/js/utils/constants/weekdays.ts @@ -0,0 +1 @@ +export const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] as const; diff --git a/frontend/src/static/js/utils/dispatcher.js b/frontend/src/static/js/utils/dispatcher.js deleted file mode 100755 index 78b9cce7..00000000 --- a/frontend/src/static/js/utils/dispatcher.js +++ /dev/null @@ -1,2 +0,0 @@ -const Dispatcher = require('flux').Dispatcher; -module.exports = new Dispatcher(); diff --git a/frontend/src/static/js/utils/dispatcher.ts b/frontend/src/static/js/utils/dispatcher.ts new file mode 100755 index 00000000..98c71eef --- /dev/null +++ b/frontend/src/static/js/utils/dispatcher.ts @@ -0,0 +1,3 @@ +import { Dispatcher } from 'flux'; + +export const dispatcher = new Dispatcher(); diff --git a/frontend/src/static/js/utils/helpers/csrfToken.js b/frontend/src/static/js/utils/helpers/csrfToken.js deleted file mode 100755 index 9de498e4..00000000 --- a/frontend/src/static/js/utils/helpers/csrfToken.js +++ /dev/null @@ -1,19 +0,0 @@ -export function csrfToken() { - var i, - cookies, - cookie, - cookieVal = null; - if (document.cookie && '' !== document.cookie) { - cookies = document.cookie.split(';'); - i = 0; - while (i < cookies.length) { - cookie = cookies[i].trim(); - if ('csrftoken=' === cookie.substring(0, 10)) { - cookieVal = decodeURIComponent(cookie.substring(10)); - break; - } - i += 1; - } - } - return cookieVal; -} diff --git a/frontend/src/static/js/utils/helpers/csrfToken.ts b/frontend/src/static/js/utils/helpers/csrfToken.ts new file mode 100755 index 00000000..6f1072df --- /dev/null +++ b/frontend/src/static/js/utils/helpers/csrfToken.ts @@ -0,0 +1,18 @@ +export function csrfToken() { + let cookieVal = null; + + if (document.cookie && '' !== document.cookie) { + const cookies = document.cookie.split(';'); + let i = 0; + while (i < cookies.length) { + const cookie = cookies[i].trim(); + if ('csrftoken=' === cookie.substring(0, 10)) { + cookieVal = decodeURIComponent(cookie.substring(10)); + break; + } + i += 1; + } + } + + return cookieVal; +} diff --git a/frontend/src/static/js/utils/helpers/dom.js b/frontend/src/static/js/utils/helpers/dom.js deleted file mode 100755 index 8e725a90..00000000 --- a/frontend/src/static/js/utils/helpers/dom.js +++ /dev/null @@ -1,79 +0,0 @@ -export function supportsSvgAsImg() { - // @link: https://github.com/Modernizr/Modernizr/blob/master/feature-detects/svg/asimg.js - return document.implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#Image', '1.1'); -} - -export function removeClassname(el, cls) { - if (el.classList) { - el.classList.remove(cls); - } else { - el.className = el.className.replace(new RegExp('(^|\\b)' + cls.split(' ').join('|') + '(\\b|$)', 'gi'), ' '); - } -} - -export function addClassname(el, cls) { - if (el.classList) { - el.classList.add(cls); - } else { - el.className += ' ' + cls; - } -} - -export function hasClassname(el, cls) { - return el.className && new RegExp('(\\s|^)' + cls + '(\\s|$)').test(el.className); -} - -export const cancelAnimationFrame = window.cancelAnimationFrame || window.mozCancelAnimationFrame; - -export const requestAnimationFrame = - window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; - -export function BrowserEvents() { - const callbacks = { - document: { - visibility: [], - }, - window: { - resize: [], - scroll: [], - }, - }; - - function onDocumentVisibilityChange() { - callbacks.document.visibility.map((fn) => fn()); - } - - function onWindowResize() { - callbacks.window.resize.map((fn) => fn()); - } - - function onWindowScroll() { - callbacks.window.scroll.map((fn) => fn()); - } - - function windowEvents(resizeCallback, scrollCallback) { - if ('function' === typeof resizeCallback) { - callbacks.window.resize.push(resizeCallback); - } - - if ('function' === typeof scrollCallback) { - callbacks.window.scroll.push(scrollCallback); - } - } - - function documentEvents(visibilityChangeCallback) { - if ('function' === typeof visibilityChangeCallback) { - callbacks.document.visibility.push(visibilityChangeCallback); - } - } - - document.addEventListener('visibilitychange', onDocumentVisibilityChange); - - window.addEventListener('resize', onWindowResize); - window.addEventListener('scroll', onWindowScroll); - - return { - doc: documentEvents, - win: windowEvents, - }; -} diff --git a/frontend/src/static/js/utils/helpers/dom.ts b/frontend/src/static/js/utils/helpers/dom.ts new file mode 100755 index 00000000..00ddb6b3 --- /dev/null +++ b/frontend/src/static/js/utils/helpers/dom.ts @@ -0,0 +1,95 @@ +export function supportsSvgAsImg() { + // @link: https://github.com/Modernizr/Modernizr/blob/master/feature-detects/svg/asimg.js + return document.implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#Image', '1.1'); +} + +export function removeClassname(el: HTMLElement, cls: string) { + if (el.classList) { + el.classList.remove(cls); + } else { + el.className = el.className.replace(new RegExp('(^|\\b)' + cls.split(' ').join('|') + '(\\b|$)', 'gi'), ' '); + } +} + +export function addClassname(el: HTMLElement, cls: string) { + if (el.classList) { + el.classList.add(cls); + } else { + el.className += ' ' + cls; + } +} + +export function hasClassname(el: HTMLElement, cls: string) { + return el.className && new RegExp('(\\s|^)' + cls + '(\\s|$)').test(el.className); +} + +type LegacyWindow = Window & { + mozCancelAnimationFrame?: Window['cancelAnimationFrame']; + mozRequestAnimationFrame?: Window['requestAnimationFrame']; + msRequestAnimationFrame?: Window['requestAnimationFrame']; + webkitRequestAnimationFrame?: Window['requestAnimationFrame']; +}; + +const legacyWindow = window as LegacyWindow; + +export const cancelAnimationFrame: Window['cancelAnimationFrame'] = + legacyWindow.cancelAnimationFrame || + legacyWindow.mozCancelAnimationFrame || + ((id: number) => window.clearTimeout(id)); + +export const requestAnimationFrame: Window['requestAnimationFrame'] = + legacyWindow.requestAnimationFrame || + legacyWindow.mozRequestAnimationFrame || + legacyWindow.webkitRequestAnimationFrame || + legacyWindow.msRequestAnimationFrame || + ((callback: FrameRequestCallback) => window.setTimeout(() => callback(performance.now()), 16)); + +export function BrowserEvents() { + const callbacks = { + document: { + visibility: [] as Function[], + }, + window: { + resize: [] as Function[], + scroll: [] as Function[], + }, + }; + + function onDocumentVisibilityChange() { + callbacks.document.visibility.map((fn) => fn()); + } + + function onWindowResize() { + callbacks.window.resize.map((fn) => fn()); + } + + function onWindowScroll() { + callbacks.window.scroll.map((fn) => fn()); + } + + function windowEvents(resizeCallback?: Function, scrollCallback?: Function) { + if ('function' === typeof resizeCallback) { + callbacks.window.resize.push(resizeCallback); + } + + if ('function' === typeof scrollCallback) { + callbacks.window.scroll.push(scrollCallback); + } + } + + function documentEvents(visibilityChangeCallback?: Function) { + if ('function' === typeof visibilityChangeCallback) { + callbacks.document.visibility.push(visibilityChangeCallback); + } + } + + document.addEventListener('visibilitychange', onDocumentVisibilityChange); + + window.addEventListener('resize', onWindowResize); + window.addEventListener('scroll', onWindowScroll); + + return { + doc: documentEvents, + win: windowEvents, + }; +} diff --git a/frontend/src/static/js/utils/helpers/embeddedApp.ts b/frontend/src/static/js/utils/helpers/embeddedApp.ts index 37b44d21..14107a5f 100644 --- a/frontend/src/static/js/utils/helpers/embeddedApp.ts +++ b/frontend/src/static/js/utils/helpers/embeddedApp.ts @@ -7,7 +7,7 @@ export function inEmbeddedApp() { sessionStorage.setItem('media_cms_embed_mode', 'true'); return true; } - + if (mode === 'standard') { sessionStorage.removeItem('media_cms_embed_mode'); return false; diff --git a/frontend/src/static/js/utils/helpers/errors.js b/frontend/src/static/js/utils/helpers/errors.js deleted file mode 100755 index e22b5dd0..00000000 --- a/frontend/src/static/js/utils/helpers/errors.js +++ /dev/null @@ -1,27 +0,0 @@ -// TODO: Improve or (even better) remove this file code. - -import { error as logErrFn, warn as logWarnFn } from './log'; - -function logAndReturnError(logFn, msgArr, ErrorConstructor) { - let err; - switch (ErrorConstructor) { - case TypeError: - case RangeError: - case SyntaxError: - case ReferenceError: - err = new ErrorConstructor(msgArr[0]); - break; - default: - err = new Error(msgArr[0]); - } - logFn(err.message, ...msgArr.slice(1)); - return err; -} - -export function logErrorAndReturnError(msgArr, ErrorConstructor) { - return logAndReturnError(logErrFn, msgArr, ErrorConstructor); -} - -export function logWarningAndReturnError(msgArr, ErrorConstructor) { - return logAndReturnError(logWarnFn, msgArr, ErrorConstructor); -} diff --git a/frontend/src/static/js/utils/helpers/errors.ts b/frontend/src/static/js/utils/helpers/errors.ts new file mode 100755 index 00000000..110ac143 --- /dev/null +++ b/frontend/src/static/js/utils/helpers/errors.ts @@ -0,0 +1,15 @@ +// @todo: Improve or (even better) remove this file. + +import { error, warn } from './log'; + +export function logErrorAndReturnError(msgArr: string[]) { + const err = new Error(msgArr[0]); + error(...msgArr); + return err; +} + +export function logWarningAndReturnError(msgArr: string[]) { + const err = new Error(msgArr[0]); + warn(...msgArr); + return err; +} diff --git a/frontend/src/static/js/utils/helpers/exportStore.js b/frontend/src/static/js/utils/helpers/exportStore.js deleted file mode 100755 index 643e2b1b..00000000 --- a/frontend/src/static/js/utils/helpers/exportStore.js +++ /dev/null @@ -1,5 +0,0 @@ -import * as dispatcher from '../dispatcher.js'; -export default function (store, handler) { - dispatcher.register(store[handler].bind(store)); - return store; -} diff --git a/frontend/src/static/js/utils/helpers/exportStore.ts b/frontend/src/static/js/utils/helpers/exportStore.ts new file mode 100755 index 00000000..1cd58320 --- /dev/null +++ b/frontend/src/static/js/utils/helpers/exportStore.ts @@ -0,0 +1,28 @@ +import EventEmitter from 'events'; +import { dispatcher } from '../dispatcher'; + +// type ClassProperties = { +// [Key in keyof C as C[Key] extends Function ? never : Key]: C[Key]; +// }; + +type ClassMethods = { + [Key in keyof C as C[Key] extends Function ? Key : never]: C[Key]; +}; + +// @todo: Check this again +export function exportStore>( + store: TStore, + handler: THandler +) { + const method = store[handler] as Function; + const callback: (payload: unknown) => void = method.bind(store); + dispatcher.register(callback); + return store; +} + +// @todo: Remove older vesion. +// export function exportStore_OLD(store, handler) { +// const callback = store[handler].bind(store); +// dispatcher.register(callback); +// return store; +// } diff --git a/frontend/src/static/js/utils/helpers/formatInnerLink.js b/frontend/src/static/js/utils/helpers/formatInnerLink.js deleted file mode 100755 index d31f0a98..00000000 --- a/frontend/src/static/js/utils/helpers/formatInnerLink.js +++ /dev/null @@ -1,11 +0,0 @@ -import urlParse from 'url-parse'; - -export function formatInnerLink(url, baseUrl) { - let link = urlParse(url, {}); - - if ('' === link.origin || 'null' === link.origin || !link.origin) { - link = urlParse(baseUrl + '/' + url.replace(/^\//g, ''), {}); - } - - return link.toString(); -} diff --git a/frontend/src/static/js/utils/helpers/formatInnerLink.ts b/frontend/src/static/js/utils/helpers/formatInnerLink.ts new file mode 100755 index 00000000..cdcd77ff --- /dev/null +++ b/frontend/src/static/js/utils/helpers/formatInnerLink.ts @@ -0,0 +1,11 @@ +import urlParse from 'url-parse'; + +export function formatInnerLink(url: string, baseUrl: string) { + let link = urlParse(url, {}); + + if ('' === link.origin || 'null' === link.origin || !link.origin) { + link = urlParse(baseUrl + '/' + url.replace(/^\//g, ''), {}); + } + + return link.toString(); +} diff --git a/frontend/src/static/js/utils/helpers/formatManagementTableDate.js b/frontend/src/static/js/utils/helpers/formatManagementTableDate.js deleted file mode 100755 index 3a610286..00000000 --- a/frontend/src/static/js/utils/helpers/formatManagementTableDate.js +++ /dev/null @@ -1,15 +0,0 @@ -import { months as monthList } from '../constants/'; - -export function formatManagementTableDate(date) { - const day = date.getDate(); - const month = monthList[date.getMonth()].substring(0, 3); - const year = date.getFullYear(); - const hours = date.getHours(); - const minutes = date.getMinutes(); - const seconds = date.getSeconds(); - let ret = month + ' ' + day + ', ' + year; - ret += ' ' + (hours < 10 ? '0' : '') + hours; - ret += ':' + (minutes < 10 ? '0' : '') + minutes; - ret += ':' + (seconds < 10 ? '0' : '') + seconds; - return ret; -} diff --git a/frontend/src/static/js/utils/helpers/formatManagementTableDate.ts b/frontend/src/static/js/utils/helpers/formatManagementTableDate.ts new file mode 100755 index 00000000..ae36a8bb --- /dev/null +++ b/frontend/src/static/js/utils/helpers/formatManagementTableDate.ts @@ -0,0 +1,15 @@ +import { months as monthList } from '../constants'; + +export function formatManagementTableDate(date: Date) { + const day = date.getDate(); + const month = monthList[date.getMonth()].substring(0, 3); + const year = date.getFullYear(); + const hours = date.getHours(); + const minutes = date.getMinutes(); + const seconds = date.getSeconds(); + let ret = month + ' ' + day + ', ' + year; + ret += ' ' + (hours < 10 ? '0' : '') + hours; + ret += ':' + (minutes < 10 ? '0' : '') + minutes; + ret += ':' + (seconds < 10 ? '0' : '') + seconds; + return ret; +} diff --git a/frontend/src/static/js/utils/helpers/formatViewsNumber.js b/frontend/src/static/js/utils/helpers/formatViewsNumber.js deleted file mode 100755 index e8dfe5f2..00000000 --- a/frontend/src/static/js/utils/helpers/formatViewsNumber.js +++ /dev/null @@ -1,18 +0,0 @@ -export default function (views_number, fullNumber) { - function formattedValue(val, lim, unit) { - return Number(parseFloat(val / lim).toFixed(val < 10 * lim ? 1 : 0)) + unit; - } - - function format(i, views, mult, compare, limit, units) { - while (views >= compare) { - limit *= mult; - compare *= mult; - i += 1; - } - return i < units.length - ? formattedValue(views, limit, units[i]) - : formattedValue(views * (mult * (i - (units.length - 1))), limit, units[units.length - 1]); - } - - return fullNumber ? views_number.toLocaleString() : format(0, views_number, 1000, 1000, 1, ['', 'K', 'M', 'B', 'T']); -} diff --git a/frontend/src/static/js/utils/helpers/formatViewsNumber.ts b/frontend/src/static/js/utils/helpers/formatViewsNumber.ts new file mode 100755 index 00000000..fa0365d3 --- /dev/null +++ b/frontend/src/static/js/utils/helpers/formatViewsNumber.ts @@ -0,0 +1,17 @@ +const formattedValue = (val: number, lim: number, unit: string) => + Number((val / lim).toFixed(val < 10 * lim ? 1 : 0)) + unit; + +function format(cntr: number, views: number, mult: number, compare: number, limit: number, units: string[]) { + let i = cntr; + while (views >= compare) { + limit *= mult; + compare *= mult; + i += 1; + } + return i < units.length + ? formattedValue(views, limit, units[i]) + : formattedValue(views * (mult * (i - (units.length - 1))), limit, units[units.length - 1]); +} + +export const formatViewsNumber = (views_number: number, fullNumber?: boolean) => + fullNumber ? views_number.toLocaleString() : format(0, views_number, 1000, 1000, 1, ['', 'K', 'M', 'B', 'T']); diff --git a/frontend/src/static/js/utils/helpers/imageExtension.js b/frontend/src/static/js/utils/helpers/imageExtension.js deleted file mode 100755 index 509aa90d..00000000 --- a/frontend/src/static/js/utils/helpers/imageExtension.js +++ /dev/null @@ -1,7 +0,0 @@ -export const imageExtension = (img) => { - if (!img) { - return; - } - const ext = img.split('.'); - return ext[ext.length - 1]; -}; diff --git a/frontend/src/static/js/utils/helpers/imageExtension.ts b/frontend/src/static/js/utils/helpers/imageExtension.ts new file mode 100755 index 00000000..7538bff6 --- /dev/null +++ b/frontend/src/static/js/utils/helpers/imageExtension.ts @@ -0,0 +1,5 @@ +export const imageExtension = (img: string) => { + if (img) { + return img.split('.').pop(); + } +}; diff --git a/frontend/src/static/js/utils/helpers/index.js b/frontend/src/static/js/utils/helpers/index.js deleted file mode 100644 index 0376b929..00000000 --- a/frontend/src/static/js/utils/helpers/index.js +++ /dev/null @@ -1,17 +0,0 @@ -export * from './dom'; -export * from './errors'; -export { default as exportStore } from './exportStore'; -export { formatInnerLink } from './formatInnerLink'; -export * from './formatManagementTableDate'; -export { default as formatViewsNumber } from './formatViewsNumber'; -export * from './csrfToken'; -export { imageExtension } from './imageExtension'; -export * from './log'; -export * from './math'; -export * from './propTypeFilters'; -export { default as publishedOnDate } from './publishedOnDate'; -export * from './quickSort'; -export * from './requests'; -export { translateString } from './translate'; -export { replaceString } from './replacementStrings'; -export * from './embeddedApp'; diff --git a/frontend/src/static/js/utils/helpers/index.ts b/frontend/src/static/js/utils/helpers/index.ts new file mode 100644 index 00000000..1fa663ed --- /dev/null +++ b/frontend/src/static/js/utils/helpers/index.ts @@ -0,0 +1,17 @@ +export * from './csrfToken'; +export * from './dom'; +export * from './embeddedApp'; +export * from './errors'; +export * from './exportStore'; +export * from './formatInnerLink'; +export * from './formatManagementTableDate'; +export * from './formatViewsNumber'; +export * from './imageExtension'; +export * from './log'; +export * from './math'; +export * from './propTypeFilters'; +export * from './publishedOnDate'; +export * from './quickSort'; +export * from './requests'; +export * from './translate'; +export * from './replacementStrings'; diff --git a/frontend/src/static/js/utils/helpers/log.js b/frontend/src/static/js/utils/helpers/log.js deleted file mode 100755 index 419e02e8..00000000 --- a/frontend/src/static/js/utils/helpers/log.js +++ /dev/null @@ -1,4 +0,0 @@ -const log = (...x) => console[x[0]](...x.slice(1)); - -export const warn = (...x) => log('warn', ...x); -export const error = (...x) => log('error', ...x); diff --git a/frontend/src/static/js/utils/helpers/log.ts b/frontend/src/static/js/utils/helpers/log.ts new file mode 100755 index 00000000..ba587cb3 --- /dev/null +++ b/frontend/src/static/js/utils/helpers/log.ts @@ -0,0 +1,9 @@ +// @todo: Delete this file + +export const warn = (...x: string[]) => { + console.warn(...x); +}; + +export const error = (...x: string[]) => { + console.error(...x); +}; diff --git a/frontend/src/static/js/utils/helpers/math.js b/frontend/src/static/js/utils/helpers/math.js deleted file mode 100755 index 124670bd..00000000 --- a/frontend/src/static/js/utils/helpers/math.js +++ /dev/null @@ -1,10 +0,0 @@ -export const isGt = (x, y) => x > y; -export const isZero = (x) => 0 === x; -export const isNumber = (x) => !isNaN(x) && x === 0 + x; -export const isInteger = (x) => x === Math.trunc(x); -export const isPositive = (x) => isGt(x, 0); -export const isPositiveNumber = (x) => isNumber(x) && isPositive(x); -export const isPositiveInteger = (x) => isInteger(x) && isPositive(x); -export const isPositiveIntegerOrZero = (x) => isInteger(x) && (isPositive(x) || isZero(x)); - -export const greaterCommonDivision = (a, b) => (!b ? a : greaterCommonDivision(b, a % b)); diff --git a/frontend/src/static/js/utils/helpers/math.ts b/frontend/src/static/js/utils/helpers/math.ts new file mode 100755 index 00000000..3b3e4ad7 --- /dev/null +++ b/frontend/src/static/js/utils/helpers/math.ts @@ -0,0 +1,10 @@ +export const isGt = (x: number, y: number) => x > y; +export const isZero = (x: number) => 0 === x; +export const isNumber = (x: number) => 'number' === typeof x && !Number.isNaN(x); +export const isInteger = (x: number) => x === Math.trunc(x); +export const isPositive = (x: number) => isGt(x, 0); +export const isPositiveNumber = (x: number) => isNumber(x) && isPositive(x); +export const isPositiveInteger = (x: number) => isInteger(x) && isPositive(x); +export const isPositiveIntegerOrZero = (x: number) => isInteger(x) && (isPositive(x) || isZero(x)); + +export const greaterCommonDivision = (a: number, b: number): number => (!b ? a : greaterCommonDivision(b, a % b)); diff --git a/frontend/src/static/js/utils/helpers/propTypeFilters.js b/frontend/src/static/js/utils/helpers/propTypeFilters.ts similarity index 71% rename from frontend/src/static/js/utils/helpers/propTypeFilters.js rename to frontend/src/static/js/utils/helpers/propTypeFilters.ts index 8aea2fde..c989f3a1 100755 --- a/frontend/src/static/js/utils/helpers/propTypeFilters.js +++ b/frontend/src/static/js/utils/helpers/propTypeFilters.ts @@ -1,10 +1,10 @@ import { logErrorAndReturnError } from './errors'; +import { isPositiveInteger, isPositiveIntegerOrZero } from './math'; +// @todo: Check this export const PositiveIntegerOrZero = (function () { - const isPositiveIntegerOrZero = (x) => x === Math.trunc(x) && x >= 0; - - return function (obj, key, comp) { - return void 0 === obj[key] || isPositiveIntegerOrZero(obj[key]) + return function (obj: Record, key: string, comp: string) { + return obj[key] === undefined || isPositiveIntegerOrZero(obj[key]) ? null : logErrorAndReturnError([ 'Invalid prop `' + @@ -20,11 +20,10 @@ export const PositiveIntegerOrZero = (function () { }; })(); +// @todo: Check this export const PositiveInteger = (function () { - const isPositiveInteger = (x) => x === Math.trunc(x) && x > 0; - - return function (obj, key, comp) { - return void 0 === obj[key] || isPositiveInteger(obj[key]) + return function (obj: Record, key: string, comp: string) { + return obj[key] === undefined || isPositiveInteger(obj[key]) ? null : logErrorAndReturnError([ 'Invalid prop `' + diff --git a/frontend/src/static/js/utils/helpers/publishedOnDate.js b/frontend/src/static/js/utils/helpers/publishedOnDate.js deleted file mode 100755 index 165d498c..00000000 --- a/frontend/src/static/js/utils/helpers/publishedOnDate.js +++ /dev/null @@ -1,17 +0,0 @@ -import { months } from '../constants'; - -export default function publishedOnDate(date, type) { - if (date instanceof Date) { - type = 0 + type; - type = 0 < type ? type : 1; - switch (type) { - case 1: - return months[date.getMonth()].substring(0, 3) + ' ' + date.getDate() + ', ' + date.getFullYear(); - case 2: - return date.getDate() + ' ' + months[date.getMonth()].substring(0, 3) + ' ' + date.getFullYear(); - case 3: - return date.getDate() + ' ' + months[date.getMonth()] + ' ' + date.getFullYear(); - } - } - return null; -} diff --git a/frontend/src/static/js/utils/helpers/publishedOnDate.ts b/frontend/src/static/js/utils/helpers/publishedOnDate.ts new file mode 100755 index 00000000..ae8f645f --- /dev/null +++ b/frontend/src/static/js/utils/helpers/publishedOnDate.ts @@ -0,0 +1,17 @@ +import { months } from '../constants'; + +export function publishedOnDate(date: Date, type: 1 | 2 | 3 = 1) { + if (!(date instanceof Date)) { + return null; + } + + if (type === 2) { + return date.getDate() + ' ' + months[date.getMonth()].substring(0, 3) + ' ' + date.getFullYear(); + } + + if (type === 3) { + return date.getDate() + ' ' + months[date.getMonth()] + ' ' + date.getFullYear(); + } + + return months[date.getMonth()].substring(0, 3) + ' ' + date.getDate() + ', ' + date.getFullYear(); +} diff --git a/frontend/src/static/js/utils/helpers/quickSort.js b/frontend/src/static/js/utils/helpers/quickSort.js deleted file mode 100755 index b0d91874..00000000 --- a/frontend/src/static/js/utils/helpers/quickSort.js +++ /dev/null @@ -1,35 +0,0 @@ -function swap(arr, i, j) { - var temp = arr[i]; - arr[i] = arr[j]; - arr[j] = temp; -} - -function partition(arr, pivot, left, right) { - var pivotValue = arr[pivot], - partitionIndex = left; - - for (var i = left; i < right; i++) { - if (arr[i] < pivotValue) { - swap(arr, i, partitionIndex); - partitionIndex++; - } - } - swap(arr, right, partitionIndex); - return partitionIndex; -} - -export function quickSort(arr, left, right) { - var len = arr.length, - pivot, - partitionIndex; - - if (left < right) { - pivot = right; - partitionIndex = partition(arr, pivot, left, right); - - //sort left and right - quickSort(arr, left, partitionIndex - 1); - quickSort(arr, partitionIndex + 1, right); - } - return arr; -} diff --git a/frontend/src/static/js/utils/helpers/quickSort.ts b/frontend/src/static/js/utils/helpers/quickSort.ts new file mode 100755 index 00000000..75edc35d --- /dev/null +++ b/frontend/src/static/js/utils/helpers/quickSort.ts @@ -0,0 +1,29 @@ +function swap(arr: unknown[], i: number, j: number) { + const temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; +} + +function partition(arr: number[], pivot: number, left: number, right: number) { + const pivotValue = arr[pivot]; + let partitionIndex = left; + for (let i = left; i < right; i++) { + if (arr[i] < pivotValue) { + swap(arr, i, partitionIndex); + partitionIndex++; + } + } + swap(arr, right, partitionIndex); + return partitionIndex; +} + +export function quickSort(arr: number[], left: number, right: number) { + if (left < right) { + const pivot = right; + const partitionIndex = partition(arr, pivot, left, right); + //sort left and right + quickSort(arr, left, partitionIndex - 1); + quickSort(arr, partitionIndex + 1, right); + } + return arr; +} diff --git a/frontend/src/static/js/utils/helpers/replacementStrings.js b/frontend/src/static/js/utils/helpers/replacementStrings.js deleted file mode 100644 index 7e193fa9..00000000 --- a/frontend/src/static/js/utils/helpers/replacementStrings.js +++ /dev/null @@ -1,15 +0,0 @@ -// check templates/config/installation/translations.html for more - -export function replaceString(word) { - if (!window.REPLACEMENTS) { - return word; - } - - let result = word; - - for (const [search, replacement] of Object.entries(window.REPLACEMENTS)) { - result = result.split(search).join(replacement); - } - - return result; -} diff --git a/frontend/src/static/js/utils/helpers/replacementStrings.ts b/frontend/src/static/js/utils/helpers/replacementStrings.ts new file mode 100644 index 00000000..bd6662f6 --- /dev/null +++ b/frontend/src/static/js/utils/helpers/replacementStrings.ts @@ -0,0 +1,47 @@ +// check templates/config/installation/translations.html for more + +declare global { + interface Window { + REPLACEMENTS?: Record; + } +} + +export function replaceString(word: string) { + if (!window.REPLACEMENTS) { + return word; + } + + let result = word; + + for (const [search, replacement] of Object.entries(window.REPLACEMENTS)) { + result = result.split(search).join(replacement); + } + + return result; +} + +// @todo: Check this alterative. +/*function replaceStringRegExp(word: string) { + if (!window.REPLACEMENTS) { + return word; + } + + const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + + let result = word; + + for (const [search, replacement] of Object.entries(window.REPLACEMENTS)) { + const regex = new RegExp(escapeRegExp(search), 'g'); + result = result.replace(regex, replacement); + } + + return result; +}*/ + +// @todo: Remove older vesion. +/*export function replaceString_OLD(string: string) { + for (const key in window.REPLACEMENTS) { + string = string.replace(key, window.REPLACEMENTS[key]); + } + return string; +}*/ diff --git a/frontend/src/static/js/utils/helpers/requests.js b/frontend/src/static/js/utils/helpers/requests.js deleted file mode 100644 index 9f55b944..00000000 --- a/frontend/src/static/js/utils/helpers/requests.js +++ /dev/null @@ -1,135 +0,0 @@ -import axios from 'axios'; - -export async function getRequest(url, sync, callback, errorCallback) { - const requestConfig = { - timeout: null, - maxContentLength: null, - }; - - function responseHandler(result) { - if (callback instanceof Function || typeof callback === 'function') { - callback(result); - } - } - - function errorHandler(error) { - if (errorCallback instanceof Function || typeof errorCallback === 'function') { - let err = error; - if (void 0 === error.response) { - err = { - type: 'network', - error: error, - }; - } else if (void 0 !== error.response.status) { - // TODO: Improve this, it's valid only in case of media requests. - switch (error.response.status) { - case 401: - err = { - type: 'private', - error: error, - message: 'Media is private', - }; - break; - case 400: - err = { - type: 'unavailable', - error: error, - message: 'Media is unavailable', - }; - break; - } - } - errorCallback(err); - } - } - - if (sync) { - await axios.get(url, requestConfig) - .then(responseHandler) - .catch(errorHandler || null); - } else { - axios.get(url, requestConfig) - .then(responseHandler) - .catch(errorHandler || null); - } -} - -export async function postRequest(url, postData, configData, sync, callback, errorCallback) { - postData = postData || {}; - - function responseHandler(result) { - if (callback instanceof Function || typeof callback === 'function') { - callback(result); - } - } - - function errorHandler(error) { - if (errorCallback instanceof Function || typeof errorCallback === 'function') { - errorCallback(error); - } - } - - if (sync) { - await axios.post(url, postData, configData || null) - .then(responseHandler) - .catch(errorHandler || null); - } else { - axios.post(url, postData, configData || null) - .then(responseHandler) - .catch(errorHandler || null); - } -} - -export async function putRequest(url, putData, configData, sync, callback, errorCallback) { - putData = putData || {}; - - function responseHandler(result) { - if (callback instanceof Function || typeof callback === 'function') { - callback(result); - } - } - - function errorHandler(error) { - if (errorCallback instanceof Function || typeof errorCallback === 'function') { - errorCallback(error); - } - } - - if (sync) { - await axios.put(url, putData, configData || null) - .then(responseHandler) - .catch(errorHandler || null); - } else { - axios.put(url, putData, configData || null) - .then(responseHandler) - .catch(errorHandler || null); - } -} - -export async function deleteRequest(url, configData, sync, callback, errorCallback) { - configData = configData || {}; - - function responseHandler(result) { - if (callback instanceof Function || typeof callback === 'function') { - callback(result); - } - } - - function errorHandler(error) { - if (errorCallback instanceof Function || typeof errorCallback === 'function') { - errorCallback(error); - } - } - - if (sync) { - await axios - .delete(url, configData || null) - .then(responseHandler) - .catch(errorHandler || null); - } else { - axios - .delete(url, configData || null) - .then(responseHandler) - .catch(errorHandler || null); - } -} diff --git a/frontend/src/static/js/utils/helpers/requests.ts b/frontend/src/static/js/utils/helpers/requests.ts new file mode 100644 index 00000000..f7eb381b --- /dev/null +++ b/frontend/src/static/js/utils/helpers/requests.ts @@ -0,0 +1,169 @@ +import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'; + +export async function getRequest( + url: string, + sync: boolean = false, + callback?: (response: AxiosResponse) => void, + errorCallback?: (err: any) => void +) { + const requestConfig = { + timeout: undefined, + maxContentLength: undefined, + }; + + function responseHandler(result: AxiosResponse) { + if (callback) { + callback(result); + } + } + + function errorHandler(reason: any) { + if (!errorCallback) { + return; + } + + let err = reason; + if (reason.response === undefined) { + err = { + type: 'network', + error: reason, + }; + } else if (reason.response.status !== undefined) { + // @todo: Improve this, it's valid only in case of media requests. + switch (reason.response.status) { + case 401: + err = { + type: 'private', + error: reason, + message: 'Media is private', + }; + break; + case 400: + err = { + type: 'unavailable', + error: reason, + message: 'Media is unavailable', + }; + break; + } + } + + errorCallback(err); + } + + if (sync) { + await axios + .get(url, requestConfig) + .then(responseHandler) + .catch(errorHandler || null); + } else { + axios + .get(url, requestConfig) + .then(responseHandler) + .catch(errorHandler || null); + } +} + +export async function postRequest( + url: string, + postData: any, + configData?: AxiosRequestConfig, + sync: boolean = false, + callback?: (response: AxiosResponse) => void, + errorCallback?: (error: any) => void +) { + postData = postData || {}; + + function responseHandler(result: AxiosResponse) { + if (callback) { + callback(result); + } + } + + function errorHandler(error: any) { + if (errorCallback) { + errorCallback(error); + } + } + + if (sync) { + await axios + .post(url, postData, configData) + .then(responseHandler) + .catch(errorHandler || null); + } else { + axios + .post(url, postData, configData) + .then(responseHandler) + .catch(errorHandler || null); + } +} + +export async function putRequest( + url: string, + putData: any, + configData?: AxiosRequestConfig, + sync: boolean = false, + callback?: (response: AxiosResponse) => void, + errorCallback?: (error: any) => void +) { + putData = putData || {}; + + function responseHandler(result: AxiosResponse) { + if (callback) { + callback(result); + } + } + + function errorHandler(error: any) { + if (errorCallback) { + errorCallback(error); + } + } + + if (sync) { + await axios + .put(url, putData, configData) + .then(responseHandler) + .catch(errorHandler || null); + } else { + axios + .put(url, putData, configData) + .then(responseHandler) + .catch(errorHandler || null); + } +} + +export async function deleteRequest( + url: string, + configData?: AxiosRequestConfig, + sync: boolean = false, + callback?: (response: AxiosResponse) => void, + errorCallback?: (error: any) => void +) { + configData = configData || {}; + + function responseHandler(result: AxiosResponse) { + if (callback) { + callback(result); + } + } + + function errorHandler(error: any) { + if (errorCallback) { + errorCallback(error); + } + } + + if (sync) { + await axios + .delete(url, configData) + .then(responseHandler) + .catch(errorHandler || null); + } else { + axios + .delete(url, configData || null) + .then(responseHandler) + .catch(errorHandler || null); + } +} diff --git a/frontend/src/static/js/utils/helpers/translate.js b/frontend/src/static/js/utils/helpers/translate.js deleted file mode 100644 index e1659167..00000000 --- a/frontend/src/static/js/utils/helpers/translate.js +++ /dev/null @@ -1,5 +0,0 @@ -// check templates/config/installation/translations.html for more - -export function translateString(str) { - return window.TRANSLATION?.[str] ?? str; -} diff --git a/frontend/src/static/js/utils/helpers/translate.ts b/frontend/src/static/js/utils/helpers/translate.ts new file mode 100644 index 00000000..ca0a1755 --- /dev/null +++ b/frontend/src/static/js/utils/helpers/translate.ts @@ -0,0 +1,11 @@ +// check templates/config/installation/translations.html for more + +declare global { + interface Window { + TRANSLATION?: Record; + } +} + +export function translateString(word: string) { + return window.TRANSLATION?.[word] ?? word; +} diff --git a/frontend/tests/utils/helpers/dom.test.ts b/frontend/tests/utils/helpers/dom.test.ts index 8e61183c..5aaac8c0 100644 --- a/frontend/tests/utils/helpers/dom.test.ts +++ b/frontend/tests/utils/helpers/dom.test.ts @@ -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).requestAnimationFrame; + } + + if (cancelAnimationFrameDescriptor) { + Object.defineProperty(window, 'cancelAnimationFrame', cancelAnimationFrameDescriptor); + } else { + delete (window as Partial).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); + }); + }); }); }); diff --git a/frontend/tests/utils/helpers/errors.test.ts b/frontend/tests/utils/helpers/errors.test.ts index 7dd40b12..9e170697 100644 --- a/frontend/tests/utils/helpers/errors.test.ts +++ b/frontend/tests/utils/helpers/errors.test.ts @@ -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); }); }); }); diff --git a/frontend/tests/utils/helpers/exportStore.test.ts b/frontend/tests/utils/helpers/exportStore.test.ts index aa0a363b..d94db3aa 100644 --- a/frontend/tests/utils/helpers/exportStore.test.ts +++ b/frontend/tests/utils/helpers/exportStore.test.ts @@ -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(); }); }); }); diff --git a/frontend/tests/utils/helpers/formatViewsNumber.test.ts b/frontend/tests/utils/helpers/formatViewsNumber.test.ts index 939b4192..269e01a6 100644 --- a/frontend/tests/utils/helpers/formatViewsNumber.test.ts +++ b/frontend/tests/utils/helpers/formatViewsNumber.test.ts @@ -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', () => { diff --git a/frontend/tests/utils/helpers/publishedOnDate.test.ts b/frontend/tests/utils/helpers/publishedOnDate.test.ts index e992adff..1b94965f 100644 --- a/frontend/tests/utils/helpers/publishedOnDate.test.ts +++ b/frontend/tests/utils/helpers/publishedOnDate.test.ts @@ -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)); diff --git a/frontend/tests/utils/helpers/requests.test.ts b/frontend/tests/utils/helpers/requests.test.ts index c78b700f..5a85ffdf 100644 --- a/frontend/tests/utils/helpers/requests.test.ts +++ b/frontend/tests/utils/helpers/requests.test.ts @@ -5,7 +5,7 @@ jest.mock('axios'); const mockedAxios = axios as jest.Mocked; -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 () => {