feat: utils/hooks unit tests

This commit is contained in:
Yiannis
2026-02-07 18:39:24 +02:00
parent 9f9dd699b2
commit c2043fafa1
14 changed files with 2819 additions and 1 deletions

View File

@@ -5,5 +5,5 @@ module.exports = {
'^.+\\.tsx?$': 'ts-jest', '^.+\\.tsx?$': 'ts-jest',
'^.+\\.jsx?$': 'babel-jest', '^.+\\.jsx?$': 'babel-jest',
}, },
collectCoverageFrom: ['src/**'], collectCoverageFrom: ['src/**', '!src/static/lib/**'],
}; };

View File

@@ -0,0 +1,749 @@
import React from 'react';
import { render, fireEvent, act } from '@testing-library/react';
import { useBulkActions } from '../../../src/static/js/utils/hooks/useBulkActions';
// Mock translateString to return the input for easier assertions
jest.mock('../../../src/static/js/utils/helpers', () => ({
translateString: (s: string) => s,
}));
// Component that exposes hook state/handlers to DOM for testing
function HookConsumer() {
const hook = useBulkActions();
return (
<div>
<div data-testid="selected-count">{Array.from(hook.selectedMedia).length}</div>
<div data-testid="available-count">{hook.availableMediaIds.length}</div>
<div data-testid="show-confirm">{String(hook.showConfirmModal)}</div>
<div data-testid="confirm-message">{hook.confirmMessage}</div>
<div data-testid="list-key">{hook.listKey}</div>
<div data-testid="notification-message">{hook.notificationMessage}</div>
<div data-testid="show-notification">{String(hook.showNotification)}</div>
{/* @todo: It doesn't used */}
{/* <div data-testid="notification-type">{hook.notificationType}</div> */}
<div data-testid="show-permission">{String(hook.showPermissionModal)}</div>
<div data-testid="permission-type">{hook.permissionType || ''}</div>
<div data-testid="show-playlist">{String(hook.showPlaylistModal)}</div>
<div data-testid="show-change-owner">{String(hook.showChangeOwnerModal)}</div>
<div data-testid="show-publish-state">{String(hook.showPublishStateModal)}</div>
<div data-testid="show-category">{String(hook.showCategoryModal)}</div>
<div data-testid="show-tag">{String(hook.showTagModal)}</div>
<button data-testid="btn-handle-media-select" onClick={() => hook.handleMediaSelection('m1', true)} />
<button data-testid="btn-handle-media-deselect" onClick={() => hook.handleMediaSelection('m1', false)} />
<button
data-testid="btn-handle-items-update"
onClick={() => hook.handleItemsUpdate([{ id: 'a' }, { uid: 'b' }, { friendly_token: 'c' }])}
/>
<button data-testid="btn-select-all" onClick={() => hook.handleSelectAll()} />
<button data-testid="btn-deselect-all" onClick={() => hook.handleDeselectAll()} />
<button data-testid="btn-clear-selection" onClick={() => hook.clearSelection()} />
<button data-testid="btn-clear-refresh" onClick={() => hook.clearSelectionAndRefresh()} />
<button data-testid="btn-bulk-delete" onClick={() => hook.handleBulkAction('delete-media')} />
<button data-testid="btn-bulk-enable-comments" onClick={() => hook.handleBulkAction('enable-comments')} />
<button data-testid="btn-bulk-disable-comments" onClick={() => hook.handleBulkAction('disable-comments')} />
<button data-testid="btn-bulk-enable-download" onClick={() => hook.handleBulkAction('enable-download')} />
<button data-testid="btn-bulk-disable-download" onClick={() => hook.handleBulkAction('disable-download')} />
<button data-testid="btn-bulk-copy" onClick={() => hook.handleBulkAction('copy-media')} />
<button data-testid="btn-bulk-perm-viewer" onClick={() => hook.handleBulkAction('add-remove-coviewers')} />
<button data-testid="btn-bulk-perm-editor" onClick={() => hook.handleBulkAction('add-remove-coeditors')} />
<button data-testid="btn-bulk-perm-owner" onClick={() => hook.handleBulkAction('add-remove-coowners')} />
<button data-testid="btn-bulk-playlist" onClick={() => hook.handleBulkAction('add-remove-playlist')} />
<button data-testid="btn-bulk-change-owner" onClick={() => hook.handleBulkAction('change-owner')} />
<button data-testid="btn-bulk-publish" onClick={() => hook.handleBulkAction('publish-state')} />
<button data-testid="btn-bulk-category" onClick={() => hook.handleBulkAction('add-remove-category')} />
<button data-testid="btn-bulk-tag" onClick={() => hook.handleBulkAction('add-remove-tags')} />
<button data-testid="btn-bulk-unknown" onClick={() => hook.handleBulkAction('unknown-action')} />
<button data-testid="btn-confirm-proceed" onClick={() => hook.handleConfirmProceed()} />
<button data-testid="btn-confirm-cancel" onClick={() => hook.handleConfirmCancel()} />
<button data-testid="btn-perm-cancel" onClick={() => hook.handlePermissionModalCancel()} />
<button data-testid="btn-perm-success" onClick={() => hook.handlePermissionModalSuccess('perm ok')} />
<button data-testid="btn-perm-error" onClick={() => hook.handlePermissionModalError('perm err')} />
<button data-testid="btn-playlist-cancel" onClick={() => hook.handlePlaylistModalCancel()} />
<button data-testid="btn-playlist-success" onClick={() => hook.handlePlaylistModalSuccess('pl ok')} />
<button data-testid="btn-playlist-error" onClick={() => hook.handlePlaylistModalError('pl err')} />
<button data-testid="btn-change-owner-cancel" onClick={() => hook.handleChangeOwnerModalCancel()} />
<button
data-testid="btn-change-owner-success"
onClick={() => hook.handleChangeOwnerModalSuccess('owner ok')}
/>
<button
data-testid="btn-change-owner-error"
onClick={() => hook.handleChangeOwnerModalError('owner err')}
/>
<button data-testid="btn-publish-cancel" onClick={() => hook.handlePublishStateModalCancel()} />
<button data-testid="btn-publish-success" onClick={() => hook.handlePublishStateModalSuccess('pub ok')} />
<button data-testid="btn-publish-error" onClick={() => hook.handlePublishStateModalError('pub err')} />
<button data-testid="btn-category-cancel" onClick={() => hook.handleCategoryModalCancel()} />
<button data-testid="btn-category-success" onClick={() => hook.handleCategoryModalSuccess('cat ok')} />
<button data-testid="btn-category-error" onClick={() => hook.handleCategoryModalError('cat err')} />
<button data-testid="btn-tag-cancel" onClick={() => hook.handleTagModalCancel()} />
<button data-testid="btn-tag-success" onClick={() => hook.handleTagModalSuccess('tag ok')} />
<button data-testid="btn-tag-error" onClick={() => hook.handleTagModalError('tag err')} />
<div data-testid="csrf">{String(hook.getCsrfToken())}</div>
</div>
);
}
describe('useBulkActions', () => {
beforeEach(() => {
jest.clearAllMocks();
document.cookie.split(';').forEach((c) => {
document.cookie = c.replace(/^ +/, '').replace(/=.*/, '=;expires=' + new Date().toUTCString() + ';path=/');
});
global.fetch = jest.fn();
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
describe('Utility Functions', () => {
test('getCsrfToken reads csrftoken from cookies', () => {
document.cookie = 'csrftoken=abc123';
const { getByTestId } = render(<HookConsumer />);
expect(getByTestId('csrf').textContent).toBe('abc123');
});
test('getCsrfToken returns null when csrftoken is not present', () => {
// No cookie set, should return null
const { getByTestId } = render(<HookConsumer />);
expect(getByTestId('csrf').textContent).toBe('null');
});
test('getCsrfToken returns null when document.cookie is empty', () => {
// Even if we try to set empty cookie, it should return null if no csrftoken
const { getByTestId } = render(<HookConsumer />);
expect(getByTestId('csrf').textContent).toBe('null');
});
});
describe('Selection Management', () => {
test('handleMediaSelection toggles selected media', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
expect(getByTestId('selected-count').textContent).toBe('1');
fireEvent.click(getByTestId('btn-handle-media-deselect'));
expect(getByTestId('selected-count').textContent).toBe('0');
});
test('handleItemsUpdate extracts ids correctly from items with different id types', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-items-update'));
expect(getByTestId('available-count').textContent).toBe('3');
});
test('handleSelectAll selects all available items', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-items-update'));
fireEvent.click(getByTestId('btn-select-all'));
expect(getByTestId('selected-count').textContent).toBe('3');
});
test('handleDeselectAll deselects all items', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-items-update'));
fireEvent.click(getByTestId('btn-select-all'));
fireEvent.click(getByTestId('btn-deselect-all'));
expect(getByTestId('selected-count').textContent).toBe('0');
});
test('clearSelection clears all selected media', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
expect(getByTestId('selected-count').textContent).toBe('1');
fireEvent.click(getByTestId('btn-clear-selection'));
expect(getByTestId('selected-count').textContent).toBe('0');
});
test('clearSelectionAndRefresh clears selection and increments listKey', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-items-update'));
fireEvent.click(getByTestId('btn-select-all'));
expect(getByTestId('list-key').textContent).toBe('0');
fireEvent.click(getByTestId('btn-clear-refresh'));
expect(getByTestId('selected-count').textContent).toBe('0');
expect(getByTestId('list-key').textContent).toBe('1');
});
});
describe('Bulk Actions - Modal Opening', () => {
test('handleBulkAction does nothing when no selection', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-bulk-delete'));
expect(getByTestId('show-confirm').textContent).toBe('false');
});
test('handleBulkAction opens confirm modal for delete, enable/disable comments and download, copy', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
fireEvent.click(getByTestId('btn-bulk-delete'));
expect(getByTestId('show-confirm').textContent).toBe('true');
fireEvent.click(getByTestId('btn-bulk-enable-comments'));
expect(getByTestId('show-confirm').textContent).toBe('true');
fireEvent.click(getByTestId('btn-bulk-disable-comments'));
expect(getByTestId('show-confirm').textContent).toBe('true');
fireEvent.click(getByTestId('btn-bulk-enable-download'));
expect(getByTestId('show-confirm').textContent).toBe('true');
fireEvent.click(getByTestId('btn-bulk-disable-download'));
expect(getByTestId('show-confirm').textContent).toBe('true');
fireEvent.click(getByTestId('btn-bulk-copy'));
expect(getByTestId('show-confirm').textContent).toBe('true');
});
test('handleBulkAction opens permission modals with correct types', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
fireEvent.click(getByTestId('btn-bulk-perm-viewer'));
expect(getByTestId('show-permission').textContent).toBe('true');
expect(getByTestId('permission-type').textContent).toBe('viewer');
fireEvent.click(getByTestId('btn-bulk-perm-editor'));
expect(getByTestId('permission-type').textContent).toBe('editor');
fireEvent.click(getByTestId('btn-bulk-perm-owner'));
expect(getByTestId('permission-type').textContent).toBe('owner');
});
test('handleBulkAction opens other modals', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
fireEvent.click(getByTestId('btn-bulk-playlist'));
expect(getByTestId('show-playlist').textContent).toBe('true');
fireEvent.click(getByTestId('btn-bulk-change-owner'));
expect(getByTestId('show-change-owner').textContent).toBe('true');
fireEvent.click(getByTestId('btn-bulk-publish'));
expect(getByTestId('show-publish-state').textContent).toBe('true');
fireEvent.click(getByTestId('btn-bulk-category'));
expect(getByTestId('show-category').textContent).toBe('true');
fireEvent.click(getByTestId('btn-bulk-tag'));
expect(getByTestId('show-tag').textContent).toBe('true');
});
test('handleBulkAction with unknown action does nothing', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
fireEvent.click(getByTestId('btn-bulk-unknown'));
expect(getByTestId('show-confirm').textContent).toBe('false');
expect(getByTestId('show-permission').textContent).toBe('false');
});
});
describe('Confirm Modal Handlers', () => {
test('handleConfirmCancel closes confirm modal and resets state', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
fireEvent.click(getByTestId('btn-bulk-delete'));
expect(getByTestId('show-confirm').textContent).toBe('true');
fireEvent.click(getByTestId('btn-confirm-cancel'));
expect(getByTestId('show-confirm').textContent).toBe('false');
expect(getByTestId('confirm-message').textContent).toBe('');
});
});
describe('Delete Media Execution', () => {
test('executeDeleteMedia success with notification', async () => {
(global.fetch as jest.Mock).mockResolvedValue({ ok: true, json: () => Promise.resolve({}) });
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
fireEvent.click(getByTestId('btn-bulk-delete'));
await act(async () => {
fireEvent.click(getByTestId('btn-confirm-proceed'));
await Promise.resolve();
});
expect(getByTestId('notification-message').textContent).toContain('The media was deleted successfully');
expect(getByTestId('show-notification').textContent).toBe('true');
act(() => {
jest.advanceTimersByTime(5000);
});
expect(getByTestId('show-notification').textContent).toBe('false');
});
test('executeDeleteMedia handles response.ok = false', async () => {
(global.fetch as jest.Mock).mockResolvedValue({ ok: false, json: () => Promise.resolve({}) });
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
fireEvent.click(getByTestId('btn-bulk-delete'));
await act(async () => {
fireEvent.click(getByTestId('btn-confirm-proceed'));
await Promise.resolve();
});
expect(getByTestId('notification-message').textContent).toContain('Failed to delete media');
});
test('executeDeleteMedia handles fetch rejection exception', async () => {
(global.fetch as jest.Mock).mockRejectedValue(new Error('Network error'));
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
fireEvent.click(getByTestId('btn-bulk-delete'));
await act(async () => {
fireEvent.click(getByTestId('btn-confirm-proceed'));
await Promise.resolve();
});
expect(getByTestId('notification-message').textContent).toContain('Failed to delete media');
expect(getByTestId('selected-count').textContent).toBe('0');
});
});
describe('Comments Management Execution', () => {
test('executeEnableComments success', async () => {
(global.fetch as jest.Mock).mockResolvedValue({ ok: true, json: () => Promise.resolve({}) });
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
fireEvent.click(getByTestId('btn-bulk-enable-comments'));
await act(async () => {
fireEvent.click(getByTestId('btn-confirm-proceed'));
await Promise.resolve();
});
expect(getByTestId('notification-message').textContent).toContain('Successfully Enabled comments');
});
test('executeEnableComments handles response.ok = false', async () => {
(global.fetch as jest.Mock).mockResolvedValue({ ok: false, json: () => Promise.resolve({}) });
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
fireEvent.click(getByTestId('btn-bulk-enable-comments'));
await act(async () => {
fireEvent.click(getByTestId('btn-confirm-proceed'));
await Promise.resolve();
});
expect(getByTestId('notification-message').textContent).toContain('Failed to enable comments');
});
test('executeEnableComments handles fetch rejection exception', async () => {
(global.fetch as jest.Mock).mockRejectedValue(new Error('Network error'));
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
fireEvent.click(getByTestId('btn-bulk-enable-comments'));
await act(async () => {
fireEvent.click(getByTestId('btn-confirm-proceed'));
await Promise.resolve();
});
expect(getByTestId('notification-message').textContent).toContain('Failed to enable comments');
expect(getByTestId('selected-count').textContent).toBe('0');
});
test('executeDisableComments success', async () => {
(global.fetch as jest.Mock).mockResolvedValue({ ok: true, json: () => Promise.resolve({}) });
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
fireEvent.click(getByTestId('btn-bulk-disable-comments'));
await act(async () => {
fireEvent.click(getByTestId('btn-confirm-proceed'));
await Promise.resolve();
});
expect(getByTestId('notification-message').textContent).toContain('Successfully Disabled comments');
expect(getByTestId('selected-count').textContent).toBe('0');
});
test('executeDisableComments handles response.ok = false', async () => {
(global.fetch as jest.Mock).mockResolvedValue({ ok: false, json: () => Promise.resolve({}) });
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
fireEvent.click(getByTestId('btn-bulk-disable-comments'));
await act(async () => {
fireEvent.click(getByTestId('btn-confirm-proceed'));
await Promise.resolve();
});
expect(getByTestId('notification-message').textContent).toContain('Failed to disable comments');
});
test('executeDisableComments handles fetch rejection exception', async () => {
(global.fetch as jest.Mock).mockRejectedValue(new Error('Network error'));
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
fireEvent.click(getByTestId('btn-bulk-disable-comments'));
await act(async () => {
fireEvent.click(getByTestId('btn-confirm-proceed'));
await Promise.resolve();
});
expect(getByTestId('notification-message').textContent).toContain('Failed to disable comments');
expect(getByTestId('selected-count').textContent).toBe('0');
});
});
describe('Download Management Execution', () => {
test('executeEnableDownload success', async () => {
(global.fetch as jest.Mock).mockResolvedValue({ ok: true, json: () => Promise.resolve({}) });
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
fireEvent.click(getByTestId('btn-bulk-enable-download'));
await act(async () => {
fireEvent.click(getByTestId('btn-confirm-proceed'));
await Promise.resolve();
});
expect(getByTestId('notification-message').textContent).toContain('Successfully Enabled Download');
});
test('executeEnableDownload handles response.ok = false', async () => {
(global.fetch as jest.Mock).mockResolvedValue({ ok: false, json: () => Promise.resolve({}) });
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
fireEvent.click(getByTestId('btn-bulk-enable-download'));
await act(async () => {
fireEvent.click(getByTestId('btn-confirm-proceed'));
await Promise.resolve();
});
expect(getByTestId('notification-message').textContent).toContain('Failed to enable download');
});
test('executeEnableDownload handles fetch rejection exception', async () => {
(global.fetch as jest.Mock).mockRejectedValue(new Error('Network error'));
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
fireEvent.click(getByTestId('btn-bulk-enable-download'));
await act(async () => {
fireEvent.click(getByTestId('btn-confirm-proceed'));
await Promise.resolve();
});
expect(getByTestId('notification-message').textContent).toContain('Failed to enable download');
expect(getByTestId('selected-count').textContent).toBe('0');
});
test('executeDisableDownload success', async () => {
(global.fetch as jest.Mock).mockResolvedValue({ ok: true, json: () => Promise.resolve({}) });
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
fireEvent.click(getByTestId('btn-bulk-disable-download'));
await act(async () => {
fireEvent.click(getByTestId('btn-confirm-proceed'));
await Promise.resolve();
});
expect(getByTestId('notification-message').textContent).toContain('Successfully Disabled Download');
expect(getByTestId('selected-count').textContent).toBe('0');
});
test('executeDisableDownload handles response.ok = false', async () => {
(global.fetch as jest.Mock).mockResolvedValue({ ok: false, json: () => Promise.resolve({}) });
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
fireEvent.click(getByTestId('btn-bulk-disable-download'));
await act(async () => {
fireEvent.click(getByTestId('btn-confirm-proceed'));
await Promise.resolve();
});
expect(getByTestId('notification-message').textContent).toContain('Failed to disable download');
});
test('executeDisableDownload handles fetch rejection exception', async () => {
(global.fetch as jest.Mock).mockRejectedValue(new Error('Network error'));
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
fireEvent.click(getByTestId('btn-bulk-disable-download'));
await act(async () => {
fireEvent.click(getByTestId('btn-confirm-proceed'));
await Promise.resolve();
});
expect(getByTestId('notification-message').textContent).toContain('Failed to disable download');
expect(getByTestId('selected-count').textContent).toBe('0');
});
});
describe('Copy Media Execution', () => {
test('executeCopyMedia success', async () => {
(global.fetch as jest.Mock).mockResolvedValue({ ok: true, json: () => Promise.resolve({}) });
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
fireEvent.click(getByTestId('btn-bulk-copy'));
await act(async () => {
fireEvent.click(getByTestId('btn-confirm-proceed'));
await Promise.resolve();
});
expect(getByTestId('notification-message').textContent).toContain('Successfully Copied');
});
test('executeCopyMedia handles response.ok = false', async () => {
(global.fetch as jest.Mock).mockResolvedValue({ ok: false, json: () => Promise.resolve({}) });
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
fireEvent.click(getByTestId('btn-bulk-copy'));
await act(async () => {
fireEvent.click(getByTestId('btn-confirm-proceed'));
await Promise.resolve();
});
expect(getByTestId('notification-message').textContent).toContain('Failed to copy media');
});
test('executeCopyMedia handles fetch rejection exception', async () => {
(global.fetch as jest.Mock).mockRejectedValue(new Error('Network error'));
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
fireEvent.click(getByTestId('btn-bulk-copy'));
await act(async () => {
fireEvent.click(getByTestId('btn-confirm-proceed'));
await Promise.resolve();
});
expect(getByTestId('notification-message').textContent).toContain('Failed to copy media');
expect(getByTestId('selected-count').textContent).toBe('0');
});
});
describe('Permission Modal Handlers', () => {
test('handlePermissionModalCancel closes permission modal', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
fireEvent.click(getByTestId('btn-bulk-perm-viewer'));
expect(getByTestId('show-permission').textContent).toBe('true');
fireEvent.click(getByTestId('btn-perm-cancel'));
expect(getByTestId('show-permission').textContent).toBe('false');
expect(getByTestId('permission-type').textContent).toBe('');
});
test('handlePermissionModalSuccess shows notification and closes modal', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-perm-success'));
expect(getByTestId('notification-message').textContent).toBe('perm ok');
expect(getByTestId('show-permission').textContent).toBe('false');
});
test('handlePermissionModalError shows error notification and closes modal', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-perm-error'));
expect(getByTestId('notification-message').textContent).toBe('perm err');
expect(getByTestId('show-permission').textContent).toBe('false');
});
});
describe('Playlist Modal Handlers', () => {
test('handlePlaylistModalCancel closes playlist modal', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
fireEvent.click(getByTestId('btn-bulk-playlist'));
expect(getByTestId('show-playlist').textContent).toBe('true');
fireEvent.click(getByTestId('btn-playlist-cancel'));
expect(getByTestId('show-playlist').textContent).toBe('false');
});
test('handlePlaylistModalSuccess shows notification and closes modal', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-playlist-success'));
expect(getByTestId('notification-message').textContent).toBe('pl ok');
expect(getByTestId('show-playlist').textContent).toBe('false');
});
test('handlePlaylistModalError shows error notification and closes modal', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-playlist-error'));
expect(getByTestId('notification-message').textContent).toBe('pl err');
expect(getByTestId('show-playlist').textContent).toBe('false');
});
});
describe('Change Owner Modal Handlers', () => {
test('handleChangeOwnerModalCancel closes change owner modal', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
fireEvent.click(getByTestId('btn-bulk-change-owner'));
expect(getByTestId('show-change-owner').textContent).toBe('true');
fireEvent.click(getByTestId('btn-change-owner-cancel'));
expect(getByTestId('show-change-owner').textContent).toBe('false');
});
test('handleChangeOwnerModalSuccess shows notification and closes modal', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-change-owner-success'));
expect(getByTestId('notification-message').textContent).toBe('owner ok');
expect(getByTestId('show-change-owner').textContent).toBe('false');
});
test('handleChangeOwnerModalError shows error notification and closes modal', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-change-owner-error'));
expect(getByTestId('notification-message').textContent).toBe('owner err');
expect(getByTestId('show-change-owner').textContent).toBe('false');
});
});
describe('Publish State Modal Handlers', () => {
test('handlePublishStateModalCancel closes publish state modal', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
fireEvent.click(getByTestId('btn-bulk-publish'));
expect(getByTestId('show-publish-state').textContent).toBe('true');
fireEvent.click(getByTestId('btn-publish-cancel'));
expect(getByTestId('show-publish-state').textContent).toBe('false');
});
test('handlePublishStateModalSuccess shows notification and closes modal', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-publish-success'));
expect(getByTestId('notification-message').textContent).toBe('pub ok');
expect(getByTestId('show-publish-state').textContent).toBe('false');
});
test('handlePublishStateModalError shows error notification and closes modal', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-publish-error'));
expect(getByTestId('notification-message').textContent).toBe('pub err');
expect(getByTestId('show-publish-state').textContent).toBe('false');
});
});
describe('Category Modal Handlers', () => {
test('handleCategoryModalCancel closes category modal', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
fireEvent.click(getByTestId('btn-bulk-category'));
expect(getByTestId('show-category').textContent).toBe('true');
fireEvent.click(getByTestId('btn-category-cancel'));
expect(getByTestId('show-category').textContent).toBe('false');
});
test('handleCategoryModalSuccess shows notification and closes modal', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-category-success'));
expect(getByTestId('notification-message').textContent).toBe('cat ok');
expect(getByTestId('show-category').textContent).toBe('false');
});
test('handleCategoryModalError shows error notification and closes modal', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-category-error'));
expect(getByTestId('notification-message').textContent).toBe('cat err');
expect(getByTestId('show-category').textContent).toBe('false');
});
});
describe('Tag Modal Handlers', () => {
test('handleTagModalCancel closes tag modal', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-handle-media-select'));
fireEvent.click(getByTestId('btn-bulk-tag'));
expect(getByTestId('show-tag').textContent).toBe('true');
fireEvent.click(getByTestId('btn-tag-cancel'));
expect(getByTestId('show-tag').textContent).toBe('false');
});
test('handleTagModalSuccess shows notification and closes modal', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-tag-success'));
expect(getByTestId('notification-message').textContent).toBe('tag ok');
expect(getByTestId('show-tag').textContent).toBe('false');
});
test('handleTagModalError shows error notification and closes modal', () => {
const { getByTestId } = render(<HookConsumer />);
fireEvent.click(getByTestId('btn-tag-error'));
expect(getByTestId('notification-message').textContent).toBe('tag err');
expect(getByTestId('show-tag').textContent).toBe('false');
});
});
});

View File

@@ -0,0 +1,380 @@
import React from 'react';
import { render } from '@testing-library/react';
import { useItem } from '../../../src/static/js/utils/hooks/useItem';
// Mock the item components
jest.mock('../../../src/static/js/components/list-item/includes/items', () => ({
ItemDescription: ({ description }: { description: string }) => (
<div data-testid="item-description">{description}</div>
),
ItemMain: ({ children }: { children: React.ReactNode }) => <div data-testid="item-main">{children}</div>,
ItemMainInLink: ({ children, link, title }: { children: React.ReactNode; link: string; title: string }) => (
<div data-testid="item-main-in-link" data-link={link} data-title={title}>
{children}
</div>
),
ItemTitle: ({ title, ariaLabel }: { title: string; ariaLabel: string }) => (
<h3 data-testid="item-title" data-aria-label={ariaLabel}>
{title}
</h3>
),
ItemTitleLink: ({ title, link, ariaLabel }: { title: string; link: string; ariaLabel: string }) => (
<h3 data-testid="item-title-link" data-link={link} data-aria-label={ariaLabel}>
{title}
</h3>
),
}));
// Mock PageStore
jest.mock('../../../src/static/js/utils/stores/PageStore.js', () => ({
__esModule: true,
default: {
get: (key: string) => (key === 'config-site' ? { url: 'https://example.com' } : null),
},
}));
// HookConsumer component to test the hook
function HookConsumer(props: any) {
const { titleComponent, descriptionComponent, thumbnailUrl, UnderThumbWrapper } = useItem(props);
return (
<div>
<div data-testid="title">{titleComponent()}</div>
<div data-testid="description">{descriptionComponent()}</div>
<div data-testid="thumbnail-url">{thumbnailUrl || 'null'}</div>
<div data-testid="wrapper-type">{(UnderThumbWrapper as any).name}</div>
<div data-testid="wrapper-component">
<div>Wrapper content</div>
</div>
</div>
);
}
// Wrapper consumer to test wrapper selection
function WrapperTest(props: any) {
const { UnderThumbWrapper } = useItem(props);
return (
<UnderThumbWrapper link={props.link} title={props.title} data-testid="wrapper-test">
<span data-testid="wrapper-content">Content</span>
</UnderThumbWrapper>
);
}
describe('utils/hooks', () => {
describe('useItem', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('titleComponent Rendering', () => {
test('Renders ItemTitle when singleLinkContent is true', () => {
const { getByTestId } = render(
<HookConsumer
title="Test Title"
description="Test Description"
link="https://example.com"
thumbnail=""
singleLinkContent={true}
/>
);
expect(getByTestId('title').querySelector('[data-testid="item-title"]')).toBeTruthy();
expect(getByTestId('title').querySelector('[data-testid="item-title-link"]')).toBeFalsy();
});
test('Renders ItemTitleLink when singleLinkContent is false', () => {
const { getByTestId } = render(
<HookConsumer
title="Test Title"
description="Test Description"
link="https://example.com"
thumbnail=""
singleLinkContent={false}
/>
);
expect(getByTestId('title').querySelector('[data-testid="item-title"]')).toBeFalsy();
expect(getByTestId('title').querySelector('[data-testid="item-title-link"]')).toBeTruthy();
});
test('Renders with default link when singleLinkContent is not provided', () => {
const { getByTestId } = render(
<HookConsumer title="Test Title" description="Test Description" link="/media/test" thumbnail="" />
);
// Default is false for singleLinkContent
expect(getByTestId('title').querySelector('[data-testid="item-title-link"]')).toBeTruthy();
});
});
describe('descriptionComponent Rendering', () => {
test('Renders single ItemDescription when hasMediaViewer is false', () => {
const { getByTestId, queryAllByTestId } = render(
<HookConsumer
title="Test Title"
description="My Description"
link="https://example.com"
thumbnail=""
hasMediaViewer={false}
/>
);
const descriptions = queryAllByTestId('item-description');
expect(descriptions.length).toBe(1);
expect(descriptions[0].textContent).toBe('My Description');
});
test('Renders single ItemDescription when hasMediaViewerDescr is false', () => {
const { getByTestId, queryAllByTestId } = render(
<HookConsumer
title="Test Title"
description="My Description"
link="https://example.com"
thumbnail=""
hasMediaViewer={true}
hasMediaViewerDescr={false}
/>
);
const descriptions = queryAllByTestId('item-description');
expect(descriptions.length).toBe(1);
expect(descriptions[0].textContent).toBe('My Description');
});
test('Renders two ItemDescriptions when hasMediaViewer and hasMediaViewerDescr are both true', () => {
const { queryAllByTestId } = render(
<HookConsumer
title="Test Title"
description="Main Description"
link="https://example.com"
thumbnail=""
hasMediaViewer={true}
hasMediaViewerDescr={true}
meta_description="Meta Description"
/>
);
const descriptions = queryAllByTestId('item-description');
expect(descriptions.length).toBe(2);
expect(descriptions[0].textContent).toBe('Meta Description');
expect(descriptions[1].textContent).toBe('Main Description');
});
test('Trims description text', () => {
const { queryAllByTestId } = render(
<HookConsumer
title="Test Title"
description=" Description with spaces "
link="https://example.com"
thumbnail=""
/>
);
expect(queryAllByTestId('item-description')[0].textContent).toBe('Description with spaces');
});
test('Trims meta_description text', () => {
const { queryAllByTestId } = render(
<HookConsumer
title="Test Title"
description="Main Description"
link="https://example.com"
thumbnail=""
hasMediaViewer={true}
hasMediaViewerDescr={true}
meta_description=" Meta with spaces "
/>
);
expect(queryAllByTestId('item-description')[0].textContent).toBe('Meta with spaces');
});
});
describe('thumbnailUrl', () => {
test('Returns null when thumbnail is empty string', () => {
const { getByTestId } = render(
<HookConsumer
title="Test Title"
description="Test Description"
link="https://example.com"
thumbnail=""
/>
);
expect(getByTestId('thumbnail-url').textContent).toBe('null');
});
test('Returns formatted URL when thumbnail has value', () => {
const { getByTestId } = render(
<HookConsumer
title="Test Title"
description="Test Description"
link="https://example.com"
thumbnail="/media/thumbnail.jpg"
/>
);
expect(getByTestId('thumbnail-url').textContent).toBe('https://example.com/media/thumbnail.jpg');
});
test('Handles absolute URLs as thumbnails', () => {
const { getByTestId } = render(
<HookConsumer
title="Test Title"
description="Test Description"
link="https://example.com"
thumbnail="https://cdn.example.com/image.jpg"
/>
);
// formatInnerLink should preserve absolute URLs
expect(getByTestId('thumbnail-url').textContent).toBe('https://cdn.example.com/image.jpg');
});
});
describe('UnderThumbWrapper', () => {
test('Uses ItemMainInLink when singleLinkContent is true', () => {
const { getByTestId } = render(
<WrapperTest
title="Test Title"
description="Test Description"
link="https://example.com"
thumbnail=""
singleLinkContent={true}
/>
);
// When singleLinkContent is true, UnderThumbWrapper should be ItemMainInLink
expect(getByTestId('item-main-in-link')).toBeTruthy();
expect(getByTestId('item-main-in-link').getAttribute('data-link')).toBe('https://example.com');
expect(getByTestId('item-main-in-link').getAttribute('data-title')).toBe('Test Title');
});
test('Uses ItemMain when singleLinkContent is false', () => {
const { getByTestId } = render(
<WrapperTest
title="Test Title"
description="Test Description"
link="https://example.com"
thumbnail=""
singleLinkContent={false}
/>
);
// When singleLinkContent is false, UnderThumbWrapper should be ItemMain
expect(getByTestId('item-main')).toBeTruthy();
});
test('Uses ItemMain by default when singleLinkContent is not provided', () => {
const { getByTestId } = render(
<WrapperTest
title="Test Title"
description="Test Description"
link="https://example.com"
thumbnail=""
/>
);
// Default is singleLinkContent=false, so ItemMain
expect(getByTestId('item-main')).toBeTruthy();
});
});
describe('onMount callback', () => {
test('Calls onMount callback when component mounts', () => {
const onMountCallback = jest.fn();
render(
<HookConsumer
title="Test Title"
description="Test Description"
link="https://example.com"
thumbnail=""
onMount={onMountCallback}
/>
);
expect(onMountCallback).toHaveBeenCalledTimes(1);
});
test('Calls onMount only once on initial mount', () => {
const onMountCallback = jest.fn();
const { rerender } = render(
<HookConsumer
title="Test Title"
description="Test Description"
link="https://example.com"
thumbnail=""
onMount={onMountCallback}
/>
);
expect(onMountCallback).toHaveBeenCalledTimes(1);
rerender(
<HookConsumer
title="Updated Title"
description="Updated Description"
link="https://example.com"
thumbnail=""
onMount={onMountCallback}
/>
);
// Should still be called only once (useEffect with empty dependency array)
expect(onMountCallback).toHaveBeenCalledTimes(1);
});
});
describe('Integration tests', () => {
test('Complete rendering with all props', () => {
const onMount = jest.fn();
const { getByTestId, queryAllByTestId } = render(
<HookConsumer
title="Complete Test"
description="Complete Description"
link="/media/complete"
thumbnail="/img/thumb.jpg"
type="media"
hasMediaViewer={true}
hasMediaViewerDescr={true}
meta_description="Complete Meta"
singleLinkContent={false}
onMount={onMount}
/>
);
const descriptions = queryAllByTestId('item-description');
expect(descriptions.length).toBe(2);
expect(onMount).toHaveBeenCalledTimes(1);
expect(getByTestId('thumbnail-url').textContent).toBe('https://example.com/img/thumb.jpg');
});
test('Minimal props required', () => {
const { getByTestId } = render(
<HookConsumer title="Title" description="Description" link="/link" thumbnail="" />
);
expect(getByTestId('title')).toBeTruthy();
expect(getByTestId('description')).toBeTruthy();
expect(getByTestId('thumbnail-url').textContent).toBe('null');
});
test('Renders with special characters in title and description', () => {
const { queryAllByTestId } = render(
<HookConsumer
title="Title with & < > special chars"
description={`Description with 'quotes' and "double quotes"`}
link="/media"
thumbnail=""
/>
);
const descriptions = queryAllByTestId('item-description');
expect(descriptions[0].textContent).toContain('Description with');
});
});
});
});

View File

@@ -0,0 +1,124 @@
import React, { createRef } from 'react';
import { render } from '@testing-library/react';
// Stub style imports used by the hook so Jest doesn't try to parse SCSS
jest.mock('../../../src/static/js/components/item-list/ItemList.scss', () => ({}), { virtual: true });
jest.mock('../../../src/static/js/components/item-list/includes/itemLists/initItemsList', () => ({
__esModule: true,
default: jest.fn((_lists: any[]) => [{ appendItems: jest.fn() }]),
}));
import initItemsList from '../../../src/static/js/components/item-list/includes/itemLists/initItemsList';
import { useItemList } from '../../../src/static/js/utils/hooks/useItemList';
function HookConsumer(props: any) {
const listRef = createRef<HTMLDivElement>();
const [items, countedItems, listHandler, setListHandler, onItemsLoad, onItemsCount, addListItems] = useItemList(
props,
listRef
) as any[];
return (
<div>
<div ref={listRef} data-testid="list" className="list">
{(items as any[]).map((_, idx) => (
<div key={idx} className="item" data-testid={`itm-${idx}`} />
))}
</div>
<div data-testid="counted">{String(countedItems)}</div>
<div data-testid="len">{items.length}</div>
<button data-testid="load-call" onClick={() => onItemsLoad([1, 2])} />
<button data-testid="count-call" onClick={() => onItemsCount(5)} />
<button data-testid="add-call" onClick={() => addListItems()} />
<button data-testid="set-handler" onClick={() => setListHandler({ foo: 'bar' })} />
<div data-testid="has-handler">{listHandler ? 'yes' : 'no'}</div>
</div>
);
}
describe('utils/hooks', () => {
describe('useItemList', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('Initial state: empty items and not counted', () => {
const { getByTestId } = render(<HookConsumer />);
expect(getByTestId('counted').textContent).toBe('false');
expect(getByTestId('len').textContent).toBe('0');
expect(getByTestId('has-handler').textContent).toBe('no');
});
test('onItemsLoad updates items and renders item nodes', () => {
const { getByTestId, getByTestId: $ } = render(<HookConsumer />);
(getByTestId('load-call') as HTMLButtonElement).click();
expect(getByTestId('len').textContent).toBe('2');
expect($('itm-0')).toBeTruthy();
expect($('itm-1')).toBeTruthy();
});
test('onItemsCount marks countedItems true and triggers callback if provided', () => {
const cb = jest.fn();
const { getByTestId } = render(<HookConsumer itemsCountCallback={cb} />);
(getByTestId('count-call') as HTMLButtonElement).click();
expect(getByTestId('counted').textContent).toBe('true');
expect(cb).toHaveBeenCalledWith(5);
});
test('addListItems initializes itemsListInstance and appends only new items', () => {
const mockInit = initItemsList as jest.Mock;
const { getByTestId, rerender } = render(<HookConsumer />);
const itemsLen = getByTestId('len') as HTMLDivElement;
const addBtn = getByTestId('add-call') as HTMLButtonElement;
const loadBtn = getByTestId('load-call') as HTMLButtonElement;
expect(itemsLen.textContent).toBe('0');
loadBtn.click();
expect(itemsLen.textContent).toBe('2');
expect(mockInit).toHaveBeenCalledTimes(0);
addBtn.click();
expect(mockInit).toHaveBeenCalledTimes(1);
expect(mockInit.mock.results[0].value[0].appendItems).toHaveBeenCalledTimes(2);
loadBtn.click();
expect(itemsLen.textContent).toBe('2');
addBtn.click();
expect(mockInit).toHaveBeenCalledTimes(2);
expect(mockInit.mock.results[1].value[0].appendItems).toHaveBeenCalledTimes(2);
rerender(<HookConsumer />);
addBtn.click();
expect(mockInit).toHaveBeenCalledTimes(3);
expect(mockInit.mock.results[2].value[0].appendItems).toHaveBeenCalledTimes(2);
});
test('addListItems does nothing when there are no .item elements in the ref', () => {
// Render, do not call onItemsLoad, then call addListItems
const mockInit = initItemsList as jest.Mock;
const { getByTestId } = render(<HookConsumer />);
(getByTestId('add-call') as HTMLButtonElement).click();
expect(mockInit).not.toHaveBeenCalled();
});
test('itemsLoadCallback is invoked when items change', () => {
const itemsLoadCallback = jest.fn();
const { getByTestId } = render(<HookConsumer itemsLoadCallback={itemsLoadCallback} />);
(getByTestId('load-call') as HTMLButtonElement).click();
expect(itemsLoadCallback).toHaveBeenCalledTimes(1);
});
test('setListHandler updates listHandler', () => {
const { getByTestId } = render(<HookConsumer />);
expect(getByTestId('has-handler').textContent).toBe('no');
(getByTestId('set-handler') as HTMLButtonElement).click();
expect(getByTestId('has-handler').textContent).toBe('yes');
});
});
});

View File

@@ -0,0 +1,346 @@
import React from 'react';
import { render, fireEvent, act } from '@testing-library/react';
jest.mock('../../../src/static/js/utils/settings/config', () => ({
config: jest.fn(() => jest.requireActual('../../tests-constants').sampleMediaCMSConfig),
}));
jest.mock('../../../src/static/js/utils/classes/', () => ({
BrowserCache: jest.fn().mockImplementation(() => ({
get: jest.fn(),
set: jest.fn(),
})),
}));
jest.mock('../../../src/static/js/utils/helpers/', () => ({
addClassname: jest.fn(),
removeClassname: jest.fn(),
}));
let mockListHandler: any;
let mockInlineSliderInstance: any;
let addListItemsSpy = jest.fn();
jest.mock('../../../src/static/js/utils/hooks/useItemList', () => ({
useItemList: (props: any, _ref: any) => {
mockListHandler = {
loadItems: jest.fn(),
totalPages: jest.fn().mockReturnValue(props.__totalPages ?? 1),
loadedAllItems: jest.fn().mockReturnValue(Boolean(props.__loadedAll ?? true)),
};
return [
props.__items ?? [], // items
props.__countedItems ?? 0, // countedItems
mockListHandler, // listHandler
jest.fn(), // setListHandler
jest.fn(), // onItemsLoad
jest.fn(), // onItemsCount
addListItemsSpy, // addListItems
];
},
}));
jest.mock('../../../src/static/js/components/item-list/includes/itemLists/ItemsInlineSlider', () =>
jest.fn().mockImplementation(() => {
mockInlineSliderInstance = {
updateDataStateOnResize: jest.fn(),
updateDataState: jest.fn(),
scrollToCurrentSlide: jest.fn(),
nextSlide: jest.fn(),
previousSlide: jest.fn(),
hasNextSlide: jest.fn().mockReturnValue(true),
hasPreviousSlide: jest.fn().mockReturnValue(true),
loadItemsToFit: jest.fn().mockReturnValue(false),
loadMoreItems: jest.fn().mockReturnValue(false),
itemsFit: jest.fn().mockReturnValue(3),
};
return mockInlineSliderInstance;
})
);
jest.mock('../../../src/static/js/components/_shared', () => ({
CircleIconButton: ({ children, onClick }: any) => (
<button data-testid="circle-icon-button" onClick={onClick}>
{children}
</button>
),
}));
import { useItemListInlineSlider } from '../../../src/static/js/utils/hooks/useItemListInlineSlider';
function HookConsumer(props: any) {
const tuple = useItemListInlineSlider(props);
const [
_items,
_countedItems,
_listHandler,
classname,
_setListHandler,
_onItemsCount,
_onItemsLoad,
_winResizeListener,
_sidebarVisibilityChangeListener,
itemsListWrapperRef,
_itemsListRef,
renderBeforeListWrap,
renderAfterListWrap,
] = tuple as any;
return (
<div ref={itemsListWrapperRef}>
<div data-testid="class-list">{classname.list}</div>
<div data-testid="class-outer">{classname.listOuter}</div>
<div data-testid="render-before">{renderBeforeListWrap()}</div>
<div data-testid="render-after">{renderAfterListWrap()}</div>
</div>
);
}
describe('utils/hooks', () => {
describe('useItemListInlineSlider', () => {
beforeEach(() => {
addListItemsSpy = jest.fn();
mockInlineSliderInstance = null;
});
afterEach(() => {
jest.clearAllMocks();
});
test('Returns correct tuple of values from hook', () => {
const TestComponent = (props: any) => {
const tuple = useItemListInlineSlider(props);
return (
<div>
<div data-testid="tuple-length">{tuple.length}</div>
<div data-testid="has-items">{tuple[0] ? 'yes' : 'no'}</div>
<div data-testid="has-classname">{tuple[3] ? 'yes' : 'no'}</div>
<div data-testid="has-listeners">{typeof tuple[7] === 'function' ? 'yes' : 'no'}</div>
</div>
);
};
const { getByTestId } = render(<TestComponent __items={[1, 2, 3]} />);
expect(getByTestId('tuple-length').textContent).toBe('13');
expect(getByTestId('has-classname').textContent).toBe('yes');
expect(getByTestId('has-listeners').textContent).toBe('yes');
});
test('Computes classname.list and classname.listOuter with optional className prop', () => {
const { getByTestId, rerender } = render(<HookConsumer className=" extra " />);
expect(getByTestId('class-outer').textContent).toBe('items-list-outer list-inline list-slider extra ');
expect(getByTestId('class-list').textContent).toBe('items-list');
rerender(<HookConsumer />);
expect(getByTestId('class-outer').textContent).toBe('items-list-outer list-inline list-slider');
expect(getByTestId('class-list').textContent).toBe('items-list');
});
test('Invokes addListItems when items change', () => {
const { rerender } = render(<HookConsumer __items={[]} />);
expect(addListItemsSpy).toHaveBeenCalledTimes(1);
rerender(<HookConsumer __items={[1]} />);
expect(addListItemsSpy).toHaveBeenCalledTimes(2);
});
test('nextSlide loads more items when loadMoreItems returns true and not all items loaded', () => {
const { getByTestId } = render(<HookConsumer __items={[1, 2, 3]} __loadedAll={false} />);
mockInlineSliderInstance.loadMoreItems.mockReturnValue(true);
const renderAfter = getByTestId('render-after');
const nextButton = renderAfter.querySelector('button[data-testid="circle-icon-button"]');
expect(mockListHandler.loadItems).toHaveBeenCalledTimes(0);
expect(mockInlineSliderInstance.loadMoreItems).toHaveBeenCalledTimes(0);
expect(mockInlineSliderInstance.nextSlide).toHaveBeenCalledTimes(0);
expect(mockInlineSliderInstance.previousSlide).toHaveBeenCalledTimes(0);
expect(mockInlineSliderInstance.hasNextSlide).toHaveBeenCalledTimes(1);
expect(mockInlineSliderInstance.hasPreviousSlide).toHaveBeenCalledTimes(1);
expect(mockInlineSliderInstance.scrollToCurrentSlide).toHaveBeenCalledTimes(1);
fireEvent.click(nextButton!);
expect(mockListHandler.loadItems).toHaveBeenCalledTimes(0);
expect(mockInlineSliderInstance.loadMoreItems).toHaveBeenCalledTimes(1);
expect(mockInlineSliderInstance.nextSlide).toHaveBeenCalledTimes(1);
expect(mockInlineSliderInstance.previousSlide).toHaveBeenCalledTimes(0);
expect(mockInlineSliderInstance.hasNextSlide).toHaveBeenCalledTimes(2);
expect(mockInlineSliderInstance.hasPreviousSlide).toHaveBeenCalledTimes(2);
expect(mockInlineSliderInstance.scrollToCurrentSlide).toHaveBeenCalledTimes(1);
});
test('nextSlide does not load items when all items already loaded', () => {
const { getByTestId } = render(<HookConsumer __items={[1, 2, 3]} __loadedAll={true} />);
mockInlineSliderInstance.loadMoreItems.mockReturnValue(false);
const renderAfter = getByTestId('render-after');
const nextButton = renderAfter.querySelector('button[data-testid="circle-icon-button"]');
expect(mockListHandler.loadItems).toHaveBeenCalledTimes(0);
expect(mockInlineSliderInstance.loadMoreItems).toHaveBeenCalledTimes(0);
expect(mockInlineSliderInstance.nextSlide).toHaveBeenCalledTimes(0);
expect(mockInlineSliderInstance.previousSlide).toHaveBeenCalledTimes(0);
expect(mockInlineSliderInstance.hasNextSlide).toHaveBeenCalledTimes(1);
expect(mockInlineSliderInstance.hasPreviousSlide).toHaveBeenCalledTimes(1);
expect(mockInlineSliderInstance.scrollToCurrentSlide).toHaveBeenCalledTimes(1);
fireEvent.click(nextButton!);
expect(mockListHandler.loadItems).toHaveBeenCalledTimes(0);
expect(mockInlineSliderInstance.loadMoreItems).toHaveBeenCalledTimes(0);
expect(mockInlineSliderInstance.nextSlide).toHaveBeenCalledTimes(1);
expect(mockInlineSliderInstance.previousSlide).toHaveBeenCalledTimes(0);
expect(mockInlineSliderInstance.hasNextSlide).toHaveBeenCalledTimes(2);
expect(mockInlineSliderInstance.hasPreviousSlide).toHaveBeenCalledTimes(2);
expect(mockInlineSliderInstance.scrollToCurrentSlide).toHaveBeenCalledTimes(2);
});
test('prevSlide calls inlineSlider.previousSlide and updates button view', () => {
const { getByTestId } = render(<HookConsumer __items={[1, 2, 3]} __loadedAll={false} />);
mockInlineSliderInstance.loadMoreItems.mockReturnValue(true);
const renderBefore = getByTestId('render-before');
const prevButton = renderBefore.querySelector('button[data-testid="circle-icon-button"]');
expect(mockListHandler.loadItems).toHaveBeenCalledTimes(0);
expect(mockInlineSliderInstance.loadMoreItems).toHaveBeenCalledTimes(0);
expect(mockInlineSliderInstance.nextSlide).toHaveBeenCalledTimes(0);
expect(mockInlineSliderInstance.previousSlide).toHaveBeenCalledTimes(0);
expect(mockInlineSliderInstance.hasNextSlide).toHaveBeenCalledTimes(1);
expect(mockInlineSliderInstance.hasPreviousSlide).toHaveBeenCalledTimes(1);
expect(mockInlineSliderInstance.scrollToCurrentSlide).toHaveBeenCalledTimes(1);
fireEvent.click(prevButton!);
expect(mockListHandler.loadItems).toHaveBeenCalledTimes(0);
expect(mockInlineSliderInstance.loadMoreItems).toHaveBeenCalledTimes(0);
expect(mockInlineSliderInstance.nextSlide).toHaveBeenCalledTimes(0);
expect(mockInlineSliderInstance.previousSlide).toHaveBeenCalledTimes(1);
expect(mockInlineSliderInstance.hasNextSlide).toHaveBeenCalledTimes(2);
expect(mockInlineSliderInstance.hasPreviousSlide).toHaveBeenCalledTimes(2);
expect(mockInlineSliderInstance.scrollToCurrentSlide).toHaveBeenCalledTimes(2);
});
test('prevSlide always scrolls to current slide regardless of item load state', () => {
const { getByTestId } = render(<HookConsumer __items={[1, 2, 3]} __loadedAll={true} />);
mockInlineSliderInstance.loadMoreItems.mockReturnValue(false);
const renderBefore = getByTestId('render-before');
const prevButton = renderBefore.querySelector('button[data-testid="circle-icon-button"]');
expect(mockListHandler.loadItems).toHaveBeenCalledTimes(0);
expect(mockInlineSliderInstance.loadMoreItems).toHaveBeenCalledTimes(0);
expect(mockInlineSliderInstance.nextSlide).toHaveBeenCalledTimes(0);
expect(mockInlineSliderInstance.previousSlide).toHaveBeenCalledTimes(0);
expect(mockInlineSliderInstance.hasNextSlide).toHaveBeenCalledTimes(1);
expect(mockInlineSliderInstance.hasPreviousSlide).toHaveBeenCalledTimes(1);
expect(mockInlineSliderInstance.scrollToCurrentSlide).toHaveBeenCalledTimes(1);
fireEvent.click(prevButton!);
expect(mockListHandler.loadItems).toHaveBeenCalledTimes(0);
expect(mockInlineSliderInstance.loadMoreItems).toHaveBeenCalledTimes(0);
expect(mockInlineSliderInstance.nextSlide).toHaveBeenCalledTimes(0);
expect(mockInlineSliderInstance.previousSlide).toHaveBeenCalledTimes(1);
expect(mockInlineSliderInstance.hasNextSlide).toHaveBeenCalledTimes(2);
expect(mockInlineSliderInstance.hasPreviousSlide).toHaveBeenCalledTimes(2);
expect(mockInlineSliderInstance.scrollToCurrentSlide).toHaveBeenCalledTimes(2);
});
test('Button state updates based on hasNextSlide and hasPreviousSlide', () => {
const { getByTestId, rerender } = render(<HookConsumer __items={[1, 2, 3]} />);
const renderBefore = getByTestId('render-before');
const renderAfter = getByTestId('render-after');
// Initially should show buttons (default mock returns true)
expect(renderBefore.querySelector('button')).toBeTruthy();
expect(renderAfter.querySelector('button')).toBeTruthy();
// Now set hasNextSlide and hasPreviousSlide to false
mockInlineSliderInstance.hasNextSlide.mockReturnValue(false);
mockInlineSliderInstance.hasPreviousSlide.mockReturnValue(false);
// Trigger re-render by changing items
rerender(<HookConsumer __items={[1, 2, 3, 4]} />);
// The next and previous buttons should not be rendered now
const newRenderAfter = getByTestId('render-after');
const newRenderBefore = getByTestId('render-before');
expect(newRenderAfter.querySelector('button')).toBeNull();
expect(newRenderBefore.querySelector('button')).toBeNull();
});
test('winResizeListener and sidebarVisibilityChangeListener are returned as callable functions', () => {
const TestComponentWithListeners = (props: any) => {
const tuple = useItemListInlineSlider(props);
const winResizeListener = tuple[7]; // winResizeListener
const sidebarListener = tuple[8]; // sidebarVisibilityChangeListener
const wrapperRef = tuple[9]; // itemsListWrapperRef
return (
<div ref={wrapperRef as any} data-testid="wrapper">
<button data-testid="trigger-resize" onClick={winResizeListener as any}>
Trigger Resize
</button>
<button data-testid="trigger-sidebar" onClick={sidebarListener as any}>
Trigger Sidebars
</button>
</div>
);
};
const { getByTestId } = render(<TestComponentWithListeners __items={[1, 2, 3]} />);
// Should not throw when called
const resizeButton = getByTestId('trigger-resize');
const sidebarButton = getByTestId('trigger-sidebar');
expect(() => fireEvent.click(resizeButton)).not.toThrow();
expect(() => fireEvent.click(sidebarButton)).not.toThrow();
});
test('winResizeListener updates resizeDate state triggering resize effect', () => {
const TestComponent = (props: any) => {
const tuple = useItemListInlineSlider(props) as any;
const winResizeListener = tuple[7];
const wrapperRef = tuple[9];
return (
<div ref={wrapperRef} data-testid="wrapper">
<button data-testid="trigger-resize" onClick={winResizeListener}>
Trigger Resize
</button>
</div>
);
};
const { getByTestId } = render(<TestComponent __items={[1, 2, 3]} />);
expect(mockInlineSliderInstance.scrollToCurrentSlide).toHaveBeenCalledTimes(1);
expect(mockInlineSliderInstance.updateDataStateOnResize).toHaveBeenCalledTimes(0);
jest.useFakeTimers();
fireEvent.click(getByTestId('trigger-resize'));
expect(mockInlineSliderInstance.scrollToCurrentSlide).toHaveBeenCalledTimes(2);
expect(mockInlineSliderInstance.updateDataStateOnResize).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(200);
expect(mockInlineSliderInstance.scrollToCurrentSlide).toHaveBeenCalledTimes(3);
expect(mockInlineSliderInstance.updateDataStateOnResize).toHaveBeenCalledTimes(2);
jest.useRealTimers();
});
});
});

View File

@@ -0,0 +1,190 @@
import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react';
jest.mock('../../../src/static/js/utils/settings/config', () => ({
config: jest.fn(() => jest.requireActual('../../tests-constants').sampleMediaCMSConfig),
}));
jest.mock('../../../src/static/js/utils/classes/', () => ({
BrowserCache: jest.fn().mockImplementation(() => ({
get: jest.fn(),
set: jest.fn(),
})),
}));
let mockListHandler: any;
let addListItemsSpy = jest.fn();
const mockRemoveListener = jest.fn();
jest.mock('../../../src/static/js/utils/hooks/useItemList', () => ({
useItemList: (props: any, _ref: any) => {
mockListHandler = {
loadItems: jest.fn(),
totalPages: jest.fn().mockReturnValue(props.__totalPages ?? 1),
loadedAllItems: jest.fn().mockReturnValue(Boolean(props.__loadedAll ?? true)),
};
return [
props.__items ?? [], // items
props.__countedItems ?? 0, // countedItems
mockListHandler, // listHandler
jest.fn(), // setListHandler
jest.fn(), // onItemsLoad
jest.fn(), // onItemsCount
addListItemsSpy, // addListItems
];
},
}));
jest.mock('../../../src/static/js/utils/stores/', () => ({
PageStore: {
removeListener: mockRemoveListener,
},
}));
import { useItemListLazyLoad } from '../../../src/static/js/utils/hooks/useItemListLazyLoad';
function HookConsumer(props: any) {
const tuple = useItemListLazyLoad(props);
const [
_items,
_countedItems,
_listHandler,
_setListHandler,
classname,
_onItemsCount,
_onItemsLoad,
_onWindowScroll,
_onDocumentVisibilityChange,
_itemsListWrapperRef,
_itemsListRef,
renderBeforeListWrap,
renderAfterListWrap,
] = tuple as any;
return (
<div>
<div data-testid="class-list">{classname.list}</div>
<div data-testid="class-outer">{classname.listOuter}</div>
<div data-testid="render-before">{renderBeforeListWrap()}</div>
<div data-testid="render-after">{renderAfterListWrap()}</div>
</div>
);
}
function HookConsumerWithRefs(props: any) {
const tuple = useItemListLazyLoad(props);
const [
_items,
_countedItems,
_listHandler,
_setListHandler,
classname,
_onItemsCount,
_onItemsLoad,
onWindowScroll,
onDocumentVisibilityChange,
itemsListWrapperRef,
itemsListRef,
renderBeforeListWrap,
renderAfterListWrap,
] = tuple as any;
return (
<div ref={itemsListWrapperRef}>
<div data-testid="class-list">{classname.list}</div>
<div data-testid="class-outer">{classname.listOuter}</div>
<div ref={itemsListRef} data-testid="list-ref-node" />
<div data-testid="render-before">{renderBeforeListWrap()}</div>
<div data-testid="render-after">{renderAfterListWrap()}</div>
<button data-testid="trigger-visibility" onClick={onDocumentVisibilityChange} type="button">
visibility
</button>
<button data-testid="trigger-scroll" onClick={onWindowScroll} type="button">
scroll
</button>
</div>
);
}
describe('utils/hooks', () => {
describe('useItemListLazyLoad', () => {
beforeEach(() => {
addListItemsSpy = jest.fn();
mockRemoveListener.mockClear();
});
afterEach(() => {
jest.clearAllMocks();
});
test('Computes classname.list and classname.listOuter with optional className prop', () => {
const { getByTestId, rerender } = render(<HookConsumer className=" extra " />);
expect(getByTestId('class-outer').textContent).toBe('items-list-outer extra');
expect(getByTestId('class-list').textContent).toBe('items-list');
rerender(<HookConsumer />);
expect(getByTestId('class-outer').textContent).toBe('items-list-outer');
expect(getByTestId('class-list').textContent).toBe('items-list');
});
test('Invokes addListItems when items change', () => {
const { rerender } = render(<HookConsumer __items={[]} />);
expect(addListItemsSpy).toHaveBeenCalledTimes(1);
rerender(<HookConsumer __items={[1]} />);
expect(addListItemsSpy).toHaveBeenCalledTimes(2);
});
test('Renders nothing in renderBeforeListWrap and renderAfterListWrap', () => {
const { getByTestId } = render(
<HookConsumer __items={[1]} __countedItems={1} __totalPages={3} __loadedAll={false} />
);
expect(getByTestId('render-before').textContent).toBe('');
expect(getByTestId('render-after').textContent).toBe('');
});
test('Does not call listHandler.loadItems when refs are not attached', () => {
render(<HookConsumer __items={[1]} />);
expect(mockListHandler.loadItems).not.toHaveBeenCalled();
});
test('Calls listHandler.loadItems when refs are set and scroll threshold is reached', async () => {
render(<HookConsumerWithRefs __items={[1]} __loadedAll={false} />);
await waitFor(() => {
expect(mockListHandler.loadItems).toHaveBeenCalled();
});
});
test('Calls PageStore.removeListener when refs are set and loadedAllItems is true', () => {
render(<HookConsumerWithRefs __items={[1]} __loadedAll={true} />);
expect(mockRemoveListener).toHaveBeenCalledWith('window_scroll', expect.any(Function));
});
test('onDocumentVisibilityChange schedules onWindowScroll when document is visible', () => {
jest.useFakeTimers();
const setTimeoutSpy = jest.spyOn(globalThis, 'setTimeout');
Object.defineProperty(document, 'hidden', { value: false, configurable: true });
const { getByTestId } = render(<HookConsumerWithRefs __items={[1]} />);
fireEvent.click(getByTestId('trigger-visibility'));
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 10);
setTimeoutSpy.mockRestore();
jest.useRealTimers();
});
test('onDocumentVisibilityChange does nothing when document is hidden', () => {
jest.useFakeTimers();
const setTimeoutSpy = jest.spyOn(globalThis, 'setTimeout');
Object.defineProperty(document, 'hidden', { value: true, configurable: true });
const { getByTestId } = render(<HookConsumerWithRefs __items={[1]} />);
fireEvent.click(getByTestId('trigger-visibility'));
expect(setTimeoutSpy).toHaveBeenCalledTimes(0);
setTimeoutSpy.mockRestore();
jest.useRealTimers();
});
});
});

View File

@@ -0,0 +1,156 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
jest.mock('../../../src/static/js/utils/settings/config', () => ({
config: jest.fn(() => jest.requireActual('../../tests-constants').sampleMediaCMSConfig),
}));
jest.mock('../../../src/static/js/utils/classes/', () => ({
BrowserCache: jest.fn().mockImplementation(() => ({
get: jest.fn(),
set: jest.fn(),
})),
}));
jest.mock('../../../src/static/js/utils/helpers/', () => ({
translateString: (s: string) => s,
}));
let mockListHandler: any;
let mockOnItemsLoad = jest.fn();
let mockOnItemsCount = jest.fn();
let addListItemsSpy = jest.fn();
// Mock useItemList to control items, counts, and listHandler
jest.mock('../../../src/static/js/utils/hooks/useItemList', () => ({
useItemList: (props: any, _ref: any) => {
mockListHandler = {
loadItems: jest.fn(),
totalPages: jest.fn().mockReturnValue(props.__totalPages ?? 1),
loadedAllItems: jest.fn().mockReturnValue(Boolean(props.__loadedAll ?? true)),
};
return [
props.__items ?? [], // items
props.__countedItems ?? 0, // countedItems
mockListHandler, // listHandler
jest.fn(), // setListHandler
mockOnItemsLoad, // onItemsLoad
mockOnItemsCount, // onItemsCount
addListItemsSpy, // addListItems
];
},
}));
import { useItemListSync } from '../../../src/static/js/utils/hooks/useItemListSync';
function HookConsumer(props: any) {
const tuple = useItemListSync(props);
const [
_countedItems,
_items,
_listHandler,
_setListHandler,
classname,
_itemsListWrapperRef,
_itemsListRef,
_onItemsCount,
_onItemsLoad,
renderBeforeListWrap,
renderAfterListWrap,
] = tuple as any;
return (
<div>
{/* <div data-testid="counted">{String(countedItems)}</div> */}
{/* <div data-testid="items">{Array.isArray(items) ? items.length : 0}</div> */}
<div data-testid="class-list">{classname.list}</div>
<div data-testid="class-outer">{classname.listOuter}</div>
{/* <div data-testid="has-handler">{listHandler ? 'yes' : 'no'}</div> */}
{/* <div data-testid="wrapper-ref">{itemsListWrapperRef.current ? 'set' : 'unset'}</div> */}
{/* <div data-testid="list-ref">{itemsListRef.current ? 'set' : 'unset'}</div> */}
<div data-testid="render-before">{renderBeforeListWrap()}</div>
<div data-testid="render-after">{renderAfterListWrap()}</div>
{/* <button data-testid="call-on-load" onClick={() => onItemsLoad([])} /> */}
{/* <button data-testid="call-on-count" onClick={() => onItemsCount(0)} /> */}
</div>
);
}
describe('utils/hooks', () => {
describe('useItemListSync', () => {
beforeEach(() => {
mockOnItemsLoad = jest.fn();
mockOnItemsCount = jest.fn();
addListItemsSpy = jest.fn();
});
afterEach(() => {
jest.clearAllMocks();
});
describe('Classname Management', () => {
test('Computes classname.listOuter with optional className prop', () => {
const { getByTestId, rerender } = render(<HookConsumer className=" extra " />);
expect(getByTestId('class-outer').textContent).toBe('items-list-outer extra');
expect(getByTestId('class-list').textContent).toBe('items-list');
rerender(<HookConsumer />);
expect(getByTestId('class-outer').textContent).toBe('items-list-outer');
expect(getByTestId('class-list').textContent).toBe('items-list');
});
});
describe('Items Management', () => {
test('Invokes addListItems and afterItemsLoad when items change', () => {
const { rerender } = render(<HookConsumer __items={[]} />);
expect(addListItemsSpy).toHaveBeenCalledTimes(1);
rerender(<HookConsumer __items={[1]} />);
// useEffect runs again due to items change
expect(addListItemsSpy).toHaveBeenCalledTimes(2);
});
});
describe('Load More Button Rendering', () => {
test('Renders SHOW MORE button when more pages exist and not loaded all', () => {
const { getByTestId } = render(
<HookConsumer __items={[1]} __countedItems={1} __totalPages={3} __loadedAll={false} />
);
const btn = getByTestId('render-after').querySelector('button.load-more') as HTMLButtonElement;
expect(btn).toBeTruthy();
expect(btn.textContent).toBe('SHOW MORE');
fireEvent.click(btn);
expect(mockListHandler.loadItems).toHaveBeenCalledTimes(1);
});
test('Hides SHOW MORE when totalPages <= 1', () => {
const { getByTestId } = render(
// With totalPages=1 the hook should not render the button regardless of loadedAll
<HookConsumer __items={[1, 2]} __countedItems={2} __totalPages={1} __loadedAll={true} />
);
expect(getByTestId('render-after').textContent).toBe('');
});
test('Hides SHOW MORE when loadedAllItems is true', () => {
const { getByTestId } = render(
<HookConsumer __items={[1, 2, 3]} __countedItems={3} __totalPages={5} __loadedAll={true} />
);
expect(getByTestId('render-after').textContent).toBe('');
});
test('Shows SHOW MORE when loadedAllItems is false even with totalPages > 1', () => {
const { getByTestId } = render(
<HookConsumer __items={[1, 2]} __countedItems={2} __totalPages={2} __loadedAll={false} />
);
const btn = getByTestId('render-after').querySelector('button.load-more');
expect(btn).toBeTruthy();
});
test('Returns null from renderBeforeListWrap', () => {
const { getByTestId } = render(
<HookConsumer __items={[1]} __countedItems={1} __totalPages={3} __loadedAll={false} />
);
expect(getByTestId('render-before').textContent).toBe('');
});
});
});
});

View File

@@ -0,0 +1,118 @@
import React from 'react';
import { act, render } from '@testing-library/react';
import { useLayout } from '../../../src/static/js/utils/hooks/useLayout';
jest.mock('../../../src/static/js/utils/classes/', () => ({
BrowserCache: jest.fn().mockImplementation(() => ({
get: (key: string) => {
let result: any = undefined;
switch (key) {
case 'visible-sidebar':
result = true;
break;
}
return result;
},
set: jest.fn(),
})),
}));
jest.mock('../../../src/static/js/utils/dispatcher.js', () => ({
register: jest.fn(),
}));
jest.mock('../../../src/static/js/utils/settings/config', () => ({
config: jest.fn(() => jest.requireActual('../../tests-constants').sampleMediaCMSConfig),
}));
import { LayoutProvider } from '../../../src/static/js/utils/contexts';
describe('utils/hooks', () => {
describe('useLayout', () => {
test('Returns default value', () => {
let received: ReturnType<typeof useLayout> | undefined;
const Comp: React.FC = () => {
received = useLayout();
return null;
};
render(
<LayoutProvider>
<Comp />
</LayoutProvider>
);
expect(received).toStrictEqual({
enabledSidebar: false,
visibleSidebar: true,
visibleMobileSearch: false,
setVisibleSidebar: expect.any(Function),
toggleMobileSearch: expect.any(Function),
toggleSidebar: expect.any(Function),
});
});
test('Returns undefined value when used without a Provider', () => {
let received: any = 'init';
const Comp: React.FC = () => {
received = useLayout();
return null;
};
render(<Comp />);
expect(received).toBe(undefined);
});
test('Toggle sidebar', () => {
jest.useFakeTimers();
let received: ReturnType<typeof useLayout> | undefined;
const Comp: React.FC = () => {
received = useLayout();
return null;
};
render(
<LayoutProvider>
<Comp />
</LayoutProvider>
);
act(() => received?.toggleSidebar());
jest.advanceTimersByTime(241);
expect(received?.visibleSidebar).toBe(false);
act(() => received?.toggleSidebar());
jest.advanceTimersByTime(241);
expect(received?.visibleSidebar).toBe(true);
jest.useRealTimers();
});
test('Toggle mobile search', () => {
let received: ReturnType<typeof useLayout> | undefined;
const Comp: React.FC = () => {
received = useLayout();
return null;
};
render(
<LayoutProvider>
<Comp />
</LayoutProvider>
);
act(() => received?.toggleMobileSearch());
expect(received?.visibleMobileSearch).toBe(true);
act(() => received?.toggleMobileSearch());
expect(received?.visibleMobileSearch).toBe(false);
});
});
});

View File

@@ -0,0 +1,134 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { useManagementTableHeader } from '../../../src/static/js/utils/hooks/useManagementTableHeader';
function HookConsumer(props: {
order: 'asc' | 'desc';
selected: boolean;
sort: string;
type: 'comments' | 'media' | 'users';
onCheckAllRows?: (newSort: string, newOrder: 'asc' | 'desc') => void;
onClickColumnSort?: (newSelected: boolean, newType: 'comments' | 'media' | 'users') => void;
}) {
const tuple = useManagementTableHeader(props) as [
string,
'asc' | 'desc',
boolean,
React.MouseEventHandler,
() => void,
];
const [sort, order, isSelected, sortByColumn, checkAll] = tuple;
return (
<div>
<div data-testid="sort">{sort}</div>
<div data-testid="order">{order}</div>
<div data-testid="selected">{String(isSelected)}</div>
<button id="title" data-testid="col-title" onClick={sortByColumn} />
<button id="views" data-testid="col-views" onClick={sortByColumn} />
<button data-testid="check-all" onClick={checkAll} />
</div>
);
}
describe('utils/hooks', () => {
describe('useManagementTableHeader', () => {
test('Returns a 5-tuple in expected order and reflects initial props', () => {
let tuple: any;
const Comp: React.FC = () => {
tuple = useManagementTableHeader({ sort: 'title', order: 'asc', selected: false });
return null;
};
render(<Comp />);
expect(Array.isArray(tuple)).toBe(true);
expect(tuple).toHaveLength(5);
const [sort, order, isSelected] = tuple;
expect(sort).toBe('title');
expect(order).toBe('asc');
expect(isSelected).toBe(false);
});
test('sortByColumn toggles order when clicking same column and updates sort when clicking different column', () => {
const onClickColumnSort = jest.fn();
const { getByTestId, rerender } = render(
<HookConsumer
sort="title"
order="desc"
type="media"
selected={false}
onClickColumnSort={onClickColumnSort}
/>
);
// Initial state
expect(getByTestId('sort').textContent).toBe('title');
expect(getByTestId('order').textContent).toBe('desc');
// Click same column -> toggle order to asc
fireEvent.click(getByTestId('col-title'));
expect(onClickColumnSort).toHaveBeenLastCalledWith('title', 'asc');
// Rerender to ensure state settled in testing DOM
rerender(
<HookConsumer
sort="title"
order="asc"
type="media"
selected={false}
onClickColumnSort={onClickColumnSort}
/>
);
// Click same column -> toggle order to desc
fireEvent.click(getByTestId('col-title'));
expect(onClickColumnSort).toHaveBeenLastCalledWith('title', 'desc');
// Click different column -> set sort to that column and default order desc
fireEvent.click(getByTestId('col-views'));
expect(onClickColumnSort).toHaveBeenLastCalledWith('views', 'desc');
});
test('checkAll inverts current selection and invokes callback with newSelected and type', () => {
const onCheckAllRows = jest.fn();
const { getByTestId } = render(
<HookConsumer sort="title" order="asc" selected={false} type="media" onCheckAllRows={onCheckAllRows} />
);
expect(getByTestId('selected').textContent).toBe('false');
fireEvent.click(getByTestId('check-all'));
// newSelected computed as !isSelected -> true
expect(onCheckAllRows).toHaveBeenCalledWith(true, 'media');
});
test('Effects update internal state when props change', () => {
const { getByTestId, rerender } = render(
<HookConsumer sort="title" order="asc" type="media" selected={false} />
);
expect(getByTestId('sort').textContent).toBe('title');
expect(getByTestId('order').textContent).toBe('asc');
expect(getByTestId('selected').textContent).toBe('false');
rerender(<HookConsumer sort="views" order="desc" type="media" selected={true} />);
expect(getByTestId('sort').textContent).toBe('views');
expect(getByTestId('order').textContent).toBe('desc');
expect(getByTestId('selected').textContent).toBe('true');
});
test('Does not throw when optional callbacks are not provided', () => {
const { getByTestId } = render(<HookConsumer sort="x" order="desc" type="media" selected={false} />);
expect(() => fireEvent.click(getByTestId('col-title'))).not.toThrow();
expect(() => fireEvent.click(getByTestId('check-all'))).not.toThrow();
});
});
});

View File

@@ -0,0 +1,119 @@
import React from 'react';
import { render } from '@testing-library/react';
import { useMediaFilter } from '../../../src/static/js/utils/hooks/useMediaFilter';
jest.mock('../../../src/static/js/components/_shared/popup/PopupContent', () => ({
PopupContent: (props: any) => React.createElement('div', props, props.children),
}));
jest.mock('../../../src/static/js/components/_shared/popup/PopupTrigger', () => ({
PopupTrigger: (props: any) => React.createElement('div', props, props.children),
}));
function HookConsumer({ initial }: { initial: string }) {
const tuple = useMediaFilter(initial) as [
React.RefObject<any>,
string,
React.Dispatch<React.SetStateAction<string>>,
React.RefObject<any>,
React.ReactNode,
React.ReactNode,
];
const [containerRef, value, setValue, popupContentRef, PopupContent, PopupTrigger] = tuple;
return (
<div>
<div data-testid="container-ref">{containerRef && typeof containerRef === 'object' ? 'ok' : 'bad'}</div>
<div data-testid="value">{value}</div>
<button data-testid="set" onClick={() => setValue('updated')} />
<div data-testid="popup-ref">{popupContentRef && typeof popupContentRef === 'object' ? 'ok' : 'bad'}</div>
{typeof PopupContent === 'function' ? React.createElement(PopupContent, null, 'c') : null}
{typeof PopupTrigger === 'function' ? React.createElement(PopupTrigger, null, 't') : null}
</div>
);
}
describe('utils/hooks', () => {
describe('useMediaFilter', () => {
test('Returns a 6-tuple in expected order', () => {
let tuple: any;
const Comp: React.FC = () => {
tuple = useMediaFilter('init');
return null;
};
render(<Comp />);
expect(Array.isArray(tuple)).toBe(true);
expect(tuple).toHaveLength(6);
const [containerRef, value, setValue, popupContentRef, PopupContent, PopupTrigger] = tuple;
expect(containerRef).toBeDefined();
expect(containerRef.current).toBe(null);
expect(value).toBe('init');
expect(typeof setValue).toBe('function');
expect(popupContentRef).toBeDefined();
expect(typeof PopupContent).toBe('function');
expect(typeof PopupTrigger).toBe('function');
});
test('Initial value is respected and can be updated via setter', () => {
const { getByTestId } = render(<HookConsumer initial="first" />);
expect(getByTestId('value').textContent).toBe('first');
getByTestId('set').click();
expect(getByTestId('value').textContent).toBe('updated');
});
test('containerRef and popupContentRef are mutable ref objects', () => {
let data: any;
const Comp: React.FC = () => {
data = useMediaFilter('x');
return null;
};
render(<Comp />);
const [containerRef, _value, _setValue, popupContentRef] = data;
expect(containerRef.current).toBe(null);
expect(popupContentRef.current).toBe(null);
});
test('PopupContent and PopupTrigger are stable functions', () => {
let first: any;
let second: any;
const First: React.FC = () => {
first = useMediaFilter('a');
return null;
};
const Second: React.FC = () => {
second = useMediaFilter('b');
return null;
};
const Parent: React.FC = () => (
<>
<First />
<Second />
</>
);
render(<Parent />);
const [, , , , PopupContent1, PopupTrigger1] = first;
const [, , , , PopupContent2, PopupTrigger2] = second;
expect(typeof PopupContent1).toBe('function');
expect(typeof PopupTrigger1).toBe('function');
expect(PopupContent1).toBe(PopupContent2);
expect(PopupTrigger1).toBe(PopupTrigger2);
});
});
});

View File

@@ -0,0 +1,289 @@
import React from 'react';
import { render } from '@testing-library/react';
import { useMediaItem, itemClassname } from '../../../src/static/js/utils/hooks/useMediaItem';
// Mock dependencies used by useMediaItem
// @todo: Revisit this
jest.mock('../../../src/static/js/utils/stores/', () => ({
PageStore: { get: (_: string) => ({ url: 'https://example.com' }) },
}));
jest.mock('../../../src/static/js/components/list-item/includes/items', () => ({
MediaItemAuthor: ({ name }: any) => <div data-testid="author" data-name={name} />,
MediaItemAuthorLink: ({ name, link }: any) => (
<a data-testid="author-link" data-name={name} href={link || undefined} />
),
MediaItemMetaViews: ({ views }: any) => <span data-testid="views" data-views={views} />,
MediaItemMetaDate: ({ time, dateTime, text }: any) => (
<time data-testid="date" data-time={String(time)} data-datetime={String(dateTime)}>
{text}
</time>
),
MediaItemEditLink: ({ link }: any) => <a data-testid="edit" href={link} />,
MediaItemViewLink: ({ link }: any) => <a data-testid="view" href={link} />,
}));
// @todo: Revisit this
// useItem returns titleComponent, descriptionComponent, thumbnailUrl, UnderThumbWrapper
jest.mock('../../../src/static/js/utils/hooks/useItem', () => ({
useItem: (props: any) => ({
titleComponent: () => <h3 data-testid="title">{props.title || 'title'}</h3>,
descriptionComponent: () => <p data-testid="desc">{props.description || 'desc'}</p>,
thumbnailUrl: props.thumb || 'thumb.jpg',
UnderThumbWrapper: ({ children }: any) => <div data-testid="under-thumb">{children}</div>,
}),
}));
function HookConsumer(props: any) {
const [TitleComp, DescComp, thumbUrl, UnderThumbComp, EditComp, MetaComp, ViewComp] = useMediaItem(props);
// The hook returns functions/components/values. To satisfy TS, render using React.createElement
return (
<div>
{typeof TitleComp === 'function' ? React.createElement(TitleComp) : null}
{typeof DescComp === 'function' ? React.createElement(DescComp) : null}
<div data-testid="thumb">{typeof thumbUrl === 'string' ? thumbUrl : ''}</div>
{typeof UnderThumbComp === 'function'
? React.createElement(
UnderThumbComp,
null,
typeof EditComp === 'function' ? React.createElement(EditComp) : null,
typeof MetaComp === 'function' ? React.createElement(MetaComp) : null,
typeof ViewComp === 'function' ? React.createElement(ViewComp) : null
)
: null}
</div>
);
}
describe('utils/hooks', () => {
describe('useMediaItem', () => {
describe('itemClassname utility function', () => {
test('Returns default classname when no modifications', () => {
expect(itemClassname('base', '', false)).toBe('base');
});
test('Appends inherited classname when provided', () => {
expect(itemClassname('base', 'extra', false)).toBe('base extra');
});
test('Appends pl-active-item when isActiveInPlaylistPlayback is true', () => {
expect(itemClassname('base', '', true)).toBe('base pl-active-item');
});
test('Appends both inherited classname and active state', () => {
expect(itemClassname('base', 'extra', true)).toBe('base extra pl-active-item');
});
});
describe('Basic Rendering', () => {
test('Renders basic components from useItem and edit/view links', () => {
// @todo: Revisit this
const props = {
title: 'My Title',
description: 'My Desc',
thumbnail: 'thumb.jpg',
link: '/watch/1',
singleLinkContent: true,
// hasMediaViewer:...
// hasMediaViewerDescr:...
// meta_description:...
// onMount:...
// type:...
// ------------------------------
editLink: '/edit/1',
showSelection: true,
// publishLink: ...
// hideAuthor:...
author_name: 'Author',
author_link: '/u/author',
// hideViews:...
views: 10,
// hideDate:...
publish_date: '2020-01-01T00:00:00Z',
// hideAllMeta:...
};
const { getByTestId, queryByTestId } = render(<HookConsumer {...props} />);
expect(getByTestId('title').textContent).toBe(props.title);
expect(getByTestId('desc').textContent).toBe(props.description);
expect(getByTestId('thumb').textContent).toBe('thumb.jpg');
expect(getByTestId('edit').getAttribute('href')).toBe(props.editLink);
expect(getByTestId('views').getAttribute('data-views')).toBe(props.views.toString());
expect(getByTestId('date')).toBeTruthy();
expect(getByTestId('view').getAttribute('href')).toBe(props.link);
expect(queryByTestId('author')).toBeTruthy();
});
});
describe('View Link Selection', () => {
test('Uses publishLink when provided and showSelection=true', () => {
const props = {
editLink: '/edit/2',
link: '/watch/2',
publishLink: '/publish/2',
showSelection: true,
singleLinkContent: true,
author_name: 'A',
author_link: '',
views: 0,
publish_date: 0,
};
const { getByTestId } = render(<HookConsumer {...props} />);
expect(getByTestId('view').getAttribute('href')).toBe(props.publishLink);
});
});
describe('Visibility Controls', () => {
test('Hides author, views, and date based on props', () => {
const props = {
editLink: '/e',
link: '/l',
showSelection: true,
hideAuthor: true,
hideViews: true,
hideDate: true,
publish_date: '2020-01-01T00:00:00Z',
views: 5,
author_name: 'Hidden',
author_link: '/u/x',
};
const { queryByTestId } = render(<HookConsumer {...props} />);
expect(queryByTestId('author')).toBeNull();
expect(queryByTestId('views')).toBeNull();
expect(queryByTestId('date')).toBeNull();
});
test('Author link resolves using formatInnerLink and PageStore base url when singleLinkContent=false', () => {
const props = {
editLink: '/e',
link: '/l',
showSelection: true,
singleLinkContent: false,
hideAuthor: false,
author_name: 'John',
author_link: '/u/john',
publish_date: '2020-01-01T00:00:00Z',
};
const { container } = render(<HookConsumer {...props} />);
const a = container.querySelector('[data-testid="author-link"]') as HTMLAnchorElement;
expect(a).toBeTruthy();
expect(a.getAttribute('href')).toBe(`https://example.com${props.author_link}`);
expect(a.getAttribute('data-name')).toBe(props.author_name);
});
});
describe('Meta Visibility', () => {
test('Meta wrapper hidden when hideAllMeta=true', () => {
const props = {
editLink: '/e',
link: '/l',
showSelection: true,
hideAllMeta: true,
publish_date: '2020-01-01T00:00:00Z',
};
const { queryByTestId } = render(<HookConsumer {...props} />);
expect(queryByTestId('author')).toBeNull();
expect(queryByTestId('views')).toBeNull();
expect(queryByTestId('date')).toBeNull();
});
test('Meta wrapper hidden individually by hideAuthor, hideViews, hideDate', () => {
const props = {
editLink: '/e',
link: '/l',
showSelection: true,
hideAuthor: true,
hideViews: false,
hideDate: false,
publish_date: '2020-01-01T00:00:00Z',
views: 5,
author_name: 'Test',
author_link: '/u/test',
};
const { queryByTestId } = render(<HookConsumer {...props} />);
expect(queryByTestId('author')).toBeNull();
expect(queryByTestId('views')).toBeTruthy();
expect(queryByTestId('date')).toBeTruthy();
});
});
describe('Edge Cases & Date Handling', () => {
test('Handles views when hideViews is false', () => {
const props = {
editLink: '/e',
link: '/l',
showSelection: true,
hideViews: false,
views: 100,
publish_date: '2020-01-01T00:00:00Z',
author_name: 'A',
author_link: '/u/a',
};
const { getByTestId } = render(<HookConsumer {...props} />);
expect(getByTestId('views')).toBeTruthy();
expect(getByTestId('views').getAttribute('data-views')).toBe('100');
});
test('Renders without showSelection', () => {
const props = {
editLink: '/e',
link: '/l',
showSelection: false,
publish_date: '2020-01-01T00:00:00Z',
author_name: 'A',
author_link: '/u/a',
};
const { queryByTestId } = render(<HookConsumer {...props} />);
expect(queryByTestId('view')).toBeNull();
});
test('Handles numeric publish_date correctly', () => {
const props = {
editLink: '/e',
link: '/l',
showSelection: true,
publish_date: 1577836800000, // 2020-01-01 as timestamp
author_name: 'A',
author_link: '/u/a',
};
const { getByTestId } = render(<HookConsumer {...props} />);
expect(getByTestId('date')).toBeTruthy();
});
test('Handles empty author_link by setting it to null', () => {
const props = {
editLink: '/e',
link: '/l',
showSelection: true,
singleLinkContent: false,
author_name: 'Anonymous',
author_link: '', // Empty link
publish_date: '2020-01-01T00:00:00Z',
};
const { container } = render(<HookConsumer {...props} />);
const authorLink = container.querySelector('[data-testid="author-link"]') as HTMLAnchorElement;
expect(authorLink).toBeTruthy();
expect(authorLink.getAttribute('href')).toBeNull();
});
});
});
});

View File

@@ -0,0 +1,68 @@
import React from 'react';
import { render } from '@testing-library/react';
// Mock popup components to avoid SCSS imports breaking Jest
jest.mock('../../../src/static/js/components/_shared/popup/Popup.jsx', () => {
const React = require('react');
const Popup = React.forwardRef((props: any, _ref: any) => React.createElement('div', props, props.children));
return { __esModule: true, default: Popup };
});
jest.mock('../../../src/static/js/components/_shared/popup/PopupContent.jsx', () => ({
PopupContent: (props: any) => React.createElement('div', props, props.children),
}));
jest.mock('../../../src/static/js/components/_shared/popup/PopupTrigger.jsx', () => ({
PopupTrigger: (props: any) => React.createElement('div', props, props.children),
}));
import { usePopup } from '../../../src/static/js/utils/hooks/usePopup';
describe('utils/hooks', () => {
describe('usePopup', () => {
test('Returns a 3-tuple: [ref, PopupContent, PopupTrigger]', () => {
let value: any;
const Comp: React.FC = () => {
value = usePopup();
return null;
};
render(<Comp />);
expect(Array.isArray(value)).toBe(true);
expect(value).toHaveLength(3);
const [ref, PopupContent, PopupTrigger] = value;
expect(ref).toBeDefined();
expect(ref.current).toBe(null);
expect(typeof PopupContent).toBe('function');
expect(typeof PopupTrigger).toBe('function');
});
test('Tuple components are stable functions and refs are unique per call', () => {
let results: any[] = [];
const Comp: React.FC = () => {
results.push(usePopup());
results.push(usePopup());
return null;
};
render(<Comp />);
const [ref1, PopupContent1, PopupTrigger1] = results[0];
const [ref2, PopupContent2, PopupTrigger2] = results[1];
expect(typeof PopupContent1).toBe('function');
expect(typeof PopupTrigger1).toBe('function');
expect(PopupContent1).toBe(PopupContent2);
expect(PopupTrigger1).toBe(PopupTrigger2);
expect(ref1).not.toBe(ref2);
});
});
});

View File

@@ -0,0 +1,100 @@
import React from 'react';
import { act, render } from '@testing-library/react';
import { useTheme as useThemeHook } from '../../../src/static/js/utils/hooks/useTheme';
import { sampleMediaCMSConfig } from '../../tests-constants';
jest.mock('../../../src/static/js/utils/classes/', () => ({
BrowserCache: jest.fn().mockImplementation(() => ({
get: jest.fn(),
set: jest.fn(),
})),
}));
jest.mock('../../../src/static/js/utils/dispatcher.js', () => ({
register: jest.fn(),
}));
function getRenderers(ThemeProvider: React.FC<{ children: React.ReactNode }>, useTheme: typeof useThemeHook) {
const data: { current: any } = { current: undefined };
const Comp: React.FC = () => {
data.current = useTheme();
return null;
};
const wrapper: typeof ThemeProvider = ({ children }) => <ThemeProvider>{children}</ThemeProvider>;
return { Comp, wrapper, data };
}
function getThemeConfig(override?: {
logo?: Partial<(typeof sampleMediaCMSConfig.theme)['logo']>;
mode?: (typeof sampleMediaCMSConfig.theme)['mode'];
switch?: Partial<(typeof sampleMediaCMSConfig.theme)['switch']>;
}) {
const { logo, mode, switch: sw } = override ?? {};
const { lightMode, darkMode } = logo ?? {};
const config = {
logo: {
lightMode: { img: lightMode?.img ?? '/img/light.png', svg: lightMode?.svg ?? '/img/light.svg' },
darkMode: { img: darkMode?.img ?? '/img/dark.png', svg: darkMode?.svg ?? '/img/dark.svg' },
},
mode: mode ?? 'dark',
switch: { enabled: sw?.enabled ?? true, position: sw?.position ?? 'sidebar' },
};
return config;
}
describe('utils/hooks', () => {
afterEach(() => {
jest.resetModules();
jest.clearAllMocks();
});
describe('useTheme', () => {
const themeConfig = getThemeConfig();
const darkThemeConfig = getThemeConfig({ mode: 'dark' });
// @todo: Revisit this
test.each([
[
darkThemeConfig,
{
logo: darkThemeConfig.logo.darkMode.svg,
currentThemeMode: darkThemeConfig.mode,
changeThemeMode: expect.any(Function),
themeModeSwitcher: themeConfig.switch,
},
],
])('Validate value', async (theme, expectedResult) => {
jest.doMock('../../../src/static/js/utils/settings/config', () => ({
config: jest.fn(() => ({ ...jest.requireActual('../../tests-constants').sampleMediaCMSConfig, theme })),
}));
const { ThemeProvider } = await import('../../../src/static/js/utils/contexts/ThemeContext');
const { useTheme } = await import('../../../src/static/js/utils/hooks/useTheme');
const { Comp, wrapper, data } = getRenderers(ThemeProvider, useTheme);
render(<Comp />, { wrapper });
expect(data.current).toStrictEqual(expectedResult);
act(() => data.current.changeThemeMode());
const newThemeMode = 'light' === expectedResult.currentThemeMode ? 'dark' : 'light';
const newThemeLogo =
'light' === newThemeMode ? themeConfig.logo.lightMode.svg : themeConfig.logo.darkMode.svg;
expect(data.current).toStrictEqual({
...expectedResult,
logo: newThemeLogo,
currentThemeMode: newThemeMode,
});
});
});
});

View File

@@ -0,0 +1,45 @@
import React from 'react';
import { render } from '@testing-library/react';
import { UserProvider } from '../../../src/static/js/utils/contexts/UserContext';
import { useUser } from '../../../src/static/js/utils/hooks/useUser';
import { sampleMediaCMSConfig } from '../../tests-constants';
jest.mock('../../../src/static/js/utils/settings/config', () => ({
config: jest.fn(() => jest.requireActual('../../tests-constants').sampleMediaCMSConfig),
}));
function getRenderers() {
const data: { current: any } = { current: undefined };
const Comp: React.FC = () => {
data.current = useUser();
return null;
};
const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => <UserProvider>{children}</UserProvider>;
return { Comp, wrapper, data };
}
describe('utils/hooks', () => {
afterEach(() => {
jest.clearAllMocks();
});
describe('useUser', () => {
test('Validate value', () => {
const { Comp, wrapper, data } = getRenderers();
render(<Comp />, { wrapper });
expect(data.current).toStrictEqual({
isAnonymous: sampleMediaCMSConfig.member.is.anonymous,
username: sampleMediaCMSConfig.member.username,
thumbnail: sampleMediaCMSConfig.member.thumbnail,
userCan: sampleMediaCMSConfig.member.can,
pages: sampleMediaCMSConfig.member.pages,
});
});
});
});