feat: frontend unit tests

This commit is contained in:
Yiannis Stergiou
2026-01-07 19:47:54 +02:00
committed by GitHub
parent ed5cfa1a84
commit 1c15880ae3
74 changed files with 6000 additions and 877 deletions

View File

@@ -0,0 +1,42 @@
name: Frontend build and test
on:
pull_request:
workflow_dispatch:
concurrency:
group: ${{ github.head_ref || github.ref }}
cancel-in-progress: true
jobs:
build-and-test:
strategy:
matrix:
os: [ubuntu-latest]
node: [20]
runs-on: ${{ matrix.os }}
name: '${{ matrix.os }} - node v${{ matrix.node }}'
permissions:
contents: read
defaults:
run:
working-directory: ./frontend
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- name: Install dependencies
run: npm install
- name: Build script
run: npm run dist
- name: Test script
run: npm run test

View File

@@ -1 +1 @@
VERSION = "7.5"
VERSION = "7.6"

View File

@@ -1,12 +1,28 @@
{
"presets": [
"@babel/react", ["@babel/env", {
"modules": false,
"useBuiltIns": "usage",
"corejs": 3,
"targets": {
"browsers": ["defaults"]
}
}]
]
"presets": [
"@babel/react",
[
"@babel/env",
{
"modules": false,
"useBuiltIns": "usage",
"corejs": 3,
"targets": {
"browsers": ["defaults"]
}
}
]
],
"env": {
"test": {
"presets": [
[
"@babel/env",
{
"targets": { "node": "current" }
}
]
]
}
}
}

View File

@@ -27,3 +27,39 @@ Open in browser: [http://localhost:8088](http://localhost:8088)
Generates the folder "**_frontend/dist_**".
Copy folders and files from "**_frontend/dist/static_**" into "**_static_**".
---
### Test Scripts
#### test
Run all unit tests once.
```sh
npm run test
```
#### test-watch
Run tests in watch mode for development.
```sh
npm run test-watch
```
#### test-coverage
Run tests with coverage reporting in `./coverage` folder.
```sh
npm run test-coverage
```
#### test-coverage-watch
Run tests with coverage in watch mode.
```sh
npm run test-coverage-watch
```

9
frontend/jest.config.js Normal file
View File

@@ -0,0 +1,9 @@
/** @type {import("jest").Config} **/
module.exports = {
testEnvironment: 'jsdom',
transform: {
'^.+\\.tsx?$': 'ts-jest',
'^.+\\.jsx?$': 'babel-jest',
},
collectCoverageFrom: ['src/**'],
};

View File

@@ -1,57 +1,69 @@
{
"name": "mediacms-frontend",
"version": "0.9.1",
"description": "",
"author": "",
"license": "",
"keywords": [],
"main": "index.js",
"scripts": {
"start": "mediacms-scripts development --config=./config/mediacms.config.js --host=0.0.0.0 --port=8088",
"dist": "mediacms-scripts rimraf ./dist && mediacms-scripts build --config=./config/mediacms.config.js --env=dist"
},
"browserslist": [
"cover 99.5%"
],
"devDependencies": {
"@babel/core": "^7.26.9",
"@babel/preset-env": "^7.26.9",
"@babel/preset-react": "^7.26.3",
"@types/minimatch": "^5.1.2",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"autoprefixer": "^10.4.21",
"babel-loader": "^10.0.0",
"compass-mixins": "^0.12.12",
"copy-webpack-plugin": "^13.0.0",
"core-js": "^3.41.0",
"css-loader": "^7.1.2",
"dotenv": "^16.4.7",
"ejs": "^3.1.10",
"ejs-compiled-loader": "^3.1.0",
"mediacms-scripts": "file:packages/scripts",
"postcss-loader": "^8.1.1",
"prettier": "^3.5.3",
"prop-types": "^15.8.1",
"sass": "^1.85.1",
"sass-loader": "^16.0.5",
"ts-loader": "^9.5.2",
"typescript": "^5.8.2",
"url-loader": "^4.1.1",
"webpack": "^5.98.0"
},
"dependencies": {
"@react-pdf-viewer/core": "^3.9.0",
"@react-pdf-viewer/default-layout": "^3.9.0",
"axios": "^1.8.2",
"flux": "^4.0.4",
"normalize.css": "^8.0.1",
"pdfjs-dist": "3.4.120",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-mentions": "^4.3.1",
"sortablejs": "^1.13.0",
"timeago.js": "^4.0.2",
"url-parse": "^1.5.10"
}
"name": "mediacms-frontend",
"version": "0.9.2",
"description": "",
"author": "",
"license": "",
"keywords": [],
"main": "index.js",
"scripts": {
"start": "mediacms-scripts development --config=./config/mediacms.config.js --host=0.0.0.0 --port=8088",
"dist": "mediacms-scripts rimraf ./dist && mediacms-scripts build --config=./config/mediacms.config.js --env=dist",
"test": "jest",
"test-coverage": "npx rimraf ./coverage && jest --coverage",
"test-coverage-watch": "npm run test-coverage -- --watchAll",
"test-watch": "jest --watch"
},
"browserslist": [
"cover 99.5%"
],
"devDependencies": {
"@babel/core": "^7.26.9",
"@babel/preset-env": "^7.26.9",
"@babel/preset-react": "^7.26.3",
"@types/flux": "^3.1.15",
"@types/jest": "^29.5.12",
"@types/minimatch": "^5.1.2",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@types/url-parse": "^1.4.11",
"autoprefixer": "^10.4.21",
"babel-jest": "^30.2.0",
"babel-loader": "^10.0.0",
"compass-mixins": "^0.12.12",
"copy-webpack-plugin": "^13.0.0",
"core-js": "^3.41.0",
"css-loader": "^7.1.2",
"dotenv": "^16.4.7",
"ejs": "^3.1.10",
"ejs-compiled-loader": "^3.1.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^30.2.0",
"jsdom": "^27.3.0",
"mediacms-scripts": "file:packages/scripts",
"postcss-loader": "^8.1.1",
"prettier": "^3.5.3",
"prop-types": "^15.8.1",
"sass": "^1.85.1",
"sass-loader": "^16.0.5",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"typescript": "^5.9.3",
"url-loader": "^4.1.1",
"webpack": "^5.98.0"
},
"dependencies": {
"@react-pdf-viewer/core": "^3.9.0",
"@react-pdf-viewer/default-layout": "^3.9.0",
"axios": "^1.8.2",
"flux": "^4.0.4",
"normalize.css": "^8.0.1",
"pdfjs-dist": "3.4.120",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-mentions": "^4.3.1",
"sortablejs": "^1.13.0",
"timeago.js": "^4.0.2",
"url-parse": "^1.5.10"
}
}

View File

@@ -1,38 +1,41 @@
import { logErrorAndReturnError } from './errors';
import { isPositiveInteger, isPositiveIntegerOrZero } from './math';
export const PositiveIntegerOrZero = (function () {
return function (obj, key, comp) {
return void 0 === obj[key] || isPositiveIntegerOrZero(obj[key])
? null
: logErrorAndReturnError([
'Invalid prop `' +
key +
'` of type `' +
typeof obj[key] +
'` supplied to `' +
(comp || 'N/A') +
'`, expected `positive integer or zero` (' +
obj[key] +
').',
]);
};
const isPositiveIntegerOrZero = (x) => x === Math.trunc(x) && x >= 0;
return function (obj, key, comp) {
return void 0 === obj[key] || isPositiveIntegerOrZero(obj[key])
? null
: logErrorAndReturnError([
'Invalid prop `' +
key +
'` of type `' +
typeof obj[key] +
'` supplied to `' +
(comp || 'N/A') +
'`, expected `positive integer or zero` (' +
obj[key] +
').',
]);
};
})();
export const PositiveInteger = (function () {
return function (obj, key, comp) {
return void 0 === obj[key] || isPositiveInteger(obj[key])
? null
: logErrorAndReturnError([
'Invalid prop `' +
key +
'` of type `' +
typeof obj[key] +
'` supplied to `' +
(comp || 'N/A') +
'`, expected `positive integer` (' +
obj[key] +
').',
]);
};
const isPositiveInteger = (x) => x === Math.trunc(x) && x > 0;
return function (obj, key, comp) {
return void 0 === obj[key] || isPositiveInteger(obj[key])
? null
: logErrorAndReturnError([
'Invalid prop `' +
key +
'` of type `' +
typeof obj[key] +
'` supplied to `' +
(comp || 'N/A') +
'`, expected `positive integer` (' +
obj[key] +
').',
]);
};
})();

View File

@@ -1,8 +1,15 @@
// check templates/config/installation/translations.html for more
export function replaceString(string) {
for (const key in window.REPLACEMENTS) {
string = string.replace(key, window.REPLACEMENTS[key]);
export function replaceString(word) {
if (!window.REPLACEMENTS) {
return word;
}
return string;
let result = word;
for (const [search, replacement] of Object.entries(window.REPLACEMENTS)) {
result = result.split(search).join(replacement);
}
return result;
}

View File

@@ -7,13 +7,13 @@ export async function getRequest(url, sync, callback, errorCallback) {
};
function responseHandler(result) {
if (callback instanceof Function) {
if (callback instanceof Function || typeof callback === 'function') {
callback(result);
}
}
function errorHandler(error) {
if (errorCallback instanceof Function) {
if (errorCallback instanceof Function || typeof errorCallback === 'function') {
let err = error;
if (void 0 === error.response) {
err = {
@@ -58,13 +58,13 @@ export async function postRequest(url, postData, configData, sync, callback, err
postData = postData || {};
function responseHandler(result) {
if (callback instanceof Function) {
if (callback instanceof Function || typeof callback === 'function') {
callback(result);
}
}
function errorHandler(error) {
if (errorCallback instanceof Function) {
if (errorCallback instanceof Function || typeof errorCallback === 'function') {
errorCallback(error);
}
}
@@ -84,13 +84,13 @@ export async function putRequest(url, putData, configData, sync, callback, error
putData = putData || {};
function responseHandler(result) {
if (callback instanceof Function) {
if (callback instanceof Function || typeof callback === 'function') {
callback(result);
}
}
function errorHandler(error) {
if (errorCallback instanceof Function) {
if (errorCallback instanceof Function || typeof errorCallback === 'function') {
errorCallback(error);
}
}
@@ -110,13 +110,13 @@ export async function deleteRequest(url, configData, sync, callback, errorCallba
configData = configData || {};
function responseHandler(result) {
if (callback instanceof Function) {
if (callback instanceof Function || typeof callback === 'function') {
callback(result);
}
}
function errorHandler(error) {
if (errorCallback instanceof Function) {
if (errorCallback instanceof Function || typeof errorCallback === 'function') {
errorCallback(error);
}
}

View File

@@ -1,9 +1,5 @@
// check templates/config/installation/translations.html for more
export function translateString(string) {
if (window.TRANSLATION && window.TRANSLATION[string]) {
return window.TRANSLATION[string];
} else {
return string;
}
export function translateString(str) {
return window.TRANSLATION?.[str] ?? str;
}

View File

@@ -0,0 +1,56 @@
import { csrfToken } from '../../../src/static/js/utils/helpers/csrfToken';
const setupDocumentCookie = () => {
if (typeof document === 'undefined') {
globalThis.document = { cookie: '' } as unknown as Document;
}
};
const setDocumentCookie = (value: string) => {
if (typeof document !== 'undefined') {
Object.defineProperty(document, 'cookie', { value, writable: true, configurable: true });
}
};
describe('js/utils/helpers', () => {
describe('csrfToken', () => {
const originalCookie = document.cookie;
beforeAll(() => {
// Initialize document environment
setupDocumentCookie();
});
afterEach(() => {
// Restore original cookie string
setDocumentCookie(originalCookie);
});
test('Returns null when document.cookie is empty', () => {
setDocumentCookie('');
expect(csrfToken()).toBeNull();
});
test('Returns null when csrftoken is not present', () => {
setDocumentCookie('sessionid=abc; theme=dark');
expect(csrfToken()).toBeNull();
});
test('Finds and decodes the csrftoken cookie value', () => {
const token = encodeURIComponent('a b+c%20');
setDocumentCookie(`sessionid=abc; csrftoken=${token}; theme=dark`);
expect(csrfToken()).toBe('a b+c%20');
});
test('Ignores leading spaces and matches exact prefix csrftoken=', () => {
setDocumentCookie(' sessionid=xyz; csrftoken=secure123; other=value');
expect(csrfToken()).toBe('secure123');
});
test('Stops scanning once csrftoken is found', () => {
// Ensure csrftoken occurs before other long tail cookies
setDocumentCookie('csrftoken=first; a=1; b=2; c=3; d=4; e=5');
expect(csrfToken()).toBe('first');
});
});
});

View File

@@ -0,0 +1,220 @@
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);
});
});
});
});

View File

@@ -0,0 +1,47 @@
// 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('errors', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('logErrorAndReturnError returns Error with first message and logs with error', () => {
const messages = ['Primary msg', 'details', 'more'];
const err = logErrorAndReturnError(messages);
expect(err).toBeInstanceOf(Error);
expect(err.message).toBe('Primary msg');
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 err = logWarningAndReturnError(messages);
expect(err).toBeInstanceOf(Error);
expect(err.message).toBe('Primary 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', () => {
const messages: string[] = [];
const err1 = logErrorAndReturnError(messages);
expect(err1).toBeInstanceOf(Error);
expect(err1.message).toBe('');
expect(mockedError).toHaveBeenCalledWith('');
jest.clearAllMocks();
const err2 = logWarningAndReturnError(messages);
expect(err2).toBeInstanceOf(Error);
expect(err2.message).toBe('');
expect(mockedWarn).toHaveBeenCalledWith('');
});
});
});

View File

@@ -0,0 +1,44 @@
// Mock the dispatcher module used by exportStore
jest.mock('../../../src/static/js/utils/dispatcher', () => ({ register: jest.fn() }));
import exportStore from '../../../src/static/js/utils/helpers/exportStore';
// Re-import the mocked dispatcher for assertions
import * as dispatcher from '../../../src/static/js/utils/dispatcher';
describe('js/utils/helpers', () => {
describe('exportStore', () => {
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('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();
});
});
});

View File

@@ -0,0 +1,23 @@
import { formatInnerLink } from '../../../src/static/js/utils/helpers/formatInnerLink';
describe('js/utils/helpers', () => {
describe('formatInnerLink', () => {
test('Returns the same absolute URL unchanged', () => {
const url = 'https://example.com/path?x=1#hash';
const base = 'https://base.example.org';
expect(formatInnerLink(url, base)).toBe(url);
});
test('Constructs absolute URL from relative path with leading slash', () => {
const url = '/images/picture.png';
const base = 'https://media.example.com';
expect(formatInnerLink(url, base)).toBe('https://media.example.com/images/picture.png');
});
test('Constructs absolute URL from relative path without leading slash', () => {
const url = 'assets/file.txt';
const base = 'https://cdn.example.com';
expect(formatInnerLink(url, base)).toBe('https://cdn.example.com/assets/file.txt');
});
});
});

View File

@@ -0,0 +1,15 @@
import { formatManagementTableDate } from '../../../src/static/js/utils/helpers/formatManagementTableDate';
describe('js/utils/helpers', () => {
describe('formatManagementTableDate', () => {
test('Formats date with zero-padded time components', () => {
const d = new Date(2021, 0, 5, 3, 7, 9); // Jan=0, day 5, 03:07:09
expect(formatManagementTableDate(d)).toBe('Jan 5, 2021 03:07:09');
});
test('Formats date with double-digit time components and month abbreviation', () => {
const d = new Date(1999, 11, 31, 23, 59, 58); // Dec=11
expect(formatManagementTableDate(d)).toBe('Dec 31, 1999 23:59:58');
});
});
});

View File

@@ -0,0 +1,106 @@
import formatViewsNumber from '../../../src/static/js/utils/helpers/formatViewsNumber';
describe('js/utils/helpers', () => {
describe('formatViewsNumber', () => {
describe('fullNumber = false (default compact formatting)', () => {
test('Formats values < 1,000 without suffix and with correct rounding', () => {
expect(formatViewsNumber(0)).toBe('0');
expect(formatViewsNumber(9)).toBe('9');
expect(formatViewsNumber(12)).toBe('12');
expect(formatViewsNumber(999)).toBe('999');
});
test('Formats thousands to K with decimals for < 10K and none for >= 10K', () => {
expect(formatViewsNumber(1000)).toBe('1K');
expect(formatViewsNumber(1500)).toBe('1.5K');
expect(formatViewsNumber(1499)).toBe('1.5K'); // rounds to 1 decimal
expect(formatViewsNumber(10_000)).toBe('10K');
expect(formatViewsNumber(10_400)).toBe('10K');
expect(formatViewsNumber(10_500)).toBe('11K'); // rounds to nearest whole
expect(formatViewsNumber(99_900)).toBe('100K'); // rounding up
});
test('Formats millions to M with decimals for < 10M and none for >= 10M', () => {
expect(formatViewsNumber(1_000_000)).toBe('1M');
expect(formatViewsNumber(1_200_000)).toBe('1.2M');
expect(formatViewsNumber(9_440_000)).toBe('9.4M');
expect(formatViewsNumber(9_960_000)).toBe('10M'); // rounds to whole when >= 10M threshold after rounding
expect(formatViewsNumber(10_000_000)).toBe('10M');
});
test('Formats billions and trillions correctly', () => {
expect(formatViewsNumber(1_000_000_000)).toBe('1B');
expect(formatViewsNumber(1_500_000_000)).toBe('1.5B');
expect(formatViewsNumber(10_000_000_000)).toBe('10B');
expect(formatViewsNumber(1_000_000_000_000)).toBe('1T');
expect(formatViewsNumber(1_230_000_000_000)).toBe('1.2T');
});
// @todo: Revisit this behavior
test('Beyond last unit keeps using the last unit with scaling', () => {
// Implementation scales beyond units by increasing the value so that the last unit remains applicable
// Here, expect a number in T with rounding behavior similar to others
expect(formatViewsNumber(9_999_999_999_999)).toBe('10T');
// With current rounding rules, this value rounds to whole trillions
expect(formatViewsNumber(12_345_678_901_234)).toBe('12T');
});
});
describe('fullNumber = true (locale formatting)', () => {
test('Returns locale string representation of the full number', () => {
// Use a fixed locale independent assertion by stripping non-digits except separators that could vary.
// However, to avoid locale variance, check that it equals toLocaleString directly.
const vals = [0, 12, 999, 1000, 1234567, 9876543210];
for (const v of vals) {
expect(formatViewsNumber(v, true)).toBe(v.toLocaleString());
}
});
});
describe('Additional edge cases and robustness', () => {
test('Handles negative values without unit suffix (no scaling applied)', () => {
expect(formatViewsNumber(-999)).toBe('-999');
expect(formatViewsNumber(-1000)).toBe('-1000');
expect(formatViewsNumber(-1500)).toBe('-1500');
expect(formatViewsNumber(-10_500)).toBe('-10500');
expect(formatViewsNumber(-1_230_000_000_000)).toBe('-1230000000000');
});
test('Handles non-integer inputs with correct rounding in compact mode', () => {
expect(formatViewsNumber(1499.5)).toBe('1.5K');
expect(formatViewsNumber(999.9)).toBe('1000');
expect(formatViewsNumber(10_499.5)).toBe('10K');
expect(formatViewsNumber(10_500.49)).toBe('11K');
expect(formatViewsNumber(9_440_000.49)).toBe('9.4M');
});
test('Respects locale formatting in fullNumber mode', () => {
const values = [1_234_567, -2_345_678, 0, 10_000, 99_999_999];
for (const v of values) {
expect(formatViewsNumber(v, true)).toBe(v.toLocaleString());
}
});
test('Caps unit at trillions for extremely large numbers', () => {
expect(formatViewsNumber(9_999_999_999_999)).toBe('10T');
expect(formatViewsNumber(12_345_678_901_234)).toBe('12T');
expect(formatViewsNumber(987_654_321_000_000)).toBe('988T');
});
// @todo: Revisit this behavior
test('Handles NaN and Infinity values gracefully', () => {
expect(formatViewsNumber(Number.NaN, true)).toBe(Number.NaN.toLocaleString());
expect(formatViewsNumber(Number.POSITIVE_INFINITY, true)).toBe(
Number.POSITIVE_INFINITY.toLocaleString()
);
expect(formatViewsNumber(Number.NEGATIVE_INFINITY, true)).toBe(
Number.NEGATIVE_INFINITY.toLocaleString()
);
expect(formatViewsNumber(Number.NaN)).toBe('NaN');
// @note: We don't test compact Infinity cases due to infinite loop behavior from while (views >= compare)
});
});
});
});

View File

@@ -0,0 +1,47 @@
import { imageExtension } from '../../../src/static/js/utils/helpers/imageExtension';
describe('js/utils/helpers', () => {
describe('imageExtension', () => {
// @todo: 'imageExtension' behaves as a 'fileExtension' function. It should be renamed...
test('Returns the extension for a typical filename', () => {
expect(imageExtension('photo.png')).toBe('png');
expect(imageExtension('document.pdf')).toBe('pdf');
});
test('Returns the last segment for filenames with multiple dots', () => {
expect(imageExtension('archive.tar.gz')).toBe('gz');
expect(imageExtension('backup.2024.12.31.zip')).toBe('zip');
});
// @todo: It shouldn't happen. Fix that
test('Returns the entire string when there is no dot in the filename', () => {
expect(imageExtension('file')).toBe('file');
expect(imageExtension('README')).toBe('README');
});
test('Handles hidden files that start with a dot', () => {
expect(imageExtension('.gitignore')).toBe('gitignore');
expect(imageExtension('.env.local')).toBe('local');
});
test('Returns undefined for falsy or empty inputs', () => {
expect(imageExtension('')).toBeUndefined();
expect(imageExtension(undefined as unknown as string)).toBeUndefined();
expect(imageExtension(null as unknown as string)).toBeUndefined();
});
test('Extracts the extension from URL-like paths', () => {
expect(imageExtension('https://example.com/images/avatar.jpeg')).toBe('jpeg');
expect(imageExtension('/static/assets/icons/favicon.ico')).toBe('ico');
});
test('Preserves case of the extension', () => {
expect(imageExtension('UPPER.CASE.JPG')).toBe('JPG');
expect(imageExtension('Mixed.Extension.PnG')).toBe('PnG');
});
test('Returns empty string when the filename ends with a trailing dot', () => {
expect(imageExtension('weird.')).toBe('');
});
});
});

View File

@@ -0,0 +1,54 @@
import { warn, error } from '../../../src/static/js/utils/helpers/log';
describe('js/utils/helpers', () => {
describe('log', () => {
beforeEach(() => {
// Setup console mocks - replaces global console methods with jest mocks
globalThis.console.warn = jest.fn();
globalThis.console.error = jest.fn();
jest.clearAllMocks();
});
afterEach(() => {
// Restore original console methods
jest.restoreAllMocks();
});
test('Warn proxies arguments to console.warn preserving order and count', () => {
warn('a', 'b', 'c');
expect(console.warn).toHaveBeenCalledTimes(1);
expect(console.warn).toHaveBeenCalledWith('a', 'b', 'c');
});
test('Error proxies arguments to console.error preserving order and count', () => {
error('x', 'y');
expect(console.error).toHaveBeenCalledTimes(1);
expect(console.error).toHaveBeenCalledWith('x', 'y');
});
test('Warn supports zero arguments', () => {
warn();
expect(console.warn).toHaveBeenCalledTimes(1);
expect((console.warn as jest.Mock).mock.calls[0].length).toBe(0);
});
test('Error supports zero arguments', () => {
error();
expect(console.error).toHaveBeenCalledTimes(1);
expect((console.error as jest.Mock).mock.calls[0].length).toBe(0);
});
test('Warn does not call console.error and error does not call console.warn', () => {
warn('only-warn');
expect(console.warn).toHaveBeenCalledTimes(1);
expect(console.error).not.toHaveBeenCalled();
jest.clearAllMocks();
error('only-error');
expect(console.error).toHaveBeenCalledTimes(1);
expect(console.warn).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,156 @@
import {
isGt,
isZero,
isNumber,
isInteger,
isPositive,
isPositiveNumber,
isPositiveInteger,
isPositiveIntegerOrZero,
greaterCommonDivision,
} from '../../../src/static/js/utils/helpers/math';
describe('js/utils/helpers', () => {
describe('math', () => {
describe('isGt', () => {
test('Returns true when x > y', () => {
expect(isGt(5, 3)).toBe(true);
});
test('Returns false when x === y', () => {
expect(isGt(3, 3)).toBe(false);
});
test('Returns false when x < y', () => {
expect(isGt(2, 3)).toBe(false);
});
});
describe('isZero', () => {
test('Returns true for 0', () => {
expect(isZero(0)).toBe(true);
});
test('Returns false for non-zero numbers', () => {
expect(isZero(1)).toBe(false);
expect(isZero(-1)).toBe(false);
});
});
describe('isNumber', () => {
test('Returns true for numbers', () => {
expect(isNumber(0)).toBe(true);
expect(isNumber(1)).toBe(true);
expect(isNumber(-1)).toBe(true);
expect(isNumber(1.5)).toBe(true);
});
test('Returns false for NaN', () => {
expect(isNumber(Number.NaN as unknown as number)).toBe(false);
});
test('Returns false for non-number types (via casting)', () => {
// TypeScript type guards prevent passing non-numbers directly; simulate via casting
expect(isNumber('3' as unknown as number)).toBe(false);
expect(isNumber(null as unknown as number)).toBe(false);
expect(isNumber(undefined as unknown as number)).toBe(false);
});
});
describe('isInteger', () => {
test('Returns true for integers', () => {
expect(isInteger(0)).toBe(true);
expect(isInteger(1)).toBe(true);
expect(isInteger(-1)).toBe(true);
});
test('Returns false for non-integers', () => {
expect(isInteger(1.1)).toBe(false);
expect(isInteger(-2.5)).toBe(false);
});
});
describe('isPositive', () => {
test('Returns true for positive numbers', () => {
expect(isPositive(1)).toBe(true);
expect(isPositive(3.14)).toBe(true);
});
test('Returns false for zero and negatives', () => {
expect(isPositive(0)).toBe(false);
expect(isPositive(-1)).toBe(false);
expect(isPositive(-3.14)).toBe(false);
});
});
describe('isPositiveNumber', () => {
test('Returns true for positive numbers', () => {
expect(isPositiveNumber(1)).toBe(true);
expect(isPositiveNumber(2.7)).toBe(true);
});
test('Returns false for zero and negatives', () => {
expect(isPositiveNumber(0)).toBe(false);
expect(isPositiveNumber(-1)).toBe(false);
expect(isPositiveNumber(-3.4)).toBe(false);
});
test('Returns false for NaN (and non-number when cast)', () => {
expect(isPositiveNumber(Number.NaN as unknown as number)).toBe(false);
expect(isPositiveNumber('3' as unknown as number)).toBe(false);
});
});
describe('isPositiveInteger', () => {
test('Returns true for positive integers', () => {
expect(isPositiveInteger(1)).toBe(true);
expect(isPositiveInteger(10)).toBe(true);
});
test('Returns false for zero, negatives, and non-integers', () => {
expect(isPositiveInteger(0)).toBe(false);
expect(isPositiveInteger(-1)).toBe(false);
expect(isPositiveInteger(1.5)).toBe(false);
});
});
describe('isPositiveIntegerOrZero', () => {
test('Returns true for positive integers and zero', () => {
expect(isPositiveIntegerOrZero(0)).toBe(true);
expect(isPositiveIntegerOrZero(1)).toBe(true);
expect(isPositiveIntegerOrZero(10)).toBe(true);
});
test('Returns false for negatives and non-integers', () => {
expect(isPositiveIntegerOrZero(-1)).toBe(false);
expect(isPositiveIntegerOrZero(1.1)).toBe(false);
});
});
describe('greaterCommonDivision', () => {
test('Computes gcd for positive integers', () => {
expect(greaterCommonDivision(54, 24)).toBe(6);
expect(greaterCommonDivision(24, 54)).toBe(6);
expect(greaterCommonDivision(21, 14)).toBe(7);
expect(greaterCommonDivision(7, 13)).toBe(1);
});
test('Handles zeros', () => {
expect(greaterCommonDivision(0, 0)).toBe(0);
expect(greaterCommonDivision(0, 5)).toBe(5);
expect(greaterCommonDivision(12, 0)).toBe(12);
});
test('Handles negative numbers by returning gcd sign of first arg (Euclid recursion behavior)', () => {
expect(greaterCommonDivision(-54, 24)).toBe(-6);
expect(greaterCommonDivision(54, -24)).toBe(6);
expect(greaterCommonDivision(-54, -24)).toBe(-6);
});
test('Works with equal numbers', () => {
expect(greaterCommonDivision(8, 8)).toBe(8);
expect(greaterCommonDivision(-8, -8)).toBe(-8);
});
});
});
});

View File

@@ -0,0 +1,111 @@
// Mock the errors helper to capture error construction without side effects
jest.mock('../../../src/static/js/utils/helpers/errors', () => ({
logErrorAndReturnError: jest.fn((messages: string[]) => new Error(messages.join('\n'))),
}));
import { logErrorAndReturnError } from '../../../src/static/js/utils/helpers/errors';
import { PositiveIntegerOrZero, PositiveInteger } from '../../../src/static/js/utils/helpers/propTypeFilters';
describe('js/utils/helpers', () => {
describe('propTypeFilters', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('PositiveIntegerOrZero', () => {
test('Returns null when property is undefined', () => {
const obj = {};
const res = PositiveIntegerOrZero(obj, 'count', 'Comp');
expect(res).toBeNull();
expect(logErrorAndReturnError).not.toHaveBeenCalled();
});
test('Returns null for zero or positive integers', () => {
const cases = [0, 1, 2, 100];
for (const val of cases) {
const res = PositiveIntegerOrZero({ count: val }, 'count', 'Comp');
expect(res).toBeNull();
}
expect(logErrorAndReturnError).not.toHaveBeenCalled();
});
test('Returns Error via logErrorAndReturnError for negative numbers', () => {
const res = PositiveIntegerOrZero({ count: -1 }, 'count', 'Counter');
expect(res).toBeInstanceOf(Error);
expect(logErrorAndReturnError).toHaveBeenCalledTimes(1);
const [messages] = (logErrorAndReturnError as jest.Mock).mock.calls[0];
expect(Array.isArray(messages)).toBe(true);
expect(messages[0]).toBe(
'Invalid prop `count` of type `number` supplied to `Counter`, expected `positive integer or zero` (-1).'
);
});
test('Returns Error for non-integer numbers (e.g., float)', () => {
const res = PositiveIntegerOrZero({ count: 1.5 }, 'count', 'Widget');
expect(res).toBeInstanceOf(Error);
expect((logErrorAndReturnError as jest.Mock).mock.calls[0][0][0]).toBe(
'Invalid prop `count` of type `number` supplied to `Widget`, expected `positive integer or zero` (1.5).'
);
});
test('Uses "N/A" component label when comp is falsy', () => {
const res = PositiveIntegerOrZero({ count: -2 }, 'count', '');
expect(res).toBeInstanceOf(Error);
expect((logErrorAndReturnError as jest.Mock).mock.calls[0][0][0]).toBe(
'Invalid prop `count` of type `number` supplied to `N/A`, expected `positive integer or zero` (-2).'
);
});
});
describe('PositiveInteger', () => {
test('Returns null when property is undefined', () => {
const obj = {};
const res = PositiveInteger(obj, 'age', 'Person');
expect(res).toBeNull();
expect(logErrorAndReturnError).not.toHaveBeenCalled();
});
test('Returns null for positive integers (excluding zero)', () => {
const cases = [1, 2, 100];
for (const val of cases) {
const res = PositiveInteger({ age: val }, 'age', 'Person');
expect(res).toBeNull();
}
expect(logErrorAndReturnError).not.toHaveBeenCalled();
});
test('Returns Error for zero', () => {
const res = PositiveInteger({ age: 0 }, 'age', 'Person');
expect(res).toBeInstanceOf(Error);
expect((logErrorAndReturnError as jest.Mock).mock.calls[0][0][0]).toContain(
'Invalid prop `age` of type `number` supplied to `Person`, expected `positive integer` (0).'
);
});
test('Returns Error for negative numbers', () => {
const res = PositiveInteger({ age: -3 }, 'age', 'Person');
expect(res).toBeInstanceOf(Error);
expect((logErrorAndReturnError as jest.Mock).mock.calls[0][0][0]).toBe(
'Invalid prop `age` of type `number` supplied to `Person`, expected `positive integer` (-3).'
);
});
test('Returns Error for non-integer numbers', () => {
const res = PositiveInteger({ age: 2.7 }, 'age', 'Person');
expect(res).toBeInstanceOf(Error);
expect((logErrorAndReturnError as jest.Mock).mock.calls[0][0][0]).toBe(
'Invalid prop `age` of type `number` supplied to `Person`, expected `positive integer` (2.7).'
);
});
test('Uses "N/A" component label when comp is falsy', () => {
const res = PositiveInteger({ age: -1 }, 'age', '');
expect(res).toBeInstanceOf(Error);
expect((logErrorAndReturnError as jest.Mock).mock.calls[0][0][0]).toBe(
'Invalid prop `age` of type `number` supplied to `N/A`, expected `positive integer` (-1).'
);
});
});
});
});

View File

@@ -0,0 +1,33 @@
import publishedOnDate from '../../../src/static/js/utils/helpers/publishedOnDate';
// 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));
describe('js/utils/helpers', () => {
describe('publishedOnDate', () => {
test('Returns null when input is not a Date instance', () => {
expect(publishedOnDate(null as unknown as Date)).toBeNull();
expect(publishedOnDate(undefined as unknown as Date)).toBeNull();
expect(publishedOnDate('2020-01-02' as any as Date)).toBeNull();
expect(publishedOnDate(1577923200000 as unknown as Date)).toBeNull();
});
test('Type 1 (default): "Mon DD, YYYY" with 3-letter month prefix before day', () => {
expect(publishedOnDate(makeDate(2020, 0, 2))).toBe('Jan 2, 2020');
expect(publishedOnDate(makeDate(1999, 11, 31))).toBe('Dec 31, 1999');
expect(publishedOnDate(makeDate(2024, 1, 29))).toBe('Feb 29, 2024');
});
test('Type 2: "DD Mon YYYY" with 3-letter month suffix', () => {
expect(publishedOnDate(makeDate(2020, 0, 2), 2)).toBe('2 Jan 2020');
expect(publishedOnDate(makeDate(1999, 11, 31), 2)).toBe('31 Dec 1999');
expect(publishedOnDate(makeDate(2024, 1, 29), 2)).toBe('29 Feb 2024');
});
test('Type 3: "DD Month YYYY" with full month name', () => {
expect(publishedOnDate(makeDate(2020, 0, 2), 3)).toBe('2 January 2020');
expect(publishedOnDate(makeDate(1999, 11, 31), 3)).toBe('31 December 1999');
expect(publishedOnDate(makeDate(2024, 1, 29), 3)).toBe('29 February 2024');
});
});
});

View File

@@ -0,0 +1,45 @@
import { quickSort } from '../../../src/static/js/utils/helpers/quickSort';
describe('js/utils/helpers', () => {
describe('quickSort', () => {
test('Returns the same array reference (in-place) and sorts ascending', () => {
const arr = [3, 1, 4, 1, 5, 9, 2];
const out = quickSort(arr, 0, arr.length - 1);
expect(out).toBe(arr);
expect(arr).toEqual([1, 1, 2, 3, 4, 5, 9]);
});
test('Handles already sorted arrays', () => {
const arr = [1, 2, 3, 4, 5];
quickSort(arr, 0, arr.length - 1);
expect(arr).toEqual([1, 2, 3, 4, 5]);
});
test('Handles arrays with duplicates and negative numbers', () => {
const arr = [0, -1, -1, 2, 2, 1, 0];
quickSort(arr, 0, arr.length - 1);
expect(arr).toEqual([-1, -1, 0, 0, 1, 2, 2]);
});
test('Handles single-element array', () => {
const single = [42];
quickSort(single, 0, single.length - 1);
expect(single).toEqual([42]);
});
test('Handles empty range without changes', () => {
const arr = [5, 4, 3];
// call with left > right (empty range)
quickSort(arr, 2, 1);
expect(arr).toEqual([5, 4, 3]);
});
test('Sorts subrange correctly without touching elements outside range', () => {
const arr = [9, 7, 5, 3, 1, 2, 4, 8, 6];
// sort only the middle [2..6]
quickSort(arr, 2, 6);
// The subrange [5,3,1,2,4] becomes [1,2,3,4,5]
expect(arr).toEqual([9, 7, 1, 2, 3, 4, 5, 8, 6]);
});
});
});

View File

@@ -0,0 +1,68 @@
import { replaceString } from '../../../src/static/js/utils/helpers/replacementStrings';
declare global {
interface Window {
REPLACEMENTS?: Record<string, string>;
}
}
describe('js/utils/helpers', () => {
describe('replacementStrings', () => {
describe('replaceString', () => {
const originalReplacements = window.REPLACEMENTS;
beforeEach(() => {
delete window.REPLACEMENTS;
});
afterEach(() => {
window.REPLACEMENTS = originalReplacements;
});
test('Returns the original word when window.REPLACEMENTS is undefined', () => {
delete window.REPLACEMENTS;
const input = 'Hello World';
const output = replaceString(input);
expect(output).toBe(input);
});
test('Replaces a single occurrence based on window.REPLACEMENTS map', () => {
window.REPLACEMENTS = { Hello: 'Hi' };
const output = replaceString('Hello World');
expect(output).toBe('Hi World');
});
test('Replaces multiple occurrences of the same key', () => {
window.REPLACEMENTS = { foo: 'bar' };
const output = replaceString('foo foo baz foo');
expect(output).toBe('bar bar baz bar');
});
test('Applies all entries in window.REPLACEMENTS (sequential split/join)', () => {
window.REPLACEMENTS = { a: 'A', A: 'X' };
// First replaces 'a'->'A' and then 'A'->'X'
const output = replaceString('aAaa');
expect(output).toBe('XXXX');
});
test('Supports empty string replacements (deletion)', () => {
window.REPLACEMENTS = { remove: '' };
const output = replaceString('please remove this');
expect(output).toBe('please this');
});
test('Handles overlapping keys by iteration order', () => {
window.REPLACEMENTS = { ab: 'X', b: 'Y' };
// First replaces 'ab' -> 'X', leaving no 'b' from that sequence, then replace standalone 'b' -> 'Y'
const output = replaceString('zab+b');
expect(output).toBe('zX+Y');
});
test('Works with special regex characters since split/join is literal', () => {
window.REPLACEMENTS = { '.': 'DOT', '*': 'STAR', '[]': 'BRACKETS' };
const output = replaceString('a.*b[]c.');
expect(output).toBe('aDOTSTARbBRACKETScDOT');
});
});
});
});

View File

@@ -0,0 +1,218 @@
import axios from 'axios';
import { getRequest, postRequest, putRequest, deleteRequest } from '../../../src/static/js/utils/helpers/requests';
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;
describe('js/utils/helpers', () => {
describe('requests', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('getRequest', () => {
const url = '/api/test';
test('Calls axios.get with url and default config (async mode)', () => {
mockedAxios.get.mockResolvedValueOnce({ data: 'ok' } as any);
const cb = jest.fn();
getRequest(url, false, cb, undefined);
expect(mockedAxios.get).toHaveBeenCalledWith(url, {
timeout: null,
maxContentLength: null,
});
});
test('Invokes callback when provided (async mode)', async () => {
const response = { data: 'ok' } as any;
mockedAxios.get.mockResolvedValueOnce(response);
const cb = jest.fn();
await getRequest(url, true, cb, undefined);
expect(cb).toHaveBeenCalledWith(response);
});
// @todo: Revisit this behavior
test('Does not throw when callback is not a function', async () => {
mockedAxios.get.mockResolvedValueOnce({ data: 'ok' } as any);
await expect(getRequest(url, true, undefined as any, undefined as any)).resolves.toBeUndefined();
});
test('Error handler wraps network errors with type network', async () => {
const networkError = new Error('Network Error');
mockedAxios.get.mockRejectedValueOnce(networkError);
const errorCb = jest.fn();
await getRequest(url, true, undefined, errorCb);
expect(errorCb).toHaveBeenCalledTimes(1);
const arg = errorCb.mock.calls[0][0];
expect(arg).toStrictEqual({ type: 'network', error: networkError });
});
test('Error handler maps status 401 to private error', async () => {
const error = { response: { status: 401 } };
mockedAxios.get.mockRejectedValueOnce(error);
const errorCb = jest.fn();
await getRequest(url, true, undefined, errorCb);
expect(errorCb).toHaveBeenCalledWith({
type: 'private',
error,
message: 'Media is private',
});
});
test('Error handler maps status 400 to unavailable error', async () => {
const error = { response: { status: 400 } };
mockedAxios.get.mockRejectedValueOnce(error);
const errorCb = jest.fn();
await getRequest(url, true, undefined, errorCb);
expect(errorCb).toHaveBeenCalledWith({
type: 'unavailable',
error,
message: 'Media is unavailable',
});
});
test('Passes through other errors with error.response defined but no status', async () => {
const error = { response: {} } as any;
mockedAxios.get.mockRejectedValueOnce(error);
const errorCb = jest.fn();
await getRequest(url, true, undefined, errorCb);
expect(errorCb).toHaveBeenCalledWith(error);
});
// @todo: Revisit this behavior
test('When no errorCallback provided, it should not crash on error (async)', async () => {
mockedAxios.get.mockRejectedValueOnce(new Error('boom'));
await expect(getRequest(url, true, undefined as any, undefined as any)).resolves.toBeUndefined();
});
});
describe('postRequest', () => {
const url = '/api/post';
test('Calls axios.post with provided data and config (async mode)', () => {
mockedAxios.post.mockResolvedValueOnce({ data: 'ok' } as any);
const cb = jest.fn();
postRequest(url, { a: 1 }, { headers: { h: 'v' } }, false, cb, undefined);
expect(mockedAxios.post).toHaveBeenCalledWith(url, { a: 1 }, { headers: { h: 'v' } });
});
test('Defaults postData to {} when undefined', async () => {
mockedAxios.post.mockResolvedValueOnce({ data: 'ok' } as any);
const cb = jest.fn();
await postRequest(url, undefined as any, undefined as any, true, cb, undefined);
expect(mockedAxios.post).toHaveBeenCalledWith(url, {}, null);
expect(cb).toHaveBeenCalled();
});
test('Invokes errorCallback on error as-is', async () => {
const error = new Error('fail');
mockedAxios.post.mockRejectedValueOnce(error);
const errorCb = jest.fn();
await postRequest(url, {}, undefined, true, undefined, errorCb);
expect(errorCb).toHaveBeenCalledWith(error);
});
});
describe('putRequest', () => {
const url = '/api/put';
test('Calls axios.put with provided data and config', async () => {
mockedAxios.put.mockResolvedValueOnce({ data: 'ok' } as any);
const cb = jest.fn();
await putRequest(url, { a: 1 }, { headers: { h: 'v' } }, true, cb, undefined);
expect(mockedAxios.put).toHaveBeenCalledWith(url, { a: 1 }, { headers: { h: 'v' } });
expect(cb).toHaveBeenCalled();
});
test('Defaults putData to {} when undefined', async () => {
mockedAxios.put.mockResolvedValueOnce({ data: 'ok' } as any);
await putRequest(url, undefined as any, undefined as any, true, undefined, undefined);
expect(mockedAxios.put).toHaveBeenCalledWith(url, {}, null);
});
test('Invokes errorCallback on error', async () => {
const error = new Error('fail');
mockedAxios.put.mockRejectedValueOnce(error);
const errorCb = jest.fn();
await putRequest(url, {}, undefined, true, undefined, errorCb);
expect(errorCb).toHaveBeenCalledWith(error);
});
});
describe('deleteRequest', () => {
const url = '/api/delete';
test('Calls axios.delete with provided config', async () => {
mockedAxios.delete.mockResolvedValueOnce({ data: 'ok' } as any);
const cb = jest.fn();
await deleteRequest(url, { headers: { h: 'v' } }, true, cb, undefined);
expect(mockedAxios.delete).toHaveBeenCalledWith(url, { headers: { h: 'v' } });
expect(cb).toHaveBeenCalled();
});
test('Defaults configData to {} when undefined', async () => {
mockedAxios.delete.mockResolvedValueOnce({ data: 'ok' } as any);
await deleteRequest(url, undefined as any, true, undefined, undefined);
expect(mockedAxios.delete).toHaveBeenCalledWith(url, {});
});
test('Invokes errorCallback on error', async () => {
const error = new Error('fail');
mockedAxios.delete.mockRejectedValueOnce(error);
const errorCb = jest.fn();
await deleteRequest(url, {}, true, undefined, errorCb);
expect(errorCb).toHaveBeenCalledWith(error);
});
});
describe('sync vs async behavior', () => {
test('sync=true awaits the axios promise', async () => {
const thenable = Promise.resolve({ data: 'ok' } as any);
mockedAxios.post.mockReturnValueOnce(thenable as any);
const cb = jest.fn();
const p = postRequest('/api/p', {}, undefined, true, cb, undefined);
// When awaited, callback should be called before next tick
await p;
expect(cb).toHaveBeenCalled();
});
test('sync=false does not need awaiting; call still issued', () => {
mockedAxios.put.mockResolvedValueOnce({ data: 'ok' } as any);
putRequest('/api/p', {}, undefined, false, undefined, undefined);
expect(mockedAxios.put).toHaveBeenCalled();
});
});
});
});

View File

@@ -0,0 +1,53 @@
import { translateString } from '../../../src/static/js/utils/helpers/translate';
declare global {
interface Window {
TRANSLATION?: Record<string, string>;
}
}
describe('js/utils/helpers', () => {
describe('translate', () => {
const originalReplacements = window.TRANSLATION;
beforeEach(() => {
delete window.TRANSLATION;
});
afterEach(() => {
window.TRANSLATION = originalReplacements;
});
test('Returns the same word when window.TRANSLATION is undefined', () => {
delete window.TRANSLATION;
expect(translateString('Hello')).toBe('Hello');
expect(translateString('NonExistingKey')).toBe('NonExistingKey');
expect(translateString('')).toBe('');
});
test('Returns mapped value when key exists in window.TRANSLATION', () => {
window.TRANSLATION = { Hello: 'Γεια', World: 'Κόσμος' };
expect(translateString('Hello')).toBe('Γεια');
expect(translateString('World')).toBe('Κόσμος');
});
test('Falls back to original word when key is missing in Twindow.RANSLATION', () => {
window.TRANSLATION = { Hello: 'Γεια' };
expect(translateString('MissingKey')).toBe('MissingKey');
expect(translateString('AnotherMissing')).toBe('AnotherMissing');
});
test('Supports empty string keys distinctly from missing keys', () => {
window.TRANSLATION = { '': '(empty)' };
expect(translateString('')).toBe('(empty)');
expect(translateString(' ')).toBe(' ');
});
test('Returns value as-is even if it is an empty string or falsy in the dictionary', () => {
window.TRANSLATION = { Empty: '', Zero: '0', False: 'false' };
expect(translateString('Empty')).toBe('');
expect(translateString('Zero')).toBe('0');
expect(translateString('False')).toBe('false');
});
});
});

View File

@@ -0,0 +1,79 @@
import { init, endpoints } from '../../../src/static/js/utils/settings/api';
const apiConfig = (url: any, ep: any) => {
init(url, ep);
return endpoints();
};
describe('utils/settings', () => {
describe('api', () => {
const sampleGlobal = {
site: { api: 'https://example.com/api/v1///' },
// The endpoints below intentionally contain leading slashes to ensure they are stripped
api: {
media: '/media/',
members: '/users//',
playlists: '/playlists',
liked: '/user/liked',
history: '/user/history',
tags: '/tags',
categories: '/categories',
manage_media: '/manage/media',
manage_users: '/manage/users',
manage_comments: '/manage/comments',
search: '/search',
},
} as const;
test('Trims trailing slashes on base and ensures single slash joins', () => {
const cfg = apiConfig(sampleGlobal.site.api, sampleGlobal.api);
// @todo: Check again the cases of trailing slashes
expect(cfg.media).toBe('https://example.com/api/v1/media/');
expect(cfg.users).toBe('https://example.com/api/v1/users//');
});
test('Adds featured/recommended query to media variants', () => {
const cfg = apiConfig(sampleGlobal.site.api, sampleGlobal.api);
expect(cfg.featured).toBe('https://example.com/api/v1/media/?show=featured');
expect(cfg.recommended).toBe('https://example.com/api/v1/media/?show=recommended');
});
test('Builds nested user, archive, manage maps', () => {
const cfg = apiConfig(sampleGlobal.site.api, sampleGlobal.api);
expect(cfg.user.liked).toBe('https://example.com/api/v1/user/liked');
expect(cfg.user.history).toBe('https://example.com/api/v1/user/history');
expect(cfg.user.playlists).toBe('https://example.com/api/v1/playlists?author=');
expect(cfg.archive.tags).toBe('https://example.com/api/v1/tags');
expect(cfg.archive.categories).toBe('https://example.com/api/v1/categories');
expect(cfg.manage.media).toBe('https://example.com/api/v1/manage/media');
expect(cfg.manage.users).toBe('https://example.com/api/v1/manage/users');
expect(cfg.manage.comments).toBe('https://example.com/api/v1/manage/comments');
});
test('Builds search endpoints with expected query fragments', () => {
const cfg = apiConfig(sampleGlobal.site.api, sampleGlobal.api);
expect(cfg.search.query).toBe('https://example.com/api/v1/search?q=');
expect(cfg.search.titles).toBe('https://example.com/api/v1/search?show=titles&q=');
expect(cfg.search.tag).toBe('https://example.com/api/v1/search?t=');
expect(cfg.search.category).toBe('https://example.com/api/v1/search?c=');
});
test('Handles base url with path and endpoint with existing query', () => {
const cfg = apiConfig('https://example.com/base/', {
media: 'items?x=1',
playlists: '/pls/',
liked: 'me/liked',
categories: '/c',
search: '/s',
});
expect(cfg.media).toBe('https://example.com/base/items?x=1');
expect(cfg.playlists).toBe('https://example.com/base/pls/');
expect(cfg.user.liked).toBe('https://example.com/base/me/liked');
expect(cfg.archive.categories).toBe('https://example.com/base/c');
expect(cfg.search.query).toBe('https://example.com/base/s?q=');
});
});
});

View File

@@ -0,0 +1,189 @@
import { config } from '../../../src/static/js/utils/settings/config';
describe('utils/settings', () => {
describe('config', () => {
const baseGlobal = {
profileId: 'john',
site: {
id: 'my-site',
url: 'https://example.com/',
api: 'https://example.com/api/',
title: 'Example',
theme: { mode: 'dark', switch: { enabled: true, position: 'sidebar' } },
logo: {
lightMode: { img: '/img/light.png', svg: '/img/light.svg' },
darkMode: { img: '/img/dark.png', svg: '/img/dark.svg' },
},
devEnv: false,
useRoundedCorners: true,
version: '2.0.0',
taxonomies: {
tags: { enabled: true, title: 'Topic Tags' },
categories: { enabled: false, title: 'Kinds' },
},
pages: {
latest: { enabled: true, title: 'Recent uploads' },
featured: { enabled: true, title: 'Featured picks' },
recommended: { enabled: false, title: 'You may like' },
},
userPages: {
members: { enabled: true, title: 'People' },
liked: { enabled: true, title: 'Favorites' },
history: { enabled: true, title: 'Watched' },
},
},
url: {
home: '/',
admin: '/admin',
error404: '/404',
latestMedia: '/latest',
featuredMedia: '/featured',
recommendedMedia: '/recommended',
signin: '/signin',
signout: '/signout',
register: '/register',
changePassword: '/password',
members: '/members',
search: '/search',
likedMedia: '/liked',
history: '/history',
addMedia: '/add',
editChannel: '/edit/channel',
editProfile: '/edit/profile',
tags: '/tags',
categories: '/categories',
manageMedia: '/manage/media',
manageUsers: '/manage/users',
manageComments: '/manage/comments',
},
api: {
media: 'v1/media/',
playlists: 'v1/playlists',
members: 'v1/users',
liked: 'v1/user/liked',
history: 'v1/user/history',
tags: 'v1/tags',
categories: 'v1/categories',
manage_media: 'v1/manage/media',
manage_users: 'v1/manage/users',
manage_comments: 'v1/manage/comments',
search: 'v1/search',
},
contents: {
notifications: {
messages: {
addToLiked: 'Yay',
removeFromLiked: 'Oops',
addToDisliked: 'nay',
removeFromDisliked: 'ok',
},
},
},
pages: {
home: { sections: { latest: { title: 'Latest T' } } },
search: { advancedFilters: true },
media: { categoriesWithTitle: true, hideViews: true, related: { initialSize: 5 } },
profile: { htmlInDescription: true, includeHistory: true, includeLikedMedia: true },
},
features: {
mediaItem: { hideAuthor: true, hideViews: false, hideDate: true },
media: {
actions: {
like: true,
dislike: true,
report: true,
comment: true,
comment_mention: true,
download: true,
save: true,
share: true,
},
shareOptions: ['embed', 'email', 'invalid'],
},
playlists: { mediaTypes: ['audio'] },
sideBar: { hideHomeLink: false, hideTagsLink: true, hideCategoriesLink: false },
embeddedVideo: { initialDimensions: { width: 640, height: 360 } },
headerBar: { hideLogin: false, hideRegister: true },
},
user: {
is: { anonymous: false, admin: true },
name: ' John ',
username: ' john ',
thumbnail: ' /img/j.png ',
can: {
changePassword: true,
deleteProfile: true,
addComment: true,
mentionComment: true,
deleteComment: true,
editMedia: true,
deleteMedia: true,
editSubtitle: true,
manageMedia: true,
manageUsers: true,
manageComments: true,
contactUser: true,
canSeeMembersPage: true,
usersNeedsToBeApproved: false,
addMedia: true,
editProfile: true,
readComment: true,
},
pages: { about: '/u/john/about ', media: '/u/john ', playlists: '/u/john/playlists ' },
},
} as const;
test('merges enabled pages and passes titles into options.pages.home sections', () => {
const cfg = config(baseGlobal);
expect(cfg.enabled.pages.latest).toStrictEqual({ enabled: true, title: 'Recent uploads' });
expect(cfg.enabled.pages.featured).toStrictEqual({ enabled: true, title: 'Featured picks' });
expect(cfg.enabled.pages.recommended).toStrictEqual({ enabled: false, title: 'You may like' });
expect(cfg.enabled.pages.members).toStrictEqual({ enabled: true, title: 'People' });
expect(cfg.options.pages.home.sections.latest.title).toBe('Latest T');
expect(cfg.options.pages.home.sections.featured.title).toBe('Featured picks');
});
test('produces api endpoints based on site.api and api endpoints', () => {
const cfg = config(baseGlobal);
expect(cfg.api.media).toBe('https://example.com/api/v1/media/');
expect(cfg.api.user.liked).toBe('https://example.com/api/v1/user/liked');
expect(cfg.api.search.query).toBe('https://example.com/api/v1/search?q=');
});
test('member and url manage links reflect user and feature flags', () => {
const cfg = config(baseGlobal);
expect(cfg.member.is).toStrictEqual({ admin: true, anonymous: false });
expect(cfg.member.can).toMatchObject({
manageMedia: true,
manageUsers: true,
manageComments: true,
likeMedia: true,
});
expect(cfg.url.manage.media).toBe('/manage/media');
expect(cfg.url.signout).toBe('/signout');
// admin visible
expect(cfg.url.admin).toBe('/admin');
});
test('theme and site defaults propagate correctly', () => {
const cfg = config(baseGlobal);
expect(cfg.theme.mode).toBe('dark');
expect(cfg.theme.switch.position).toBe('sidebar');
expect(cfg.theme.logo.darkMode.img).toBe('/img/dark.png');
expect(cfg.site.id).toBe('my-site');
expect(cfg.site.version).toBe('2.0.0');
});
test('memoizes and returns the same object instance on repeated calls', () => {
const first = config(baseGlobal);
const second = config(baseGlobal);
expect(second).toBe(first);
});
test('url profile paths use site.url when not in dev env', () => {
const cfg = config(baseGlobal);
expect(cfg.url.profile.media).toBe('https://example.com/user/john');
expect(cfg.url.embed).toBe('https://example.com/embed?m=');
});
});
});

View File

@@ -0,0 +1,83 @@
import { init, settings } from '../../../src/static/js/utils/settings/contents';
const contentsConfig = (obj: any) => {
init(obj);
return settings();
};
describe('utils/settings', () => {
describe('contents', () => {
test('Strings are trimmed and default to empty', () => {
const cfg1 = contentsConfig({
header: { right: ' R ', onLogoRight: ' OLR ' },
sidebar: { belowNavMenu: ' X ', belowThemeSwitcher: ' Y ', footer: ' Z ' },
uploader: { belowUploadArea: ' U1 ', postUploadMessage: ' U2 ' },
});
const cfg2 = contentsConfig({});
const cfg3 = contentsConfig({ header: {}, sidebar: {}, uploader: {} });
expect(cfg1).toStrictEqual({
header: { right: 'R', onLogoRight: 'OLR' },
sidebar: {
navMenu: { items: [] },
mainMenuExtra: { items: [] },
belowNavMenu: 'X',
belowThemeSwitcher: 'Y',
footer: 'Z',
},
uploader: { belowUploadArea: 'U1', postUploadMessage: 'U2' },
});
expect(cfg2).toStrictEqual({
header: { right: '', onLogoRight: '' },
sidebar: {
navMenu: { items: [] },
mainMenuExtra: { items: [] },
belowNavMenu: '',
belowThemeSwitcher: '',
footer: '',
},
uploader: { belowUploadArea: '', postUploadMessage: '' },
});
expect(cfg3).toStrictEqual({
header: { right: '', onLogoRight: '' },
sidebar: {
navMenu: { items: [] },
mainMenuExtra: { items: [] },
belowNavMenu: '',
belowThemeSwitcher: '',
footer: '',
},
uploader: { belowUploadArea: '', postUploadMessage: '' },
});
});
// @todo: Revisit this behavior
test('Sidebar menu items require text, link, icon and NOT get trimmed', () => {
const cfg = contentsConfig({
sidebar: {
mainMenuExtraItems: [
{ text: ' A ', link: ' /a ', icon: ' i-a ', className: ' cls ' },
{ text: 'no-link', icon: 'i' },
{ link: '/missing-text', icon: 'i' },
{ text: 'no-icon', link: '/x' },
],
navMenuItems: [
{ text: ' B ', link: ' /b ', icon: ' i-b ' },
{ text: ' ', link: '/bad', icon: 'i' },
],
},
});
expect(cfg.sidebar.mainMenuExtra.items).toStrictEqual([
{ text: ' A ', link: ' /a ', icon: ' i-a ', className: ' cls ' },
]);
expect(cfg.sidebar.navMenu.items).toStrictEqual([
{ text: ' B ', link: ' /b ', icon: ' i-b ', className: undefined },
{ text: ' ', link: '/bad', icon: 'i', className: undefined },
]);
});
});
});

View File

@@ -0,0 +1,41 @@
import { init, settings } from '../../../src/static/js/utils/settings/media';
const mediaConfig = (item?: any, shareOptions?: any) => {
init(item, shareOptions);
return settings();
};
describe('utils/settings', () => {
describe('media', () => {
test('Defaults display flags to true when not hidden', () => {
const cfg = mediaConfig();
expect(cfg.item.displayAuthor).toBe(true);
expect(cfg.item.displayViews).toBe(true);
expect(cfg.item.displayPublishDate).toBe(true);
expect(cfg.share.options).toEqual([]);
});
test('Respects hide flags for author, views and date', () => {
const cfg = mediaConfig({ hideAuthor: true, hideViews: true, hideDate: true });
expect(cfg.item.displayAuthor).toBe(false);
expect(cfg.item.displayViews).toBe(false);
expect(cfg.item.displayPublishDate).toBe(false);
});
test('Returns empty share options when not provided', () => {
const cfg = mediaConfig({ hideAuthor: false }, undefined);
expect(cfg.share.options).toEqual([]);
});
// @todo: Revisit this behavior
test('Filters share options to valid ones and trims whitespace', () => {
const cfg = mediaConfig(undefined, [' embed ', 'email', ' email ']);
expect(cfg.share.options).toEqual(['email']);
});
test('Ignores falsy and invalid share options', () => {
const cfg = mediaConfig(undefined, [undefined, '', ' ', 'invalid', 'share', 'EMBED']);
expect(cfg.share.options).toEqual([]);
});
});
});

View File

@@ -0,0 +1,162 @@
import { init, settings } from '../../../src/static/js/utils/settings/member';
const memberConfig = (user?: any, features?: any) => {
init(user, features);
return settings();
};
describe('utils/settings', () => {
describe('member', () => {
// @todo: Revisit this behavior
test('Returns anonymous defaults when user not provided', () => {
const cfg = memberConfig();
expect(cfg).toStrictEqual({
name: null,
username: null,
thumbnail: null,
is: { admin: false, anonymous: true },
can: {
login: true,
register: true,
addMedia: false,
editProfile: false,
canSeeMembersPage: true,
usersNeedsToBeApproved: true,
changePassword: true,
deleteProfile: false,
readComment: true,
addComment: false,
mentionComment: false,
deleteComment: false,
editMedia: false,
deleteMedia: false,
editSubtitle: false,
manageMedia: false,
manageUsers: false,
manageComments: false,
reportMedia: false,
downloadMedia: false,
saveMedia: false,
likeMedia: true,
dislikeMedia: true,
shareMedia: true,
contactUser: false,
},
pages: { home: null, about: null, media: null, playlists: null },
});
});
test('Trims user strings and applies user capability booleans when authenticated', () => {
const cfg = memberConfig({
is: { anonymous: false, admin: true },
name: ' John Doe ',
username: ' johnd ',
thumbnail: ' /img/j.png ',
can: {
changePassword: true,
deleteProfile: true,
addComment: true,
mentionComment: true,
deleteComment: true,
editMedia: true,
deleteMedia: true,
editSubtitle: true,
manageMedia: true,
manageUsers: true,
manageComments: true,
contactUser: true,
addMedia: true,
editProfile: true,
readComment: true,
canSeeMembersPage: true,
usersNeedsToBeApproved: false,
},
pages: { about: ' /u/john/about ', media: ' /u/john ', playlists: ' /u/john/playlists ' },
});
expect(cfg).toStrictEqual({
name: 'John Doe',
username: 'johnd',
thumbnail: '/img/j.png',
is: { admin: true, anonymous: false },
can: {
login: true,
register: true,
addMedia: true,
editProfile: true,
canSeeMembersPage: true,
usersNeedsToBeApproved: false,
changePassword: true,
deleteProfile: true,
readComment: true,
addComment: true,
mentionComment: true,
deleteComment: true,
editMedia: true,
deleteMedia: true,
editSubtitle: true,
manageMedia: true,
manageUsers: true,
manageComments: true,
reportMedia: false,
downloadMedia: false,
saveMedia: false,
likeMedia: true,
dislikeMedia: true,
shareMedia: true,
contactUser: true,
},
pages: { home: null, about: '/u/john/about', media: '/u/john', playlists: '/u/john/playlists' },
});
});
test('Comment capabilities require both user.can and features.media.actions', () => {
const cfg1 = memberConfig(
{ is: { anonymous: false }, can: { addComment: true, mentionComment: true } },
{ media: { actions: { comment: false, comment_mention: true } } }
);
expect(cfg1.can.addComment).toBe(false);
expect(cfg1.can.mentionComment).toBe(true);
const cfg2 = memberConfig(
{ is: { anonymous: false }, can: { addComment: true, mentionComment: true } },
{ media: { actions: { comment: true, comment_mention: true } } }
);
expect(cfg2.can.addComment).toBe(true);
expect(cfg2.can.mentionComment).toBe(true);
});
test('Header login/register reflect headerBar feature flags', () => {
expect(memberConfig(undefined, { headerBar: { hideLogin: true } }).can.login).toBe(false);
expect(memberConfig(undefined, { headerBar: { hideRegister: true } }).can.register).toBe(false);
expect(memberConfig(undefined, { headerBar: { hideLogin: false, hideRegister: false } }).can).toMatchObject(
{ login: true, register: true }
);
});
test('Media actions flags set like/dislike/share/report/download/save with correct defaults', () => {
const cfg1 = memberConfig(undefined, {
media: {
actions: { like: false, dislike: false, share: false, report: true, download: true, save: true },
},
});
expect(cfg1.can.likeMedia).toBe(false);
expect(cfg1.can.dislikeMedia).toBe(false);
expect(cfg1.can.shareMedia).toBe(false);
expect(cfg1.can.reportMedia).toBe(true);
expect(cfg1.can.downloadMedia).toBe(true);
expect(cfg1.can.saveMedia).toBe(true);
});
test('User flags canSeeMembersPage/usersNeedsToBeApproved/readComment default handling', () => {
const cfg1 = memberConfig({
is: { anonymous: false },
can: { canSeeMembersPage: false, usersNeedsToBeApproved: false, readComment: false },
});
expect(cfg1.can.canSeeMembersPage).toBe(false);
expect(cfg1.can.usersNeedsToBeApproved).toBe(false);
expect(cfg1.can.readComment).toBe(false);
});
});
});

View File

@@ -0,0 +1,70 @@
import { init, settings } from '../../../src/static/js/utils/settings/notifications';
const notificationsConfig = (sett?: any) => {
init(sett);
return settings();
};
describe('utils/settings', () => {
describe('notifications', () => {
test('Returns defaults when no settings provided', () => {
const cfg = notificationsConfig();
expect(cfg).toStrictEqual({
messages: {
addToLiked: 'Added to liked media',
removeFromLiked: 'Removed from liked media',
addToDisliked: 'Added to disliked media',
removeFromDisliked: 'Removed from disliked media',
},
});
});
// @todo: Revisit this behavior
test('Keep incoming message values without processing', () => {
const cfg = notificationsConfig({
messages: {
addToLiked: ' Yay ',
removeFromLiked: ' ',
addToDisliked: '\nNope',
removeFromDisliked: '\t OK\t',
},
});
expect(cfg.messages.addToLiked).toBe(' Yay ');
expect(cfg.messages.removeFromLiked).toBe(' ');
expect(cfg.messages.addToDisliked).toBe('\nNope');
expect(cfg.messages.removeFromDisliked).toBe('\t OK\t');
});
test('Ignores undefined, keeping defaults', () => {
const cfg = notificationsConfig({
messages: {
addToLiked: undefined,
removeFromLiked: undefined,
addToDisliked: undefined,
removeFromDisliked: undefined,
},
});
expect(cfg.messages.addToLiked).toBe('Added to liked media');
expect(cfg.messages.removeFromLiked).toBe('Removed from liked media');
expect(cfg.messages.addToDisliked).toBe('Added to disliked media');
expect(cfg.messages.removeFromDisliked).toBe('Removed from disliked media');
});
test('Allows partial overrides without affecting other keys', () => {
const cfg = notificationsConfig({ messages: { addToLiked: 'Nice!' } });
expect(cfg.messages.addToLiked).toBe('Nice!');
expect(cfg.messages.removeFromLiked).toBe('Removed from liked media');
expect(cfg.messages.addToDisliked).toBe('Added to disliked media');
expect(cfg.messages.removeFromDisliked).toBe('Removed from disliked media');
});
test('Handles extraneous keys by passing them through while keeping known defaults intact', () => {
const cfg = notificationsConfig({ messages: { addToLiked: 'A', notARealKey: 'x' } });
expect(cfg.messages.notARealKey).toBeUndefined();
expect(cfg.messages.addToLiked).toBe('A');
expect(cfg.messages.removeFromLiked).toBe('Removed from liked media');
expect(cfg.messages.addToDisliked).toBe('Added to disliked media');
expect(cfg.messages.removeFromDisliked).toBe('Removed from disliked media');
});
});
});

View File

@@ -0,0 +1,60 @@
import { init, settings } from '../../../src/static/js/utils/settings/optionsEmbedded';
const optionsEmbeddedConfig = (embeddedVideo?: any) => {
init(embeddedVideo);
return settings();
};
describe('utils/settings', () => {
describe('optionsEmbedded', () => {
test('Returns default dimensions when settings is undefined', () => {
const cfg = optionsEmbeddedConfig(undefined);
expect(cfg.video.dimensions).toStrictEqual({ width: 560, widthUnit: 'px', height: 315, heightUnit: 'px' });
});
test('Returns default dimensions when settings.initialDimensions is undefined', () => {
const cfg = optionsEmbeddedConfig({});
expect(cfg.video.dimensions).toStrictEqual({ width: 560, widthUnit: 'px', height: 315, heightUnit: 'px' });
});
test('Applies valid numeric width and height from initialDimensions', () => {
const cfg = optionsEmbeddedConfig({ initialDimensions: { width: 640, height: 360 } });
expect(cfg.video.dimensions).toStrictEqual({ width: 640, widthUnit: 'px', height: 360, heightUnit: 'px' });
});
// @todo: Revisit this behavior
test('Ignores NaN and non-numeric width/height and keeps defaults', () => {
const cfg1 = optionsEmbeddedConfig({ initialDimensions: { width: NaN, height: NaN } });
expect(cfg1.video.dimensions).toStrictEqual({ width: 560, widthUnit: 'px', height: 315, heightUnit: 'px' });
const cfg2 = optionsEmbeddedConfig({ initialDimensions: { width: '640', height: '360' } });
expect(cfg2.video.dimensions).toStrictEqual({
width: '640',
widthUnit: 'px',
height: '360',
heightUnit: 'px',
});
});
// @todo: Revisit this behavior
test('Ignores provided widthUnit/heightUnit as they are not used', () => {
const cfg = optionsEmbeddedConfig({
initialDimensions: { width: 800, height: 450, widthUnit: 'percent', heightUnit: 'percent' },
});
expect(cfg.video.dimensions.width).toBe(800);
expect(cfg.video.dimensions.height).toBe(450);
expect(cfg.video.dimensions.widthUnit).toBe('px');
expect(cfg.video.dimensions.heightUnit).toBe('px');
});
// @todo: Revisit this behavior
test('Does not mutate the provided settings object', () => {
const input = {
initialDimensions: { width: 700, height: 400, widthUnit: 'percent', heightUnit: 'percent' },
};
const copy = JSON.parse(JSON.stringify(input));
optionsEmbeddedConfig(input);
expect(input).toStrictEqual(copy);
});
});
});

View File

@@ -0,0 +1,125 @@
import { init, settings } from '../../../src/static/js/utils/settings/optionsPages';
const optionsPagesConfig = (home?: any, search?: any, media?: any, profile?: any, VALID_PAGES?: any) => {
init(home, search, media, profile, VALID_PAGES);
return settings();
};
describe('utils/settings', () => {
describe('optionsPages', () => {
test('Uses VALID_PAGES titles as defaults for home sections when provided', () => {
const cfg = optionsPagesConfig(undefined, undefined, undefined, undefined, {
latest: { title: 'Recent' },
featured: { title: 'Spotlight' },
recommended: { title: 'You may like' },
});
expect(cfg.home.sections.latest.title).toBe('Recent');
expect(cfg.home.sections.featured.title).toBe('Spotlight');
expect(cfg.home.sections.recommended.title).toBe('You may like');
});
test('Trims custom home section titles from input', () => {
const cfg = optionsPagesConfig({
sections: {
latest: { title: ' LATEST ' },
featured: { title: '\nFeatured ' },
recommended: { title: ' Recommended' },
},
});
expect(cfg.home.sections.latest.title).toBe('LATEST');
expect(cfg.home.sections.featured.title).toBe('Featured');
expect(cfg.home.sections.recommended.title).toBe('Recommended');
});
test('Sets search.advancedFilters true only when explicitly true', () => {
const def = optionsPagesConfig(undefined, undefined, undefined, undefined, {});
expect(def.search.advancedFilters).toBe(false);
const falsy = optionsPagesConfig(undefined, { advancedFilters: false }, undefined, undefined, {});
expect(falsy.search.advancedFilters).toBe(false);
const truthy = optionsPagesConfig(undefined, { advancedFilters: true }, undefined, undefined, {});
expect(truthy.search.advancedFilters).toBe(true);
});
test('Configures media options with correct defaults and overrides', () => {
const def = optionsPagesConfig(undefined, undefined, undefined, undefined, {});
expect(def.media.categoriesWithTitle).toBe(false);
expect(def.media.htmlInDescription).toBe(false);
expect(def.media.displayViews).toBe(true);
expect(def.media.related.initialSize).toBe(10);
const override = optionsPagesConfig(
undefined,
undefined,
{
categoriesWithTitle: true,
htmlInDescription: true,
hideViews: true,
related: { initialSize: 25 },
},
undefined,
{}
);
expect(override.media.categoriesWithTitle).toBe(true);
expect(override.media.htmlInDescription).toBe(true);
expect(override.media.displayViews).toBe(false);
expect(override.media.related.initialSize).toBe(10); // @todo: Fix this! It should return 25.
});
test('Ignores NaN and non-numeric media.related.initialSize and keeps default 10', () => {
const cfg1 = optionsPagesConfig(undefined, undefined, { related: { initialSize: NaN } }, undefined, {});
expect(cfg1.media.related.initialSize).toBe(10);
const cfg2 = optionsPagesConfig(undefined, undefined, { related: { initialSize: '12' } }, undefined, {});
expect(cfg2.media.related.initialSize).toBe(10);
});
test('Profile settings true only when explicitly true', () => {
const def = optionsPagesConfig(undefined, undefined, undefined, undefined, {});
expect(def.profile.htmlInDescription).toBe(false);
expect(def.profile.includeHistory).toBe(false);
expect(def.profile.includeLikedMedia).toBe(false);
const truthy = optionsPagesConfig(
undefined,
undefined,
undefined,
{
htmlInDescription: true,
includeHistory: true,
includeLikedMedia: true,
},
{}
);
expect(truthy.profile.htmlInDescription).toBe(true);
expect(truthy.profile.includeHistory).toBe(true);
expect(truthy.profile.includeLikedMedia).toBe(true);
});
// @todo: Revisit this behavior
test('Does not mutate provided input objects', () => {
const home = { sections: { latest: { title: ' A ' } } };
const search = { advancedFilters: true };
const media = { hideViews: true, related: { initialSize: 5 } };
const profile = { includeHistory: true };
const validPages = { latest: { title: 'L' }, featured: { title: 'F' }, recommended: { title: 'R' } };
const homeCopy = JSON.parse(JSON.stringify(home));
const searchCopy = JSON.parse(JSON.stringify(search));
const mediaCopy = JSON.parse(JSON.stringify(media));
const profileCopy = JSON.parse(JSON.stringify(profile));
const validPagesCopy = JSON.parse(JSON.stringify(validPages));
optionsPagesConfig(home, search, media, profile, validPages);
expect(home).toStrictEqual(homeCopy);
expect(search).toStrictEqual(searchCopy);
expect(media).toStrictEqual(mediaCopy);
expect(profile).toStrictEqual(profileCopy);
expect(validPages).toStrictEqual(validPagesCopy);
});
});
});

View File

@@ -0,0 +1,63 @@
import { init, settings } from '../../../src/static/js/utils/settings/pages';
const pagesConfig = (sett?: any) => {
init(sett);
return settings();
};
describe('utils/settings', () => {
describe('pages', () => {
test('Defaults: all known pages disabled with default titles', () => {
const cfg = pagesConfig();
expect(cfg).toStrictEqual({
latest: { enabled: false, title: 'Recent uploads' },
featured: { enabled: false, title: 'Featured' },
recommended: { enabled: false, title: 'Recommended' },
members: { enabled: false, title: 'Members' },
liked: { enabled: false, title: 'Liked media' },
history: { enabled: false, title: 'History' },
});
});
test('Enables each page unless explicitly disabled', () => {
const cfg = pagesConfig({
latest: {},
featured: { enabled: true },
recommended: { enabled: false },
members: { enabled: undefined },
liked: { enabled: null },
history: { enabled: 0 },
});
expect(cfg.latest.enabled).toBe(true);
expect(cfg.featured.enabled).toBe(true);
expect(cfg.recommended.enabled).toBe(false);
expect(cfg.members.enabled).toBe(true);
expect(cfg.liked.enabled).toBe(true);
expect(cfg.history.enabled).toBe(true);
});
test('Trims provided titles and preserves defaults when title is undefined', () => {
const cfg = pagesConfig({
latest: { title: ' Latest ' },
featured: { title: '\nFeatured' },
recommended: {},
});
expect(cfg.latest.title).toBe('Latest');
expect(cfg.featured.title).toBe('Featured');
expect(cfg.recommended.title).toBe('Recommended');
});
test('Ignores unknown keys in settings', () => {
const cfg = pagesConfig({ unknownKey: { enabled: true, title: 'X' }, latest: { enabled: true } });
expect(cfg.latest.enabled).toBe(true);
expect(cfg.unknownKey).toBeUndefined();
});
test('Does not mutate the input settings object', () => {
const input = { latest: { enabled: false, title: ' A ' }, featured: { enabled: true, title: ' B ' } };
const snapshot = JSON.parse(JSON.stringify(input));
pagesConfig(input);
expect(input).toStrictEqual(snapshot);
});
});
});

View File

@@ -0,0 +1,52 @@
import { init, settings } from '../../../src/static/js/utils/settings/playlists';
const playlistsConfig = (plists?: any) => {
init(plists);
return settings();
};
describe('utils/settings', () => {
describe('playlists', () => {
test('Defaults to both audio and video when no settings provided', () => {
const cfg = playlistsConfig();
expect(cfg.mediaTypes).toEqual(['audio', 'video']);
});
test('Returns default when provided mediaTypes array is empty', () => {
const cfg = playlistsConfig({ mediaTypes: [] });
expect(cfg.mediaTypes).toEqual(['audio', 'video']);
});
test('Includes only valid media types when both valid and invalid are provided', () => {
const cfg = playlistsConfig({ mediaTypes: ['audio', 'invalid', 'video', 'something'] });
expect(cfg.mediaTypes).toEqual(['audio', 'video']);
});
test('Returns default when provided mediaTypes is non-array or undefined/null', () => {
expect(playlistsConfig({}).mediaTypes).toEqual(['audio', 'video']);
expect(playlistsConfig({ mediaTypes: undefined }).mediaTypes).toEqual(['audio', 'video']);
// expect(playlistsConfig({ mediaTypes: null }).mediaTypes).toEqual(['audio', 'video']); // @todo: Revisit this behavior
expect(playlistsConfig({ mediaTypes: 'audio' }).mediaTypes).toEqual(['audio', 'video']);
expect(playlistsConfig({ mediaTypes: 123 }).mediaTypes).toEqual(['audio', 'video']);
});
// @todo: Revisit this behavior
test('Handles duplicates and preserves order among valid items', () => {
const cfg = playlistsConfig({ mediaTypes: ['video', 'audio', 'video', 'audio', 'invalid'] });
expect(cfg.mediaTypes).toEqual(['video', 'audio', 'video', 'audio']);
});
// @todo: Revisit this behavior
test('Rejects non-exact case values (e.g., \"Audio\")', () => {
const cfg = playlistsConfig({ mediaTypes: ['Audio', 'Video'] });
expect(cfg.mediaTypes).toEqual(['audio', 'video']);
});
test('does not mutate the input object', () => {
const input = { mediaTypes: ['audio', 'video', 'invalid'] };
const copy = JSON.parse(JSON.stringify(input));
playlistsConfig(input);
expect(input).toEqual(copy);
});
});
});

View File

@@ -0,0 +1,53 @@
import { init, settings } from '../../../src/static/js/utils/settings/sidebar';
const sidebarConfig = (sett?: any) => {
init(sett);
return settings();
};
describe('utils/settings', () => {
describe('sidebar', () => {
test('Defaults to all links visible when no settings provided', () => {
const cfg = sidebarConfig();
expect(cfg).toStrictEqual({ hideHomeLink: false, hideTagsLink: false, hideCategoriesLink: false });
});
test('Hides only those explicitly set to true', () => {
const cfg1 = sidebarConfig({ hideHomeLink: true });
expect(cfg1).toStrictEqual({ hideHomeLink: true, hideTagsLink: false, hideCategoriesLink: false });
const cfg2 = sidebarConfig({ hideTagsLink: true });
expect(cfg2).toStrictEqual({ hideHomeLink: false, hideTagsLink: true, hideCategoriesLink: false });
const cfg3 = sidebarConfig({ hideCategoriesLink: true });
expect(cfg3).toStrictEqual({ hideHomeLink: false, hideTagsLink: false, hideCategoriesLink: true });
const cfgAll = sidebarConfig({ hideHomeLink: true, hideTagsLink: true, hideCategoriesLink: true });
expect(cfgAll).toStrictEqual({ hideHomeLink: true, hideTagsLink: true, hideCategoriesLink: true });
});
test('Treats non-true values as false', () => {
// false
expect(sidebarConfig({ hideHomeLink: false }).hideHomeLink).toBe(false);
// undefined
expect(sidebarConfig({}).hideHomeLink).toBe(false);
// null
expect(sidebarConfig({ hideTagsLink: null }).hideTagsLink).toBe(false);
// other types
expect(sidebarConfig({ hideCategoriesLink: 'yes' }).hideCategoriesLink).toBe(false);
expect(sidebarConfig({ hideCategoriesLink: 1 }).hideCategoriesLink).toBe(false);
});
test('Is resilient to partial inputs and ignores extra properties', () => {
const cfg = sidebarConfig({ hideTagsLink: true, extra: 'prop' });
expect(cfg).toStrictEqual({ hideHomeLink: false, hideTagsLink: true, hideCategoriesLink: false });
});
test('Does not mutate input object', () => {
const input: any = { hideHomeLink: true };
const copy = JSON.parse(JSON.stringify(input));
sidebarConfig(input);
expect(input).toStrictEqual(copy);
});
});
});

View File

@@ -0,0 +1,63 @@
import { init, settings } from '../../../src/static/js/utils/settings/site';
const siteConfig = (sett?: any) => {
init(sett);
return settings();
};
describe('utils/settings', () => {
describe('site', () => {
test('Applies defaults when no settings provided', () => {
const cfg = siteConfig();
expect(cfg).toStrictEqual({
id: 'media-cms',
url: '',
api: '',
title: '',
useRoundedCorners: true,
version: '1.0.0',
});
});
test('Trims string fields (id, url, api, title, version)', () => {
const cfg = siteConfig({
id: ' my-site ',
url: ' https://example.com/ ',
api: ' https://example.com/api/ ',
title: ' Media CMS ',
version: ' 2.3.4 ',
});
expect(cfg).toStrictEqual({
id: 'my-site',
url: 'https://example.com/',
api: 'https://example.com/api/',
title: 'Media CMS',
useRoundedCorners: true,
version: '2.3.4',
});
});
test('Handles useRoundedCorners: defaults to true unless explicitly false', () => {
expect(siteConfig({}).useRoundedCorners).toBe(true);
expect(siteConfig({ useRoundedCorners: true }).useRoundedCorners).toBe(true);
expect(siteConfig({ useRoundedCorners: false }).useRoundedCorners).toBe(false);
// non-boolean should still evaluate to default true because only === false toggles it off
expect(siteConfig({ useRoundedCorners: 'no' }).useRoundedCorners).toBe(true);
expect(siteConfig({ useRoundedCorners: 0 }).useRoundedCorners).toBe(true);
expect(siteConfig({ useRoundedCorners: null }).useRoundedCorners).toBe(true);
});
test('Is resilient to partial inputs and ignores extra properties', () => {
const cfg = siteConfig({ id: ' x ', extra: 'y' });
expect(cfg).toMatchObject({ id: 'x' });
expect(Object.keys(cfg).sort()).toEqual(['api', 'id', 'title', 'url', 'useRoundedCorners', 'version']);
});
test('Does not mutate input object', () => {
const input = { id: ' my-id ', useRoundedCorners: false };
const copy = JSON.parse(JSON.stringify(input));
siteConfig(input);
expect(input).toEqual(copy);
});
});
});

View File

@@ -0,0 +1,53 @@
import { init, settings } from '../../../src/static/js/utils/settings/taxonomies';
const taxonomiesConfig = (sett?: any) => {
init(sett);
return settings();
};
describe('utils-settings/taxonomies', () => {
test('Should return defaults when settings is undefined', () => {
const res = taxonomiesConfig();
expect(res).toStrictEqual({
tags: { enabled: false, title: 'Tags' },
categories: { enabled: false, title: 'Categories' },
});
});
test('Should enable a taxonomy when enabled is true', () => {
const res = taxonomiesConfig({ tags: { enabled: true } });
expect(res.tags).toStrictEqual({ enabled: true, title: 'Tags' });
});
test('Should keep taxonomy disabled when enabled is true', () => {
const res = taxonomiesConfig({ categories: { enabled: true } });
expect(res.categories).toStrictEqual({ enabled: true, title: 'Categories' });
});
test('Should default to enabled=true when enabled is omitted but key exists', () => {
const res = taxonomiesConfig({ tags: {} });
expect(res.tags).toStrictEqual({ enabled: true, title: 'Tags' });
});
test('Should trim title when provided', () => {
const res = taxonomiesConfig({ tags: { title: ' My Tags ' } });
expect(res.tags).toStrictEqual({ enabled: true, title: 'My Tags' });
});
test('Should ignore unknown taxonomy keys', () => {
const input = {
unknownKey: { enabled: true, title: 'X' },
tags: { enabled: true, title: 'Tagz' },
};
const res = taxonomiesConfig(input);
expect(res).toStrictEqual({
tags: { enabled: true, title: 'Tagz' },
categories: { enabled: false, title: 'Categories' },
});
});
test('Should not change title when title is undefined', () => {
const res = taxonomiesConfig({ categories: { enabled: true, title: undefined } });
expect(res.categories).toStrictEqual({ enabled: true, title: 'Categories' });
});
});

View File

@@ -0,0 +1,77 @@
import { init, settings } from '../../../src/static/js/utils/settings/theme';
const themeConfig = (theme?: any, logo?: any) => {
init(theme, logo);
return settings();
};
describe('utils/settings', () => {
describe('theme', () => {
test('Applies defaults when no inputs provided', () => {
const cfg = themeConfig();
expect(cfg).toStrictEqual({
mode: 'light',
switch: { enabled: true, position: 'header' },
logo: { lightMode: { img: '', svg: '' }, darkMode: { img: '', svg: '' } },
});
});
test("Sets dark mode only when theme.mode is exactly 'dark' after trim", () => {
expect(themeConfig({ mode: 'dark' }).mode).toBe('dark');
expect(themeConfig({ mode: ' dark ' }).mode).toBe('dark');
expect(themeConfig({ mode: 'Dark' }).mode).toBe('light');
expect(themeConfig({ mode: 'light' }).mode).toBe('light');
expect(themeConfig({ mode: ' ' }).mode).toBe('light');
});
test('Switch config: enabled only toggles off when explicitly false; position set to sidebar only when exactly sidebar after trim', () => {
expect(themeConfig({ switch: { enabled: false } }).switch.enabled).toBe(false);
expect(themeConfig({ switch: { enabled: true } }).switch.enabled).toBe(true);
expect(themeConfig({ switch: { enabled: undefined } }).switch.enabled).toBe(true);
expect(themeConfig({ switch: { position: 'sidebar' } }).switch.position).toBe('sidebar');
expect(themeConfig({ switch: { position: ' sidebar ' } }).switch.position).toBe('header'); // @todo: Fix this. It should be 'sidebar'
expect(themeConfig({ switch: { position: 'header' } }).switch.position).toBe('header');
expect(themeConfig({ switch: { position: 'foot' } }).switch.position).toBe('header');
});
test('Trims and maps logo URLs for both light and dark modes; ignores missing fields', () => {
const cfg = themeConfig(undefined, {
lightMode: { img: ' /img/light.png ', svg: ' /img/light.svg ' },
darkMode: { img: ' /img/dark.png ', svg: ' /img/dark.svg ' },
});
expect(cfg).toStrictEqual({
mode: 'light',
switch: { enabled: true, position: 'header' },
logo: {
lightMode: { img: '/img/light.png', svg: '/img/light.svg' },
darkMode: { img: '/img/dark.png', svg: '/img/dark.svg' },
},
});
const partial = themeConfig(undefined, { lightMode: { img: ' /only-light.png ' } });
expect(partial).toStrictEqual({
mode: 'light',
switch: { enabled: true, position: 'header' },
logo: {
lightMode: { img: '/only-light.png', svg: '' },
darkMode: { img: '', svg: '' },
},
});
});
test('Does not mutate input objects', () => {
const themeIn = { mode: ' dark ', switch: { enabled: false, position: ' sidebar ' } };
const logoIn = { lightMode: { img: ' x ', svg: ' y ' }, darkMode: { img: ' z ', svg: ' w ' } };
const themeCopy = JSON.parse(JSON.stringify(themeIn));
const logoCopy = JSON.parse(JSON.stringify(logoIn));
themeConfig(themeIn, logoIn);
expect(themeIn).toStrictEqual(themeCopy);
expect(logoIn).toStrictEqual(logoCopy);
});
});
});

View File

@@ -0,0 +1,99 @@
import { init, pages } from '../../../src/static/js/utils/settings/url';
const urlConfig = (pages_url?: any) => {
init(pages_url);
return pages();
};
describe('utils/settings', () => {
describe('url', () => {
const baseGlobal = {
profileId: 'john',
site: {
url: 'https://example.com/',
devEnv: false,
},
url: {
home: '/',
admin: '/admin',
error404: '/404',
latestMedia: '/latest',
featuredMedia: '/featured',
recommendedMedia: '/recommended',
signin: '/signin',
signout: '/signout',
register: '/register',
changePassword: '/password',
members: '/members',
search: '/search',
likedMedia: '/liked',
history: '/history',
addMedia: '/add',
editChannel: '/edit/channel',
editProfile: '/edit/profile',
tags: '/tags',
categories: '/categories',
manageMedia: '/manage/media',
manageUsers: '/manage/users',
manageComments: '/manage/comments',
},
user: {
is: { anonymous: false, admin: false },
pages: { media: '/u/john', about: '/u/john/about', playlists: '/u/john/playlists' },
},
} as const;
test('Authenticated non-admin user', () => {
const cfg = urlConfig(baseGlobal);
expect(cfg).toStrictEqual({
profileId: 'john',
site: { url: 'https://example.com/', devEnv: false },
url: {
home: '/',
admin: '/admin',
error404: '/404',
latestMedia: '/latest',
featuredMedia: '/featured',
recommendedMedia: '/recommended',
signin: '/signin',
signout: '/signout',
register: '/register',
changePassword: '/password',
members: '/members',
search: '/search',
likedMedia: '/liked',
history: '/history',
addMedia: '/add',
editChannel: '/edit/channel',
editProfile: '/edit/profile',
tags: '/tags',
categories: '/categories',
manageMedia: '/manage/media',
manageUsers: '/manage/users',
manageComments: '/manage/comments',
},
user: {
is: { anonymous: false, admin: false },
pages: { media: '/u/john', about: '/u/john/about', playlists: '/u/john/playlists' },
},
});
});
test('Admin user', () => {
const cfg = urlConfig({
...baseGlobal,
user: { ...baseGlobal.user, is: { anonymous: false, admin: true } },
});
expect(cfg.user.is).toStrictEqual({ anonymous: false, admin: true });
});
test('Anonymous user', () => {
const cfg = urlConfig({
...baseGlobal,
user: { ...baseGlobal.user, is: { anonymous: true, admin: true } },
});
expect(cfg.user.is).toStrictEqual({ anonymous: true, admin: true });
});
});
});

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
!function(){"use strict";var n,e={4256:function(n,e,r){(0,r(2985).C)()}},r={};function t(n){var o=r[n];if(void 0!==o)return o.exports;var i=r[n]={exports:{}};return e[n].call(i.exports,i,i.exports,t),i.exports}t.m=e,n=[],t.O=function(e,r,o,i){if(!r){var u=1/0;for(l=0;l<n.length;l++){r=n[l][0],o=n[l][1],i=n[l][2];for(var f=!0,c=0;c<r.length;c++)(!1&i||u>=i)&&Object.keys(t.O).every((function(n){return t.O[n](r[c])}))?r.splice(c--,1):(f=!1,i<u&&(u=i));if(f){n.splice(l--,1);var a=o();void 0!==a&&(e=a)}}return e}i=i||0;for(var l=n.length;l>0&&n[l-1][2]>i;l--)n[l]=n[l-1];n[l]=[r,o,i]},t.n=function(n){var e=n&&n.__esModule?function(){return n.default}:function(){return n};return t.d(e,{a:e}),e},t.d=function(n,e){for(var r in e)t.o(e,r)&&!t.o(n,r)&&Object.defineProperty(n,r,{enumerable:!0,get:e[r]})},t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(n){if("object"==typeof window)return window}}(),t.o=function(n,e){return Object.prototype.hasOwnProperty.call(n,e)},t.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},t.j=594,function(){var n={594:0};t.O.j=function(e){return 0===n[e]};var e=function(e,r){var o,i,u=r[0],f=r[1],c=r[2],a=0;if(u.some((function(e){return 0!==n[e]}))){for(o in f)t.o(f,o)&&(t.m[o]=f[o]);if(c)var l=c(t)}for(e&&e(r);a<u.length;a++)i=u[a],t.o(n,i)&&n[i]&&n[i][0](),n[i]=0;return t.O(l)},r=self.webpackChunkmediacms_frontend=self.webpackChunkmediacms_frontend||[];r.forEach(e.bind(null,0)),r.push=e.bind(null,r.push.bind(r))}();var o=t.O(void 0,[276],(function(){return t(4256)}));o=t.O(o)}();
!function(){"use strict";var n,e={4256:function(n,e,r){(0,r(2985).C)()}},r={};function t(n){var o=r[n];if(void 0!==o)return o.exports;var i=r[n]={exports:{}};return e[n].call(i.exports,i,i.exports,t),i.exports}t.m=e,n=[],t.O=function(e,r,o,i){if(!r){var u=1/0;for(l=0;l<n.length;l++){r=n[l][0],o=n[l][1],i=n[l][2];for(var f=!0,c=0;c<r.length;c++)(!1&i||u>=i)&&Object.keys(t.O).every(function(n){return t.O[n](r[c])})?r.splice(c--,1):(f=!1,i<u&&(u=i));if(f){n.splice(l--,1);var a=o();void 0!==a&&(e=a)}}return e}i=i||0;for(var l=n.length;l>0&&n[l-1][2]>i;l--)n[l]=n[l-1];n[l]=[r,o,i]},t.n=function(n){var e=n&&n.__esModule?function(){return n.default}:function(){return n};return t.d(e,{a:e}),e},t.d=function(n,e){for(var r in e)t.o(e,r)&&!t.o(n,r)&&Object.defineProperty(n,r,{enumerable:!0,get:e[r]})},t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(n){if("object"==typeof window)return window}}(),t.o=function(n,e){return Object.prototype.hasOwnProperty.call(n,e)},t.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},t.j=594,function(){var n={594:0};t.O.j=function(e){return 0===n[e]};var e=function(e,r){var o,i,u=r[0],f=r[1],c=r[2],a=0;if(u.some(function(e){return 0!==n[e]})){for(o in f)t.o(f,o)&&(t.m[o]=f[o]);if(c)var l=c(t)}for(e&&e(r);a<u.length;a++)i=u[a],t.o(n,i)&&n[i]&&n[i][0](),n[i]=0;return t.O(l)},r=self.webpackChunkmediacms_frontend=self.webpackChunkmediacms_frontend||[];r.forEach(e.bind(null,0)),r.push=e.bind(null,r.push.bind(r))}();var o=t.O(void 0,[276],function(){return t(4256)});o=t.O(o)}();

View File

@@ -1 +1 @@
!function(){"use strict";var n,e={5879:function(n,e,r){(0,r(2985).C)()}},r={};function t(n){var o=r[n];if(void 0!==o)return o.exports;var i=r[n]={exports:{}};return e[n].call(i.exports,i,i.exports,t),i.exports}t.m=e,n=[],t.O=function(e,r,o,i){if(!r){var u=1/0;for(l=0;l<n.length;l++){r=n[l][0],o=n[l][1],i=n[l][2];for(var f=!0,c=0;c<r.length;c++)(!1&i||u>=i)&&Object.keys(t.O).every((function(n){return t.O[n](r[c])}))?r.splice(c--,1):(f=!1,i<u&&(u=i));if(f){n.splice(l--,1);var a=o();void 0!==a&&(e=a)}}return e}i=i||0;for(var l=n.length;l>0&&n[l-1][2]>i;l--)n[l]=n[l-1];n[l]=[r,o,i]},t.n=function(n){var e=n&&n.__esModule?function(){return n.default}:function(){return n};return t.d(e,{a:e}),e},t.d=function(n,e){for(var r in e)t.o(e,r)&&!t.o(n,r)&&Object.defineProperty(n,r,{enumerable:!0,get:e[r]})},t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(n){if("object"==typeof window)return window}}(),t.o=function(n,e){return Object.prototype.hasOwnProperty.call(n,e)},t.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},t.j=543,function(){var n={543:0};t.O.j=function(e){return 0===n[e]};var e=function(e,r){var o,i,u=r[0],f=r[1],c=r[2],a=0;if(u.some((function(e){return 0!==n[e]}))){for(o in f)t.o(f,o)&&(t.m[o]=f[o]);if(c)var l=c(t)}for(e&&e(r);a<u.length;a++)i=u[a],t.o(n,i)&&n[i]&&n[i][0](),n[i]=0;return t.O(l)},r=self.webpackChunkmediacms_frontend=self.webpackChunkmediacms_frontend||[];r.forEach(e.bind(null,0)),r.push=e.bind(null,r.push.bind(r))}();var o=t.O(void 0,[276],(function(){return t(5879)}));o=t.O(o)}();
!function(){"use strict";var n,e={5879:function(n,e,r){(0,r(2985).C)()}},r={};function t(n){var o=r[n];if(void 0!==o)return o.exports;var i=r[n]={exports:{}};return e[n].call(i.exports,i,i.exports,t),i.exports}t.m=e,n=[],t.O=function(e,r,o,i){if(!r){var u=1/0;for(l=0;l<n.length;l++){r=n[l][0],o=n[l][1],i=n[l][2];for(var f=!0,c=0;c<r.length;c++)(!1&i||u>=i)&&Object.keys(t.O).every(function(n){return t.O[n](r[c])})?r.splice(c--,1):(f=!1,i<u&&(u=i));if(f){n.splice(l--,1);var a=o();void 0!==a&&(e=a)}}return e}i=i||0;for(var l=n.length;l>0&&n[l-1][2]>i;l--)n[l]=n[l-1];n[l]=[r,o,i]},t.n=function(n){var e=n&&n.__esModule?function(){return n.default}:function(){return n};return t.d(e,{a:e}),e},t.d=function(n,e){for(var r in e)t.o(e,r)&&!t.o(n,r)&&Object.defineProperty(n,r,{enumerable:!0,get:e[r]})},t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(n){if("object"==typeof window)return window}}(),t.o=function(n,e){return Object.prototype.hasOwnProperty.call(n,e)},t.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},t.j=543,function(){var n={543:0};t.O.j=function(e){return 0===n[e]};var e=function(e,r){var o,i,u=r[0],f=r[1],c=r[2],a=0;if(u.some(function(e){return 0!==n[e]})){for(o in f)t.o(f,o)&&(t.m[o]=f[o]);if(c)var l=c(t)}for(e&&e(r);a<u.length;a++)i=u[a],t.o(n,i)&&n[i]&&n[i][0](),n[i]=0;return t.O(l)},r=self.webpackChunkmediacms_frontend=self.webpackChunkmediacms_frontend||[];r.forEach(e.bind(null,0)),r.push=e.bind(null,r.push.bind(r))}();var o=t.O(void 0,[276],function(){return t(5879)});o=t.O(o)}();

View File

@@ -1 +1 @@
!function(){"use strict";var n,e={1684:function(n,e,r){(0,r(2985).C)()}},r={};function t(n){var o=r[n];if(void 0!==o)return o.exports;var i=r[n]={exports:{}};return e[n].call(i.exports,i,i.exports,t),i.exports}t.m=e,n=[],t.O=function(e,r,o,i){if(!r){var u=1/0;for(l=0;l<n.length;l++){r=n[l][0],o=n[l][1],i=n[l][2];for(var f=!0,c=0;c<r.length;c++)(!1&i||u>=i)&&Object.keys(t.O).every((function(n){return t.O[n](r[c])}))?r.splice(c--,1):(f=!1,i<u&&(u=i));if(f){n.splice(l--,1);var a=o();void 0!==a&&(e=a)}}return e}i=i||0;for(var l=n.length;l>0&&n[l-1][2]>i;l--)n[l]=n[l-1];n[l]=[r,o,i]},t.n=function(n){var e=n&&n.__esModule?function(){return n.default}:function(){return n};return t.d(e,{a:e}),e},t.d=function(n,e){for(var r in e)t.o(e,r)&&!t.o(n,r)&&Object.defineProperty(n,r,{enumerable:!0,get:e[r]})},t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(n){if("object"==typeof window)return window}}(),t.o=function(n,e){return Object.prototype.hasOwnProperty.call(n,e)},t.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},t.j=152,function(){var n={152:0};t.O.j=function(e){return 0===n[e]};var e=function(e,r){var o,i,u=r[0],f=r[1],c=r[2],a=0;if(u.some((function(e){return 0!==n[e]}))){for(o in f)t.o(f,o)&&(t.m[o]=f[o]);if(c)var l=c(t)}for(e&&e(r);a<u.length;a++)i=u[a],t.o(n,i)&&n[i]&&n[i][0](),n[i]=0;return t.O(l)},r=self.webpackChunkmediacms_frontend=self.webpackChunkmediacms_frontend||[];r.forEach(e.bind(null,0)),r.push=e.bind(null,r.push.bind(r))}();var o=t.O(void 0,[276],(function(){return t(1684)}));o=t.O(o)}();
!function(){"use strict";var n,e={1684:function(n,e,r){(0,r(2985).C)()}},r={};function t(n){var o=r[n];if(void 0!==o)return o.exports;var i=r[n]={exports:{}};return e[n].call(i.exports,i,i.exports,t),i.exports}t.m=e,n=[],t.O=function(e,r,o,i){if(!r){var u=1/0;for(l=0;l<n.length;l++){r=n[l][0],o=n[l][1],i=n[l][2];for(var f=!0,c=0;c<r.length;c++)(!1&i||u>=i)&&Object.keys(t.O).every(function(n){return t.O[n](r[c])})?r.splice(c--,1):(f=!1,i<u&&(u=i));if(f){n.splice(l--,1);var a=o();void 0!==a&&(e=a)}}return e}i=i||0;for(var l=n.length;l>0&&n[l-1][2]>i;l--)n[l]=n[l-1];n[l]=[r,o,i]},t.n=function(n){var e=n&&n.__esModule?function(){return n.default}:function(){return n};return t.d(e,{a:e}),e},t.d=function(n,e){for(var r in e)t.o(e,r)&&!t.o(n,r)&&Object.defineProperty(n,r,{enumerable:!0,get:e[r]})},t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(n){if("object"==typeof window)return window}}(),t.o=function(n,e){return Object.prototype.hasOwnProperty.call(n,e)},t.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},t.j=152,function(){var n={152:0};t.O.j=function(e){return 0===n[e]};var e=function(e,r){var o,i,u=r[0],f=r[1],c=r[2],a=0;if(u.some(function(e){return 0!==n[e]})){for(o in f)t.o(f,o)&&(t.m[o]=f[o]);if(c)var l=c(t)}for(e&&e(r);a<u.length;a++)i=u[a],t.o(n,i)&&n[i]&&n[i][0](),n[i]=0;return t.O(l)},r=self.webpackChunkmediacms_frontend=self.webpackChunkmediacms_frontend||[];r.forEach(e.bind(null,0)),r.push=e.bind(null,r.push.bind(r))}();var o=t.O(void 0,[276],function(){return t(1684)});o=t.O(o)}();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
!function(){"use strict";var t,e={282:function(t,e,n){var r=n(2985),o=n(9471),i=n(8713),s=n.n(i),a=n(8790),u=n(285),l=n(2855),c=n(9835),m=n(9479);function f(t,e,n){return t+"?"+e+(""===e?"":"&")+"page="+n}class h extends m.Y{constructor(t){super(t,"manage-comments"),this.state={resultsCount:null,requestUrl:a.ApiUrlContext._currentValue.manage.comments,currentPage:1,sortingArgs:"",sortBy:"add_date",ordering:"desc",refresh:0},this.getCountFunc=this.getCountFunc.bind(this),this.onTablePageChange=this.onTablePageChange.bind(this),this.onColumnSortClick=this.onColumnSortClick.bind(this),this.onItemsRemoval=this.onItemsRemoval.bind(this),this.onItemsRemovalFail=this.onItemsRemovalFail.bind(this)}onTablePageChange(t,e){this.setState({currentPage:e,requestUrl:f(a.ApiUrlContext._currentValue.manage.comments,this.state.sortingArgs,e)})}getCountFunc(t){this.setState({resultsCount:t})}onColumnSortClick(t,e){const n="sort_by="+t+"&ordering="+e;this.setState({sortBy:t,ordering:e,sortingArgs:n,requestUrl:f(a.ApiUrlContext._currentValue.manage.comments,n,this.state.currentPage)})}onItemsRemoval(t){this.setState({resultsCount:null,refresh:this.state.refresh+1,requestUrl:a.ApiUrlContext._currentValue.manage.comments},(function(){t?u.PageActions.addNotification("The comments deleted successfully.","commentsRemovalSucceed"):u.PageActions.addNotification("The comment deleted successfully.","commentRemovalSucceed")}))}onItemsRemovalFail(t){t?u.PageActions.addNotification("The comments removal failed. Please try again.","commentsRemovalFailed"):u.PageActions.addNotification("The comment removal failed. Please try again.","commentRemovalFailed")}pageContent(){return o.createElement(l.MediaListWrapper,{title:this.props.title+(null===this.state.resultsCount?"":" ("+this.state.resultsCount+")"),className:"search-results-wrap items-list-hor"},o.createElement(c.D,{pageItems:50,manageType:"comments",key:this.state.requestUrl+"["+this.state.refresh+"]",itemsCountCallback:this.getCountFunc,requestUrl:this.state.requestUrl,onPageChange:this.onTablePageChange,sortBy:this.state.sortBy,ordering:this.state.ordering,onRowsDelete:this.onItemsRemoval,onRowsDeleteFail:this.onItemsRemovalFail,onClickColumnSort:this.onColumnSortClick}))}}h.propTypes={title:s().string.isRequired},h.defaultProps={title:"Manage comments"},(0,r.C)("page-manage-comments",h)},7664:function(t,e,n){n.r(e),n.d(e,{CircleIconButton:function(){return r.i},FilterOptions:function(){return o.P},FiltersToggleButton:function(){return i.I},MaterialIcon:function(){return s.Z},NavigationContentApp:function(){return a.V},NavigationMenuList:function(){return u.S},Notifications:function(){return l.$},NumericInputWithUnit:function(){return c._},PopupMain:function(){return m.AP},PopupTop:function(){return m.cp},SpinnerLoader:function(){return f.x},UserThumbnail:function(){return h.c}});var r=n(5321),o=n(7256),i=n(3135),s=n(2828),a=n(5305),u=n(7201),l=n(6089),c=n(3818),m=n(2901),f=n(6568),h=n(878)}},n={};function r(t){var o=n[t];if(void 0!==o)return o.exports;var i=n[t]={exports:{}};return e[t].call(i.exports,i,i.exports,r),i.exports}r.m=e,t=[],r.O=function(e,n,o,i){if(!n){var s=1/0;for(c=0;c<t.length;c++){n=t[c][0],o=t[c][1],i=t[c][2];for(var a=!0,u=0;u<n.length;u++)(!1&i||s>=i)&&Object.keys(r.O).every((function(t){return r.O[t](n[u])}))?n.splice(u--,1):(a=!1,i<s&&(s=i));if(a){t.splice(c--,1);var l=o();void 0!==l&&(e=l)}}return e}i=i||0;for(var c=t.length;c>0&&t[c-1][2]>i;c--)t[c]=t[c-1];t[c]=[n,o,i]},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,{a:e}),e},r.d=function(t,e){for(var n in e)r.o(e,n)&&!r.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:e[n]})},r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}(),r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.j=401,function(){var t={401:0};r.O.j=function(e){return 0===t[e]};var e=function(e,n){var o,i,s=n[0],a=n[1],u=n[2],l=0;if(s.some((function(e){return 0!==t[e]}))){for(o in a)r.o(a,o)&&(r.m[o]=a[o]);if(u)var c=u(r)}for(e&&e(n);l<s.length;l++)i=s[l],r.o(t,i)&&t[i]&&t[i][0](),t[i]=0;return r.O(c)},n=self.webpackChunkmediacms_frontend=self.webpackChunkmediacms_frontend||[];n.forEach(e.bind(null,0)),n.push=e.bind(null,n.push.bind(n))}();var o=r.O(void 0,[276],(function(){return r(282)}));o=r.O(o)}();
!function(){"use strict";var t,e={282:function(t,e,n){var r=n(2985),o=n(9471),i=n(8713),s=n.n(i),a=n(8790),u=n(285),l=n(2855),c=n(9835),m=n(9479);function f(t,e,n){return t+"?"+e+(""===e?"":"&")+"page="+n}class h extends m.Y{constructor(t){super(t,"manage-comments"),this.state={resultsCount:null,requestUrl:a.ApiUrlContext._currentValue.manage.comments,currentPage:1,sortingArgs:"",sortBy:"add_date",ordering:"desc",refresh:0},this.getCountFunc=this.getCountFunc.bind(this),this.onTablePageChange=this.onTablePageChange.bind(this),this.onColumnSortClick=this.onColumnSortClick.bind(this),this.onItemsRemoval=this.onItemsRemoval.bind(this),this.onItemsRemovalFail=this.onItemsRemovalFail.bind(this)}onTablePageChange(t,e){this.setState({currentPage:e,requestUrl:f(a.ApiUrlContext._currentValue.manage.comments,this.state.sortingArgs,e)})}getCountFunc(t){this.setState({resultsCount:t})}onColumnSortClick(t,e){const n="sort_by="+t+"&ordering="+e;this.setState({sortBy:t,ordering:e,sortingArgs:n,requestUrl:f(a.ApiUrlContext._currentValue.manage.comments,n,this.state.currentPage)})}onItemsRemoval(t){this.setState({resultsCount:null,refresh:this.state.refresh+1,requestUrl:a.ApiUrlContext._currentValue.manage.comments},function(){t?u.PageActions.addNotification("The comments deleted successfully.","commentsRemovalSucceed"):u.PageActions.addNotification("The comment deleted successfully.","commentRemovalSucceed")})}onItemsRemovalFail(t){t?u.PageActions.addNotification("The comments removal failed. Please try again.","commentsRemovalFailed"):u.PageActions.addNotification("The comment removal failed. Please try again.","commentRemovalFailed")}pageContent(){return o.createElement(l.MediaListWrapper,{title:this.props.title+(null===this.state.resultsCount?"":" ("+this.state.resultsCount+")"),className:"search-results-wrap items-list-hor"},o.createElement(c.D,{pageItems:50,manageType:"comments",key:this.state.requestUrl+"["+this.state.refresh+"]",itemsCountCallback:this.getCountFunc,requestUrl:this.state.requestUrl,onPageChange:this.onTablePageChange,sortBy:this.state.sortBy,ordering:this.state.ordering,onRowsDelete:this.onItemsRemoval,onRowsDeleteFail:this.onItemsRemovalFail,onClickColumnSort:this.onColumnSortClick}))}}h.propTypes={title:s().string.isRequired},h.defaultProps={title:"Manage comments"},(0,r.C)("page-manage-comments",h)},7664:function(t,e,n){n.r(e),n.d(e,{CircleIconButton:function(){return r.i},FilterOptions:function(){return o.P},FiltersToggleButton:function(){return i.I},MaterialIcon:function(){return s.Z},NavigationContentApp:function(){return a.V},NavigationMenuList:function(){return u.S},Notifications:function(){return l.$},NumericInputWithUnit:function(){return c._},PopupMain:function(){return m.AP},PopupTop:function(){return m.cp},SpinnerLoader:function(){return f.x},UserThumbnail:function(){return h.c}});var r=n(5321),o=n(7256),i=n(3135),s=n(2828),a=n(5305),u=n(7201),l=n(6089),c=n(3818),m=n(2901),f=n(6568),h=n(878)}},n={};function r(t){var o=n[t];if(void 0!==o)return o.exports;var i=n[t]={exports:{}};return e[t].call(i.exports,i,i.exports,r),i.exports}r.m=e,t=[],r.O=function(e,n,o,i){if(!n){var s=1/0;for(c=0;c<t.length;c++){n=t[c][0],o=t[c][1],i=t[c][2];for(var a=!0,u=0;u<n.length;u++)(!1&i||s>=i)&&Object.keys(r.O).every(function(t){return r.O[t](n[u])})?n.splice(u--,1):(a=!1,i<s&&(s=i));if(a){t.splice(c--,1);var l=o();void 0!==l&&(e=l)}}return e}i=i||0;for(var c=t.length;c>0&&t[c-1][2]>i;c--)t[c]=t[c-1];t[c]=[n,o,i]},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,{a:e}),e},r.d=function(t,e){for(var n in e)r.o(e,n)&&!r.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:e[n]})},r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}(),r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.j=401,function(){var t={401:0};r.O.j=function(e){return 0===t[e]};var e=function(e,n){var o,i,s=n[0],a=n[1],u=n[2],l=0;if(s.some(function(e){return 0!==t[e]})){for(o in a)r.o(a,o)&&(r.m[o]=a[o]);if(u)var c=u(r)}for(e&&e(n);l<s.length;l++)i=s[l],r.o(t,i)&&t[i]&&t[i][0](),t[i]=0;return r.O(c)},n=self.webpackChunkmediacms_frontend=self.webpackChunkmediacms_frontend||[];n.forEach(e.bind(null,0)),n.push=e.bind(null,n.push.bind(n))}();var o=r.O(void 0,[276],function(){return r(282)});o=r.O(o)}();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long