mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-17 16:50:53 +00:00
Switch to LF line endings and apply prettier formatting.
This commit is contained in:
parent
78a53e03db
commit
631f3d4528
100 changed files with 2880 additions and 1690 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import js from '@eslint/js';
|
||||
import globals from 'globals';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import reactRefresh from 'eslint-plugin-react-refresh';
|
||||
|
||||
export default [
|
||||
{ ignores: ['dist'] },
|
||||
|
|
@ -30,4 +30,4 @@ export default [
|
|||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
|
|
|||
|
|
@ -4,10 +4,24 @@
|
|||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/logo.png" />
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
|
||||
<link rel="manifest" href="/static/site.webmanifest">
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/static/apple-touch-icon.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/static/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/static/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="manifest" href="/static/site.webmanifest" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Dispatcharr</title>
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@ export default {
|
|||
semi: true, // Add semicolons at the end of statements
|
||||
singleQuote: true, // Use single quotes instead of double
|
||||
tabWidth: 2, // Set the indentation width
|
||||
trailingComma: "es5", // Add trailing commas where valid in ES5
|
||||
trailingComma: 'es5', // Add trailing commas where valid in ES5
|
||||
printWidth: 80, // Wrap lines at 80 characters
|
||||
bracketSpacing: true, // Add spaces inside object braces
|
||||
arrowParens: "always", // Always include parentheses around arrow function parameters
|
||||
arrowParens: 'always', // Always include parentheses around arrow function parameters
|
||||
endOfLine: 'lf', // Enforce LF for all files
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1 +1,19 @@
|
|||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ export default class API {
|
|||
|
||||
static async logout() {
|
||||
return await request(`${host}/api/accounts/auth/logout/`, {
|
||||
auth: true, // Send JWT token so backend can identify the user
|
||||
auth: true, // Send JWT token so backend can identify the user
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
|
@ -261,15 +261,11 @@ export default class API {
|
|||
API.lastQueryParams = newParams;
|
||||
|
||||
const [response, ids] = await Promise.all([
|
||||
request(
|
||||
`${host}/api/channels/channels/?${newParams.toString()}`
|
||||
),
|
||||
request(`${host}/api/channels/channels/?${newParams.toString()}`),
|
||||
API.getAllChannelIds(newParams),
|
||||
]);
|
||||
|
||||
useChannelsTableStore
|
||||
.getState()
|
||||
.queryChannels(response, newParams);
|
||||
useChannelsTableStore.getState().queryChannels(response, newParams);
|
||||
useChannelsTableStore.getState().setAllQueryIds(ids);
|
||||
|
||||
return response;
|
||||
|
|
@ -390,7 +386,8 @@ export default class API {
|
|||
channelData.channel_number === '' ||
|
||||
channelData.channel_number === null ||
|
||||
channelData.channel_number === undefined ||
|
||||
(typeof channelData.channel_number === 'string' && channelData.channel_number.trim() === '')
|
||||
(typeof channelData.channel_number === 'string' &&
|
||||
channelData.channel_number.trim() === '')
|
||||
) {
|
||||
delete channelData.channel_number;
|
||||
}
|
||||
|
|
@ -719,7 +716,11 @@ export default class API {
|
|||
}
|
||||
}
|
||||
|
||||
static async createChannelsFromStreamsAsync(streamIds, channelProfileIds = null, startingChannelNumber = null) {
|
||||
static async createChannelsFromStreamsAsync(
|
||||
streamIds,
|
||||
channelProfileIds = null,
|
||||
startingChannelNumber = null
|
||||
) {
|
||||
try {
|
||||
const requestBody = {
|
||||
stream_ids: streamIds,
|
||||
|
|
@ -815,15 +816,11 @@ export default class API {
|
|||
|
||||
try {
|
||||
const [response, ids] = await Promise.all([
|
||||
request(
|
||||
`${host}/api/channels/streams/?${params.toString()}`
|
||||
),
|
||||
request(`${host}/api/channels/streams/?${params.toString()}`),
|
||||
API.getAllStreamIds(params),
|
||||
]);
|
||||
|
||||
useStreamsTableStore
|
||||
.getState()
|
||||
.queryStreams(response, params);
|
||||
useStreamsTableStore.getState().queryStreams(response, params);
|
||||
useStreamsTableStore.getState().setAllQueryIds(ids);
|
||||
|
||||
return response;
|
||||
|
|
@ -1179,13 +1176,10 @@ export default class API {
|
|||
|
||||
static async getCurrentPrograms(channelIds = null) {
|
||||
try {
|
||||
const response = await request(
|
||||
`${host}/api/epg/current-programs/`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: { channel_ids: channelIds },
|
||||
}
|
||||
);
|
||||
const response = await request(`${host}/api/epg/current-programs/`, {
|
||||
method: 'POST',
|
||||
body: { channel_ids: channelIds },
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
|
|
@ -1318,9 +1312,15 @@ export default class API {
|
|||
errorNotification('Failed to retrieve timezones', e);
|
||||
// Return fallback data instead of throwing
|
||||
return {
|
||||
timezones: ['UTC', 'US/Eastern', 'US/Central', 'US/Mountain', 'US/Pacific'],
|
||||
timezones: [
|
||||
'UTC',
|
||||
'US/Eastern',
|
||||
'US/Central',
|
||||
'US/Mountain',
|
||||
'US/Pacific',
|
||||
],
|
||||
grouped: {},
|
||||
count: 5
|
||||
count: 5,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1444,16 +1444,22 @@ export default class API {
|
|||
|
||||
static async refreshAccountInfo(profileId) {
|
||||
try {
|
||||
const response = await request(`${host}/api/m3u/refresh-account-info/${profileId}/`, {
|
||||
method: 'POST',
|
||||
});
|
||||
const response = await request(
|
||||
`${host}/api/m3u/refresh-account-info/${profileId}/`,
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
);
|
||||
return response;
|
||||
} catch (e) {
|
||||
// If it's a structured error response, return it instead of throwing
|
||||
if (e.body && typeof e.body === 'object') {
|
||||
return e.body;
|
||||
}
|
||||
errorNotification(`Failed to refresh account info for profile ${profileId}`, e);
|
||||
errorNotification(
|
||||
`Failed to refresh account info for profile ${profileId}`,
|
||||
e
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
|
@ -1580,7 +1586,11 @@ export default class API {
|
|||
});
|
||||
|
||||
// Wait for the task to complete using token for auth
|
||||
const result = await API.waitForBackupTask(response.task_id, onProgress, response.task_token);
|
||||
const result = await API.waitForBackupTask(
|
||||
response.task_id,
|
||||
onProgress,
|
||||
response.task_token
|
||||
);
|
||||
return result;
|
||||
} catch (e) {
|
||||
errorNotification('Failed to create backup', e);
|
||||
|
|
@ -1593,13 +1603,10 @@ export default class API {
|
|||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await request(
|
||||
`${host}/api/backups/upload/`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
}
|
||||
);
|
||||
const response = await request(`${host}/api/backups/upload/`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
return response;
|
||||
} catch (e) {
|
||||
errorNotification('Failed to upload backup', e);
|
||||
|
|
@ -1622,7 +1629,9 @@ export default class API {
|
|||
static async getDownloadToken(filename) {
|
||||
// Get a download token from the server
|
||||
try {
|
||||
const response = await request(`${host}/api/backups/${encodeURIComponent(filename)}/download-token/`);
|
||||
const response = await request(
|
||||
`${host}/api/backups/${encodeURIComponent(filename)}/download-token/`
|
||||
);
|
||||
return response.token;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
|
|
@ -1666,7 +1675,11 @@ export default class API {
|
|||
|
||||
// Wait for the task to complete using token for auth
|
||||
// Token-based auth allows status polling even after DB restore invalidates user sessions
|
||||
const result = await API.waitForBackupTask(response.task_id, onProgress, response.task_token);
|
||||
const result = await API.waitForBackupTask(
|
||||
response.task_id,
|
||||
onProgress,
|
||||
response.task_token
|
||||
);
|
||||
return result;
|
||||
} catch (e) {
|
||||
errorNotification('Failed to restore backup', e);
|
||||
|
|
@ -1741,17 +1754,27 @@ export default class API {
|
|||
return response;
|
||||
} catch (e) {
|
||||
// Show only the concise error message for plugin import
|
||||
const msg = (e?.body && (e.body.error || e.body.detail)) || e?.message || 'Failed to import plugin';
|
||||
notifications.show({ title: 'Import failed', message: msg, color: 'red' });
|
||||
const msg =
|
||||
(e?.body && (e.body.error || e.body.detail)) ||
|
||||
e?.message ||
|
||||
'Failed to import plugin';
|
||||
notifications.show({
|
||||
title: 'Import failed',
|
||||
message: msg,
|
||||
color: 'red',
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
static async deletePlugin(key) {
|
||||
try {
|
||||
const response = await request(`${host}/api/plugins/plugins/${key}/delete/`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
const response = await request(
|
||||
`${host}/api/plugins/plugins/${key}/delete/`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
}
|
||||
);
|
||||
return response;
|
||||
} catch (e) {
|
||||
errorNotification('Failed to delete plugin', e);
|
||||
|
|
@ -1776,10 +1799,13 @@ export default class API {
|
|||
|
||||
static async runPluginAction(key, action, params = {}) {
|
||||
try {
|
||||
const response = await request(`${host}/api/plugins/plugins/${key}/run/`, {
|
||||
method: 'POST',
|
||||
body: { action, params },
|
||||
});
|
||||
const response = await request(
|
||||
`${host}/api/plugins/plugins/${key}/run/`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: { action, params },
|
||||
}
|
||||
);
|
||||
return response;
|
||||
} catch (e) {
|
||||
errorNotification('Failed to run plugin action', e);
|
||||
|
|
@ -1788,10 +1814,13 @@ export default class API {
|
|||
|
||||
static async setPluginEnabled(key, enabled) {
|
||||
try {
|
||||
const response = await request(`${host}/api/plugins/plugins/${key}/enabled/`, {
|
||||
method: 'POST',
|
||||
body: { enabled },
|
||||
});
|
||||
const response = await request(
|
||||
`${host}/api/plugins/plugins/${key}/enabled/`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: { enabled },
|
||||
}
|
||||
);
|
||||
return response;
|
||||
} catch (e) {
|
||||
errorNotification('Failed to update plugin enabled state', e);
|
||||
|
|
@ -1973,7 +2002,7 @@ export default class API {
|
|||
if (!logoIds || logoIds.length === 0) return [];
|
||||
|
||||
const params = new URLSearchParams();
|
||||
logoIds.forEach(id => params.append('ids', id));
|
||||
logoIds.forEach((id) => params.append('ids', id));
|
||||
// Disable pagination for ID-based queries to get all matching logos
|
||||
params.append('no_pagination', 'true');
|
||||
|
||||
|
|
@ -2440,10 +2469,13 @@ export default class API {
|
|||
|
||||
static async updateRecurringRule(ruleId, payload) {
|
||||
try {
|
||||
const response = await request(`${host}/api/channels/recurring-rules/${ruleId}/`, {
|
||||
method: 'PATCH',
|
||||
body: payload,
|
||||
});
|
||||
const response = await request(
|
||||
`${host}/api/channels/recurring-rules/${ruleId}/`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
body: payload,
|
||||
}
|
||||
);
|
||||
return response;
|
||||
} catch (e) {
|
||||
errorNotification(`Failed to update recurring rule ${ruleId}`, e);
|
||||
|
|
@ -2462,9 +2494,13 @@ export default class API {
|
|||
|
||||
static async deleteRecording(id) {
|
||||
try {
|
||||
await request(`${host}/api/channels/recordings/${id}/`, { method: 'DELETE' });
|
||||
await request(`${host}/api/channels/recordings/${id}/`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
// Optimistically remove locally for instant UI update
|
||||
try { useChannelsStore.getState().removeRecording(id); } catch {}
|
||||
try {
|
||||
useChannelsStore.getState().removeRecording(id);
|
||||
} catch {}
|
||||
} catch (e) {
|
||||
errorNotification(`Failed to delete recording ${id}`, e);
|
||||
}
|
||||
|
|
@ -2472,9 +2508,12 @@ export default class API {
|
|||
|
||||
static async runComskip(recordingId) {
|
||||
try {
|
||||
const resp = await request(`${host}/api/channels/recordings/${recordingId}/comskip/`, {
|
||||
method: 'POST',
|
||||
});
|
||||
const resp = await request(
|
||||
`${host}/api/channels/recordings/${recordingId}/comskip/`,
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
);
|
||||
// Refresh recordings list to reflect comskip status when done later
|
||||
// This endpoint just queues the task; the websocket/refresh will update eventually
|
||||
return resp;
|
||||
|
|
@ -2512,7 +2551,9 @@ export default class API {
|
|||
static async deleteSeriesRule(tvgId) {
|
||||
try {
|
||||
const encodedTvgId = encodeURIComponent(tvgId);
|
||||
await request(`${host}/api/channels/series-rules/${encodedTvgId}/`, { method: 'DELETE' });
|
||||
await request(`${host}/api/channels/series-rules/${encodedTvgId}/`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
notifications.show({ title: 'Series rule removed' });
|
||||
} catch (e) {
|
||||
errorNotification('Failed to remove series rule', e);
|
||||
|
|
@ -2522,9 +2563,12 @@ export default class API {
|
|||
|
||||
static async deleteAllUpcomingRecordings() {
|
||||
try {
|
||||
const resp = await request(`${host}/api/channels/recordings/bulk-delete-upcoming/`, {
|
||||
method: 'POST',
|
||||
});
|
||||
const resp = await request(
|
||||
`${host}/api/channels/recordings/bulk-delete-upcoming/`,
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
);
|
||||
notifications.show({ title: `Removed ${resp.removed || 0} upcoming` });
|
||||
useChannelsStore.getState().fetchRecordings();
|
||||
return resp;
|
||||
|
|
@ -2545,12 +2589,19 @@ export default class API {
|
|||
}
|
||||
}
|
||||
|
||||
static async bulkRemoveSeriesRecordings({ tvg_id, title = null, scope = 'title' }) {
|
||||
static async bulkRemoveSeriesRecordings({
|
||||
tvg_id,
|
||||
title = null,
|
||||
scope = 'title',
|
||||
}) {
|
||||
try {
|
||||
const resp = await request(`${host}/api/channels/series-rules/bulk-remove/`, {
|
||||
method: 'POST',
|
||||
body: { tvg_id, title, scope },
|
||||
});
|
||||
const resp = await request(
|
||||
`${host}/api/channels/series-rules/bulk-remove/`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: { tvg_id, title, scope },
|
||||
}
|
||||
);
|
||||
notifications.show({ title: `Removed ${resp.removed || 0} scheduled` });
|
||||
return resp;
|
||||
} catch (e) {
|
||||
|
|
@ -2712,13 +2763,10 @@ export default class API {
|
|||
try {
|
||||
// Use POST for large ID lists to avoid URL length limitations
|
||||
if (ids.length > 50) {
|
||||
const response = await request(
|
||||
`${host}/api/channels/streams/by-ids/`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: { ids },
|
||||
}
|
||||
);
|
||||
const response = await request(`${host}/api/channels/streams/by-ids/`, {
|
||||
method: 'POST',
|
||||
body: { ids },
|
||||
});
|
||||
return response;
|
||||
} else {
|
||||
// Use GET for small ID lists for backward compatibility
|
||||
|
|
@ -2744,8 +2792,9 @@ export default class API {
|
|||
return response;
|
||||
} catch (e) {
|
||||
// Don't show error notification for "Invalid page" errors as they're handled gracefully
|
||||
const isInvalidPage = e.body?.detail?.includes('Invalid page') ||
|
||||
e.message?.includes('Invalid page');
|
||||
const isInvalidPage =
|
||||
e.body?.detail?.includes('Invalid page') ||
|
||||
e.message?.includes('Invalid page');
|
||||
|
||||
if (!isInvalidPage) {
|
||||
errorNotification('Failed to retrieve movies', e);
|
||||
|
|
@ -2762,8 +2811,9 @@ export default class API {
|
|||
return response;
|
||||
} catch (e) {
|
||||
// Don't show error notification for "Invalid page" errors as they're handled gracefully
|
||||
const isInvalidPage = e.body?.detail?.includes('Invalid page') ||
|
||||
e.message?.includes('Invalid page');
|
||||
const isInvalidPage =
|
||||
e.body?.detail?.includes('Invalid page') ||
|
||||
e.message?.includes('Invalid page');
|
||||
|
||||
if (!isInvalidPage) {
|
||||
errorNotification('Failed to retrieve series', e);
|
||||
|
|
@ -2774,7 +2824,10 @@ export default class API {
|
|||
|
||||
static async getAllContent(params = new URLSearchParams()) {
|
||||
try {
|
||||
console.log('Calling getAllContent with URL:', `${host}/api/vod/all/?${params.toString()}`);
|
||||
console.log(
|
||||
'Calling getAllContent with URL:',
|
||||
`${host}/api/vod/all/?${params.toString()}`
|
||||
);
|
||||
const response = await request(
|
||||
`${host}/api/vod/all/?${params.toString()}`
|
||||
);
|
||||
|
|
@ -2787,8 +2840,9 @@ export default class API {
|
|||
console.error('Error message:', e.message);
|
||||
|
||||
// Don't show error notification for "Invalid page" errors as they're handled gracefully
|
||||
const isInvalidPage = e.body?.detail?.includes('Invalid page') ||
|
||||
e.message?.includes('Invalid page');
|
||||
const isInvalidPage =
|
||||
e.body?.detail?.includes('Invalid page') ||
|
||||
e.message?.includes('Invalid page');
|
||||
|
||||
if (!isInvalidPage) {
|
||||
errorNotification('Failed to retrieve content', e);
|
||||
|
|
@ -2911,9 +2965,8 @@ export default class API {
|
|||
);
|
||||
|
||||
// Update the store with fetched notifications
|
||||
const { default: useNotificationsStore } = await import(
|
||||
'./store/notifications'
|
||||
);
|
||||
const { default: useNotificationsStore } =
|
||||
await import('./store/notifications');
|
||||
useNotificationsStore.getState().setNotifications(response.notifications);
|
||||
|
||||
return response;
|
||||
|
|
@ -2928,9 +2981,8 @@ export default class API {
|
|||
const response = await request(`${host}/api/core/notifications/count/`);
|
||||
|
||||
// Update the store with the count
|
||||
const { default: useNotificationsStore } = await import(
|
||||
'./store/notifications'
|
||||
);
|
||||
const { default: useNotificationsStore } =
|
||||
await import('./store/notifications');
|
||||
useNotificationsStore.getState().setUnreadCount(response.unread_count);
|
||||
|
||||
return response;
|
||||
|
|
@ -2962,10 +3014,11 @@ export default class API {
|
|||
);
|
||||
|
||||
// Update the store
|
||||
const { default: useNotificationsStore } = await import(
|
||||
'./store/notifications'
|
||||
);
|
||||
useNotificationsStore.getState().dismissNotification(response.notification_key);
|
||||
const { default: useNotificationsStore } =
|
||||
await import('./store/notifications');
|
||||
useNotificationsStore
|
||||
.getState()
|
||||
.dismissNotification(response.notification_key);
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
|
|
@ -2984,9 +3037,8 @@ export default class API {
|
|||
);
|
||||
|
||||
// Update the store
|
||||
const { default: useNotificationsStore } = await import(
|
||||
'./store/notifications'
|
||||
);
|
||||
const { default: useNotificationsStore } =
|
||||
await import('./store/notifications');
|
||||
useNotificationsStore.getState().dismissAllNotifications();
|
||||
|
||||
return response;
|
||||
|
|
|
|||
|
|
@ -1 +1,19 @@
|
|||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,4 +15,4 @@ class ErrorBoundary extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
export default ErrorBoundary;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,11 @@
|
|||
import { NumberInput, Select, Switch, Text, Textarea, TextInput } from '@mantine/core';
|
||||
import {
|
||||
NumberInput,
|
||||
Select,
|
||||
Switch,
|
||||
Text,
|
||||
Textarea,
|
||||
TextInput,
|
||||
} from '@mantine/core';
|
||||
import React from 'react';
|
||||
|
||||
export const Field = ({ field, value, onChange }) => {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import React from "react";
|
||||
import React from 'react';
|
||||
import {
|
||||
CHANNEL_WIDTH,
|
||||
EXPANDED_PROGRAM_HEIGHT,
|
||||
HOUR_WIDTH,
|
||||
PROGRAM_HEIGHT,
|
||||
} from '../pages/guideUtils.js';
|
||||
import {Box, Flex, Text} from "@mantine/core";
|
||||
import {Play} from "lucide-react";
|
||||
import logo from "../images/logo.png";
|
||||
import { Box, Flex, Text } from '@mantine/core';
|
||||
import { Play } from 'lucide-react';
|
||||
import logo from '../images/logo.png';
|
||||
|
||||
const GuideRow = React.memo(({ index, style, data }) => {
|
||||
const {
|
||||
|
|
@ -36,31 +36,33 @@ const GuideRow = React.memo(({ index, style, data }) => {
|
|||
: PROGRAM_HEIGHT);
|
||||
|
||||
const PlaceholderProgram = () => {
|
||||
return <>
|
||||
{Array.from({length: Math.ceil(24 / 2)}).map(
|
||||
(_, placeholderIndex) => (
|
||||
<Box
|
||||
key={`placeholder-${channel.id}-${placeholderIndex}`}
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
pos='absolute'
|
||||
left={placeholderIndex * (HOUR_WIDTH * 2)}
|
||||
top={0}
|
||||
w={HOUR_WIDTH * 2}
|
||||
h={rowHeight - 4}
|
||||
bd={'1px dashed #2D3748'}
|
||||
bdrs={4}
|
||||
display={'flex'}
|
||||
c='#4A5568'
|
||||
>
|
||||
<Text size="sm">No program data</Text>
|
||||
</Box>
|
||||
)
|
||||
)}
|
||||
</>;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{Array.from({ length: Math.ceil(24 / 2) }).map(
|
||||
(_, placeholderIndex) => (
|
||||
<Box
|
||||
key={`placeholder-${channel.id}-${placeholderIndex}`}
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
pos="absolute"
|
||||
left={placeholderIndex * (HOUR_WIDTH * 2)}
|
||||
top={0}
|
||||
w={HOUR_WIDTH * 2}
|
||||
h={rowHeight - 4}
|
||||
bd={'1px dashed #2D3748'}
|
||||
bdrs={4}
|
||||
display={'flex'}
|
||||
c="#4A5568"
|
||||
>
|
||||
<Text size="sm">No program data</Text>
|
||||
</Box>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -75,7 +77,7 @@ const GuideRow = React.memo(({ index, style, data }) => {
|
|||
}}
|
||||
display={'flex'}
|
||||
h={'100%'}
|
||||
pos='relative'
|
||||
pos="relative"
|
||||
>
|
||||
<Box
|
||||
className="channel-logo"
|
||||
|
|
@ -96,7 +98,7 @@ const GuideRow = React.memo(({ index, style, data }) => {
|
|||
display={'flex'}
|
||||
left={0}
|
||||
h={'100%'}
|
||||
pos='relative'
|
||||
pos="relative"
|
||||
onClick={(event) => handleLogoClick(channel, event)}
|
||||
onMouseEnter={() => setHoveredChannelId(channel.id)}
|
||||
onMouseLeave={() => setHoveredChannelId(null)}
|
||||
|
|
@ -110,7 +112,7 @@ const GuideRow = React.memo(({ index, style, data }) => {
|
|||
zIndex: 10,
|
||||
animation: 'fadeIn 0.2s',
|
||||
}}
|
||||
pos='absolute'
|
||||
pos="absolute"
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
|
|
@ -133,7 +135,7 @@ const GuideRow = React.memo(({ index, style, data }) => {
|
|||
w={'100%'}
|
||||
h={'100%'}
|
||||
p={'4px'}
|
||||
pos='relative'
|
||||
pos="relative"
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
|
|
@ -167,7 +169,7 @@ const GuideRow = React.memo(({ index, style, data }) => {
|
|||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
pos='absolute'
|
||||
pos="absolute"
|
||||
bottom={4}
|
||||
left={'50%'}
|
||||
p={'2px 8px'}
|
||||
|
|
@ -188,7 +190,7 @@ const GuideRow = React.memo(({ index, style, data }) => {
|
|||
transition: 'height 0.2s ease',
|
||||
}}
|
||||
flex={1}
|
||||
pos='relative'
|
||||
pos="relative"
|
||||
h={'100%'}
|
||||
pl={0}
|
||||
>
|
||||
|
|
@ -196,11 +198,13 @@ const GuideRow = React.memo(({ index, style, data }) => {
|
|||
channelPrograms.map((program) =>
|
||||
renderProgram(program, undefined, channel)
|
||||
)
|
||||
) : <PlaceholderProgram />}
|
||||
) : (
|
||||
<PlaceholderProgram />
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default GuideRow;
|
||||
export default GuideRow;
|
||||
|
|
|
|||
|
|
@ -3,103 +3,102 @@ import { Box, Text } from '@mantine/core';
|
|||
import { format } from '../utils/dateTimeUtils.js';
|
||||
import { HOUR_WIDTH } from '../pages/guideUtils.js';
|
||||
|
||||
const HourBlock = React.memo(({ hourData, timeFormat, formatDayLabel, handleTimeClick }) => {
|
||||
const { time, isNewDay } = hourData;
|
||||
const HourBlock = React.memo(
|
||||
({ hourData, timeFormat, formatDayLabel, handleTimeClick }) => {
|
||||
const { time, isNewDay } = hourData;
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={format(time)}
|
||||
style={{
|
||||
borderRight: '1px solid #8DAFAA',
|
||||
cursor: 'pointer',
|
||||
borderLeft: isNewDay ? '2px solid #3BA882' : 'none',
|
||||
backgroundColor: isNewDay ? '#1E2A27' : '#1B2421',
|
||||
}}
|
||||
w={HOUR_WIDTH}
|
||||
h={'40px'}
|
||||
pos='relative'
|
||||
c='#a0aec0'
|
||||
onClick={(e) => handleTimeClick(time, e)}
|
||||
>
|
||||
<Text
|
||||
size="sm"
|
||||
style={{ transform: 'none' }}
|
||||
pos='absolute'
|
||||
top={8}
|
||||
left={4}
|
||||
bdrs={2}
|
||||
lh={1.2}
|
||||
ta='left'
|
||||
return (
|
||||
<Box
|
||||
key={format(time)}
|
||||
style={{
|
||||
borderRight: '1px solid #8DAFAA',
|
||||
cursor: 'pointer',
|
||||
borderLeft: isNewDay ? '2px solid #3BA882' : 'none',
|
||||
backgroundColor: isNewDay ? '#1E2A27' : '#1B2421',
|
||||
}}
|
||||
w={HOUR_WIDTH}
|
||||
h={'40px'}
|
||||
pos="relative"
|
||||
c="#a0aec0"
|
||||
onClick={(e) => handleTimeClick(time, e)}
|
||||
>
|
||||
<Text
|
||||
span
|
||||
size="xs"
|
||||
display={'block'}
|
||||
opacity={0.7}
|
||||
fw={isNewDay ? 600 : 400}
|
||||
c={isNewDay ? '#3BA882' : undefined}
|
||||
size="sm"
|
||||
style={{ transform: 'none' }}
|
||||
pos="absolute"
|
||||
top={8}
|
||||
left={4}
|
||||
bdrs={2}
|
||||
lh={1.2}
|
||||
ta="left"
|
||||
>
|
||||
{formatDayLabel(time)}
|
||||
<Text
|
||||
span
|
||||
size="xs"
|
||||
display={'block'}
|
||||
opacity={0.7}
|
||||
fw={isNewDay ? 600 : 400}
|
||||
c={isNewDay ? '#3BA882' : undefined}
|
||||
>
|
||||
{formatDayLabel(time)}
|
||||
</Text>
|
||||
{format(time, timeFormat)}
|
||||
<Text span size="xs" ml={1} opacity={0.7} />
|
||||
</Text>
|
||||
{format(time, timeFormat)}
|
||||
<Text span size="xs" ml={1} opacity={0.7} />
|
||||
</Text>
|
||||
|
||||
<Box
|
||||
style={{
|
||||
backgroundColor: '#27272A',
|
||||
zIndex: 10,
|
||||
}}
|
||||
pos='absolute'
|
||||
left={0}
|
||||
top={0}
|
||||
bottom={0}
|
||||
w={'1px'}
|
||||
/>
|
||||
<Box
|
||||
style={{
|
||||
backgroundColor: '#27272A',
|
||||
zIndex: 10,
|
||||
}}
|
||||
pos="absolute"
|
||||
left={0}
|
||||
top={0}
|
||||
bottom={0}
|
||||
w={'1px'}
|
||||
/>
|
||||
|
||||
<Box
|
||||
style={{ justifyContent: 'space-between' }}
|
||||
pos='absolute'
|
||||
bottom={0}
|
||||
w={'100%'}
|
||||
display={'flex'}
|
||||
p={'0 1px'}
|
||||
>
|
||||
{[15, 30, 45].map((minute) => (
|
||||
<Box
|
||||
key={minute}
|
||||
style={{ backgroundColor: '#718096' }}
|
||||
w={'1px'}
|
||||
h={'8px'}
|
||||
pos='absolute'
|
||||
bottom={0}
|
||||
left={`${(minute / 60) * 100}%`}
|
||||
<Box
|
||||
style={{ justifyContent: 'space-between' }}
|
||||
pos="absolute"
|
||||
bottom={0}
|
||||
w={'100%'}
|
||||
display={'flex'}
|
||||
p={'0 1px'}
|
||||
>
|
||||
{[15, 30, 45].map((minute) => (
|
||||
<Box
|
||||
key={minute}
|
||||
style={{ backgroundColor: '#718096' }}
|
||||
w={'1px'}
|
||||
h={'8px'}
|
||||
pos="absolute"
|
||||
bottom={0}
|
||||
left={`${(minute / 60) * 100}%`}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const HourTimeline = React.memo(
|
||||
({ hourTimeline, timeFormat, formatDayLabel, handleTimeClick }) => {
|
||||
return (
|
||||
<>
|
||||
{hourTimeline.map((hourData) => (
|
||||
<HourBlock
|
||||
key={format(hourData.time)}
|
||||
hourData={hourData}
|
||||
timeFormat={timeFormat}
|
||||
formatDayLabel={formatDayLabel}
|
||||
handleTimeClick={handleTimeClick}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
const HourTimeline = React.memo(({
|
||||
hourTimeline,
|
||||
timeFormat,
|
||||
formatDayLabel,
|
||||
handleTimeClick
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{hourTimeline.map((hourData) => (
|
||||
<HourBlock
|
||||
key={format(hourData.time)}
|
||||
hourData={hourData}
|
||||
timeFormat={timeFormat}
|
||||
formatDayLabel={formatDayLabel}
|
||||
handleTimeClick={handleTimeClick}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default HourTimeline;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Text, } from '@mantine/core';
|
||||
import { Text } from '@mantine/core';
|
||||
|
||||
// Short preview that triggers the details modal when clicked
|
||||
const RecordingSynopsis = ({ description, onOpen }) => {
|
||||
|
|
@ -23,4 +23,4 @@ const RecordingSynopsis = ({ description, onOpen }) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default RecordingSynopsis;
|
||||
export default RecordingSynopsis;
|
||||
|
|
|
|||
|
|
@ -29,7 +29,12 @@ const PluginFieldList = ({ plugin, settings, updateField }) => {
|
|||
));
|
||||
};
|
||||
|
||||
const PluginActionList = ({ plugin, enabled, runningActionId, handlePluginRun }) => {
|
||||
const PluginActionList = ({
|
||||
plugin,
|
||||
enabled,
|
||||
runningActionId,
|
||||
handlePluginRun,
|
||||
}) => {
|
||||
return plugin.actions.map((action) => (
|
||||
<Group key={action.id} justify="space-between">
|
||||
<div>
|
||||
|
|
@ -226,7 +231,12 @@ const PluginCard = ({
|
|||
style={{ opacity: !missing && enabled ? 1 : 0.6 }}
|
||||
>
|
||||
<Group justify="space-between" mb="xs" align="flex-start" wrap="nowrap">
|
||||
<Group gap="sm" align="flex-start" wrap="nowrap" style={{ minWidth: 0, flex: 1 }}>
|
||||
<Group
|
||||
gap="sm"
|
||||
align="flex-start"
|
||||
wrap="nowrap"
|
||||
style={{ minWidth: 0, flex: 1 }}
|
||||
>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
|
|
@ -306,35 +316,51 @@ const PluginCard = ({
|
|||
</Text>
|
||||
)}
|
||||
|
||||
{expanded && !missing && enabled && plugin.fields && plugin.fields.length > 0 && (
|
||||
<Stack gap="xs" mt="sm">
|
||||
<PluginFieldList
|
||||
plugin={plugin}
|
||||
settings={settings}
|
||||
updateField={updateField}
|
||||
/>
|
||||
<Group>
|
||||
<Button loading={saving} onClick={save} variant="default" size="xs">
|
||||
Save Settings
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{expanded && !missing && enabled && plugin.actions && plugin.actions.length > 0 && (
|
||||
<>
|
||||
<Divider my="sm" />
|
||||
<Stack gap="xs">
|
||||
<PluginActionList
|
||||
{expanded &&
|
||||
!missing &&
|
||||
enabled &&
|
||||
plugin.fields &&
|
||||
plugin.fields.length > 0 && (
|
||||
<Stack gap="xs" mt="sm">
|
||||
<PluginFieldList
|
||||
plugin={plugin}
|
||||
enabled={enabled}
|
||||
runningActionId={runningActionId}
|
||||
handlePluginRun={handlePluginRun}
|
||||
settings={settings}
|
||||
updateField={updateField}
|
||||
/>
|
||||
<PluginActionStatus running={!!runningActionId} lastResult={lastResult} />
|
||||
<Group>
|
||||
<Button
|
||||
loading={saving}
|
||||
onClick={save}
|
||||
variant="default"
|
||||
size="xs"
|
||||
>
|
||||
Save Settings
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
|
||||
{expanded &&
|
||||
!missing &&
|
||||
enabled &&
|
||||
plugin.actions &&
|
||||
plugin.actions.length > 0 && (
|
||||
<>
|
||||
<Divider my="sm" />
|
||||
<Stack gap="xs">
|
||||
<PluginActionList
|
||||
plugin={plugin}
|
||||
enabled={enabled}
|
||||
runningActionId={runningActionId}
|
||||
handlePluginRun={handlePluginRun}
|
||||
/>
|
||||
<PluginActionStatus
|
||||
running={!!runningActionId}
|
||||
lastResult={lastResult}
|
||||
/>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import {
|
|||
Stack,
|
||||
Text,
|
||||
} from '@mantine/core';
|
||||
import {Calendar, Play, Star} from "lucide-react";
|
||||
import React from "react";
|
||||
import { Calendar, Play, Star } from 'lucide-react';
|
||||
import React from 'react';
|
||||
|
||||
const SeriesCard = ({ series, onClick }) => {
|
||||
return (
|
||||
|
|
@ -82,4 +82,4 @@ const SeriesCard = ({ series, onClick }) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default SeriesCard;
|
||||
export default SeriesCard;
|
||||
|
|
|
|||
|
|
@ -140,4 +140,4 @@ const VODCard = ({ vod, onClick }) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default VODCard;
|
||||
export default VODCard;
|
||||
|
|
|
|||
|
|
@ -74,23 +74,33 @@ export default function ProgramRecordingModal({
|
|||
Just this one
|
||||
</Button>
|
||||
|
||||
<Button variant="light" onClick={() => {
|
||||
onRecordSeriesAll();
|
||||
onClose();
|
||||
}}>
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => {
|
||||
onRecordSeriesAll();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Every episode
|
||||
</Button>
|
||||
|
||||
<Button variant="light" onClick={() => {
|
||||
onRecordSeriesNew();
|
||||
onClose();
|
||||
}}>
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => {
|
||||
onRecordSeriesNew();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
New episodes only
|
||||
</Button>
|
||||
|
||||
{recording && (
|
||||
<>
|
||||
<Button color="orange" variant="light" onClick={handleRemoveRecording}>
|
||||
<Button
|
||||
color="orange"
|
||||
variant="light"
|
||||
onClick={handleRemoveRecording}
|
||||
>
|
||||
Remove this recording
|
||||
</Button>
|
||||
<Button color="red" variant="light" onClick={handleRemoveSeries}>
|
||||
|
|
|
|||
|
|
@ -44,7 +44,11 @@ const toIsoIfDate = (value) => {
|
|||
const toTimeString = (value) => {
|
||||
if (!value) return '00:00';
|
||||
if (typeof value === 'string') {
|
||||
const parsed = dayjs(value, ['HH:mm', 'hh:mm A', 'h:mm A', 'HH:mm:ss'], true);
|
||||
const parsed = dayjs(
|
||||
value,
|
||||
['HH:mm', 'hh:mm A', 'h:mm A', 'HH:mm:ss'],
|
||||
true
|
||||
);
|
||||
if (parsed.isValid()) return parsed.format('HH:mm');
|
||||
return value;
|
||||
}
|
||||
|
|
@ -77,7 +81,12 @@ const timeChange = (setter) => (valOrEvent) => {
|
|||
else if (valOrEvent?.currentTarget) setter(valOrEvent.currentTarget.value);
|
||||
};
|
||||
|
||||
const RecordingModal = ({ recording = null, channel = null, isOpen, onClose }) => {
|
||||
const RecordingModal = ({
|
||||
recording = null,
|
||||
channel = null,
|
||||
isOpen,
|
||||
onClose,
|
||||
}) => {
|
||||
const channels = useChannelsStore((s) => s.channels);
|
||||
const fetchRecordings = useChannelsStore((s) => s.fetchRecordings);
|
||||
const fetchRecurringRules = useChannelsStore((s) => s.fetchRecurringRules);
|
||||
|
|
@ -93,9 +102,17 @@ const RecordingModal = ({ recording = null, channel = null, isOpen, onClose }) =
|
|||
const singleForm = useForm({
|
||||
mode: 'controlled',
|
||||
initialValues: {
|
||||
channel_id: recording ? `${recording.channel}` : channel ? `${channel.id}` : '',
|
||||
start_time: recording ? asDate(recording.start_time) || defaultStart : defaultStart,
|
||||
end_time: recording ? asDate(recording.end_time) || defaultEnd : defaultEnd,
|
||||
channel_id: recording
|
||||
? `${recording.channel}`
|
||||
: channel
|
||||
? `${channel.id}`
|
||||
: '',
|
||||
start_time: recording
|
||||
? asDate(recording.start_time) || defaultStart
|
||||
: defaultStart,
|
||||
end_time: recording
|
||||
? asDate(recording.end_time) || defaultEnd
|
||||
: defaultEnd,
|
||||
},
|
||||
validate: {
|
||||
channel_id: isNotEmpty('Select a channel'),
|
||||
|
|
@ -126,13 +143,22 @@ const RecordingModal = ({ recording = null, channel = null, isOpen, onClose }) =
|
|||
},
|
||||
validate: {
|
||||
channel_id: isNotEmpty('Select a channel'),
|
||||
days_of_week: (value) => (value && value.length ? null : 'Pick at least one day'),
|
||||
days_of_week: (value) =>
|
||||
value && value.length ? null : 'Pick at least one day',
|
||||
start_time: (value) => (value ? null : 'Select a start time'),
|
||||
end_time: (value, values) => {
|
||||
if (!value) return 'Select an end time';
|
||||
const start = dayjs(values.start_time, ['HH:mm', 'hh:mm A', 'h:mm A'], true);
|
||||
const start = dayjs(
|
||||
values.start_time,
|
||||
['HH:mm', 'hh:mm A', 'h:mm A'],
|
||||
true
|
||||
);
|
||||
const end = dayjs(value, ['HH:mm', 'hh:mm A', 'h:mm A'], true);
|
||||
if (start.isValid() && end.isValid() && end.diff(start, 'minute') === 0) {
|
||||
if (
|
||||
start.isValid() &&
|
||||
end.isValid() &&
|
||||
end.diff(start, 'minute') === 0
|
||||
) {
|
||||
return 'End time must differ from start time';
|
||||
}
|
||||
return null;
|
||||
|
|
@ -192,7 +218,10 @@ const RecordingModal = ({ recording = null, channel = null, isOpen, onClose }) =
|
|||
if (aNum === bNum) return (a.name || '').localeCompare(b.name || '');
|
||||
return aNum - bNum;
|
||||
});
|
||||
return list.map((item) => ({ value: `${item.id}`, label: item.name || `Channel ${item.id}` }));
|
||||
return list.map((item) => ({
|
||||
value: `${item.id}`,
|
||||
label: item.name || `Channel ${item.id}`,
|
||||
}));
|
||||
}, [channels]);
|
||||
|
||||
const resetForms = () => {
|
||||
|
|
@ -287,7 +316,8 @@ const RecordingModal = ({ recording = null, channel = null, isOpen, onClose }) =
|
|||
icon={<CircleAlert />}
|
||||
style={{ paddingBottom: 5, marginBottom: 12 }}
|
||||
>
|
||||
Recordings may fail if active streams or overlapping recordings use up all available tuners.
|
||||
Recordings may fail if active streams or overlapping recordings use up
|
||||
all available tuners.
|
||||
</Alert>
|
||||
|
||||
<Stack gap="md">
|
||||
|
|
@ -330,14 +360,24 @@ const RecordingModal = ({ recording = null, channel = null, isOpen, onClose }) =
|
|||
key={singleForm.key('start_time')}
|
||||
label="Start"
|
||||
valueFormat="MMM D, YYYY h:mm A"
|
||||
timeInputProps={{ format: '12', withSeconds: false, amLabel: 'AM', pmLabel: 'PM' }}
|
||||
timeInputProps={{
|
||||
format: '12',
|
||||
withSeconds: false,
|
||||
amLabel: 'AM',
|
||||
pmLabel: 'PM',
|
||||
}}
|
||||
/>
|
||||
<DateTimePicker
|
||||
{...singleForm.getInputProps('end_time')}
|
||||
key={singleForm.key('end_time')}
|
||||
label="End"
|
||||
valueFormat="MMM D, YYYY h:mm A"
|
||||
timeInputProps={{ format: '12', withSeconds: false, amLabel: 'AM', pmLabel: 'PM' }}
|
||||
timeInputProps={{
|
||||
format: '12',
|
||||
withSeconds: false,
|
||||
amLabel: 'AM',
|
||||
pmLabel: 'PM',
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
|
@ -364,14 +404,19 @@ const RecordingModal = ({ recording = null, channel = null, isOpen, onClose }) =
|
|||
label="Start date"
|
||||
value={recurringForm.values.start_date}
|
||||
onChange={(value) =>
|
||||
recurringForm.setFieldValue('start_date', value || new Date())
|
||||
recurringForm.setFieldValue(
|
||||
'start_date',
|
||||
value || new Date()
|
||||
)
|
||||
}
|
||||
valueFormat="MMM D, YYYY"
|
||||
/>
|
||||
<DatePickerInput
|
||||
label="End date"
|
||||
value={recurringForm.values.end_date}
|
||||
onChange={(value) => recurringForm.setFieldValue('end_date', value)}
|
||||
onChange={(value) =>
|
||||
recurringForm.setFieldValue('end_date', value)
|
||||
}
|
||||
valueFormat="MMM D, YYYY"
|
||||
minDate={recurringForm.values.start_date || undefined}
|
||||
/>
|
||||
|
|
@ -382,11 +427,14 @@ const RecordingModal = ({ recording = null, channel = null, isOpen, onClose }) =
|
|||
label="Start time"
|
||||
value={recurringForm.values.start_time}
|
||||
onChange={timeChange((val) =>
|
||||
recurringForm.setFieldValue('start_time', toTimeString(val))
|
||||
recurringForm.setFieldValue(
|
||||
'start_time',
|
||||
toTimeString(val)
|
||||
)
|
||||
)}
|
||||
onBlur={() => recurringForm.validateField('start_time')}
|
||||
withSeconds={false}
|
||||
format="12" // shows 12-hour (so "00:00" renders "12:00 AM")
|
||||
format="12" // shows 12-hour (so "00:00" renders "12:00 AM")
|
||||
inputMode="numeric"
|
||||
amLabel="AM"
|
||||
pmLabel="PM"
|
||||
|
|
|
|||
|
|
@ -2,14 +2,17 @@ import React from 'react';
|
|||
import { Modal, Stack, Text, Flex, Group, Button } from '@mantine/core';
|
||||
import useChannelsStore from '../../store/channels.jsx';
|
||||
import { deleteSeriesAndRule } from '../../utils/cards/RecordingCardUtils.js';
|
||||
import { evaluateSeriesRulesByTvgId, fetchRules } from '../../pages/guideUtils.js';
|
||||
import {
|
||||
evaluateSeriesRulesByTvgId,
|
||||
fetchRules,
|
||||
} from '../../pages/guideUtils.js';
|
||||
import { showNotification } from '../../utils/notificationUtils.js';
|
||||
|
||||
export default function SeriesRecordingModal({
|
||||
opened,
|
||||
onClose,
|
||||
rules,
|
||||
onRulesUpdate
|
||||
opened,
|
||||
onClose,
|
||||
rules,
|
||||
onRulesUpdate,
|
||||
}) {
|
||||
const handleEvaluateNow = async (r) => {
|
||||
await evaluateSeriesRulesByTvgId(r.tvg_id);
|
||||
|
|
@ -56,35 +59,36 @@ export default function SeriesRecordingModal({
|
|||
No series rules configured
|
||||
</Text>
|
||||
)}
|
||||
{rules && rules.map((r) => (
|
||||
<Flex
|
||||
key={`${r.tvg_id}-${r.mode}`}
|
||||
justify="space-between"
|
||||
align="center"
|
||||
>
|
||||
<Text size="sm">
|
||||
{r.title || r.tvg_id} —{' '}
|
||||
{r.mode === 'new' ? 'New episodes' : 'Every episode'}
|
||||
</Text>
|
||||
<Group gap="xs">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
onClick={() => handleEvaluateNow(r)}
|
||||
>
|
||||
Evaluate Now
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
color="orange"
|
||||
onClick={() => handleRemoveSeries(r)}
|
||||
>
|
||||
Remove this series (scheduled)
|
||||
</Button>
|
||||
</Group>
|
||||
</Flex>
|
||||
))}
|
||||
{rules &&
|
||||
rules.map((r) => (
|
||||
<Flex
|
||||
key={`${r.tvg_id}-${r.mode}`}
|
||||
justify="space-between"
|
||||
align="center"
|
||||
>
|
||||
<Text size="sm">
|
||||
{r.title || r.tvg_id} —{' '}
|
||||
{r.mode === 'new' ? 'New episodes' : 'Every episode'}
|
||||
</Text>
|
||||
<Group gap="xs">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
onClick={() => handleEvaluateNow(r)}
|
||||
>
|
||||
Evaluate Now
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
color="orange"
|
||||
onClick={() => handleRemoveSeries(r)}
|
||||
>
|
||||
Remove this series (scheduled)
|
||||
</Button>
|
||||
</Group>
|
||||
</Flex>
|
||||
))}
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -22,10 +22,14 @@ const getEpgSettingsFromStore = (settings) => {
|
|||
const epgSettings = settings?.['epg_settings']?.value;
|
||||
return {
|
||||
epg_match_mode: epgSettings?.epg_match_mode || 'default',
|
||||
epg_match_ignore_prefixes: Array.isArray(epgSettings?.epg_match_ignore_prefixes)
|
||||
epg_match_ignore_prefixes: Array.isArray(
|
||||
epgSettings?.epg_match_ignore_prefixes
|
||||
)
|
||||
? epgSettings.epg_match_ignore_prefixes
|
||||
: [],
|
||||
epg_match_ignore_suffixes: Array.isArray(epgSettings?.epg_match_ignore_suffixes)
|
||||
epg_match_ignore_suffixes: Array.isArray(
|
||||
epgSettings?.epg_match_ignore_suffixes
|
||||
)
|
||||
? epgSettings.epg_match_ignore_suffixes
|
||||
: [],
|
||||
epg_match_ignore_custom: Array.isArray(epgSettings?.epg_match_ignore_custom)
|
||||
|
|
@ -34,11 +38,7 @@ const getEpgSettingsFromStore = (settings) => {
|
|||
};
|
||||
};
|
||||
|
||||
const EPGMatchModal = ({
|
||||
opened,
|
||||
onClose,
|
||||
selectedChannelIds = [],
|
||||
}) => {
|
||||
const EPGMatchModal = ({ opened, onClose, selectedChannelIds = [] }) => {
|
||||
const settings = useSettingsStore((s) => s.settings);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
|
@ -107,9 +107,10 @@ const EPGMatchModal = ({
|
|||
}
|
||||
};
|
||||
|
||||
const scopeText = selectedChannelIds.length > 0
|
||||
? `${selectedChannelIds.length} selected channel(s)`
|
||||
: 'all channels without EPG';
|
||||
const scopeText =
|
||||
selectedChannelIds.length > 0
|
||||
? `${selectedChannelIds.length} selected channel(s)`
|
||||
: 'all channels without EPG';
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
|
@ -191,8 +192,8 @@ const EPGMatchModal = ({
|
|||
/>
|
||||
|
||||
<Text size="xs" c="dimmed">
|
||||
Channel display names are never modified. These settings only affect
|
||||
the matching algorithm.
|
||||
Channel display names are never modified. These settings only
|
||||
affect the matching algorithm.
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,14 @@ vi.mock('../../../store/settings', () => ({
|
|||
vi.mock('@mantine/core', () => {
|
||||
const React = require('react');
|
||||
|
||||
const RadioComponent = ({ label, value, checked, description, groupValue, groupOnChange }) => {
|
||||
const RadioComponent = ({
|
||||
label,
|
||||
value,
|
||||
checked,
|
||||
description,
|
||||
groupValue,
|
||||
groupOnChange,
|
||||
}) => {
|
||||
const isChecked = checked !== undefined ? checked : groupValue === value;
|
||||
const handleChange = groupOnChange || (() => {});
|
||||
|
||||
|
|
@ -67,17 +74,26 @@ vi.mock('@mantine/core', () => {
|
|||
const enhancedChildren = React.Children.map(children, (child) => {
|
||||
if (React.isValidElement(child)) {
|
||||
// If it's a Stack or other container, recursively enhance its children
|
||||
if (child.type?.name === 'Stack' || child.props['data-testid'] === 'stack') {
|
||||
if (
|
||||
child.type?.name === 'Stack' ||
|
||||
child.props['data-testid'] === 'stack'
|
||||
) {
|
||||
return React.cloneElement(child, {
|
||||
children: React.Children.map(child.props.children, (nestedChild) => {
|
||||
if (React.isValidElement(nestedChild) && nestedChild.type === RadioComponent) {
|
||||
return React.cloneElement(nestedChild, {
|
||||
groupValue: value,
|
||||
groupOnChange: onChange,
|
||||
});
|
||||
children: React.Children.map(
|
||||
child.props.children,
|
||||
(nestedChild) => {
|
||||
if (
|
||||
React.isValidElement(nestedChild) &&
|
||||
nestedChild.type === RadioComponent
|
||||
) {
|
||||
return React.cloneElement(nestedChild, {
|
||||
groupValue: value,
|
||||
groupOnChange: onChange,
|
||||
});
|
||||
}
|
||||
return nestedChild;
|
||||
}
|
||||
return nestedChild;
|
||||
}),
|
||||
),
|
||||
});
|
||||
}
|
||||
// If it's a Radio component, inject props directly
|
||||
|
|
@ -157,7 +173,9 @@ describe('EPGMatchModal', () => {
|
|||
|
||||
it('should not show advanced fields in default mode', () => {
|
||||
render(<EPGMatchModal {...defaultProps} />);
|
||||
expect(screen.queryByLabelText('Ignore Prefixes')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByLabelText('Ignore Prefixes')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show advanced fields when advanced mode is selected', async () => {
|
||||
|
|
@ -169,7 +187,9 @@ describe('EPGMatchModal', () => {
|
|||
await waitFor(() => {
|
||||
expect(screen.getByLabelText('Ignore Prefixes')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Ignore Suffixes')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Ignore Custom Strings')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByLabelText('Ignore Custom Strings')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -199,7 +219,9 @@ describe('EPGMatchModal', () => {
|
|||
|
||||
describe('Form Submission', () => {
|
||||
it('should save mode and trigger auto-match', async () => {
|
||||
SettingsUtils.getChangedSettings.mockReturnValue({ epg_match_mode: 'default' });
|
||||
SettingsUtils.getChangedSettings.mockReturnValue({
|
||||
epg_match_mode: 'default',
|
||||
});
|
||||
SettingsUtils.saveChangedSettings.mockResolvedValue();
|
||||
API.matchEpg.mockResolvedValue();
|
||||
|
||||
|
|
@ -220,7 +242,9 @@ describe('EPGMatchModal', () => {
|
|||
SettingsUtils.getChangedSettings.mockReturnValue({});
|
||||
API.matchEpg.mockResolvedValue();
|
||||
|
||||
render(<EPGMatchModal {...defaultProps} selectedChannelIds={selectedIds} />);
|
||||
render(
|
||||
<EPGMatchModal {...defaultProps} selectedChannelIds={selectedIds} />
|
||||
);
|
||||
|
||||
const submitButton = screen.getByText('Start Auto-Match');
|
||||
fireEvent.click(submitButton);
|
||||
|
|
@ -232,7 +256,9 @@ describe('EPGMatchModal', () => {
|
|||
|
||||
it('should handle save errors gracefully', async () => {
|
||||
const error = new Error('Save failed');
|
||||
SettingsUtils.getChangedSettings.mockReturnValue({ epg_match_mode: 'default' });
|
||||
SettingsUtils.getChangedSettings.mockReturnValue({
|
||||
epg_match_mode: 'default',
|
||||
});
|
||||
SettingsUtils.saveChangedSettings.mockRejectedValue(error);
|
||||
|
||||
render(<EPGMatchModal {...defaultProps} />);
|
||||
|
|
@ -270,13 +296,23 @@ describe('EPGMatchModal', () => {
|
|||
|
||||
describe('UI Text', () => {
|
||||
it('should show correct text for selected channels', () => {
|
||||
render(<EPGMatchModal {...defaultProps} selectedChannelIds={[1, 2, 3]} />);
|
||||
expect(screen.getByText(/Match channels to EPG data for 3 selected channel\(s\)/)).toBeInTheDocument();
|
||||
render(
|
||||
<EPGMatchModal {...defaultProps} selectedChannelIds={[1, 2, 3]} />
|
||||
);
|
||||
expect(
|
||||
screen.getByText(
|
||||
/Match channels to EPG data for 3 selected channel\(s\)/
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show correct text for all channels', () => {
|
||||
render(<EPGMatchModal {...defaultProps} selectedChannelIds={[]} />);
|
||||
expect(screen.getByText(/Match channels to EPG data for all channels without EPG/)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
/Match channels to EPG data for all channels without EPG/
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,21 +13,21 @@
|
|||
|
||||
/* Active state styles */
|
||||
.navlink.navlink-active {
|
||||
color: #FFFFFF;
|
||||
color: #ffffff;
|
||||
background-color: #245043;
|
||||
border: 1px solid #3BA882;
|
||||
border: 1px solid #3ba882;
|
||||
}
|
||||
|
||||
/* Hover effect */
|
||||
.navlink:hover {
|
||||
background-color: #2A2F34; /* Gray hover effect when not active */
|
||||
border: 1px solid #3D3D42;
|
||||
background-color: #2a2f34; /* Gray hover effect when not active */
|
||||
border: 1px solid #3d3d42;
|
||||
}
|
||||
|
||||
/* Hover effect for active state */
|
||||
.navlink.navlink-active:hover {
|
||||
background-color: #3A3A40;
|
||||
border: 1px solid #3BA882;
|
||||
background-color: #3a3a40;
|
||||
border: 1px solid #3ba882;
|
||||
}
|
||||
|
||||
/* Collapse condition for justifyContent */
|
||||
|
|
|
|||
|
|
@ -180,7 +180,6 @@ const ChannelTableHeader = ({
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
const assignChannels = async () => {
|
||||
try {
|
||||
// Call our custom API endpoint
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ html {
|
|||
opacity: 0;
|
||||
}
|
||||
|
||||
*:hover>.resizer {
|
||||
*:hover > .resizer {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -104,7 +104,7 @@ html {
|
|||
|
||||
/* .table-striped .tbody .tr:nth-child(even), */
|
||||
.table-striped .tbody .tr-even {
|
||||
background-color: #27272A;
|
||||
background-color: #27272a;
|
||||
}
|
||||
|
||||
/* Style for rows with no streams */
|
||||
|
|
@ -138,7 +138,7 @@ html {
|
|||
/* Always allow text selection in editable elements */
|
||||
.shift-key-active input,
|
||||
.shift-key-active textarea,
|
||||
.shift-key-active [contenteditable="true"],
|
||||
.shift-key-active [contenteditable='true'],
|
||||
.shift-key-active .table-input-header input {
|
||||
user-select: text !important;
|
||||
-webkit-user-select: text !important;
|
||||
|
|
@ -169,7 +169,7 @@ html {
|
|||
/* Always allow text selection in inputs even when shift is pressed */
|
||||
.shift-key-active input,
|
||||
.shift-key-active textarea,
|
||||
.shift-key-active [contenteditable="true"],
|
||||
.shift-key-active [contenteditable='true'],
|
||||
.shift-key-active select,
|
||||
.shift-key-active .mantine-Select-input,
|
||||
.shift-key-active .mantine-MultiSelect-input,
|
||||
|
|
|
|||
|
|
@ -326,22 +326,31 @@ export const REGION_CHOICES = [
|
|||
|
||||
export const VOD_TYPES = {
|
||||
MOVIE: 'movie',
|
||||
EPISODE: 'episode'
|
||||
EPISODE: 'episode',
|
||||
};
|
||||
|
||||
export const VOD_FILTERS = {
|
||||
ALL: 'all',
|
||||
MOVIES: 'movies',
|
||||
SERIES: 'series'
|
||||
SERIES: 'series',
|
||||
};
|
||||
|
||||
export const VOD_SORT_OPTIONS = [
|
||||
{ value: 'name', label: 'Name' },
|
||||
{ value: 'year', label: 'Year' },
|
||||
{ value: 'created_at', label: 'Date Added' },
|
||||
{ value: 'rating', label: 'Rating' }
|
||||
{ value: 'rating', label: 'Rating' },
|
||||
];
|
||||
|
||||
export const CONTAINER_EXTENSIONS = [
|
||||
'mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'webm', 'm4v', 'ts', 'mpg'
|
||||
'mp4',
|
||||
'mkv',
|
||||
'avi',
|
||||
'mov',
|
||||
'wmv',
|
||||
'flv',
|
||||
'webm',
|
||||
'm4v',
|
||||
'ts',
|
||||
'mpg',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const localStorageMock = (() => {
|
|||
}),
|
||||
removeItem: vi.fn((key) => {
|
||||
delete store[key];
|
||||
})
|
||||
}),
|
||||
};
|
||||
})();
|
||||
|
||||
|
|
@ -32,7 +32,9 @@ describe('useLocalStorage', () => {
|
|||
});
|
||||
|
||||
it('should initialize with default value when localStorage is empty', () => {
|
||||
const { result } = renderHook(() => useLocalStorage('testKey', 'defaultValue'));
|
||||
const { result } = renderHook(() =>
|
||||
useLocalStorage('testKey', 'defaultValue')
|
||||
);
|
||||
|
||||
expect(result.current[0]).toBe('defaultValue');
|
||||
});
|
||||
|
|
@ -40,7 +42,9 @@ describe('useLocalStorage', () => {
|
|||
it('should initialize with value from localStorage if available', () => {
|
||||
localStorageMock.setItem('testKey', JSON.stringify('storedValue'));
|
||||
|
||||
const { result } = renderHook(() => useLocalStorage('testKey', 'defaultValue'));
|
||||
const { result } = renderHook(() =>
|
||||
useLocalStorage('testKey', 'defaultValue')
|
||||
);
|
||||
|
||||
expect(result.current[0]).toBe('storedValue');
|
||||
});
|
||||
|
|
@ -52,14 +56,19 @@ describe('useLocalStorage', () => {
|
|||
result.current[1]('updated');
|
||||
});
|
||||
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith('testKey', JSON.stringify('updated'));
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
'testKey',
|
||||
JSON.stringify('updated')
|
||||
);
|
||||
expect(result.current[0]).toBe('updated');
|
||||
});
|
||||
|
||||
it('should handle complex objects', () => {
|
||||
const complexObject = { name: 'test', count: 42, nested: { value: true } };
|
||||
|
||||
const { result } = renderHook(() => useLocalStorage('testKey', complexObject));
|
||||
const { result } = renderHook(() =>
|
||||
useLocalStorage('testKey', complexObject)
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current[1]({ name: 'updated', count: 100 });
|
||||
|
|
@ -73,7 +82,9 @@ describe('useLocalStorage', () => {
|
|||
throw new Error('Read error');
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useLocalStorage('testKey', 'defaultValue'));
|
||||
const { result } = renderHook(() =>
|
||||
useLocalStorage('testKey', 'defaultValue')
|
||||
);
|
||||
|
||||
expect(result.current[0]).toBe('defaultValue');
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
|
|
@ -102,7 +113,9 @@ describe('useLocalStorage', () => {
|
|||
it('should handle invalid JSON in localStorage', () => {
|
||||
localStorageMock.getItem.mockReturnValueOnce('invalid json{');
|
||||
|
||||
const { result } = renderHook(() => useLocalStorage('testKey', 'defaultValue'));
|
||||
const { result } = renderHook(() =>
|
||||
useLocalStorage('testKey', 'defaultValue')
|
||||
);
|
||||
|
||||
expect(result.current[0]).toBe('defaultValue');
|
||||
expect(console.error).toHaveBeenCalled();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { useLogoSelection, useChannelLogoSelection, useLogosById } from '../useSmartLogos';
|
||||
import {
|
||||
useLogoSelection,
|
||||
useChannelLogoSelection,
|
||||
useLogosById,
|
||||
} from '../useSmartLogos';
|
||||
import useLogosStore from '../../store/logos';
|
||||
|
||||
// Mock the logos store
|
||||
|
|
@ -13,10 +17,12 @@ describe('useSmartLogos', () => {
|
|||
});
|
||||
|
||||
it('should initialize with empty state', () => {
|
||||
useLogosStore.mockImplementation((selector) => selector({
|
||||
logos: {},
|
||||
fetchLogos: vi.fn(),
|
||||
}));
|
||||
useLogosStore.mockImplementation((selector) =>
|
||||
selector({
|
||||
logos: {},
|
||||
fetchLogos: vi.fn(),
|
||||
})
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useLogoSelection());
|
||||
|
||||
|
|
@ -27,10 +33,12 @@ describe('useSmartLogos', () => {
|
|||
|
||||
it('should load logos when ensureLogosLoaded is called', async () => {
|
||||
const mockFetchLogos = vi.fn().mockResolvedValue();
|
||||
useLogosStore.mockImplementation((selector) => selector({
|
||||
logos: {},
|
||||
fetchLogos: mockFetchLogos,
|
||||
}));
|
||||
useLogosStore.mockImplementation((selector) =>
|
||||
selector({
|
||||
logos: {},
|
||||
fetchLogos: mockFetchLogos,
|
||||
})
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useLogoSelection());
|
||||
|
||||
|
|
@ -43,10 +51,12 @@ describe('useSmartLogos', () => {
|
|||
|
||||
it('should not reload logos if already loaded', async () => {
|
||||
const mockFetchLogos = vi.fn().mockResolvedValue();
|
||||
useLogosStore.mockImplementation((selector) => selector({
|
||||
logos: { logo1: { id: 'logo1' } },
|
||||
fetchLogos: mockFetchLogos,
|
||||
}));
|
||||
useLogosStore.mockImplementation((selector) =>
|
||||
selector({
|
||||
logos: { logo1: { id: 'logo1' } },
|
||||
fetchLogos: mockFetchLogos,
|
||||
})
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useLogoSelection());
|
||||
|
||||
|
|
@ -62,12 +72,18 @@ describe('useSmartLogos', () => {
|
|||
});
|
||||
|
||||
it('should handle errors when fetching logos', async () => {
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const mockFetchLogos = vi.fn().mockRejectedValue(new Error('Fetch failed'));
|
||||
useLogosStore.mockImplementation((selector) => selector({
|
||||
logos: {},
|
||||
fetchLogos: mockFetchLogos,
|
||||
}));
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
const mockFetchLogos = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('Fetch failed'));
|
||||
useLogosStore.mockImplementation((selector) =>
|
||||
selector({
|
||||
logos: {},
|
||||
fetchLogos: mockFetchLogos,
|
||||
})
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useLogoSelection());
|
||||
|
||||
|
|
@ -84,10 +100,12 @@ describe('useSmartLogos', () => {
|
|||
});
|
||||
|
||||
it('should indicate hasLogos when logos are present', () => {
|
||||
useLogosStore.mockImplementation((selector) => selector({
|
||||
logos: { logo1: { id: 'logo1' }, logo2: { id: 'logo2' } },
|
||||
fetchLogos: vi.fn(),
|
||||
}));
|
||||
useLogosStore.mockImplementation((selector) =>
|
||||
selector({
|
||||
logos: { logo1: { id: 'logo1' }, logo2: { id: 'logo2' } },
|
||||
fetchLogos: vi.fn(),
|
||||
})
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useLogoSelection());
|
||||
|
||||
|
|
@ -101,12 +119,14 @@ describe('useSmartLogos', () => {
|
|||
});
|
||||
|
||||
it('should initialize with channel logos state', () => {
|
||||
useLogosStore.mockImplementation((selector) => selector({
|
||||
channelLogos: {},
|
||||
hasLoadedChannelLogos: false,
|
||||
backgroundLoading: false,
|
||||
fetchChannelAssignableLogos: vi.fn(),
|
||||
}));
|
||||
useLogosStore.mockImplementation((selector) =>
|
||||
selector({
|
||||
channelLogos: {},
|
||||
hasLoadedChannelLogos: false,
|
||||
backgroundLoading: false,
|
||||
fetchChannelAssignableLogos: vi.fn(),
|
||||
})
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useChannelLogoSelection());
|
||||
|
||||
|
|
@ -117,12 +137,14 @@ describe('useSmartLogos', () => {
|
|||
|
||||
it('should load channel logos when ensureLogosLoaded is called', async () => {
|
||||
const mockFetchChannelLogos = vi.fn().mockResolvedValue();
|
||||
useLogosStore.mockImplementation((selector) => selector({
|
||||
channelLogos: {},
|
||||
hasLoadedChannelLogos: false,
|
||||
backgroundLoading: false,
|
||||
fetchChannelAssignableLogos: mockFetchChannelLogos,
|
||||
}));
|
||||
useLogosStore.mockImplementation((selector) =>
|
||||
selector({
|
||||
channelLogos: {},
|
||||
hasLoadedChannelLogos: false,
|
||||
backgroundLoading: false,
|
||||
fetchChannelAssignableLogos: mockFetchChannelLogos,
|
||||
})
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useChannelLogoSelection());
|
||||
|
||||
|
|
@ -135,12 +157,14 @@ describe('useSmartLogos', () => {
|
|||
|
||||
it('should not reload if already loaded', async () => {
|
||||
const mockFetchChannelLogos = vi.fn().mockResolvedValue();
|
||||
useLogosStore.mockImplementation((selector) => selector({
|
||||
channelLogos: { logo1: { id: 'logo1' } },
|
||||
hasLoadedChannelLogos: true,
|
||||
backgroundLoading: false,
|
||||
fetchChannelAssignableLogos: mockFetchChannelLogos,
|
||||
}));
|
||||
useLogosStore.mockImplementation((selector) =>
|
||||
selector({
|
||||
channelLogos: { logo1: { id: 'logo1' } },
|
||||
hasLoadedChannelLogos: true,
|
||||
backgroundLoading: false,
|
||||
fetchChannelAssignableLogos: mockFetchChannelLogos,
|
||||
})
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useChannelLogoSelection());
|
||||
|
||||
|
|
@ -153,12 +177,14 @@ describe('useSmartLogos', () => {
|
|||
|
||||
it('should not load if backgroundLoading is true', async () => {
|
||||
const mockFetchChannelLogos = vi.fn().mockResolvedValue();
|
||||
useLogosStore.mockImplementation((selector) => selector({
|
||||
channelLogos: {},
|
||||
hasLoadedChannelLogos: false,
|
||||
backgroundLoading: true,
|
||||
fetchChannelAssignableLogos: mockFetchChannelLogos,
|
||||
}));
|
||||
useLogosStore.mockImplementation((selector) =>
|
||||
selector({
|
||||
channelLogos: {},
|
||||
hasLoadedChannelLogos: false,
|
||||
backgroundLoading: true,
|
||||
fetchChannelAssignableLogos: mockFetchChannelLogos,
|
||||
})
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useChannelLogoSelection());
|
||||
|
||||
|
|
@ -170,14 +196,20 @@ describe('useSmartLogos', () => {
|
|||
});
|
||||
|
||||
it('should handle errors when fetching channel logos', async () => {
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const mockFetchChannelLogos = vi.fn().mockRejectedValue(new Error('Fetch failed'));
|
||||
useLogosStore.mockImplementation((selector) => selector({
|
||||
channelLogos: {},
|
||||
hasLoadedChannelLogos: false,
|
||||
backgroundLoading: false,
|
||||
fetchChannelAssignableLogos: mockFetchChannelLogos,
|
||||
}));
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
const mockFetchChannelLogos = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('Fetch failed'));
|
||||
useLogosStore.mockImplementation((selector) =>
|
||||
selector({
|
||||
channelLogos: {},
|
||||
hasLoadedChannelLogos: false,
|
||||
backgroundLoading: false,
|
||||
fetchChannelAssignableLogos: mockFetchChannelLogos,
|
||||
})
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useChannelLogoSelection());
|
||||
|
||||
|
|
@ -200,10 +232,12 @@ describe('useSmartLogos', () => {
|
|||
});
|
||||
|
||||
it('should initialize with empty logos', () => {
|
||||
useLogosStore.mockImplementation((selector) => selector({
|
||||
logos: {},
|
||||
fetchLogosByIds: vi.fn(),
|
||||
}));
|
||||
useLogosStore.mockImplementation((selector) =>
|
||||
selector({
|
||||
logos: {},
|
||||
fetchLogosByIds: vi.fn(),
|
||||
})
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useLogosById([]));
|
||||
|
||||
|
|
@ -214,10 +248,12 @@ describe('useSmartLogos', () => {
|
|||
|
||||
it('should fetch missing logos by IDs', async () => {
|
||||
const mockFetchLogosByIds = vi.fn().mockResolvedValue();
|
||||
useLogosStore.mockImplementation((selector) => selector({
|
||||
logos: {},
|
||||
fetchLogosByIds: mockFetchLogosByIds,
|
||||
}));
|
||||
useLogosStore.mockImplementation((selector) =>
|
||||
selector({
|
||||
logos: {},
|
||||
fetchLogosByIds: mockFetchLogosByIds,
|
||||
})
|
||||
);
|
||||
|
||||
renderHook(() => useLogosById(['logo1', 'logo2']));
|
||||
|
||||
|
|
@ -228,10 +264,12 @@ describe('useSmartLogos', () => {
|
|||
|
||||
it('should not fetch logos that are already loaded', async () => {
|
||||
const mockFetchLogosByIds = vi.fn().mockResolvedValue();
|
||||
useLogosStore.mockImplementation((selector) => selector({
|
||||
logos: { logo1: { id: 'logo1' } },
|
||||
fetchLogosByIds: mockFetchLogosByIds,
|
||||
}));
|
||||
useLogosStore.mockImplementation((selector) =>
|
||||
selector({
|
||||
logos: { logo1: { id: 'logo1' } },
|
||||
fetchLogosByIds: mockFetchLogosByIds,
|
||||
})
|
||||
);
|
||||
|
||||
renderHook(() => useLogosById(['logo1', 'logo2']));
|
||||
|
||||
|
|
@ -241,12 +279,18 @@ describe('useSmartLogos', () => {
|
|||
});
|
||||
|
||||
it('should handle errors when fetching logos by IDs', async () => {
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const mockFetchLogosByIds = vi.fn().mockRejectedValue(new Error('Fetch failed'));
|
||||
useLogosStore.mockImplementation((selector) => selector({
|
||||
logos: {},
|
||||
fetchLogosByIds: mockFetchLogosByIds,
|
||||
}));
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
const mockFetchLogosByIds = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('Fetch failed'));
|
||||
useLogosStore.mockImplementation((selector) =>
|
||||
selector({
|
||||
logos: {},
|
||||
fetchLogosByIds: mockFetchLogosByIds,
|
||||
})
|
||||
);
|
||||
|
||||
renderHook(() => useLogosById(['logo1']));
|
||||
|
||||
|
|
@ -262,10 +306,12 @@ describe('useSmartLogos', () => {
|
|||
|
||||
it('should filter out null/undefined IDs', async () => {
|
||||
const mockFetchLogosByIds = vi.fn().mockResolvedValue();
|
||||
useLogosStore.mockImplementation((selector) => selector({
|
||||
logos: {},
|
||||
fetchLogosByIds: mockFetchLogosByIds,
|
||||
}));
|
||||
useLogosStore.mockImplementation((selector) =>
|
||||
selector({
|
||||
logos: {},
|
||||
fetchLogosByIds: mockFetchLogosByIds,
|
||||
})
|
||||
);
|
||||
|
||||
renderHook(() => useLogosById(['logo1', null, undefined, 'logo2']));
|
||||
|
||||
|
|
@ -276,10 +322,12 @@ describe('useSmartLogos', () => {
|
|||
|
||||
it('should not refetch the same IDs multiple times', async () => {
|
||||
const mockFetchLogosByIds = vi.fn().mockResolvedValue();
|
||||
useLogosStore.mockImplementation((selector) => selector({
|
||||
logos: {},
|
||||
fetchLogosByIds: mockFetchLogosByIds,
|
||||
}));
|
||||
useLogosStore.mockImplementation((selector) =>
|
||||
selector({
|
||||
logos: {},
|
||||
fetchLogosByIds: mockFetchLogosByIds,
|
||||
})
|
||||
);
|
||||
|
||||
const { rerender } = renderHook(() => useLogosById(['logo1']));
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const localStorageMock = (() => {
|
|||
}),
|
||||
removeItem: vi.fn((key) => {
|
||||
delete store[key];
|
||||
})
|
||||
}),
|
||||
};
|
||||
})();
|
||||
|
||||
|
|
@ -50,7 +50,10 @@ describe('useTablePreferences', () => {
|
|||
});
|
||||
|
||||
it('should initialize headerPinned from localStorage', () => {
|
||||
localStorageMock.setItem('table-preferences', JSON.stringify({ headerPinned: true }));
|
||||
localStorageMock.setItem(
|
||||
'table-preferences',
|
||||
JSON.stringify({ headerPinned: true })
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useTablePreferences());
|
||||
|
||||
|
|
@ -58,7 +61,10 @@ describe('useTablePreferences', () => {
|
|||
});
|
||||
|
||||
it('should initialize tableSize from localStorage', () => {
|
||||
localStorageMock.setItem('table-preferences', JSON.stringify({ tableSize: 'compact' }));
|
||||
localStorageMock.setItem(
|
||||
'table-preferences',
|
||||
JSON.stringify({ tableSize: 'compact' })
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useTablePreferences());
|
||||
|
||||
|
|
@ -66,7 +72,10 @@ describe('useTablePreferences', () => {
|
|||
});
|
||||
|
||||
it('should initialize both preferences from localStorage', () => {
|
||||
localStorageMock.setItem('table-preferences', JSON.stringify({ headerPinned: true, tableSize: 'comfortable' }));
|
||||
localStorageMock.setItem(
|
||||
'table-preferences',
|
||||
JSON.stringify({ headerPinned: true, tableSize: 'comfortable' })
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useTablePreferences());
|
||||
|
||||
|
|
@ -83,7 +92,10 @@ describe('useTablePreferences', () => {
|
|||
});
|
||||
|
||||
it('should prefer new localStorage location over old location', () => {
|
||||
localStorageMock.setItem('table-preferences', JSON.stringify({ tableSize: 'comfortable' }));
|
||||
localStorageMock.setItem(
|
||||
'table-preferences',
|
||||
JSON.stringify({ tableSize: 'comfortable' })
|
||||
);
|
||||
localStorageMock.setItem('table-size', JSON.stringify('compact'));
|
||||
|
||||
const { result } = renderHook(() => useTablePreferences());
|
||||
|
|
@ -127,7 +139,10 @@ describe('useTablePreferences', () => {
|
|||
});
|
||||
|
||||
it('should preserve existing preferences when updating headerPinned', () => {
|
||||
localStorageMock.setItem('table-preferences', JSON.stringify({ tableSize: 'compact' }));
|
||||
localStorageMock.setItem(
|
||||
'table-preferences',
|
||||
JSON.stringify({ tableSize: 'compact' })
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useTablePreferences());
|
||||
|
||||
|
|
@ -205,7 +220,10 @@ describe('useTablePreferences', () => {
|
|||
});
|
||||
|
||||
it('should preserve existing preferences when updating tableSize', () => {
|
||||
localStorageMock.setItem('table-preferences', JSON.stringify({ headerPinned: true }));
|
||||
localStorageMock.setItem(
|
||||
'table-preferences',
|
||||
JSON.stringify({ headerPinned: true })
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useTablePreferences());
|
||||
|
||||
|
|
@ -320,7 +338,10 @@ describe('useTablePreferences', () => {
|
|||
});
|
||||
|
||||
it('should not update if value is the same', () => {
|
||||
localStorageMock.setItem('table-preferences', JSON.stringify({ headerPinned: true }));
|
||||
localStorageMock.setItem(
|
||||
'table-preferences',
|
||||
JSON.stringify({ headerPinned: true })
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useTablePreferences());
|
||||
const initialHeaderPinned = result.current.headerPinned;
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@
|
|||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
|
||||
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: #18181b;
|
||||
|
|
@ -18,8 +18,8 @@ body {
|
|||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
font-family:
|
||||
source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* Example scrollbars - optional, to match a dark theme. */
|
||||
|
|
@ -28,7 +28,7 @@ code {
|
|||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #3B3C41;
|
||||
background: #3b3c41;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
|
|
@ -45,7 +45,9 @@ code {
|
|||
}
|
||||
}
|
||||
|
||||
table.mrt-table tr.mantine-Table-tr.mantine-Table-tr-detail-panel td.mantine-Table-td-detail-panel {
|
||||
table.mrt-table
|
||||
tr.mantine-Table-tr.mantine-Table-tr-detail-panel
|
||||
td.mantine-Table-td-detail-panel {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
|
|
@ -71,7 +73,7 @@ table.mrt-table tr.mantine-Table-tr.mantine-Table-tr-detail-panel td.mantine-Tab
|
|||
|
||||
/* Create a short vertical bar */
|
||||
.sash.sash-vertical::before {
|
||||
content: "";
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
|
|
@ -103,7 +105,7 @@ table.mrt-table tr.mantine-Table-tr.mantine-Table-tr-detail-panel td.mantine-Tab
|
|||
}
|
||||
|
||||
/* For elements that specifically need transforms (like draggable elements) */
|
||||
[data-draggable="true"] {
|
||||
[data-draggable='true'] {
|
||||
transform: translate3d(0, 0, 0) !important;
|
||||
}
|
||||
|
||||
|
|
@ -137,4 +139,4 @@ table.mrt-table tr.mantine-Table-tr.mantine-Table-tr-detail-panel td.mantine-Tab
|
|||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,6 @@ import App from './App';
|
|||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import useUserAgentsStore from '../store/userAgents';
|
|||
import M3UsTable from '../components/tables/M3UsTable';
|
||||
import EPGsTable from '../components/tables/EPGsTable';
|
||||
import { Box, Stack } from '@mantine/core';
|
||||
import ErrorBoundary from '../components/ErrorBoundary'
|
||||
import ErrorBoundary from '../components/ErrorBoundary';
|
||||
|
||||
const PageContent = () => {
|
||||
const error = useUserAgentsStore((state) => state.error);
|
||||
|
|
@ -28,14 +28,14 @@ const PageContent = () => {
|
|||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const M3UPage = () => {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<PageContent/>
|
||||
<PageContent />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default M3UPage;
|
||||
|
|
|
|||
|
|
@ -10,24 +10,23 @@ import {
|
|||
Title,
|
||||
useMantineTheme,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
SquarePlus,
|
||||
} from 'lucide-react';
|
||||
import { SquarePlus } from 'lucide-react';
|
||||
import useChannelsStore from '../store/channels';
|
||||
import useSettingsStore from '../store/settings';
|
||||
import useVideoStore from '../store/useVideoStore';
|
||||
import RecordingForm from '../components/forms/Recording';
|
||||
import {
|
||||
isAfter,
|
||||
isBefore,
|
||||
useTimeHelpers,
|
||||
} from '../utils/dateTimeUtils.js';
|
||||
const RecordingDetailsModal = lazy(() =>
|
||||
import('../components/forms/RecordingDetailsModal'));
|
||||
import { isAfter, isBefore, useTimeHelpers } from '../utils/dateTimeUtils.js';
|
||||
const RecordingDetailsModal = lazy(
|
||||
() => import('../components/forms/RecordingDetailsModal')
|
||||
);
|
||||
import RecurringRuleModal from '../components/forms/RecurringRuleModal.jsx';
|
||||
import RecordingCard from '../components/cards/RecordingCard.jsx';
|
||||
import { categorizeRecordings } from '../utils/pages/DVRUtils.js';
|
||||
import { getPosterUrl, getRecordingUrl, getShowVideoUrl } from '../utils/cards/RecordingCardUtils.js';
|
||||
import {
|
||||
getPosterUrl,
|
||||
getRecordingUrl,
|
||||
getShowVideoUrl,
|
||||
} from '../utils/cards/RecordingCardUtils.js';
|
||||
import ErrorBoundary from '../components/ErrorBoundary.jsx';
|
||||
|
||||
const RecordingList = ({ list, onOpenDetails, onOpenRecurring }) => {
|
||||
|
|
@ -113,32 +112,35 @@ const DVRPage = () => {
|
|||
const now = userNow();
|
||||
const s = toUserTime(rec.start_time);
|
||||
const e = toUserTime(rec.end_time);
|
||||
if(isAfter(now, s) && isBefore(now, e)) {
|
||||
if (isAfter(now, s) && isBefore(now, e)) {
|
||||
// call into child RecordingCard behavior by constructing a URL like there
|
||||
const channel = channels[rec.channel];
|
||||
if (!channel) return;
|
||||
const url = getShowVideoUrl(channel, useSettingsStore.getState().environment.env_mode);
|
||||
const url = getShowVideoUrl(
|
||||
channel,
|
||||
useSettingsStore.getState().environment.env_mode
|
||||
);
|
||||
useVideoStore.getState().showVideo(url, 'live');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnWatchRecording = () => {
|
||||
const url = getRecordingUrl(
|
||||
detailsRecording.custom_properties, useSettingsStore.getState().environment.env_mode);
|
||||
if(!url) return;
|
||||
detailsRecording.custom_properties,
|
||||
useSettingsStore.getState().environment.env_mode
|
||||
);
|
||||
if (!url) return;
|
||||
useVideoStore.getState().showVideo(url, 'vod', {
|
||||
name:
|
||||
detailsRecording.custom_properties?.program?.title ||
|
||||
'Recording',
|
||||
name: detailsRecording.custom_properties?.program?.title || 'Recording',
|
||||
logo: {
|
||||
url: getPosterUrl(
|
||||
detailsRecording.custom_properties?.poster_logo_id,
|
||||
undefined,
|
||||
channels[detailsRecording.channel]?.logo?.cache_url
|
||||
)
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Box p={10}>
|
||||
<Button
|
||||
|
|
@ -170,11 +172,13 @@ const DVRPage = () => {
|
|||
{ maxWidth: '36rem', cols: 1 },
|
||||
]}
|
||||
>
|
||||
{<RecordingList
|
||||
list={inProgress}
|
||||
onOpenDetails={openDetails}
|
||||
onOpenRecurring={openRuleModal}
|
||||
/>}
|
||||
{
|
||||
<RecordingList
|
||||
list={inProgress}
|
||||
onOpenDetails={openDetails}
|
||||
onOpenRecurring={openRuleModal}
|
||||
/>
|
||||
}
|
||||
{inProgress.length === 0 && (
|
||||
<Text size="sm" c="dimmed">
|
||||
Nothing recording right now.
|
||||
|
|
@ -196,11 +200,13 @@ const DVRPage = () => {
|
|||
{ maxWidth: '36rem', cols: 1 },
|
||||
]}
|
||||
>
|
||||
{<RecordingList
|
||||
list={upcoming}
|
||||
onOpenDetails={openDetails}
|
||||
onOpenRecurring={openRuleModal}
|
||||
/>}
|
||||
{
|
||||
<RecordingList
|
||||
list={upcoming}
|
||||
onOpenDetails={openDetails}
|
||||
onOpenRecurring={openRuleModal}
|
||||
/>
|
||||
}
|
||||
{upcoming.length === 0 && (
|
||||
<Text size="sm" c="dimmed">
|
||||
No upcoming recordings.
|
||||
|
|
@ -222,11 +228,13 @@ const DVRPage = () => {
|
|||
{ maxWidth: '36rem', cols: 1 },
|
||||
]}
|
||||
>
|
||||
{<RecordingList
|
||||
list={completed}
|
||||
onOpenDetails={openDetails}
|
||||
onOpenRecurring={openRuleModal}
|
||||
/>}
|
||||
{
|
||||
<RecordingList
|
||||
list={completed}
|
||||
onOpenDetails={openDetails}
|
||||
onOpenRecurring={openRuleModal}
|
||||
/>
|
||||
}
|
||||
{completed.length === 0 && (
|
||||
<Text size="sm" c="dimmed">
|
||||
No completed recordings yet.
|
||||
|
|
@ -286,4 +294,4 @@ const DVRPage = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default DVRPage;
|
||||
export default DVRPage;
|
||||
|
|
|
|||
|
|
@ -7,12 +7,11 @@ import VODLogosTable from '../components/tables/VODLogosTable';
|
|||
import { showNotification } from '../utils/notificationUtils.js';
|
||||
|
||||
const LogosPage = () => {
|
||||
const logos = useLogosStore(s => s.logos);
|
||||
const totalCount = useVODLogosStore(s => s.totalCount);
|
||||
const logos = useLogosStore((s) => s.logos);
|
||||
const totalCount = useVODLogosStore((s) => s.totalCount);
|
||||
const [activeTab, setActiveTab] = useState('channel');
|
||||
const logoCount = activeTab === 'channel'
|
||||
? Object.keys(logos).length
|
||||
: totalCount;
|
||||
const logoCount =
|
||||
activeTab === 'channel' ? Object.keys(logos).length : totalCount;
|
||||
|
||||
const loadChannelLogos = useCallback(async () => {
|
||||
try {
|
||||
|
|
@ -38,11 +37,7 @@ const LogosPage = () => {
|
|||
return (
|
||||
<Box>
|
||||
{/* Header with title and tabs */}
|
||||
<Box
|
||||
style={{ justifyContent: 'center' }}
|
||||
display={'flex'}
|
||||
p={'10px 0'}
|
||||
>
|
||||
<Box style={{ justifyContent: 'center' }} display={'flex'} p={'10px 0'}>
|
||||
<Flex
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
|
|
@ -58,7 +53,7 @@ const LogosPage = () => {
|
|||
fz={'20px'}
|
||||
fw={500}
|
||||
lh={1}
|
||||
c='white'
|
||||
c="white"
|
||||
mb={0}
|
||||
lts={'-0.3px'}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,10 @@ import {
|
|||
Text,
|
||||
} from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { showNotification, updateNotification, } from '../utils/notificationUtils.js';
|
||||
import {
|
||||
showNotification,
|
||||
updateNotification,
|
||||
} from '../utils/notificationUtils.js';
|
||||
import { usePluginStore } from '../store/plugins.jsx';
|
||||
import {
|
||||
deletePluginByKey,
|
||||
|
|
@ -34,8 +37,9 @@ import {
|
|||
} from '../utils/pages/PluginsUtils.js';
|
||||
import { RefreshCcw } from 'lucide-react';
|
||||
import ErrorBoundary from '../components/ErrorBoundary.jsx';
|
||||
const PluginCard = React.lazy(() =>
|
||||
import('../components/cards/PluginCard.jsx'));
|
||||
const PluginCard = React.lazy(
|
||||
() => import('../components/cards/PluginCard.jsx')
|
||||
);
|
||||
|
||||
const PluginsList = ({ onRequestDelete, onRequireTrust, onRequestConfirm }) => {
|
||||
const plugins = usePluginStore((state) => state.plugins);
|
||||
|
|
@ -68,7 +72,7 @@ const PluginsList = ({ onRequestDelete, onRequireTrust, onRequestConfirm }) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
{plugins.length > 0 &&
|
||||
{plugins.length > 0 && (
|
||||
<SimpleGrid
|
||||
cols={2}
|
||||
spacing="md"
|
||||
|
|
@ -91,13 +95,13 @@ const PluginsList = ({ onRequestDelete, onRequireTrust, onRequestConfirm }) => {
|
|||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</SimpleGrid>
|
||||
}
|
||||
)}
|
||||
|
||||
{plugins.length === 0 && (
|
||||
<Box>
|
||||
<Text c="dimmed">
|
||||
No plugins found. Drop a plugin into <code>/data/plugins</code>{' '}
|
||||
and reload.
|
||||
No plugins found. Drop a plugin into <code>/data/plugins</code> and
|
||||
reload.
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
|
@ -255,12 +259,15 @@ export default function PluginsPage() {
|
|||
};
|
||||
};
|
||||
|
||||
const handleConfirm = useCallback((confirmed) => {
|
||||
const resolver = confirmConfig.resolve;
|
||||
setConfirmOpen(false);
|
||||
setConfirmConfig({ title: '', message: '', resolve: null });
|
||||
if (resolver) resolver(confirmed);
|
||||
}, [confirmConfig.resolve]);
|
||||
const handleConfirm = useCallback(
|
||||
(confirmed) => {
|
||||
const resolver = confirmConfig.resolve;
|
||||
setConfirmOpen(false);
|
||||
setConfirmConfig({ title: '', message: '', resolve: null });
|
||||
if (resolver) resolver(confirmed);
|
||||
},
|
||||
[confirmConfig.resolve]
|
||||
);
|
||||
|
||||
return (
|
||||
<AppShellMain p={16}>
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ const PageContent = () => {
|
|||
<UsersTable />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const UsersPage = () => {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<PageContent/>
|
||||
<PageContent />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import ChannelsPage from '../Channels';
|
|||
vi.mock('../../store/auth');
|
||||
vi.mock('../../hooks/useLocalStorage');
|
||||
vi.mock('../../components/tables/ChannelsTable', () => ({
|
||||
default: () => <div data-testid="channels-table">ChannelsTable</div>
|
||||
default: () => <div data-testid="channels-table">ChannelsTable</div>,
|
||||
}));
|
||||
vi.mock('../../components/tables/StreamsTable', () => ({
|
||||
default: () => <div data-testid="streams-table">StreamsTable</div>
|
||||
default: () => <div data-testid="streams-table">StreamsTable</div>,
|
||||
}));
|
||||
vi.mock('@mantine/core', () => ({
|
||||
Box: ({ children, ...props }) => <div {...props}>{children}</div>,
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ import useUserAgentsStore from '../../store/userAgents';
|
|||
|
||||
vi.mock('../../store/userAgents');
|
||||
vi.mock('../../components/tables/M3UsTable', () => ({
|
||||
default: () => <div data-testid="m3us-table">M3UsTable</div>
|
||||
default: () => <div data-testid="m3us-table">M3UsTable</div>,
|
||||
}));
|
||||
vi.mock('../../components/tables/EPGsTable', () => ({
|
||||
default: () => <div data-testid="epgs-table">EPGsTable</div>
|
||||
default: () => <div data-testid="epgs-table">EPGsTable</div>,
|
||||
}));
|
||||
vi.mock('@mantine/core', () => ({
|
||||
Box: ({ children, ...props }) => <div {...props}>{children}</div>,
|
||||
|
|
@ -30,4 +30,4 @@ describe('ContentSourcesPage', () => {
|
|||
expect(screen.getByTestId('m3us-table')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('epgs-table')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ import useAuthStore from '../../store/auth';
|
|||
|
||||
vi.mock('../../store/auth');
|
||||
vi.mock('../../components/forms/LoginForm', () => ({
|
||||
default: () => <div data-testid="login-form">LoginForm</div>
|
||||
default: () => <div data-testid="login-form">LoginForm</div>,
|
||||
}));
|
||||
vi.mock('../../components/forms/SuperuserForm', () => ({
|
||||
default: () => <div data-testid="superuser-form">SuperuserForm</div>
|
||||
default: () => <div data-testid="superuser-form">SuperuserForm</div>,
|
||||
}));
|
||||
vi.mock('@mantine/core', () => ({
|
||||
Text: ({ children }) => <div>{children}</div>,
|
||||
|
|
@ -18,7 +18,7 @@ describe('Login', () => {
|
|||
it('renders SuperuserForm when superuser does not exist', async () => {
|
||||
useAuthStore.mockReturnValue(false);
|
||||
|
||||
render(<Login/>);
|
||||
render(<Login />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('superuser-form')).toBeInTheDocument();
|
||||
|
|
@ -29,7 +29,7 @@ describe('Login', () => {
|
|||
it('renders LoginForm when superuser exists', () => {
|
||||
useAuthStore.mockReturnValue(true);
|
||||
|
||||
render(<Login/>);
|
||||
render(<Login />);
|
||||
|
||||
expect(screen.getByTestId('login-form')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('superuser-form')).not.toBeInTheDocument();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|||
import LogosPage from '../Logos';
|
||||
import useLogosStore from '../../store/logos';
|
||||
import useVODLogosStore from '../../store/vodLogos';
|
||||
import { showNotification, updateNotification } from '../../utils/notificationUtils.js';
|
||||
import {
|
||||
showNotification,
|
||||
updateNotification,
|
||||
} from '../../utils/notificationUtils.js';
|
||||
|
||||
vi.mock('../../store/logos');
|
||||
vi.mock('../../store/vodLogos');
|
||||
|
|
@ -12,16 +15,21 @@ vi.mock('../../utils/notificationUtils.js', () => ({
|
|||
updateNotification: vi.fn(),
|
||||
}));
|
||||
vi.mock('../../components/tables/LogosTable', () => ({
|
||||
default: () => <div data-testid="logos-table">LogosTable</div>
|
||||
default: () => <div data-testid="logos-table">LogosTable</div>,
|
||||
}));
|
||||
vi.mock('../../components/tables/VODLogosTable', () => ({
|
||||
default: () => <div data-testid="vod-logos-table">VODLogosTable</div>
|
||||
default: () => <div data-testid="vod-logos-table">VODLogosTable</div>,
|
||||
}));
|
||||
vi.mock('@mantine/core', () => {
|
||||
const tabsComponent = ({ children, value, onChange }) =>
|
||||
<div data-testid="tabs" data-value={value} onClick={() => onChange('vod')}>{children}</div>;
|
||||
const tabsComponent = ({ children, value, onChange }) => (
|
||||
<div data-testid="tabs" data-value={value} onClick={() => onChange('vod')}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
tabsComponent.List = ({ children }) => <div>{children}</div>;
|
||||
tabsComponent.Tab = ({ children, value }) => <button data-value={value}>{children}</button>;
|
||||
tabsComponent.Tab = ({ children, value }) => (
|
||||
<button data-value={value}>{children}</button>
|
||||
);
|
||||
|
||||
return {
|
||||
Box: ({ children, ...props }) => <div {...props}>{children}</div>,
|
||||
|
|
@ -113,7 +121,9 @@ describe('LogosPage', () => {
|
|||
});
|
||||
|
||||
it('shows error notification when fetching logos fails', async () => {
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
const error = new Error('Failed to fetch');
|
||||
mockFetchAllLogos.mockRejectedValue(error);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import PluginsPage from '../Plugins';
|
||||
import { showNotification, updateNotification } from '../../utils/notificationUtils.js';
|
||||
import {
|
||||
showNotification,
|
||||
updateNotification,
|
||||
} from '../../utils/notificationUtils.js';
|
||||
import {
|
||||
deletePluginByKey,
|
||||
importPlugin,
|
||||
|
|
@ -47,7 +50,16 @@ vi.mock('@mantine/core', async () => {
|
|||
{children}
|
||||
</span>
|
||||
),
|
||||
Button: ({ children, onClick, leftSection, variant, color, loading, disabled, fullWidth }) => (
|
||||
Button: ({
|
||||
children,
|
||||
onClick,
|
||||
leftSection,
|
||||
variant,
|
||||
color,
|
||||
loading,
|
||||
disabled,
|
||||
fullWidth,
|
||||
}) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={loading || disabled}
|
||||
|
|
@ -73,13 +85,16 @@ vi.mock('@mantine/core', async () => {
|
|||
),
|
||||
Divider: ({ my }) => <hr data-my={my} />,
|
||||
ActionIcon: ({ children, onClick, color, variant, title }) => (
|
||||
<button onClick={onClick} data-color={color} data-variant={variant} title={title}>
|
||||
<button
|
||||
onClick={onClick}
|
||||
data-color={color}
|
||||
data-variant={variant}
|
||||
title={title}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
SimpleGrid: ({ children, cols }) => (
|
||||
<div data-cols={cols}>{children}</div>
|
||||
),
|
||||
SimpleGrid: ({ children, cols }) => <div data-cols={cols}>{children}</div>,
|
||||
Modal: ({ opened, onClose, title, children, size, centered }) =>
|
||||
opened ? (
|
||||
<div data-testid="modal" data-size={size} data-centered={centered}>
|
||||
|
|
@ -109,7 +124,9 @@ vi.mock('@mantine/dropzone', () => ({
|
|||
data-accept={accept}
|
||||
data-max-size={maxSize}
|
||||
onClick={() => {
|
||||
const file = new File(['content'], 'plugin.zip', { type: 'application/zip' });
|
||||
const file = new File(['content'], 'plugin.zip', {
|
||||
type: 'application/zip',
|
||||
});
|
||||
onDrop([file]);
|
||||
}}
|
||||
>
|
||||
|
|
@ -188,7 +205,11 @@ describe('PluginsPage', () => {
|
|||
});
|
||||
|
||||
it('shows loader when loading and no plugins', () => {
|
||||
const loadingState = { plugins: [], loading: true, fetchPlugins: vi.fn() };
|
||||
const loadingState = {
|
||||
plugins: [],
|
||||
loading: true,
|
||||
fetchPlugins: vi.fn(),
|
||||
};
|
||||
usePluginStore.mockImplementation((selector) => {
|
||||
return selector ? selector(loadingState) : loadingState;
|
||||
});
|
||||
|
|
@ -219,7 +240,9 @@ describe('PluginsPage', () => {
|
|||
fireEvent.click(screen.getByText('Import Plugin'));
|
||||
|
||||
expect(screen.getByTestId('modal')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('modal-title')).toHaveTextContent('Import Plugin');
|
||||
expect(screen.getByTestId('modal-title')).toHaveTextContent(
|
||||
'Import Plugin'
|
||||
);
|
||||
});
|
||||
|
||||
it('shows dropzone and file input in import modal', () => {
|
||||
|
|
@ -228,7 +251,9 @@ describe('PluginsPage', () => {
|
|||
fireEvent.click(screen.getByText('Import Plugin'));
|
||||
|
||||
expect(screen.getByTestId('dropzone')).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText('Select plugin .zip')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByPlaceholderText('Select plugin .zip')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('closes import modal when close button is clicked', () => {
|
||||
|
|
@ -245,7 +270,11 @@ describe('PluginsPage', () => {
|
|||
it('handles file upload via dropzone', async () => {
|
||||
importPlugin.mockResolvedValue({
|
||||
success: true,
|
||||
plugin: { key: 'new-plugin', name: 'New Plugin', description: 'New Description' },
|
||||
plugin: {
|
||||
key: 'new-plugin',
|
||||
name: 'New Plugin',
|
||||
description: 'New Description',
|
||||
},
|
||||
});
|
||||
|
||||
render(<PluginsPage />);
|
||||
|
|
@ -255,9 +284,9 @@ describe('PluginsPage', () => {
|
|||
fireEvent.click(dropzone);
|
||||
|
||||
await waitFor(() => {
|
||||
const uploadButton = screen.getAllByText('Upload').find(btn =>
|
||||
btn.tagName === 'BUTTON'
|
||||
);
|
||||
const uploadButton = screen
|
||||
.getAllByText('Upload')
|
||||
.find((btn) => btn.tagName === 'BUTTON');
|
||||
expect(uploadButton).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
|
@ -279,12 +308,14 @@ describe('PluginsPage', () => {
|
|||
fireEvent.click(screen.getByText('Import Plugin'));
|
||||
|
||||
const fileInput = screen.getByPlaceholderText('Select plugin .zip');
|
||||
const file = new File(['content'], 'plugin.zip', { type: 'application/zip' });
|
||||
const file = new File(['content'], 'plugin.zip', {
|
||||
type: 'application/zip',
|
||||
});
|
||||
fireEvent.change(fileInput, { target: { files: [file] } });
|
||||
|
||||
const uploadButton = screen.getAllByText('Upload').find(btn =>
|
||||
btn.tagName === 'BUTTON'
|
||||
);
|
||||
const uploadButton = screen
|
||||
.getAllByText('Upload')
|
||||
.find((btn) => btn.tagName === 'BUTTON');
|
||||
fireEvent.click(uploadButton);
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
@ -305,12 +336,14 @@ describe('PluginsPage', () => {
|
|||
fireEvent.click(screen.getByText('Import Plugin'));
|
||||
|
||||
const fileInput = screen.getByPlaceholderText('Select plugin .zip');
|
||||
const file = new File(['content'], 'plugin.zip', { type: 'application/zip' });
|
||||
const file = new File(['content'], 'plugin.zip', {
|
||||
type: 'application/zip',
|
||||
});
|
||||
fireEvent.change(fileInput, { target: { files: [file] } });
|
||||
|
||||
const uploadButton = screen.getAllByText('Upload').find(btn =>
|
||||
btn.tagName === 'BUTTON'
|
||||
);
|
||||
const uploadButton = screen
|
||||
.getAllByText('Upload')
|
||||
.find((btn) => btn.tagName === 'BUTTON');
|
||||
fireEvent.click(uploadButton);
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
@ -340,12 +373,14 @@ describe('PluginsPage', () => {
|
|||
fireEvent.click(screen.getByText('Import Plugin'));
|
||||
|
||||
const fileInput = screen.getByPlaceholderText('Select plugin .zip');
|
||||
const file = new File(['content'], 'plugin.zip', { type: 'application/zip' });
|
||||
const file = new File(['content'], 'plugin.zip', {
|
||||
type: 'application/zip',
|
||||
});
|
||||
fireEvent.change(fileInput, { target: { files: [file] } });
|
||||
|
||||
const uploadButton = screen.getAllByText('Upload').find(btn =>
|
||||
btn.tagName === 'BUTTON'
|
||||
);
|
||||
const uploadButton = screen
|
||||
.getAllByText('Upload')
|
||||
.find((btn) => btn.tagName === 'BUTTON');
|
||||
fireEvent.click(uploadButton);
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
@ -372,12 +407,14 @@ describe('PluginsPage', () => {
|
|||
fireEvent.click(screen.getByText('Import Plugin'));
|
||||
|
||||
const fileInput = screen.getByPlaceholderText('Select plugin .zip');
|
||||
const file = new File(['content'], 'plugin.zip', { type: 'application/zip' });
|
||||
const file = new File(['content'], 'plugin.zip', {
|
||||
type: 'application/zip',
|
||||
});
|
||||
fireEvent.change(fileInput, { target: { files: [file] } });
|
||||
|
||||
const uploadButton = screen.getAllByText('Upload').find(btn =>
|
||||
btn.tagName === 'BUTTON'
|
||||
);
|
||||
const uploadButton = screen
|
||||
.getAllByText('Upload')
|
||||
.find((btn) => btn.tagName === 'BUTTON');
|
||||
fireEvent.click(uploadButton);
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
@ -387,9 +424,9 @@ describe('PluginsPage', () => {
|
|||
const enableSwitch = screen.getByRole('checkbox');
|
||||
fireEvent.click(enableSwitch);
|
||||
|
||||
const enableButton = screen.getAllByText('Enable').find(btn =>
|
||||
btn.tagName === 'BUTTON'
|
||||
);
|
||||
const enableButton = screen
|
||||
.getAllByText('Enable')
|
||||
.find((btn) => btn.tagName === 'BUTTON');
|
||||
fireEvent.click(enableButton);
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
@ -417,12 +454,14 @@ describe('PluginsPage', () => {
|
|||
fireEvent.click(screen.getByText('Import Plugin'));
|
||||
|
||||
const fileInput = screen.getByPlaceholderText('Select plugin .zip');
|
||||
const file = new File(['content'], 'plugin.zip', { type: 'application/zip' });
|
||||
const file = new File(['content'], 'plugin.zip', {
|
||||
type: 'application/zip',
|
||||
});
|
||||
fireEvent.change(fileInput, { target: { files: [file] } });
|
||||
|
||||
const uploadButton = screen.getAllByText('Upload').find(btn =>
|
||||
btn.tagName === 'BUTTON'
|
||||
);
|
||||
const uploadButton = screen
|
||||
.getAllByText('Upload')
|
||||
.find((btn) => btn.tagName === 'BUTTON');
|
||||
fireEvent.click(uploadButton);
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
@ -432,13 +471,15 @@ describe('PluginsPage', () => {
|
|||
const enableSwitch = screen.getByRole('checkbox');
|
||||
fireEvent.click(enableSwitch);
|
||||
|
||||
const enableButton = screen.getAllByText('Enable').find(btn =>
|
||||
btn.tagName === 'BUTTON'
|
||||
);
|
||||
const enableButton = screen
|
||||
.getAllByText('Enable')
|
||||
.find((btn) => btn.tagName === 'BUTTON');
|
||||
fireEvent.click(enableButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Enable third-party plugins?')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Enable third-party plugins?')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -460,12 +501,14 @@ describe('PluginsPage', () => {
|
|||
fireEvent.click(screen.getByText('Import Plugin'));
|
||||
|
||||
const fileInput = screen.getByPlaceholderText('Select plugin .zip');
|
||||
const file = new File(['content'], 'plugin.zip', { type: 'application/zip' });
|
||||
const file = new File(['content'], 'plugin.zip', {
|
||||
type: 'application/zip',
|
||||
});
|
||||
fireEvent.change(fileInput, { target: { files: [file] } });
|
||||
|
||||
const uploadButton = screen.getAllByText('Upload').find(btn =>
|
||||
btn.tagName === 'BUTTON'
|
||||
);
|
||||
const uploadButton = screen
|
||||
.getAllByText('Upload')
|
||||
.find((btn) => btn.tagName === 'BUTTON');
|
||||
fireEvent.click(uploadButton);
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
@ -475,9 +518,9 @@ describe('PluginsPage', () => {
|
|||
const enableSwitch = screen.getByRole('checkbox');
|
||||
fireEvent.click(enableSwitch);
|
||||
|
||||
const enableButton = screen.getAllByText('Enable').find(btn =>
|
||||
btn.tagName === 'BUTTON'
|
||||
);
|
||||
const enableButton = screen
|
||||
.getAllByText('Enable')
|
||||
.find((btn) => btn.tagName === 'BUTTON');
|
||||
fireEvent.click(enableButton);
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
@ -508,12 +551,14 @@ describe('PluginsPage', () => {
|
|||
fireEvent.click(screen.getByText('Import Plugin'));
|
||||
|
||||
const fileInput = screen.getByPlaceholderText('Select plugin .zip');
|
||||
const file = new File(['content'], 'plugin.zip', { type: 'application/zip' });
|
||||
const file = new File(['content'], 'plugin.zip', {
|
||||
type: 'application/zip',
|
||||
});
|
||||
fireEvent.change(fileInput, { target: { files: [file] } });
|
||||
|
||||
const uploadButton = screen.getAllByText('Upload').find(btn =>
|
||||
btn.tagName === 'BUTTON'
|
||||
);
|
||||
const uploadButton = screen
|
||||
.getAllByText('Upload')
|
||||
.find((btn) => btn.tagName === 'BUTTON');
|
||||
fireEvent.click(uploadButton);
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
@ -523,9 +568,9 @@ describe('PluginsPage', () => {
|
|||
const enableSwitch = screen.getByRole('checkbox');
|
||||
fireEvent.click(enableSwitch);
|
||||
|
||||
const enableButton = screen.getAllByText('Enable').find(btn =>
|
||||
btn.tagName === 'BUTTON'
|
||||
);
|
||||
const enableButton = screen
|
||||
.getAllByText('Enable')
|
||||
.find((btn) => btn.tagName === 'BUTTON');
|
||||
fireEvent.click(enableButton);
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import useAuthStore from '../../store/auth';
|
|||
|
||||
vi.mock('../../store/auth');
|
||||
vi.mock('../../components/tables/UsersTable', () => ({
|
||||
default: () => <div data-testid="users-table">UsersTable</div>
|
||||
default: () => <div data-testid="users-table">UsersTable</div>,
|
||||
}));
|
||||
vi.mock('@mantine/core', () => ({
|
||||
Box: ({ children, ...props }) => <div {...props}>{children}</div>,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ vi.mock('../../components/SeriesModal', () => ({
|
|||
<div data-testid="series-name">{series?.name}</div>
|
||||
<button onClick={onClose}>Close</button>
|
||||
</div>
|
||||
) : null
|
||||
) : null,
|
||||
}));
|
||||
vi.mock('../../components/VODModal', () => ({
|
||||
default: ({ opened, vod, onClose }) =>
|
||||
|
|
@ -25,26 +25,30 @@ vi.mock('../../components/VODModal', () => ({
|
|||
<div data-testid="vod-name">{vod?.name}</div>
|
||||
<button onClick={onClose}>Close</button>
|
||||
</div>
|
||||
) : null
|
||||
) : null,
|
||||
}));
|
||||
vi.mock('../../components/cards/VODCard', () => ({
|
||||
default: ({ vod, onClick }) => (
|
||||
<div data-testid="vod-card" onClick={() => onClick(vod)}>
|
||||
<div>{vod.name}</div>
|
||||
</div>
|
||||
)
|
||||
),
|
||||
}));
|
||||
vi.mock('../../components/cards/SeriesCard', () => ({
|
||||
default: ({ series, onClick }) => (
|
||||
<div data-testid="series-card" onClick={() => onClick(series)}>
|
||||
<div>{series.name}</div>
|
||||
</div>
|
||||
)
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('@mantine/core', () => {
|
||||
const gridComponent = ({ children, ...props }) => <div {...props}>{children}</div>;
|
||||
gridComponent.Col = ({ children, ...props }) => <div {...props}>{children}</div>;
|
||||
const gridComponent = ({ children, ...props }) => (
|
||||
<div {...props}>{children}</div>
|
||||
);
|
||||
gridComponent.Col = ({ children, ...props }) => (
|
||||
<div {...props}>{children}</div>
|
||||
);
|
||||
|
||||
return {
|
||||
Box: ({ children, ...props }) => <div {...props}>{children}</div>,
|
||||
|
|
@ -97,7 +101,9 @@ vi.mock('@mantine/core', () => {
|
|||
<button onClick={() => onChange(page - 1)} disabled={page === 1}>
|
||||
Prev
|
||||
</button>
|
||||
<span>{page} of {total}</span>
|
||||
<span>
|
||||
{page} of {total}
|
||||
</span>
|
||||
<button onClick={() => onChange(page + 1)} disabled={page === total}>
|
||||
Next
|
||||
</button>
|
||||
|
|
@ -208,9 +214,7 @@ describe('VODsPage', () => {
|
|||
it('renders series cards for series', async () => {
|
||||
const stateWithSeries = {
|
||||
...defaultStoreState,
|
||||
currentPageContent: [
|
||||
{ id: 1, name: 'Series 1', contentType: 'series' },
|
||||
],
|
||||
currentPageContent: [{ id: 1, name: 'Series 1', contentType: 'series' }],
|
||||
};
|
||||
useVODStore.mockImplementation((selector) => selector(stateWithSeries));
|
||||
|
||||
|
|
@ -224,9 +228,7 @@ describe('VODsPage', () => {
|
|||
it('opens VOD modal when VOD card is clicked', async () => {
|
||||
const stateWithMovies = {
|
||||
...defaultStoreState,
|
||||
currentPageContent: [
|
||||
{ id: 1, name: 'Test Movie', contentType: 'movie' },
|
||||
],
|
||||
currentPageContent: [{ id: 1, name: 'Test Movie', contentType: 'movie' }],
|
||||
};
|
||||
useVODStore.mockImplementation((selector) => selector(stateWithMovies));
|
||||
|
||||
|
|
@ -262,9 +264,7 @@ describe('VODsPage', () => {
|
|||
it('closes VOD modal when close button is clicked', async () => {
|
||||
const stateWithMovies = {
|
||||
...defaultStoreState,
|
||||
currentPageContent: [
|
||||
{ id: 1, name: 'Test Movie', contentType: 'movie' },
|
||||
],
|
||||
currentPageContent: [{ id: 1, name: 'Test Movie', contentType: 'movie' }],
|
||||
};
|
||||
useVODStore.mockImplementation((selector) => selector(stateWithMovies));
|
||||
|
||||
|
|
@ -326,9 +326,7 @@ describe('VODsPage', () => {
|
|||
});
|
||||
|
||||
it('updates filters and resets page when category changes', async () => {
|
||||
getCategoryOptions.mockReturnValue([
|
||||
{ value: 'action', label: 'Action' },
|
||||
]);
|
||||
getCategoryOptions.mockReturnValue([{ value: 'action', label: 'Action' }]);
|
||||
|
||||
render(<VODsPage />);
|
||||
|
||||
|
|
@ -370,9 +368,7 @@ describe('VODsPage', () => {
|
|||
totalCount: 25,
|
||||
pageSize: 12,
|
||||
};
|
||||
useVODStore.mockImplementation((selector) =>
|
||||
selector(stateWithPagination)
|
||||
);
|
||||
useVODStore.mockImplementation((selector) => selector(stateWithPagination));
|
||||
|
||||
render(<VODsPage />);
|
||||
|
||||
|
|
@ -405,9 +401,7 @@ describe('VODsPage', () => {
|
|||
pageSize: 12,
|
||||
currentPage: 1,
|
||||
};
|
||||
useVODStore.mockImplementation((selector) =>
|
||||
selector(stateWithPagination)
|
||||
);
|
||||
useVODStore.mockImplementation((selector) => selector(stateWithPagination));
|
||||
|
||||
render(<VODsPage />);
|
||||
|
||||
|
|
|
|||
|
|
@ -63,9 +63,7 @@ describe('guideUtils', () => {
|
|||
});
|
||||
|
||||
it('should use tvg_id from EPG data for regular sources', () => {
|
||||
const channels = [
|
||||
{ id: 1, uuid: 'uuid-1', epg_data_id: 'epg-1' },
|
||||
];
|
||||
const channels = [{ id: 1, uuid: 'uuid-1', epg_data_id: 'epg-1' }];
|
||||
const tvgsById = {
|
||||
'epg-1': { tvg_id: 'tvg-123', epg_source: 'source-1' },
|
||||
};
|
||||
|
|
@ -79,9 +77,7 @@ describe('guideUtils', () => {
|
|||
});
|
||||
|
||||
it('should use channel UUID for dummy EPG sources', () => {
|
||||
const channels = [
|
||||
{ id: 1, uuid: 'uuid-1', epg_data_id: 'epg-1' },
|
||||
];
|
||||
const channels = [{ id: 1, uuid: 'uuid-1', epg_data_id: 'epg-1' }];
|
||||
const tvgsById = {
|
||||
'epg-1': { tvg_id: 'tvg-123', epg_source: 'source-1' },
|
||||
};
|
||||
|
|
@ -113,9 +109,7 @@ describe('guideUtils', () => {
|
|||
});
|
||||
|
||||
it('should fall back to UUID when tvg_id is null', () => {
|
||||
const channels = [
|
||||
{ id: 1, uuid: 'uuid-1', epg_data_id: 'epg-1' },
|
||||
];
|
||||
const channels = [{ id: 1, uuid: 'uuid-1', epg_data_id: 'epg-1' }];
|
||||
const tvgsById = {
|
||||
'epg-1': { tvg_id: null, epg_source: 'source-1' },
|
||||
};
|
||||
|
|
@ -160,7 +154,10 @@ describe('guideUtils', () => {
|
|||
];
|
||||
const channelIdByTvgId = new Map([['tvg-1', [1]]]);
|
||||
|
||||
const result = guideUtils.mapProgramsByChannel(programs, channelIdByTvgId);
|
||||
const result = guideUtils.mapProgramsByChannel(
|
||||
programs,
|
||||
channelIdByTvgId
|
||||
);
|
||||
|
||||
expect(result.get(1)).toHaveLength(1);
|
||||
expect(result.get(1)[0]).toMatchObject({
|
||||
|
|
@ -185,7 +182,10 @@ describe('guideUtils', () => {
|
|||
];
|
||||
const channelIdByTvgId = new Map([['tvg-1', [1]]]);
|
||||
|
||||
const result = guideUtils.mapProgramsByChannel(programs, channelIdByTvgId);
|
||||
const result = guideUtils.mapProgramsByChannel(
|
||||
programs,
|
||||
channelIdByTvgId
|
||||
);
|
||||
|
||||
expect(result.get(1)[0]).toHaveProperty('startMs');
|
||||
expect(result.get(1)[0]).toHaveProperty('endMs');
|
||||
|
|
@ -209,7 +209,10 @@ describe('guideUtils', () => {
|
|||
];
|
||||
const channelIdByTvgId = new Map([['tvg-1', [1]]]);
|
||||
|
||||
const result = guideUtils.mapProgramsByChannel(programs, channelIdByTvgId);
|
||||
const result = guideUtils.mapProgramsByChannel(
|
||||
programs,
|
||||
channelIdByTvgId
|
||||
);
|
||||
|
||||
expect(result.get(1)[0].isLive).toBe(true);
|
||||
expect(result.get(1)[0].isPast).toBe(false);
|
||||
|
|
@ -233,7 +236,10 @@ describe('guideUtils', () => {
|
|||
];
|
||||
const channelIdByTvgId = new Map([['tvg-1', [1]]]);
|
||||
|
||||
const result = guideUtils.mapProgramsByChannel(programs, channelIdByTvgId);
|
||||
const result = guideUtils.mapProgramsByChannel(
|
||||
programs,
|
||||
channelIdByTvgId
|
||||
);
|
||||
|
||||
expect(result.get(1)[0].isLive).toBe(false);
|
||||
expect(result.get(1)[0].isPast).toBe(true);
|
||||
|
|
@ -252,7 +258,10 @@ describe('guideUtils', () => {
|
|||
];
|
||||
const channelIdByTvgId = new Map([['tvg-1', [1, 2, 3]]]);
|
||||
|
||||
const result = guideUtils.mapProgramsByChannel(programs, channelIdByTvgId);
|
||||
const result = guideUtils.mapProgramsByChannel(
|
||||
programs,
|
||||
channelIdByTvgId
|
||||
);
|
||||
|
||||
expect(result.get(1)).toHaveLength(1);
|
||||
expect(result.get(2)).toHaveLength(1);
|
||||
|
|
@ -282,7 +291,10 @@ describe('guideUtils', () => {
|
|||
];
|
||||
const channelIdByTvgId = new Map([['tvg-1', [1]]]);
|
||||
|
||||
const result = guideUtils.mapProgramsByChannel(programs, channelIdByTvgId);
|
||||
const result = guideUtils.mapProgramsByChannel(
|
||||
programs,
|
||||
channelIdByTvgId
|
||||
);
|
||||
|
||||
expect(result.get(1)[0].id).toBe(1);
|
||||
expect(result.get(1)[1].id).toBe(2);
|
||||
|
|
@ -300,9 +312,16 @@ describe('guideUtils', () => {
|
|||
const channels = [{ id: 1 }, { id: 2 }];
|
||||
const programsByChannelId = new Map();
|
||||
|
||||
const result = guideUtils.computeRowHeights(channels, programsByChannelId, null);
|
||||
const result = guideUtils.computeRowHeights(
|
||||
channels,
|
||||
programsByChannelId,
|
||||
null
|
||||
);
|
||||
|
||||
expect(result).toEqual([guideUtils.PROGRAM_HEIGHT, guideUtils.PROGRAM_HEIGHT]);
|
||||
expect(result).toEqual([
|
||||
guideUtils.PROGRAM_HEIGHT,
|
||||
guideUtils.PROGRAM_HEIGHT,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return expanded height for channel with expanded program', () => {
|
||||
|
|
@ -312,9 +331,16 @@ describe('guideUtils', () => {
|
|||
[2, [{ id: 'program-2' }]],
|
||||
]);
|
||||
|
||||
const result = guideUtils.computeRowHeights(channels, programsByChannelId, 'program-1');
|
||||
const result = guideUtils.computeRowHeights(
|
||||
channels,
|
||||
programsByChannelId,
|
||||
'program-1'
|
||||
);
|
||||
|
||||
expect(result).toEqual([guideUtils.EXPANDED_PROGRAM_HEIGHT, guideUtils.PROGRAM_HEIGHT]);
|
||||
expect(result).toEqual([
|
||||
guideUtils.EXPANDED_PROGRAM_HEIGHT,
|
||||
guideUtils.PROGRAM_HEIGHT,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should use custom heights when provided', () => {
|
||||
|
|
@ -393,7 +419,13 @@ describe('guideUtils', () => {
|
|||
{ id: 2, name: 'Channel 2' },
|
||||
];
|
||||
|
||||
const result = guideUtils.filterGuideChannels(channels, '', 'all', 'all', {});
|
||||
const result = guideUtils.filterGuideChannels(
|
||||
channels,
|
||||
'',
|
||||
'all',
|
||||
'all',
|
||||
{}
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
|
@ -404,7 +436,13 @@ describe('guideUtils', () => {
|
|||
{ id: 2, name: 'CNN' },
|
||||
];
|
||||
|
||||
const result = guideUtils.filterGuideChannels(channels, 'espn', 'all', 'all', {});
|
||||
const result = guideUtils.filterGuideChannels(
|
||||
channels,
|
||||
'espn',
|
||||
'all',
|
||||
'all',
|
||||
{}
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].name).toBe('ESPN');
|
||||
|
|
@ -416,7 +454,13 @@ describe('guideUtils', () => {
|
|||
{ id: 2, name: 'Channel 2', channel_group_id: 2 },
|
||||
];
|
||||
|
||||
const result = guideUtils.filterGuideChannels(channels, '', '1', 'all', {});
|
||||
const result = guideUtils.filterGuideChannels(
|
||||
channels,
|
||||
'',
|
||||
'1',
|
||||
'all',
|
||||
{}
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].channel_group_id).toBe(1);
|
||||
|
|
@ -436,7 +480,13 @@ describe('guideUtils', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = guideUtils.filterGuideChannels(channels, '', 'all', 'profile1', profiles);
|
||||
const result = guideUtils.filterGuideChannels(
|
||||
channels,
|
||||
'',
|
||||
'all',
|
||||
'profile1',
|
||||
profiles
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe(1);
|
||||
|
|
@ -453,7 +503,13 @@ describe('guideUtils', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = guideUtils.filterGuideChannels(channels, '', 'all', 'profile1', profiles);
|
||||
const result = guideUtils.filterGuideChannels(
|
||||
channels,
|
||||
'',
|
||||
'all',
|
||||
'profile1',
|
||||
profiles
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe(1);
|
||||
|
|
@ -474,7 +530,13 @@ describe('guideUtils', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = guideUtils.filterGuideChannels(channels, 'espn', '1', 'profile1', profiles);
|
||||
const result = guideUtils.filterGuideChannels(
|
||||
channels,
|
||||
'espn',
|
||||
'1',
|
||||
'profile1',
|
||||
profiles
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe(1);
|
||||
|
|
@ -491,8 +553,12 @@ describe('guideUtils', () => {
|
|||
});
|
||||
|
||||
it('should return earliest program start', () => {
|
||||
dateTimeUtils.initializeTime.mockImplementation((time) => dayjs.utc(time));
|
||||
dateTimeUtils.isBefore.mockImplementation((a, b) => dayjs(a).isBefore(dayjs(b)));
|
||||
dateTimeUtils.initializeTime.mockImplementation((time) =>
|
||||
dayjs.utc(time)
|
||||
);
|
||||
dateTimeUtils.isBefore.mockImplementation((a, b) =>
|
||||
dayjs(a).isBefore(dayjs(b))
|
||||
);
|
||||
|
||||
const programs = [
|
||||
{ start_time: '2024-01-15T12:00:00Z' },
|
||||
|
|
@ -501,7 +567,10 @@ describe('guideUtils', () => {
|
|||
];
|
||||
const defaultStart = dayjs.utc('2024-01-16T00:00:00Z');
|
||||
|
||||
const result = guideUtils.calculateEarliestProgramStart(programs, defaultStart);
|
||||
const result = guideUtils.calculateEarliestProgramStart(
|
||||
programs,
|
||||
defaultStart
|
||||
);
|
||||
|
||||
expect(result.hour()).toBe(10);
|
||||
});
|
||||
|
|
@ -517,8 +586,12 @@ describe('guideUtils', () => {
|
|||
});
|
||||
|
||||
it('should return latest program end', () => {
|
||||
dateTimeUtils.initializeTime.mockImplementation((time) => dayjs.utc(time));
|
||||
dateTimeUtils.isAfter.mockImplementation((a, b) => dayjs(a).isAfter(dayjs(b)));
|
||||
dateTimeUtils.initializeTime.mockImplementation((time) =>
|
||||
dayjs.utc(time)
|
||||
);
|
||||
dateTimeUtils.isAfter.mockImplementation((a, b) =>
|
||||
dayjs(a).isAfter(dayjs(b))
|
||||
);
|
||||
|
||||
const programs = [
|
||||
{ end_time: '2024-01-15T12:00:00Z' },
|
||||
|
|
@ -638,8 +711,12 @@ describe('guideUtils', () => {
|
|||
it('should return "Today" for today', () => {
|
||||
const today = dayjs();
|
||||
dateTimeUtils.getNow.mockReturnValue(today);
|
||||
dateTimeUtils.startOfDay.mockImplementation((time) => dayjs(time).startOf('day'));
|
||||
dateTimeUtils.add.mockImplementation((time, amount, unit) => dayjs(time).add(amount, unit));
|
||||
dateTimeUtils.startOfDay.mockImplementation((time) =>
|
||||
dayjs(time).startOf('day')
|
||||
);
|
||||
dateTimeUtils.add.mockImplementation((time, amount, unit) =>
|
||||
dayjs(time).add(amount, unit)
|
||||
);
|
||||
dateTimeUtils.isSame.mockReturnValueOnce(true);
|
||||
|
||||
const result = guideUtils.formatTime(today, 'MM/DD');
|
||||
|
|
@ -651,8 +728,12 @@ describe('guideUtils', () => {
|
|||
const today = dayjs();
|
||||
const tomorrow = today.add(1, 'day');
|
||||
dateTimeUtils.getNow.mockReturnValue(today);
|
||||
dateTimeUtils.startOfDay.mockImplementation((time) => dayjs(time).startOf('day'));
|
||||
dateTimeUtils.add.mockImplementation((time, amount, unit) => dayjs(time).add(amount, unit));
|
||||
dateTimeUtils.startOfDay.mockImplementation((time) =>
|
||||
dayjs(time).startOf('day')
|
||||
);
|
||||
dateTimeUtils.add.mockImplementation((time, amount, unit) =>
|
||||
dayjs(time).add(amount, unit)
|
||||
);
|
||||
dateTimeUtils.isSame.mockReturnValueOnce(false).mockReturnValueOnce(true);
|
||||
|
||||
const result = guideUtils.formatTime(tomorrow, 'MM/DD');
|
||||
|
|
@ -664,8 +745,12 @@ describe('guideUtils', () => {
|
|||
const today = dayjs();
|
||||
const future = today.add(3, 'day');
|
||||
dateTimeUtils.getNow.mockReturnValue(today);
|
||||
dateTimeUtils.startOfDay.mockImplementation((time) => dayjs(time).startOf('day'));
|
||||
dateTimeUtils.add.mockImplementation((time, amount, unit) => dayjs(time).add(amount, unit));
|
||||
dateTimeUtils.startOfDay.mockImplementation((time) =>
|
||||
dayjs(time).startOf('day')
|
||||
);
|
||||
dateTimeUtils.add.mockImplementation((time, amount, unit) =>
|
||||
dayjs(time).add(amount, unit)
|
||||
);
|
||||
dateTimeUtils.isSame.mockReturnValue(false);
|
||||
dateTimeUtils.isBefore.mockReturnValue(true);
|
||||
dateTimeUtils.format.mockReturnValue('Wednesday');
|
||||
|
|
@ -679,8 +764,12 @@ describe('guideUtils', () => {
|
|||
const today = dayjs();
|
||||
const future = today.add(10, 'day');
|
||||
dateTimeUtils.getNow.mockReturnValue(today);
|
||||
dateTimeUtils.startOfDay.mockImplementation((time) => dayjs(time).startOf('day'));
|
||||
dateTimeUtils.add.mockImplementation((time, amount, unit) => dayjs(time).add(amount, unit));
|
||||
dateTimeUtils.startOfDay.mockImplementation((time) =>
|
||||
dayjs(time).startOf('day')
|
||||
);
|
||||
dateTimeUtils.add.mockImplementation((time, amount, unit) =>
|
||||
dayjs(time).add(amount, unit)
|
||||
);
|
||||
dateTimeUtils.isSame.mockReturnValue(false);
|
||||
dateTimeUtils.isBefore.mockReturnValue(false);
|
||||
dateTimeUtils.format.mockReturnValue('01/25');
|
||||
|
|
@ -695,13 +784,23 @@ describe('guideUtils', () => {
|
|||
it('should generate hours between start and end', () => {
|
||||
const start = dayjs('2024-01-15T10:00:00Z');
|
||||
const end = dayjs('2024-01-15T13:00:00Z');
|
||||
dateTimeUtils.isBefore.mockImplementation((a, b) => dayjs(a).isBefore(dayjs(b)));
|
||||
dateTimeUtils.add.mockImplementation((time, amount, unit) => dayjs(time).add(amount, unit));
|
||||
dateTimeUtils.startOfDay.mockImplementation((time) => dayjs(time).startOf('day'));
|
||||
dateTimeUtils.isBefore.mockImplementation((a, b) =>
|
||||
dayjs(a).isBefore(dayjs(b))
|
||||
);
|
||||
dateTimeUtils.add.mockImplementation((time, amount, unit) =>
|
||||
dayjs(time).add(amount, unit)
|
||||
);
|
||||
dateTimeUtils.startOfDay.mockImplementation((time) =>
|
||||
dayjs(time).startOf('day')
|
||||
);
|
||||
dateTimeUtils.isSame.mockReturnValue(true);
|
||||
|
||||
const formatDayLabel = vi.fn((time) => 'Today');
|
||||
const result = guideUtils.calculateHourTimeline(start, end, formatDayLabel);
|
||||
const result = guideUtils.calculateHourTimeline(
|
||||
start,
|
||||
end,
|
||||
formatDayLabel
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(formatDayLabel).toHaveBeenCalledTimes(3);
|
||||
|
|
@ -710,13 +809,25 @@ describe('guideUtils', () => {
|
|||
it('should mark new day transitions', () => {
|
||||
const start = dayjs('2024-01-15T23:00:00Z');
|
||||
const end = dayjs('2024-01-16T02:00:00Z');
|
||||
dateTimeUtils.isBefore.mockImplementation((a, b) => dayjs(a).isBefore(dayjs(b)));
|
||||
dateTimeUtils.add.mockImplementation((time, amount, unit) => dayjs(time).add(amount, unit));
|
||||
dateTimeUtils.startOfDay.mockImplementation((time) => dayjs(time).startOf('day'));
|
||||
dateTimeUtils.isSame.mockImplementation((a, b, unit) => dayjs(a).isSame(dayjs(b), unit));
|
||||
dateTimeUtils.isBefore.mockImplementation((a, b) =>
|
||||
dayjs(a).isBefore(dayjs(b))
|
||||
);
|
||||
dateTimeUtils.add.mockImplementation((time, amount, unit) =>
|
||||
dayjs(time).add(amount, unit)
|
||||
);
|
||||
dateTimeUtils.startOfDay.mockImplementation((time) =>
|
||||
dayjs(time).startOf('day')
|
||||
);
|
||||
dateTimeUtils.isSame.mockImplementation((a, b, unit) =>
|
||||
dayjs(a).isSame(dayjs(b), unit)
|
||||
);
|
||||
|
||||
const formatDayLabel = vi.fn((time) => 'Day');
|
||||
const result = guideUtils.calculateHourTimeline(start, end, formatDayLabel);
|
||||
const result = guideUtils.calculateHourTimeline(
|
||||
start,
|
||||
end,
|
||||
formatDayLabel
|
||||
);
|
||||
|
||||
expect(result[0].isNewDay).toBe(true);
|
||||
});
|
||||
|
|
@ -791,7 +902,11 @@ describe('guideUtils', () => {
|
|||
const channelIdByTvgId = new Map();
|
||||
const channelById = new Map();
|
||||
|
||||
const result = guideUtils.matchChannelByTvgId(channelIdByTvgId, channelById, 'tvg-1');
|
||||
const result = guideUtils.matchChannelByTvgId(
|
||||
channelIdByTvgId,
|
||||
channelById,
|
||||
'tvg-1'
|
||||
);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
|
@ -801,7 +916,11 @@ describe('guideUtils', () => {
|
|||
const channelIdByTvgId = new Map([['tvg-1', [1, 2, 3]]]);
|
||||
const channelById = new Map([[1, channel]]);
|
||||
|
||||
const result = guideUtils.matchChannelByTvgId(channelIdByTvgId, channelById, 'tvg-1');
|
||||
const result = guideUtils.matchChannelByTvgId(
|
||||
channelIdByTvgId,
|
||||
channelById,
|
||||
'tvg-1'
|
||||
);
|
||||
|
||||
expect(result).toBe(channel);
|
||||
});
|
||||
|
|
@ -810,7 +929,11 @@ describe('guideUtils', () => {
|
|||
const channelIdByTvgId = new Map([['tvg-1', [999]]]);
|
||||
const channelById = new Map();
|
||||
|
||||
const result = guideUtils.matchChannelByTvgId(channelIdByTvgId, channelById, 'tvg-1');
|
||||
const result = guideUtils.matchChannelByTvgId(
|
||||
channelIdByTvgId,
|
||||
channelById,
|
||||
'tvg-1'
|
||||
);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
|
@ -933,7 +1056,9 @@ describe('guideUtils', () => {
|
|||
start_time: '2024-01-15T10:30:00Z',
|
||||
};
|
||||
const start = '2024-01-15T10:00:00Z';
|
||||
dateTimeUtils.convertToMs.mockImplementation((time) => dayjs(time).valueOf());
|
||||
dateTimeUtils.convertToMs.mockImplementation((time) =>
|
||||
dayjs(time).valueOf()
|
||||
);
|
||||
|
||||
const result = guideUtils.calculateLeftScrollPosition(program, start);
|
||||
|
||||
|
|
@ -965,10 +1090,16 @@ describe('guideUtils', () => {
|
|||
};
|
||||
const clickedTime = dayjs('2024-01-15T10:00:00Z');
|
||||
const start = dayjs('2024-01-15T09:00:00Z');
|
||||
dateTimeUtils.add.mockImplementation((time, amount, unit) => dayjs(time).add(amount, unit));
|
||||
dateTimeUtils.add.mockImplementation((time, amount, unit) =>
|
||||
dayjs(time).add(amount, unit)
|
||||
);
|
||||
dateTimeUtils.diff.mockReturnValue(60);
|
||||
|
||||
const result = guideUtils.calculateScrollPositionByTimeClick(event, clickedTime, start);
|
||||
const result = guideUtils.calculateScrollPositionByTimeClick(
|
||||
event,
|
||||
clickedTime,
|
||||
start
|
||||
);
|
||||
|
||||
expect(result).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
|
@ -982,7 +1113,9 @@ describe('guideUtils', () => {
|
|||
};
|
||||
const clickedTime = dayjs('2024-01-15T10:00:00Z');
|
||||
const start = dayjs('2024-01-15T09:00:00Z');
|
||||
dateTimeUtils.add.mockImplementation((time, amount, unit) => dayjs(time).add(amount, unit));
|
||||
dateTimeUtils.add.mockImplementation((time, amount, unit) =>
|
||||
dayjs(time).add(amount, unit)
|
||||
);
|
||||
dateTimeUtils.diff.mockReturnValue(75);
|
||||
|
||||
guideUtils.calculateScrollPositionByTimeClick(event, clickedTime, start);
|
||||
|
|
@ -999,12 +1132,22 @@ describe('guideUtils', () => {
|
|||
};
|
||||
const clickedTime = dayjs('2024-01-15T10:00:00Z');
|
||||
const start = dayjs('2024-01-15T09:00:00Z');
|
||||
dateTimeUtils.add.mockImplementation((time, amount, unit) => dayjs(time).add(amount, unit));
|
||||
dateTimeUtils.add.mockImplementation((time, amount, unit) =>
|
||||
dayjs(time).add(amount, unit)
|
||||
);
|
||||
dateTimeUtils.diff.mockReturnValue(120);
|
||||
|
||||
const result = guideUtils.calculateScrollPositionByTimeClick(event, clickedTime, start);
|
||||
const result = guideUtils.calculateScrollPositionByTimeClick(
|
||||
event,
|
||||
clickedTime,
|
||||
start
|
||||
);
|
||||
|
||||
expect(dateTimeUtils.add).toHaveBeenCalledWith(expect.anything(), 1, 'hour');
|
||||
expect(dateTimeUtils.add).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
1,
|
||||
'hour'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -1037,9 +1180,7 @@ describe('guideUtils', () => {
|
|||
1: { id: 1, name: 'Sports' },
|
||||
2: { id: 2, name: 'News' },
|
||||
};
|
||||
const channels = [
|
||||
{ id: 1, channel_group_id: 1 },
|
||||
];
|
||||
const channels = [{ id: 1, channel_group_id: 1 }];
|
||||
|
||||
const result = guideUtils.getGroupOptions(channelGroups, channels);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,22 +7,22 @@
|
|||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(to right, #2D2D2F, #1F1F20);
|
||||
background: linear-gradient(to right, #2d2d2f, #1f1f20);
|
||||
/* Default background */
|
||||
color: #fff;
|
||||
transition: all 0.2s ease-out;
|
||||
}
|
||||
|
||||
.tv-guide .guide-program-container .guide-program.live {
|
||||
background: linear-gradient(to right, #3BA882, #245043);
|
||||
background: linear-gradient(to right, #3ba882, #245043);
|
||||
}
|
||||
|
||||
.tv-guide .guide-program-container .guide-program.live:hover {
|
||||
background: linear-gradient(to right, #2E9E80, #206E5E);
|
||||
background: linear-gradient(to right, #2e9e80, #206e5e);
|
||||
}
|
||||
|
||||
.tv-guide .guide-program-container .guide-program.not-live:hover {
|
||||
background: linear-gradient(to right, #2F3F3A, #1E2926);
|
||||
background: linear-gradient(to right, #2f3f3a, #1e2926);
|
||||
}
|
||||
|
||||
/* New styles for expanded programs */
|
||||
|
|
@ -33,15 +33,15 @@
|
|||
}
|
||||
|
||||
.tv-guide .guide-program-container .guide-program.expanded.live {
|
||||
background: linear-gradient(to right, #226F5D, #3BA882);
|
||||
background: linear-gradient(to right, #226f5d, #3ba882);
|
||||
}
|
||||
|
||||
.tv-guide .guide-program-container .guide-program.expanded.not-live {
|
||||
background: linear-gradient(to right, #2C3F3A, #206E5E);
|
||||
background: linear-gradient(to right, #2c3f3a, #206e5e);
|
||||
}
|
||||
|
||||
.tv-guide .guide-program-container .guide-program.expanded.past {
|
||||
background: linear-gradient(to right, #1F2423, #2F3A37);
|
||||
background: linear-gradient(to right, #1f2423, #2f3a37);
|
||||
}
|
||||
|
||||
/* Ensure channel logo is always on top */
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
format,
|
||||
getNow,
|
||||
getNowMs,
|
||||
roundToNearest
|
||||
roundToNearest,
|
||||
} from '../utils/dateTimeUtils.js';
|
||||
import API from '../api.js';
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ export function buildChannelIdMap(channels, tvgsById, epgs = {}) {
|
|||
const tvgRecord = channel.epg_data_id
|
||||
? tvgsById[channel.epg_data_id]
|
||||
: null;
|
||||
|
||||
|
||||
// For dummy EPG sources, ALWAYS use channel UUID to ensure unique programs per channel
|
||||
// This prevents multiple channels with the same dummy EPG from showing identical data
|
||||
let tvgId;
|
||||
|
|
@ -45,7 +45,7 @@ export function buildChannelIdMap(channels, tvgsById, epgs = {}) {
|
|||
// No EPG data: use channel UUID
|
||||
tvgId = channel.uuid;
|
||||
}
|
||||
|
||||
|
||||
if (tvgId) {
|
||||
const tvgKey = String(tvgId);
|
||||
if (!map.has(tvgKey)) {
|
||||
|
|
@ -138,19 +138,25 @@ export const fetchPrograms = async () => {
|
|||
export const sortChannels = (channels) => {
|
||||
// Include ALL channels, sorted by channel number - don't filter by EPG data
|
||||
const sortedChannels = Object.values(channels).sort(
|
||||
(a, b) =>
|
||||
(a.channel_number || Infinity) - (b.channel_number || Infinity)
|
||||
(a, b) => (a.channel_number || Infinity) - (b.channel_number || Infinity)
|
||||
);
|
||||
|
||||
console.log(`Using all ${sortedChannels.length} available channels`);
|
||||
return sortedChannels;
|
||||
}
|
||||
};
|
||||
|
||||
export const filterGuideChannels = (guideChannels, searchQuery, selectedGroupId, selectedProfileId, profiles) => {
|
||||
export const filterGuideChannels = (
|
||||
guideChannels,
|
||||
searchQuery,
|
||||
selectedGroupId,
|
||||
selectedProfileId,
|
||||
profiles
|
||||
) => {
|
||||
return guideChannels.filter((channel) => {
|
||||
// Search filter
|
||||
if (searchQuery) {
|
||||
if (!channel.name.toLowerCase().includes(searchQuery.toLowerCase())) return false;
|
||||
if (!channel.name.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
return false;
|
||||
}
|
||||
|
||||
// Channel group filter
|
||||
|
|
@ -162,17 +168,17 @@ export const filterGuideChannels = (guideChannels, searchQuery, selectedGroupId,
|
|||
if (selectedProfileId !== 'all') {
|
||||
const profileChannels = profiles[selectedProfileId]?.channels || [];
|
||||
const enabledChannelIds = Array.isArray(profileChannels)
|
||||
? profileChannels.filter((pc) => pc.enabled).map((pc) => pc.id)
|
||||
: profiles[selectedProfileId]?.channels instanceof Set
|
||||
? Array.from(profiles[selectedProfileId].channels)
|
||||
: [];
|
||||
? profileChannels.filter((pc) => pc.enabled).map((pc) => pc.id)
|
||||
: profiles[selectedProfileId]?.channels instanceof Set
|
||||
? Array.from(profiles[selectedProfileId].channels)
|
||||
: [];
|
||||
|
||||
if (!enabledChannelIds.includes(channel.id)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const calculateEarliestProgramStart = (programs, defaultStart) => {
|
||||
if (!programs.length) return defaultStart;
|
||||
|
|
@ -180,7 +186,7 @@ export const calculateEarliestProgramStart = (programs, defaultStart) => {
|
|||
const s = initializeTime(p.start_time);
|
||||
return isBefore(s, acc) ? s : acc;
|
||||
}, defaultStart);
|
||||
}
|
||||
};
|
||||
|
||||
export const calculateLatestProgramEnd = (programs, defaultEnd) => {
|
||||
if (!programs.length) return defaultEnd;
|
||||
|
|
@ -188,17 +194,17 @@ export const calculateLatestProgramEnd = (programs, defaultEnd) => {
|
|||
const e = initializeTime(p.end_time);
|
||||
return isAfter(e, acc) ? e : acc;
|
||||
}, defaultEnd);
|
||||
}
|
||||
};
|
||||
|
||||
export const calculateStart = (earliestProgramStart, defaultStart) => {
|
||||
return isBefore(earliestProgramStart, defaultStart)
|
||||
? earliestProgramStart
|
||||
: defaultStart;
|
||||
}
|
||||
};
|
||||
|
||||
export const calculateEnd = (latestProgramEnd, defaultEnd) => {
|
||||
return isAfter(latestProgramEnd, defaultEnd) ? latestProgramEnd : defaultEnd;
|
||||
}
|
||||
};
|
||||
|
||||
export const mapChannelsById = (guideChannels) => {
|
||||
const map = new Map();
|
||||
|
|
@ -206,7 +212,7 @@ export const mapChannelsById = (guideChannels) => {
|
|||
map.set(channel.id, channel);
|
||||
});
|
||||
return map;
|
||||
}
|
||||
};
|
||||
|
||||
export const mapRecordingsByProgramId = (recordings) => {
|
||||
const map = new Map();
|
||||
|
|
@ -217,7 +223,7 @@ export const mapRecordingsByProgramId = (recordings) => {
|
|||
}
|
||||
});
|
||||
return map;
|
||||
}
|
||||
};
|
||||
|
||||
export const formatTime = (time, dateFormat) => {
|
||||
const today = startOfDay(getNow());
|
||||
|
|
@ -236,7 +242,7 @@ export const formatTime = (time, dateFormat) => {
|
|||
// Beyond a week, show month and day
|
||||
return format(time, dateFormat);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const calculateHourTimeline = (start, end, formatDayLabel) => {
|
||||
const hours = [];
|
||||
|
|
@ -262,7 +268,7 @@ export const calculateHourTimeline = (start, end, formatDayLabel) => {
|
|||
current = add(current, 1, 'hour');
|
||||
}
|
||||
return hours;
|
||||
}
|
||||
};
|
||||
|
||||
export const calculateNowPosition = (now, start, end) => {
|
||||
if (isBefore(now, start) || isAfter(now, end)) return -1;
|
||||
|
|
@ -286,11 +292,11 @@ export const matchChannelByTvgId = (channelIdByTvgId, channelById, tvgId) => {
|
|||
}
|
||||
// Return the first channel that matches this TVG ID
|
||||
return channelById.get(channelIds[0]) || null;
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchRules = async () => {
|
||||
return await API.listSeriesRules();
|
||||
}
|
||||
};
|
||||
|
||||
export const getRuleByProgram = (rules, program) => {
|
||||
return (rules || []).find(
|
||||
|
|
@ -298,7 +304,7 @@ export const getRuleByProgram = (rules, program) => {
|
|||
String(r.tvg_id) === String(program.tvg_id) &&
|
||||
(!r.title || r.title === program.title)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const createRecording = async (channel, program) => {
|
||||
await API.createRecording({
|
||||
|
|
@ -307,7 +313,7 @@ export const createRecording = async (channel, program) => {
|
|||
end_time: program.end_time,
|
||||
custom_properties: { program },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const createSeriesRule = async (program, mode) => {
|
||||
await API.createSeriesRule({
|
||||
|
|
@ -315,15 +321,14 @@ export const createSeriesRule = async (program, mode) => {
|
|||
mode,
|
||||
title: program.title,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const evaluateSeriesRule = async (program) => {
|
||||
await API.evaluateSeriesRules(program.tvg_id);
|
||||
}
|
||||
};
|
||||
|
||||
export const calculateLeftScrollPosition = (program, start) => {
|
||||
const programStartMs =
|
||||
program.startMs ?? convertToMs(program.start_time);
|
||||
const programStartMs = program.startMs ?? convertToMs(program.start_time);
|
||||
const startOffsetMinutes = (programStartMs - convertToMs(start)) / 60000;
|
||||
|
||||
return (startOffsetMinutes / MINUTE_INCREMENT) * MINUTE_BLOCK_WIDTH;
|
||||
|
|
@ -331,9 +336,13 @@ export const calculateLeftScrollPosition = (program, start) => {
|
|||
|
||||
export const calculateDesiredScrollPosition = (leftPx) => {
|
||||
return Math.max(0, leftPx - 20);
|
||||
}
|
||||
};
|
||||
|
||||
export const calculateScrollPositionByTimeClick = (event, clickedTime, start) => {
|
||||
export const calculateScrollPositionByTimeClick = (
|
||||
event,
|
||||
clickedTime,
|
||||
start
|
||||
) => {
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
const clickPositionX = event.clientX - rect.left;
|
||||
const percentageAcross = clickPositionX / rect.width;
|
||||
|
|
@ -341,9 +350,10 @@ export const calculateScrollPositionByTimeClick = (event, clickedTime, start) =>
|
|||
|
||||
const snappedMinute = Math.round(minuteWithinHour / 15) * 15;
|
||||
|
||||
const adjustedTime = (snappedMinute === 60)
|
||||
? add(clickedTime, 1, 'hour').minute(0)
|
||||
: clickedTime.minute(snappedMinute);
|
||||
const adjustedTime =
|
||||
snappedMinute === 60
|
||||
? add(clickedTime, 1, 'hour').minute(0)
|
||||
: clickedTime.minute(snappedMinute);
|
||||
|
||||
const snappedOffset = diff(adjustedTime, start, 'minute');
|
||||
return (snappedOffset / MINUTE_INCREMENT) * MINUTE_BLOCK_WIDTH;
|
||||
|
|
@ -372,7 +382,7 @@ export const getGroupOptions = (channelGroups, guideChannels) => {
|
|||
});
|
||||
}
|
||||
return options;
|
||||
}
|
||||
};
|
||||
|
||||
export const getProfileOptions = (profiles) => {
|
||||
const options = [{ value: 'all', label: 'All Profiles' }];
|
||||
|
|
@ -390,12 +400,12 @@ export const getProfileOptions = (profiles) => {
|
|||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteSeriesRuleByTvgId = async (tvg_id) => {
|
||||
await API.deleteSeriesRule(tvg_id);
|
||||
}
|
||||
};
|
||||
|
||||
export const evaluateSeriesRulesByTvgId = async (tvg_id) => {
|
||||
await API.evaluateSeriesRules(tvg_id);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -54,36 +54,50 @@ describe('useAuthStore', () => {
|
|||
localStorageMock.clear();
|
||||
|
||||
// Setup default store mocks
|
||||
useSettingsStore.mockImplementation((selector) => selector({
|
||||
fetchSettings: vi.fn().mockResolvedValue(),
|
||||
}));
|
||||
useSettingsStore.mockImplementation((selector) =>
|
||||
selector({
|
||||
fetchSettings: vi.fn().mockResolvedValue(),
|
||||
})
|
||||
);
|
||||
|
||||
useChannelsStore.mockImplementation((selector) => selector({
|
||||
fetchChannels: vi.fn().mockResolvedValue(),
|
||||
fetchChannelGroups: vi.fn().mockResolvedValue(),
|
||||
fetchChannelProfiles: vi.fn().mockResolvedValue(),
|
||||
}));
|
||||
useChannelsStore.mockImplementation((selector) =>
|
||||
selector({
|
||||
fetchChannels: vi.fn().mockResolvedValue(),
|
||||
fetchChannelGroups: vi.fn().mockResolvedValue(),
|
||||
fetchChannelProfiles: vi.fn().mockResolvedValue(),
|
||||
})
|
||||
);
|
||||
|
||||
usePlaylistsStore.mockImplementation((selector) => selector({
|
||||
fetchPlaylists: vi.fn().mockResolvedValue(),
|
||||
}));
|
||||
usePlaylistsStore.mockImplementation((selector) =>
|
||||
selector({
|
||||
fetchPlaylists: vi.fn().mockResolvedValue(),
|
||||
})
|
||||
);
|
||||
|
||||
useEPGsStore.mockImplementation((selector) => selector({
|
||||
fetchEPGs: vi.fn().mockResolvedValue(),
|
||||
fetchEPGData: vi.fn().mockResolvedValue(),
|
||||
}));
|
||||
useEPGsStore.mockImplementation((selector) =>
|
||||
selector({
|
||||
fetchEPGs: vi.fn().mockResolvedValue(),
|
||||
fetchEPGData: vi.fn().mockResolvedValue(),
|
||||
})
|
||||
);
|
||||
|
||||
useStreamProfilesStore.mockImplementation((selector) => selector({
|
||||
fetchProfiles: vi.fn().mockResolvedValue(),
|
||||
}));
|
||||
useStreamProfilesStore.mockImplementation((selector) =>
|
||||
selector({
|
||||
fetchProfiles: vi.fn().mockResolvedValue(),
|
||||
})
|
||||
);
|
||||
|
||||
useUserAgentsStore.mockImplementation((selector) => selector({
|
||||
fetchUserAgents: vi.fn().mockResolvedValue(),
|
||||
}));
|
||||
useUserAgentsStore.mockImplementation((selector) =>
|
||||
selector({
|
||||
fetchUserAgents: vi.fn().mockResolvedValue(),
|
||||
})
|
||||
);
|
||||
|
||||
useUsersStore.mockImplementation((selector) => selector({
|
||||
fetchUsers: vi.fn().mockResolvedValue(),
|
||||
}));
|
||||
useUsersStore.mockImplementation((selector) =>
|
||||
selector({
|
||||
fetchUsers: vi.fn().mockResolvedValue(),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -140,13 +154,25 @@ describe('useAuthStore', () => {
|
|||
const { result } = renderHook(() => useAuthStore());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.login({ username: 'testuser', password: 'password' });
|
||||
await result.current.login({
|
||||
username: 'testuser',
|
||||
password: 'password',
|
||||
});
|
||||
});
|
||||
|
||||
expect(API.login).toHaveBeenCalledWith('testuser', 'password');
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith('accessToken', mockAccessToken);
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith('refreshToken', mockRefreshToken);
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith('tokenExpiration', expect.any(Number));
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
'accessToken',
|
||||
mockAccessToken
|
||||
);
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
'refreshToken',
|
||||
mockRefreshToken
|
||||
);
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
'tokenExpiration',
|
||||
expect.any(Number)
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle login failure', async () => {
|
||||
|
|
@ -182,7 +208,10 @@ describe('useAuthStore', () => {
|
|||
|
||||
expect(API.refreshToken).toHaveBeenCalledWith('old-refresh-token');
|
||||
expect(newToken).toBe(mockNewAccessToken);
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith('accessToken', mockNewAccessToken);
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
'accessToken',
|
||||
mockNewAccessToken
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false if no refresh token exists', async () => {
|
||||
|
|
@ -213,7 +242,9 @@ describe('useAuthStore', () => {
|
|||
expect(result.current.isAuthenticated).toBe(false);
|
||||
expect(localStorageMock.removeItem).toHaveBeenCalledWith('accessToken');
|
||||
expect(localStorageMock.removeItem).toHaveBeenCalledWith('refreshToken');
|
||||
expect(localStorageMock.removeItem).toHaveBeenCalledWith('tokenExpiration');
|
||||
expect(localStorageMock.removeItem).toHaveBeenCalledWith(
|
||||
'tokenExpiration'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -277,7 +308,9 @@ describe('useAuthStore', () => {
|
|||
expect(result.current.user).toBeNull();
|
||||
expect(localStorageMock.removeItem).toHaveBeenCalledWith('accessToken');
|
||||
expect(localStorageMock.removeItem).toHaveBeenCalledWith('refreshToken');
|
||||
expect(localStorageMock.removeItem).toHaveBeenCalledWith('tokenExpiration');
|
||||
expect(localStorageMock.removeItem).toHaveBeenCalledWith(
|
||||
'tokenExpiration'
|
||||
);
|
||||
});
|
||||
|
||||
it('should continue logout even if API call fails', async () => {
|
||||
|
|
@ -372,7 +405,6 @@ describe('useAuthStore', () => {
|
|||
expect(fetchUsers).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it('should not fetch users for non-admin user', async () => {
|
||||
const mockUser = {
|
||||
username: 'reseller',
|
||||
|
|
@ -418,7 +450,9 @@ describe('useAuthStore', () => {
|
|||
|
||||
API.me.mockResolvedValue(mockUser);
|
||||
|
||||
const fetchChannels = vi.fn().mockRejectedValue(new Error('Fetch failed'));
|
||||
const fetchChannels = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('Fetch failed'));
|
||||
|
||||
useChannelsStore.getState = vi.fn(() => ({
|
||||
fetchChannels,
|
||||
|
|
@ -437,7 +471,11 @@ describe('useAuthStore', () => {
|
|||
describe('setUser', () => {
|
||||
it('should update user state', () => {
|
||||
const { result } = renderHook(() => useAuthStore());
|
||||
const newUser = { username: 'test', email: 'test@test.com', user_level: USER_LEVELS.ADMIN };
|
||||
const newUser = {
|
||||
username: 'test',
|
||||
email: 'test@test.com',
|
||||
user_level: USER_LEVELS.ADMIN,
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.setUser(newUser);
|
||||
|
|
@ -494,7 +532,10 @@ describe('useAuthStore', () => {
|
|||
const { result } = renderHook(() => useAuthStore());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.login({ username: 'testuser', password: 'password' });
|
||||
await result.current.login({
|
||||
username: 'testuser',
|
||||
password: 'password',
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.accessToken).toBeNull();
|
||||
|
|
@ -577,7 +618,7 @@ describe('useAuthStore', () => {
|
|||
// Reset state before the test
|
||||
useAuthStore.setState({
|
||||
isInitializing: false,
|
||||
isInitialized: false
|
||||
isInitialized: false,
|
||||
});
|
||||
|
||||
API.me.mockRejectedValue(new Error('API error'));
|
||||
|
|
@ -620,7 +661,7 @@ describe('useAuthStore', () => {
|
|||
|
||||
// Wait for the background call to complete
|
||||
await act(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
});
|
||||
|
||||
// The background fetchChannels is called synchronously without await
|
||||
|
|
|
|||
|
|
@ -243,7 +243,11 @@ describe('useChannelsStore', () => {
|
|||
const { result } = renderHook(() => useChannelsStore());
|
||||
|
||||
act(() => {
|
||||
result.current.updateProfile({ id: '1', name: 'Updated', channels: [3] });
|
||||
result.current.updateProfile({
|
||||
id: '1',
|
||||
name: 'Updated',
|
||||
channels: [3],
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.profiles['1'].name).toBe('Updated');
|
||||
|
|
@ -254,7 +258,7 @@ describe('useChannelsStore', () => {
|
|||
|
||||
act(() => {
|
||||
useChannelsStore.setState({
|
||||
profiles: { '1': { id: '1' }, '2': { id: '2' } },
|
||||
profiles: { 1: { id: '1' }, 2: { id: '2' } },
|
||||
selectedProfileId: '1',
|
||||
});
|
||||
});
|
||||
|
|
@ -274,7 +278,7 @@ describe('useChannelsStore', () => {
|
|||
|
||||
act(() => {
|
||||
useChannelsStore.setState({
|
||||
profiles: { '1': { id: '1', channels: new Set([1]) } },
|
||||
profiles: { 1: { id: '1', channels: new Set([1]) } },
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -291,7 +295,7 @@ describe('useChannelsStore', () => {
|
|||
|
||||
act(() => {
|
||||
useChannelsStore.setState({
|
||||
profiles: { '1': { id: '1', channels: new Set([1, 2, 3]) } },
|
||||
profiles: { 1: { id: '1', channels: new Set([1, 2, 3]) } },
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -316,9 +320,7 @@ describe('useChannelsStore', () => {
|
|||
});
|
||||
|
||||
const newStats = {
|
||||
channels: [
|
||||
{ channel_id: 'uuid-1', clients: [] },
|
||||
],
|
||||
channels: [{ channel_id: 'uuid-1', clients: [] }],
|
||||
};
|
||||
|
||||
act(() => {
|
||||
|
|
|
|||
|
|
@ -40,7 +40,9 @@ describe('useChannelsTableStore', () => {
|
|||
expect(result.current.channels).toEqual([]);
|
||||
expect(result.current.pageCount).toBe(0);
|
||||
expect(result.current.totalCount).toBe(0);
|
||||
expect(result.current.sorting).toEqual([{ id: 'channel_number', desc: false }]);
|
||||
expect(result.current.sorting).toEqual([
|
||||
{ id: 'channel_number', desc: false },
|
||||
]);
|
||||
expect(result.current.pagination.pageIndex).toBe(0);
|
||||
expect(result.current.pagination.pageSize).toBe(50);
|
||||
expect(result.current.selectedChannelIds).toEqual([]);
|
||||
|
|
@ -187,9 +189,7 @@ describe('useChannelsTableStore', () => {
|
|||
it('should return empty array for channel without streams', () => {
|
||||
const { result } = renderHook(() => useChannelsTableStore());
|
||||
|
||||
const mockChannels = [
|
||||
{ id: 1, name: 'Channel 1' },
|
||||
];
|
||||
const mockChannels = [{ id: 1, name: 'Channel 1' }];
|
||||
|
||||
act(() => {
|
||||
useChannelsTableStore.setState({ channels: mockChannels });
|
||||
|
|
@ -203,9 +203,7 @@ describe('useChannelsTableStore', () => {
|
|||
it('should return empty array for non-existent channel', () => {
|
||||
const { result } = renderHook(() => useChannelsTableStore());
|
||||
|
||||
const mockChannels = [
|
||||
{ id: 1, name: 'Channel 1', streams: ['stream1'] },
|
||||
];
|
||||
const mockChannels = [{ id: 1, name: 'Channel 1', streams: ['stream1'] }];
|
||||
|
||||
act(() => {
|
||||
useChannelsTableStore.setState({ channels: mockChannels });
|
||||
|
|
@ -343,7 +341,11 @@ describe('useChannelsTableStore', () => {
|
|||
useChannelsTableStore.setState({ channels: mockChannels });
|
||||
});
|
||||
|
||||
const updatedChannel = { id: 2, name: 'Updated Channel 2', channel_number: 22 };
|
||||
const updatedChannel = {
|
||||
id: 2,
|
||||
name: 'Updated Channel 2',
|
||||
channel_number: 22,
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.updateChannel(updatedChannel);
|
||||
|
|
@ -368,7 +370,11 @@ describe('useChannelsTableStore', () => {
|
|||
useChannelsTableStore.setState({ channels: mockChannels });
|
||||
});
|
||||
|
||||
const updatedChannel = { id: 999, name: 'Non-existent', channel_number: 999 };
|
||||
const updatedChannel = {
|
||||
id: 999,
|
||||
name: 'Non-existent',
|
||||
channel_number: 999,
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.updateChannel(updatedChannel);
|
||||
|
|
@ -389,7 +395,11 @@ describe('useChannelsTableStore', () => {
|
|||
useChannelsTableStore.setState({ channels: mockChannels });
|
||||
});
|
||||
|
||||
const updatedChannel = { id: 1, name: 'Updated Channel 1', channel_number: 10 };
|
||||
const updatedChannel = {
|
||||
id: 1,
|
||||
name: 'Updated Channel 1',
|
||||
channel_number: 10,
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.updateChannel(updatedChannel);
|
||||
|
|
|
|||
|
|
@ -65,7 +65,9 @@ describe('useEPGsStore', () => {
|
|||
});
|
||||
|
||||
it('should set loading state while fetching', async () => {
|
||||
api.getEPGs.mockImplementation(() => new Promise(resolve => setTimeout(() => resolve([]), 100)));
|
||||
api.getEPGs.mockImplementation(
|
||||
() => new Promise((resolve) => setTimeout(() => resolve([]), 100))
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useEPGsStore());
|
||||
|
||||
|
|
@ -87,7 +89,9 @@ describe('useEPGsStore', () => {
|
|||
const mockError = new Error('Network error');
|
||||
api.getEPGs.mockRejectedValue(mockError);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
const { result } = renderHook(() => useEPGsStore());
|
||||
|
||||
await act(async () => {
|
||||
|
|
@ -96,7 +100,10 @@ describe('useEPGsStore', () => {
|
|||
|
||||
expect(result.current.error).toBe('Failed to load epgs.');
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Failed to fetch epgs:', mockError);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Failed to fetch epgs:',
|
||||
mockError
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
|
@ -142,7 +149,9 @@ describe('useEPGsStore', () => {
|
|||
});
|
||||
|
||||
it('should set loading state while fetching', async () => {
|
||||
api.getEPGData.mockImplementation(() => new Promise(resolve => setTimeout(() => resolve([]), 100)));
|
||||
api.getEPGData.mockImplementation(
|
||||
() => new Promise((resolve) => setTimeout(() => resolve([]), 100))
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useEPGsStore());
|
||||
|
||||
|
|
@ -163,7 +172,9 @@ describe('useEPGsStore', () => {
|
|||
const mockError = new Error('API error');
|
||||
api.getEPGData.mockRejectedValue(mockError);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
const { result } = renderHook(() => useEPGsStore());
|
||||
|
||||
await act(async () => {
|
||||
|
|
@ -173,7 +184,10 @@ describe('useEPGsStore', () => {
|
|||
expect(result.current.error).toBe('Failed to load tvgs.');
|
||||
expect(result.current.tvgsLoaded).toBe(true);
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Failed to fetch tvgs:', mockError);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Failed to fetch tvgs:',
|
||||
mockError
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
|
@ -274,7 +288,9 @@ describe('useEPGsStore', () => {
|
|||
});
|
||||
|
||||
it('should not update state when called with invalid epg (null)', () => {
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
const { result } = renderHook(() => useEPGsStore());
|
||||
|
||||
const initialEPGs = { epg1: { id: 'epg1', name: 'Test' } };
|
||||
|
|
@ -287,13 +303,18 @@ describe('useEPGsStore', () => {
|
|||
});
|
||||
|
||||
expect(result.current.epgs).toEqual(initialEPGs);
|
||||
expect(consoleSpy).toHaveBeenCalledWith('updateEPG called with invalid epg:', null);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'updateEPG called with invalid epg:',
|
||||
null
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not update state when called with invalid epg (missing id)', () => {
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
const { result } = renderHook(() => useEPGsStore());
|
||||
|
||||
const initialEPGs = { epg1: { id: 'epg1', name: 'Test' } };
|
||||
|
|
@ -308,13 +329,18 @@ describe('useEPGsStore', () => {
|
|||
});
|
||||
|
||||
expect(result.current.epgs).toEqual(initialEPGs);
|
||||
expect(consoleSpy).toHaveBeenCalledWith('updateEPG called with invalid epg:', invalidEPG);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'updateEPG called with invalid epg:',
|
||||
invalidEPG
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not update state when called with invalid epg (non-object)', () => {
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
const { result } = renderHook(() => useEPGsStore());
|
||||
|
||||
const initialEPGs = { epg1: { id: 'epg1', name: 'Test' } };
|
||||
|
|
@ -327,7 +353,10 @@ describe('useEPGsStore', () => {
|
|||
});
|
||||
|
||||
expect(result.current.epgs).toEqual(initialEPGs);
|
||||
expect(consoleSpy).toHaveBeenCalledWith('updateEPG called with invalid epg:', 'invalid');
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'updateEPG called with invalid epg:',
|
||||
'invalid'
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
|
@ -518,7 +547,9 @@ describe('useEPGsStore', () => {
|
|||
});
|
||||
});
|
||||
|
||||
expect(result.current.epgs.source1.last_message).toBe('Connection failed');
|
||||
expect(result.current.epgs.source1.last_message).toBe(
|
||||
'Connection failed'
|
||||
);
|
||||
});
|
||||
|
||||
it('should use default error message if error is not provided', () => {
|
||||
|
|
@ -535,7 +566,9 @@ describe('useEPGsStore', () => {
|
|||
});
|
||||
|
||||
it('should not update state when called with invalid data (null)', () => {
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
const { result } = renderHook(() => useEPGsStore());
|
||||
|
||||
const initialEPGs = { ...result.current.epgs };
|
||||
|
|
@ -547,13 +580,18 @@ describe('useEPGsStore', () => {
|
|||
|
||||
expect(result.current.epgs).toEqual(initialEPGs);
|
||||
expect(result.current.refreshProgress).toEqual(initialProgress);
|
||||
expect(consoleSpy).toHaveBeenCalledWith('updateEPGProgress called with invalid data:', null);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'updateEPGProgress called with invalid data:',
|
||||
null
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not update state when called with invalid data (missing source)', () => {
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
const { result } = renderHook(() => useEPGsStore());
|
||||
|
||||
const initialEPGs = { ...result.current.epgs };
|
||||
|
|
@ -565,7 +603,10 @@ describe('useEPGsStore', () => {
|
|||
|
||||
expect(result.current.epgs).toEqual(initialEPGs);
|
||||
expect(result.current.refreshProgress).toEqual(initialProgress);
|
||||
expect(consoleSpy).toHaveBeenCalledWith('updateEPGProgress called with invalid data:', { progress: 50 });
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'updateEPGProgress called with invalid data:',
|
||||
{ progress: 50 }
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
|
@ -667,7 +708,11 @@ describe('useEPGsStore', () => {
|
|||
act(() => {
|
||||
useEPGsStore.setState({
|
||||
epgs: {
|
||||
source1: { id: 'source1', status: 'parsing', last_message: 'Processing' },
|
||||
source1: {
|
||||
id: 'source1',
|
||||
status: 'parsing',
|
||||
last_message: 'Processing',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -58,7 +58,11 @@ describe('useLogosStore', () => {
|
|||
it('should add logo to main logos store', () => {
|
||||
const { result } = renderHook(() => useLogosStore());
|
||||
|
||||
const newLogo = { id: 'logo1', name: 'Logo 1', url: 'http://example.com/logo1.png' };
|
||||
const newLogo = {
|
||||
id: 'logo1',
|
||||
name: 'Logo 1',
|
||||
url: 'http://example.com/logo1.png',
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.addLogo(newLogo);
|
||||
|
|
@ -76,7 +80,11 @@ describe('useLogosStore', () => {
|
|||
useLogosStore.setState({ hasLoadedChannelLogos: true });
|
||||
});
|
||||
|
||||
const newLogo = { id: 'logo1', name: 'Logo 1', url: 'http://example.com/logo1.png' };
|
||||
const newLogo = {
|
||||
id: 'logo1',
|
||||
name: 'Logo 1',
|
||||
url: 'http://example.com/logo1.png',
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.addLogo(newLogo);
|
||||
|
|
@ -89,7 +97,11 @@ describe('useLogosStore', () => {
|
|||
it('should not add logo to channelLogos if hasLoadedChannelLogos is false', () => {
|
||||
const { result } = renderHook(() => useLogosStore());
|
||||
|
||||
const newLogo = { id: 'logo1', name: 'Logo 1', url: 'http://example.com/logo1.png' };
|
||||
const newLogo = {
|
||||
id: 'logo1',
|
||||
name: 'Logo 1',
|
||||
url: 'http://example.com/logo1.png',
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.addLogo(newLogo);
|
||||
|
|
@ -104,8 +116,16 @@ describe('useLogosStore', () => {
|
|||
it('should update logo in main logos store', () => {
|
||||
const { result } = renderHook(() => useLogosStore());
|
||||
|
||||
const originalLogo = { id: 'logo1', name: 'Original', url: 'http://example.com/original.png' };
|
||||
const updatedLogo = { id: 'logo1', name: 'Updated', url: 'http://example.com/updated.png' };
|
||||
const originalLogo = {
|
||||
id: 'logo1',
|
||||
name: 'Original',
|
||||
url: 'http://example.com/original.png',
|
||||
};
|
||||
const updatedLogo = {
|
||||
id: 'logo1',
|
||||
name: 'Updated',
|
||||
url: 'http://example.com/updated.png',
|
||||
};
|
||||
|
||||
act(() => {
|
||||
useLogosStore.setState({ logos: { logo1: originalLogo } });
|
||||
|
|
@ -121,8 +141,16 @@ describe('useLogosStore', () => {
|
|||
it('should update logo in channelLogos if it exists there', () => {
|
||||
const { result } = renderHook(() => useLogosStore());
|
||||
|
||||
const originalLogo = { id: 'logo1', name: 'Original', url: 'http://example.com/original.png' };
|
||||
const updatedLogo = { id: 'logo1', name: 'Updated', url: 'http://example.com/updated.png' };
|
||||
const originalLogo = {
|
||||
id: 'logo1',
|
||||
name: 'Original',
|
||||
url: 'http://example.com/original.png',
|
||||
};
|
||||
const updatedLogo = {
|
||||
id: 'logo1',
|
||||
name: 'Updated',
|
||||
url: 'http://example.com/updated.png',
|
||||
};
|
||||
|
||||
act(() => {
|
||||
useLogosStore.setState({
|
||||
|
|
@ -142,8 +170,16 @@ describe('useLogosStore', () => {
|
|||
it('should not update channelLogos if logo does not exist there', () => {
|
||||
const { result } = renderHook(() => useLogosStore());
|
||||
|
||||
const originalLogo = { id: 'logo1', name: 'Original', url: 'http://example.com/original.png' };
|
||||
const updatedLogo = { id: 'logo1', name: 'Updated', url: 'http://example.com/updated.png' };
|
||||
const originalLogo = {
|
||||
id: 'logo1',
|
||||
name: 'Original',
|
||||
url: 'http://example.com/original.png',
|
||||
};
|
||||
const updatedLogo = {
|
||||
id: 'logo1',
|
||||
name: 'Updated',
|
||||
url: 'http://example.com/updated.png',
|
||||
};
|
||||
|
||||
act(() => {
|
||||
useLogosStore.setState({ logos: { logo1: originalLogo } });
|
||||
|
|
@ -249,7 +285,9 @@ describe('useLogosStore', () => {
|
|||
const mockError = new Error('Network error');
|
||||
api.getLogos.mockRejectedValue(mockError);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
const { result } = renderHook(() => useLogosStore());
|
||||
|
||||
await expect(
|
||||
|
|
@ -261,7 +299,10 @@ describe('useLogosStore', () => {
|
|||
await waitFor(() => {
|
||||
expect(result.current.error).toBe('Failed to load logos.');
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Failed to fetch logos:', mockError);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Failed to fetch logos:',
|
||||
mockError
|
||||
);
|
||||
});
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
|
|
@ -354,7 +395,9 @@ describe('useLogosStore', () => {
|
|||
const mockError = new Error('API error');
|
||||
api.getLogos.mockRejectedValue(mockError);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
const { result } = renderHook(() => useLogosStore());
|
||||
|
||||
await expect(
|
||||
|
|
@ -366,7 +409,10 @@ describe('useLogosStore', () => {
|
|||
await waitFor(() => {
|
||||
expect(result.current.error).toBe('Failed to load all logos.');
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Failed to fetch all logos:', mockError);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Failed to fetch all logos:',
|
||||
mockError
|
||||
);
|
||||
});
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
|
|
@ -396,7 +442,10 @@ describe('useLogosStore', () => {
|
|||
logo2: { id: 'logo2', name: 'Used Logo 2' },
|
||||
});
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(api.getLogos).toHaveBeenCalledWith({ used: 'true', page_size: 100 });
|
||||
expect(api.getLogos).toHaveBeenCalledWith({
|
||||
used: 'true',
|
||||
page_size: 100,
|
||||
});
|
||||
expect(response).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
|
|
@ -426,7 +475,9 @@ describe('useLogosStore', () => {
|
|||
const mockError = new Error('Fetch error');
|
||||
api.getLogos.mockRejectedValue(mockError);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
const { result } = renderHook(() => useLogosStore());
|
||||
|
||||
await expect(
|
||||
|
|
@ -437,7 +488,10 @@ describe('useLogosStore', () => {
|
|||
|
||||
await waitFor(() => {
|
||||
expect(result.current.error).toBe('Failed to load used logos.');
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Failed to fetch used logos:', mockError);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Failed to fetch used logos:',
|
||||
mockError
|
||||
);
|
||||
});
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
|
|
@ -537,7 +591,9 @@ describe('useLogosStore', () => {
|
|||
const mockError = new Error('Fetch error');
|
||||
api.getLogosByIds.mockRejectedValue(mockError);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
const { result } = renderHook(() => useLogosStore());
|
||||
|
||||
await expect(
|
||||
|
|
@ -546,7 +602,10 @@ describe('useLogosStore', () => {
|
|||
})
|
||||
).rejects.toThrow('Fetch error');
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Failed to fetch logos by IDs:', mockError);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Failed to fetch logos by IDs:',
|
||||
mockError
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
|
@ -565,9 +624,7 @@ describe('useLogosStore', () => {
|
|||
next: null,
|
||||
};
|
||||
|
||||
api.getLogos
|
||||
.mockResolvedValueOnce(page1)
|
||||
.mockResolvedValueOnce(page2);
|
||||
api.getLogos.mockResolvedValueOnce(page1).mockResolvedValueOnce(page2);
|
||||
|
||||
const { result } = renderHook(() => useLogosStore());
|
||||
|
||||
|
|
@ -589,7 +646,9 @@ describe('useLogosStore', () => {
|
|||
const mockError = new Error('Network error');
|
||||
api.getLogos.mockRejectedValue(mockError);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
const { result } = renderHook(() => useLogosStore());
|
||||
|
||||
await act(async () => {
|
||||
|
|
@ -597,7 +656,10 @@ describe('useLogosStore', () => {
|
|||
});
|
||||
|
||||
expect(result.current.backgroundLoading).toBe(false);
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Background logo loading failed:', mockError);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Background logo loading failed:',
|
||||
mockError
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
|
@ -665,7 +727,9 @@ describe('useLogosStore', () => {
|
|||
const mockError = new Error('Fetch error');
|
||||
api.getLogos.mockRejectedValue(mockError);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
const { result } = renderHook(() => useLogosStore());
|
||||
|
||||
result.current.backgroundLoadAllLogos();
|
||||
|
|
@ -675,7 +739,10 @@ describe('useLogosStore', () => {
|
|||
});
|
||||
|
||||
expect(result.current.backgroundLoading).toBe(false);
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Background all logos loading failed:', mockError);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Background all logos loading failed:',
|
||||
mockError
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
|
||||
|
|
@ -713,7 +780,10 @@ describe('useLogosStore', () => {
|
|||
});
|
||||
|
||||
it('should not start if channelLogos already has many items', async () => {
|
||||
const channelLogos = Array.from({ length: 150 }, (_, i) => [`logo${i}`, { id: `logo${i}` }]);
|
||||
const channelLogos = Array.from({ length: 150 }, (_, i) => [
|
||||
`logo${i}`,
|
||||
{ id: `logo${i}` },
|
||||
]);
|
||||
const channelLogosObj = Object.fromEntries(channelLogos);
|
||||
|
||||
const { result } = renderHook(() => useLogosStore());
|
||||
|
|
@ -747,8 +817,12 @@ describe('useLogosStore', () => {
|
|||
expect(result.current.hasLoadedChannelLogos).toBe(true);
|
||||
expect(result.current.backgroundLoading).toBe(false);
|
||||
expect(Object.keys(result.current.channelLogos).length).toBe(2);
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Background loading channel logos...');
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Background loaded 2 channel logos');
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Background loading channel logos...'
|
||||
);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Background loaded 2 channel logos'
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
|
@ -757,8 +831,12 @@ describe('useLogosStore', () => {
|
|||
const mockError = new Error('Fetch error');
|
||||
api.getLogos.mockRejectedValue(mockError);
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
const consoleLogSpy = vi
|
||||
.spyOn(console, 'log')
|
||||
.mockImplementation(() => {});
|
||||
const { result } = renderHook(() => useLogosStore());
|
||||
|
||||
await act(async () => {
|
||||
|
|
@ -766,7 +844,10 @@ describe('useLogosStore', () => {
|
|||
});
|
||||
|
||||
expect(result.current.backgroundLoading).toBe(false);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Background channel logo loading failed:', mockError);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Background channel logo loading failed:',
|
||||
mockError
|
||||
);
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
consoleLogSpy.mockRestore();
|
||||
|
|
@ -801,7 +882,9 @@ describe('useLogosStore', () => {
|
|||
const mockError = new Error('Background error');
|
||||
api.getLogos.mockRejectedValue(mockError);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
const { result } = renderHook(() => useLogosStore());
|
||||
|
||||
result.current.startBackgroundLoading();
|
||||
|
|
|
|||
|
|
@ -65,13 +65,17 @@ describe('usePlaylistsStore', () => {
|
|||
|
||||
expect(api.getPlaylist).toHaveBeenCalledWith('playlist1');
|
||||
expect(result.current.playlists).toEqual([mockPlaylist]);
|
||||
expect(result.current.profiles).toEqual({ playlist1: ['profile1', 'profile2'] });
|
||||
expect(result.current.profiles).toEqual({
|
||||
playlist1: ['profile1', 'profile2'],
|
||||
});
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.error).toBe(null);
|
||||
});
|
||||
|
||||
it('should handle fetch playlist error', async () => {
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
api.getPlaylist.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
const { result } = renderHook(() => usePlaylistsStore());
|
||||
|
|
@ -110,7 +114,9 @@ describe('usePlaylistsStore', () => {
|
|||
});
|
||||
|
||||
it('should handle fetch playlists error', async () => {
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
api.getPlaylists.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
const { result } = renderHook(() => usePlaylistsStore());
|
||||
|
|
@ -142,8 +148,16 @@ describe('usePlaylistsStore', () => {
|
|||
|
||||
it('should update playlist', () => {
|
||||
const { result } = renderHook(() => usePlaylistsStore());
|
||||
const existingPlaylist = { id: 'playlist1', name: 'Old Name', profiles: ['profile1'] };
|
||||
const updatedPlaylist = { id: 'playlist1', name: 'New Name', profiles: ['profile1', 'profile2'] };
|
||||
const existingPlaylist = {
|
||||
id: 'playlist1',
|
||||
name: 'Old Name',
|
||||
profiles: ['profile1'],
|
||||
};
|
||||
const updatedPlaylist = {
|
||||
id: 'playlist1',
|
||||
name: 'New Name',
|
||||
profiles: ['profile1', 'profile2'],
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.playlists = [existingPlaylist];
|
||||
|
|
@ -155,7 +169,9 @@ describe('usePlaylistsStore', () => {
|
|||
});
|
||||
|
||||
expect(result.current.playlists).toEqual([updatedPlaylist]);
|
||||
expect(result.current.profiles).toEqual({ playlist1: ['profile1', 'profile2'] });
|
||||
expect(result.current.profiles).toEqual({
|
||||
playlist1: ['profile1', 'profile2'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should update profiles', () => {
|
||||
|
|
@ -166,10 +182,16 @@ describe('usePlaylistsStore', () => {
|
|||
});
|
||||
|
||||
act(() => {
|
||||
result.current.updateProfiles('playlist1', ['profile1', 'profile2', 'profile3']);
|
||||
result.current.updateProfiles('playlist1', [
|
||||
'profile1',
|
||||
'profile2',
|
||||
'profile3',
|
||||
]);
|
||||
});
|
||||
|
||||
expect(result.current.profiles).toEqual({ playlist1: ['profile1', 'profile2', 'profile3'] });
|
||||
expect(result.current.profiles).toEqual({
|
||||
playlist1: ['profile1', 'profile2', 'profile3'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove playlists', () => {
|
||||
|
|
@ -187,7 +209,9 @@ describe('usePlaylistsStore', () => {
|
|||
result.current.removePlaylists(['playlist1', 'playlist3']);
|
||||
});
|
||||
|
||||
expect(result.current.playlists).toEqual([{ id: 'playlist2', name: 'Playlist 2' }]);
|
||||
expect(result.current.playlists).toEqual([
|
||||
{ id: 'playlist2', name: 'Playlist 2' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should set refresh progress with two parameters', () => {
|
||||
|
|
@ -228,7 +252,11 @@ describe('usePlaylistsStore', () => {
|
|||
expect(result.current.refreshProgress.account1.action).toBe('initializing');
|
||||
|
||||
act(() => {
|
||||
result.current.setRefreshProgress({ account: 'account1', action: 'refreshing', progress: 25 });
|
||||
result.current.setRefreshProgress({
|
||||
account: 'account1',
|
||||
action: 'refreshing',
|
||||
progress: 25,
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.refreshProgress.account1.action).toBe('refreshing');
|
||||
|
|
|
|||
|
|
@ -136,7 +136,11 @@ describe('usePluginStore', () => {
|
|||
result.current.updatePlugin('plugin1', { name: 'Updated Plugin' });
|
||||
});
|
||||
|
||||
expect(result.current.plugins[1]).toEqual({ key: 'plugin2', name: 'Plugin 2', enabled: false });
|
||||
expect(result.current.plugins[1]).toEqual({
|
||||
key: 'plugin2',
|
||||
name: 'Plugin 2',
|
||||
enabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should add plugin', () => {
|
||||
|
|
@ -152,7 +156,11 @@ describe('usePluginStore', () => {
|
|||
|
||||
it('should add plugin to existing plugins', () => {
|
||||
const { result } = renderHook(() => usePluginStore());
|
||||
const existingPlugin = { key: 'plugin1', name: 'Existing Plugin', enabled: true };
|
||||
const existingPlugin = {
|
||||
key: 'plugin1',
|
||||
name: 'Existing Plugin',
|
||||
enabled: true,
|
||||
};
|
||||
const newPlugin = { key: 'plugin2', name: 'New Plugin', enabled: false };
|
||||
|
||||
act(() => {
|
||||
|
|
@ -192,9 +200,7 @@ describe('usePluginStore', () => {
|
|||
|
||||
act(() => {
|
||||
usePluginStore.setState({
|
||||
plugins: [
|
||||
{ key: 'plugin1', name: 'Plugin 1', enabled: true },
|
||||
],
|
||||
plugins: [{ key: 'plugin1', name: 'Plugin 1', enabled: true }],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -208,9 +214,7 @@ describe('usePluginStore', () => {
|
|||
});
|
||||
|
||||
it('should invalidate plugins and refetch', async () => {
|
||||
const mockPlugins = [
|
||||
{ key: 'plugin1', name: 'Plugin 1', enabled: true },
|
||||
];
|
||||
const mockPlugins = [{ key: 'plugin1', name: 'Plugin 1', enabled: true }];
|
||||
|
||||
API.getPlugins.mockResolvedValue(mockPlugins);
|
||||
|
||||
|
|
@ -218,9 +222,7 @@ describe('usePluginStore', () => {
|
|||
|
||||
act(() => {
|
||||
usePluginStore.setState({
|
||||
plugins: [
|
||||
{ key: 'old-plugin', name: 'Old Plugin', enabled: false },
|
||||
],
|
||||
plugins: [{ key: 'old-plugin', name: 'Old Plugin', enabled: false }],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -190,7 +190,10 @@ describe('useSettingsStore', () => {
|
|||
result.current.updateSetting({ key: 'setting1', value: 'updated' });
|
||||
});
|
||||
|
||||
expect(result.current.settings.setting2).toEqual({ key: 'setting2', value: 'value2' });
|
||||
expect(result.current.settings.setting2).toEqual({
|
||||
key: 'setting2',
|
||||
value: 'value2',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty settings array', async () => {
|
||||
|
|
@ -249,7 +252,10 @@ describe('useSettingsStore', () => {
|
|||
},
|
||||
});
|
||||
|
||||
api.getVersion.mockResolvedValue({ version: '2.0.0', timestamp: '2024-01-01T00:00:00Z' });
|
||||
api.getVersion.mockResolvedValue({
|
||||
version: '2.0.0',
|
||||
timestamp: '2024-01-01T00:00:00Z',
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useSettingsStore());
|
||||
|
||||
|
|
@ -336,7 +342,10 @@ describe('useSettingsStore', () => {
|
|||
|
||||
api.getSettings.mockResolvedValue(mockSettings);
|
||||
api.getEnvironmentSettings.mockResolvedValue(mockEnv);
|
||||
api.getVersion.mockResolvedValue({ version: '2.0.0', timestamp: '2024-01-01T00:00:00Z' });
|
||||
api.getVersion.mockResolvedValue({
|
||||
version: '2.0.0',
|
||||
timestamp: '2024-01-01T00:00:00Z',
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useSettingsStore());
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,9 @@ describe('useStreamProfilesStore', () => {
|
|||
const mockError = new Error('Network error');
|
||||
api.getStreamProfiles.mockRejectedValue(mockError);
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const { result } = renderHook(() => useStreamProfilesStore());
|
||||
|
||||
|
|
@ -57,7 +59,10 @@ describe('useStreamProfilesStore', () => {
|
|||
|
||||
expect(result.current.error).toBe('Failed to load profiles.');
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch profiles:', mockError);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Failed to fetch profiles:',
|
||||
mockError
|
||||
);
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
|
@ -152,7 +157,11 @@ describe('useStreamProfilesStore', () => {
|
|||
result.current.updateStreamProfile(updatedProfile);
|
||||
});
|
||||
|
||||
expect(result.current.profiles[1]).toEqual({ id: 2, name: 'Profile 2', bitrate: 8000 });
|
||||
expect(result.current.profiles[1]).toEqual({
|
||||
id: 2,
|
||||
name: 'Profile 2',
|
||||
bitrate: 8000,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not modify profiles when updating non-existent profile', () => {
|
||||
|
|
@ -166,7 +175,11 @@ describe('useStreamProfilesStore', () => {
|
|||
});
|
||||
|
||||
const { result } = renderHook(() => useStreamProfilesStore());
|
||||
const nonExistentProfile = { id: 999, name: 'Non-existent', bitrate: 10000 };
|
||||
const nonExistentProfile = {
|
||||
id: 999,
|
||||
name: 'Non-existent',
|
||||
bitrate: 10000,
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.updateStreamProfile(nonExistentProfile);
|
||||
|
|
@ -246,9 +259,7 @@ describe('useStreamProfilesStore', () => {
|
|||
});
|
||||
|
||||
it('should handle empty array when removing profiles', () => {
|
||||
const initialProfiles = [
|
||||
{ id: 1, name: 'Profile 1', bitrate: 5000 },
|
||||
];
|
||||
const initialProfiles = [{ id: 1, name: 'Profile 1', bitrate: 5000 }];
|
||||
|
||||
useStreamProfilesStore.setState({
|
||||
profiles: initialProfiles,
|
||||
|
|
|
|||
|
|
@ -53,7 +53,9 @@ describe('useStreamsStore', () => {
|
|||
const mockError = new Error('Network error');
|
||||
api.getStreams.mockRejectedValue(mockError);
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const { result } = renderHook(() => useStreamsStore());
|
||||
|
||||
|
|
@ -63,7 +65,10 @@ describe('useStreamsStore', () => {
|
|||
|
||||
expect(result.current.error).toBe('Failed to load streams.');
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch streams:', mockError);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Failed to fetch streams:',
|
||||
mockError
|
||||
);
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
|
@ -131,7 +136,11 @@ describe('useStreamsStore', () => {
|
|||
});
|
||||
|
||||
const { result } = renderHook(() => useStreamsStore());
|
||||
const updatedStream = { id: 1, name: 'Updated Stream', url: 'http://example.com/updated' };
|
||||
const updatedStream = {
|
||||
id: 1,
|
||||
name: 'Updated Stream',
|
||||
url: 'http://example.com/updated',
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.updateStream(updatedStream);
|
||||
|
|
@ -152,13 +161,21 @@ describe('useStreamsStore', () => {
|
|||
});
|
||||
|
||||
const { result } = renderHook(() => useStreamsStore());
|
||||
const updatedStream = { id: 1, name: 'Updated Stream', url: 'http://example.com/updated' };
|
||||
const updatedStream = {
|
||||
id: 1,
|
||||
name: 'Updated Stream',
|
||||
url: 'http://example.com/updated',
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.updateStream(updatedStream);
|
||||
});
|
||||
|
||||
expect(result.current.streams[1]).toEqual({ id: 2, name: 'Stream 2', url: 'http://example.com/2' });
|
||||
expect(result.current.streams[1]).toEqual({
|
||||
id: 2,
|
||||
name: 'Stream 2',
|
||||
url: 'http://example.com/2',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not modify streams when updating non-existent stream', () => {
|
||||
|
|
@ -172,7 +189,11 @@ describe('useStreamsStore', () => {
|
|||
});
|
||||
|
||||
const { result } = renderHook(() => useStreamsStore());
|
||||
const nonExistentStream = { id: 999, name: 'Non-existent', url: 'http://example.com/999' };
|
||||
const nonExistentStream = {
|
||||
id: 999,
|
||||
name: 'Non-existent',
|
||||
url: 'http://example.com/999',
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.updateStream(nonExistentStream);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const localStorageMock = (() => {
|
|||
}),
|
||||
removeItem: vi.fn((key) => {
|
||||
delete store[key];
|
||||
})
|
||||
}),
|
||||
};
|
||||
})();
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ describe('useStreamsTableStore', () => {
|
|||
pagination: {
|
||||
pageIndex: 0,
|
||||
pageSize: JSON.parse(localStorage.getItem('streams-page-size')) || 50,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useStreamsTableStore());
|
||||
|
|
@ -74,7 +74,7 @@ describe('useStreamsTableStore', () => {
|
|||
pagination: {
|
||||
pageIndex: 0,
|
||||
pageSize: JSON.parse(localStorage.getItem('streams-page-size')) || 50,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useStreamsTableStore());
|
||||
|
|
@ -112,10 +112,7 @@ describe('useStreamsTableStore', () => {
|
|||
const mockParams = new URLSearchParams({ page_size: '25' });
|
||||
|
||||
act(() => {
|
||||
result.current.queryStreams(
|
||||
{ results: [], count: 75 },
|
||||
mockParams
|
||||
);
|
||||
result.current.queryStreams({ results: [], count: 75 }, mockParams);
|
||||
});
|
||||
|
||||
expect(result.current.pageCount).toBe(3); // Math.ceil(75 / 25)
|
||||
|
|
@ -127,10 +124,7 @@ describe('useStreamsTableStore', () => {
|
|||
const mockParams = new URLSearchParams({ page_size: '50' });
|
||||
|
||||
act(() => {
|
||||
result.current.queryStreams(
|
||||
{ results: [], count: 0 },
|
||||
mockParams
|
||||
);
|
||||
result.current.queryStreams({ results: [], count: 0 }, mockParams);
|
||||
});
|
||||
|
||||
expect(result.current.streams).toEqual([]);
|
||||
|
|
@ -340,7 +334,9 @@ describe('useStreamsTableStore', () => {
|
|||
expect(result.current.totalCount).toBe(50);
|
||||
expect(result.current.pageCount).toBe(2);
|
||||
expect(result.current.pagination).toEqual({ pageIndex: 0, pageSize: 25 });
|
||||
expect(result.current.sorting).toEqual([{ id: 'created_at', desc: true }]);
|
||||
expect(result.current.sorting).toEqual([
|
||||
{ id: 'created_at', desc: true },
|
||||
]);
|
||||
expect(result.current.allQueryIds).toEqual([1, 2, 3, 4, 5]);
|
||||
expect(result.current.selectedStreamIds).toEqual([1, 2]);
|
||||
expect(result.current.lastQueryParams).toBe(mockParams);
|
||||
|
|
|
|||
|
|
@ -103,7 +103,12 @@ describe('useVODStore', () => {
|
|||
expect(api.getAllContent).toHaveBeenCalled();
|
||||
expect(result.current.currentPageContent).toEqual([
|
||||
{ id: 1, name: 'Movie 1', content_type: 'movie', contentType: 'movie' },
|
||||
{ id: 2, name: 'Series 1', content_type: 'series', contentType: 'series' },
|
||||
{
|
||||
id: 2,
|
||||
name: 'Series 1',
|
||||
content_type: 'series',
|
||||
contentType: 'series',
|
||||
},
|
||||
]);
|
||||
expect(result.current.totalCount).toBe(2);
|
||||
expect(result.current.loading).toBe(false);
|
||||
|
|
@ -171,7 +176,9 @@ describe('useVODStore', () => {
|
|||
const mockError = new Error('Network error');
|
||||
api.getAllContent.mockRejectedValue(mockError);
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const { result } = renderHook(() => useVODStore());
|
||||
|
||||
|
|
@ -181,7 +188,10 @@ describe('useVODStore', () => {
|
|||
|
||||
expect(result.current.error).toBe('Failed to load content.');
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch content:', mockError);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Failed to fetch content:',
|
||||
mockError
|
||||
);
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
|
@ -189,7 +199,9 @@ describe('useVODStore', () => {
|
|||
it('should handle invalid response format', async () => {
|
||||
api.getAllContent.mockResolvedValue({ results: 'not-an-array' });
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const { result } = renderHook(() => useVODStore());
|
||||
|
||||
|
|
@ -234,7 +246,9 @@ describe('useVODStore', () => {
|
|||
const mockError = new Error('Not found');
|
||||
api.getMovieDetails.mockRejectedValue(mockError);
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const { result } = renderHook(() => useVODStore());
|
||||
|
||||
|
|
@ -248,7 +262,10 @@ describe('useVODStore', () => {
|
|||
|
||||
expect(result.current.error).toBe('Failed to load movie details.');
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch movie details:', mockError);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Failed to fetch movie details:',
|
||||
mockError
|
||||
);
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
|
@ -283,7 +300,9 @@ describe('useVODStore', () => {
|
|||
const mockError = new Error('Provider error');
|
||||
api.getMovieProviderInfo.mockRejectedValue(mockError);
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const { result } = renderHook(() => useVODStore());
|
||||
|
||||
|
|
@ -295,8 +314,13 @@ describe('useVODStore', () => {
|
|||
}
|
||||
});
|
||||
|
||||
expect(result.current.error).toBe('Failed to load movie details from provider.');
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch movie details from provider:', mockError);
|
||||
expect(result.current.error).toBe(
|
||||
'Failed to load movie details from provider.'
|
||||
);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Failed to fetch movie details from provider:',
|
||||
mockError
|
||||
);
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
|
@ -324,7 +348,9 @@ describe('useVODStore', () => {
|
|||
const mockError = new Error('Providers error');
|
||||
api.getMovieProviders.mockRejectedValue(mockError);
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const { result } = renderHook(() => useVODStore());
|
||||
|
||||
|
|
@ -336,7 +362,10 @@ describe('useVODStore', () => {
|
|||
}
|
||||
});
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch movie providers:', mockError);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Failed to fetch movie providers:',
|
||||
mockError
|
||||
);
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
|
@ -361,7 +390,9 @@ describe('useVODStore', () => {
|
|||
const mockError = new Error('Series providers error');
|
||||
api.getSeriesProviders.mockRejectedValue(mockError);
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const { result } = renderHook(() => useVODStore());
|
||||
|
||||
|
|
@ -373,7 +404,10 @@ describe('useVODStore', () => {
|
|||
}
|
||||
});
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch series providers:', mockError);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Failed to fetch series providers:',
|
||||
mockError
|
||||
);
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
|
@ -428,7 +462,9 @@ describe('useVODStore', () => {
|
|||
const mockError = new Error('Series not found');
|
||||
api.getSeriesInfo.mockRejectedValue(mockError);
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const { result } = renderHook(() => useVODStore());
|
||||
|
||||
|
|
@ -442,7 +478,10 @@ describe('useVODStore', () => {
|
|||
|
||||
expect(result.current.error).toBe('Failed to load series details.');
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch series info:', mockError);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Failed to fetch series info:',
|
||||
mockError
|
||||
);
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
|
@ -494,7 +533,9 @@ describe('useVODStore', () => {
|
|||
const mockError = new Error('Categories error');
|
||||
api.getVODCategories.mockRejectedValue(mockError);
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const { result } = renderHook(() => useVODStore());
|
||||
|
||||
|
|
@ -503,7 +544,10 @@ describe('useVODStore', () => {
|
|||
});
|
||||
|
||||
expect(result.current.error).toBe('Failed to load categories.');
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch VOD categories:', mockError);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Failed to fetch VOD categories:',
|
||||
mockError
|
||||
);
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -68,7 +68,9 @@ describe('useVideoStore', () => {
|
|||
const { result } = renderHook(() => useVideoStore());
|
||||
|
||||
act(() => {
|
||||
result.current.showVideo('http://example.com/stream.ts', 'vod', { title: 'Test' });
|
||||
result.current.showVideo('http://example.com/stream.ts', 'vod', {
|
||||
title: 'Test',
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.isVisible).toBe(true);
|
||||
|
|
@ -121,7 +123,7 @@ describe('useVideoStore', () => {
|
|||
const metadata = {
|
||||
title: 'Test Video',
|
||||
duration: 120,
|
||||
thumbnailUrl: 'http://example.com/thumb.jpg'
|
||||
thumbnailUrl: 'http://example.com/thumb.jpg',
|
||||
};
|
||||
|
||||
act(() => {
|
||||
|
|
@ -139,13 +141,21 @@ describe('useVideoStore', () => {
|
|||
const secondMetadata = { title: 'Second Video' };
|
||||
|
||||
act(() => {
|
||||
result.current.showVideo('http://example.com/first.mp4', 'vod', firstMetadata);
|
||||
result.current.showVideo(
|
||||
'http://example.com/first.mp4',
|
||||
'vod',
|
||||
firstMetadata
|
||||
);
|
||||
});
|
||||
|
||||
expect(result.current.metadata).toEqual(firstMetadata);
|
||||
|
||||
act(() => {
|
||||
result.current.showVideo('http://example.com/second.mp4', 'vod', secondMetadata);
|
||||
result.current.showVideo(
|
||||
'http://example.com/second.mp4',
|
||||
'vod',
|
||||
secondMetadata
|
||||
);
|
||||
});
|
||||
|
||||
expect(result.current.metadata).toEqual(secondMetadata);
|
||||
|
|
|
|||
|
|
@ -47,7 +47,9 @@ describe('useUserAgentsStore', () => {
|
|||
const mockError = new Error('Network error');
|
||||
api.getUserAgents.mockRejectedValue(mockError);
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const { result } = renderHook(() => useUserAgentsStore());
|
||||
|
||||
|
|
@ -57,7 +59,10 @@ describe('useUserAgentsStore', () => {
|
|||
|
||||
expect(result.current.error).toBe('Failed to load userAgents.');
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch userAgents:', mockError);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Failed to fetch userAgents:',
|
||||
mockError
|
||||
);
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
|
@ -125,7 +130,11 @@ describe('useUserAgentsStore', () => {
|
|||
});
|
||||
|
||||
const { result } = renderHook(() => useUserAgentsStore());
|
||||
const updatedUserAgent = { id: 1, name: 'Chrome Updated', string: 'Mozilla/5.0 Updated...' };
|
||||
const updatedUserAgent = {
|
||||
id: 1,
|
||||
name: 'Chrome Updated',
|
||||
string: 'Mozilla/5.0 Updated...',
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.updateUserAgent(updatedUserAgent);
|
||||
|
|
@ -146,13 +155,21 @@ describe('useUserAgentsStore', () => {
|
|||
});
|
||||
|
||||
const { result } = renderHook(() => useUserAgentsStore());
|
||||
const updatedUserAgent = { id: 1, name: 'Chrome Updated', string: 'Mozilla/5.0 Updated...' };
|
||||
const updatedUserAgent = {
|
||||
id: 1,
|
||||
name: 'Chrome Updated',
|
||||
string: 'Mozilla/5.0 Updated...',
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.updateUserAgent(updatedUserAgent);
|
||||
});
|
||||
|
||||
expect(result.current.userAgents[1]).toEqual({ id: 2, name: 'Firefox', string: 'Mozilla/5.0...' });
|
||||
expect(result.current.userAgents[1]).toEqual({
|
||||
id: 2,
|
||||
name: 'Firefox',
|
||||
string: 'Mozilla/5.0...',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not modify user agents when updating non-existent user agent', () => {
|
||||
|
|
@ -166,7 +183,11 @@ describe('useUserAgentsStore', () => {
|
|||
});
|
||||
|
||||
const { result } = renderHook(() => useUserAgentsStore());
|
||||
const nonExistentUserAgent = { id: 999, name: 'Non-existent', string: 'Mozilla/5.0...' };
|
||||
const nonExistentUserAgent = {
|
||||
id: 999,
|
||||
name: 'Non-existent',
|
||||
string: 'Mozilla/5.0...',
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.updateUserAgent(nonExistentUserAgent);
|
||||
|
|
|
|||
|
|
@ -47,7 +47,9 @@ describe('useUsersStore', () => {
|
|||
const mockError = new Error('Network error');
|
||||
api.getUsers.mockRejectedValue(mockError);
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const { result } = renderHook(() => useUsersStore());
|
||||
|
||||
|
|
@ -57,7 +59,10 @@ describe('useUsersStore', () => {
|
|||
|
||||
expect(result.current.error).toBe('Failed to load users.');
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch users:', mockError);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Failed to fetch users:',
|
||||
mockError
|
||||
);
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
|
@ -125,7 +130,11 @@ describe('useUsersStore', () => {
|
|||
});
|
||||
|
||||
const { result } = renderHook(() => useUsersStore());
|
||||
const updatedUser = { id: 1, name: 'Updated User', email: 'updated@example.com' };
|
||||
const updatedUser = {
|
||||
id: 1,
|
||||
name: 'Updated User',
|
||||
email: 'updated@example.com',
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.updateUser(updatedUser);
|
||||
|
|
@ -146,13 +155,21 @@ describe('useUsersStore', () => {
|
|||
});
|
||||
|
||||
const { result } = renderHook(() => useUsersStore());
|
||||
const updatedUser = { id: 1, name: 'Updated User', email: 'updated@example.com' };
|
||||
const updatedUser = {
|
||||
id: 1,
|
||||
name: 'Updated User',
|
||||
email: 'updated@example.com',
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.updateUser(updatedUser);
|
||||
});
|
||||
|
||||
expect(result.current.users[1]).toEqual({ id: 2, name: 'User 2', email: 'user2@example.com' });
|
||||
expect(result.current.users[1]).toEqual({
|
||||
id: 2,
|
||||
name: 'User 2',
|
||||
email: 'user2@example.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not modify users when updating non-existent user', () => {
|
||||
|
|
@ -166,7 +183,11 @@ describe('useUsersStore', () => {
|
|||
});
|
||||
|
||||
const { result } = renderHook(() => useUsersStore());
|
||||
const nonExistentUser = { id: 999, name: 'Non-existent', email: 'none@example.com' };
|
||||
const nonExistentUser = {
|
||||
id: 999,
|
||||
name: 'Non-existent',
|
||||
email: 'none@example.com',
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.updateUser(nonExistentUser);
|
||||
|
|
@ -252,7 +273,15 @@ describe('useUsersStore', () => {
|
|||
result.current.removeUser(2);
|
||||
});
|
||||
|
||||
expect(result.current.users[0]).toEqual({ id: 1, name: 'User 1', email: 'user1@example.com' });
|
||||
expect(result.current.users[1]).toEqual({ id: 3, name: 'User 3', email: 'user3@example.com' });
|
||||
expect(result.current.users[0]).toEqual({
|
||||
id: 1,
|
||||
name: 'User 1',
|
||||
email: 'user1@example.com',
|
||||
});
|
||||
expect(result.current.users[1]).toEqual({
|
||||
id: 3,
|
||||
name: 'User 3',
|
||||
email: 'user3@example.com',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -105,7 +105,9 @@ describe('useVODLogosStore', () => {
|
|||
const mockError = new Error('Network error');
|
||||
api.getVODLogos.mockRejectedValue(mockError);
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const { result } = renderHook(() => useVODLogosStore());
|
||||
|
||||
|
|
@ -119,7 +121,10 @@ describe('useVODLogosStore', () => {
|
|||
|
||||
expect(result.current.error).toBe('Failed to load VOD logos.');
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch VOD logos:', mockError);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Failed to fetch VOD logos:',
|
||||
mockError
|
||||
);
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
|
@ -205,9 +210,7 @@ describe('useVODLogosStore', () => {
|
|||
expect(result.current.vodLogos).toEqual({
|
||||
2: { id: 2, name: 'Logo 2' },
|
||||
});
|
||||
expect(result.current.logos).toEqual([
|
||||
{ id: 2, name: 'Logo 2' },
|
||||
]);
|
||||
expect(result.current.logos).toEqual([{ id: 2, name: 'Logo 2' }]);
|
||||
expect(result.current.totalCount).toBe(1);
|
||||
});
|
||||
|
||||
|
|
@ -236,9 +239,7 @@ describe('useVODLogosStore', () => {
|
|||
expect(result.current.vodLogos).toEqual({
|
||||
2: { id: 2, name: 'Logo 2' },
|
||||
});
|
||||
expect(result.current.logos).toEqual([
|
||||
{ id: 2, name: 'Logo 2' },
|
||||
]);
|
||||
expect(result.current.logos).toEqual([{ id: 2, name: 'Logo 2' }]);
|
||||
expect(result.current.totalCount).toBe(1);
|
||||
});
|
||||
|
||||
|
|
@ -246,7 +247,9 @@ describe('useVODLogosStore', () => {
|
|||
const mockError = new Error('Delete failed');
|
||||
api.deleteVODLogo.mockRejectedValue(mockError);
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const { result } = renderHook(() => useVODLogosStore());
|
||||
|
||||
|
|
@ -258,7 +261,10 @@ describe('useVODLogosStore', () => {
|
|||
}
|
||||
});
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to delete VOD logo:', mockError);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Failed to delete VOD logo:',
|
||||
mockError
|
||||
);
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
|
@ -290,9 +296,7 @@ describe('useVODLogosStore', () => {
|
|||
expect(result.current.vodLogos).toEqual({
|
||||
3: { id: 3, name: 'Logo 3' },
|
||||
});
|
||||
expect(result.current.logos).toEqual([
|
||||
{ id: 3, name: 'Logo 3' },
|
||||
]);
|
||||
expect(result.current.logos).toEqual([{ id: 3, name: 'Logo 3' }]);
|
||||
expect(result.current.totalCount).toBe(1);
|
||||
});
|
||||
|
||||
|
|
@ -300,7 +304,9 @@ describe('useVODLogosStore', () => {
|
|||
const mockError = new Error('Bulk delete failed');
|
||||
api.deleteVODLogos.mockRejectedValue(mockError);
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const { result } = renderHook(() => useVODLogosStore());
|
||||
|
||||
|
|
@ -312,7 +318,10 @@ describe('useVODLogosStore', () => {
|
|||
}
|
||||
});
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to delete VOD logos:', mockError);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Failed to delete VOD logos:',
|
||||
mockError
|
||||
);
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
|
@ -348,7 +357,9 @@ describe('useVODLogosStore', () => {
|
|||
const mockError = new Error('Cleanup failed');
|
||||
api.cleanupUnusedVODLogos.mockRejectedValue(mockError);
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const { result } = renderHook(() => useVODLogosStore());
|
||||
|
||||
|
|
@ -360,7 +371,10 @@ describe('useVODLogosStore', () => {
|
|||
}
|
||||
});
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to cleanup unused VOD logos:', mockError);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Failed to cleanup unused VOD logos:',
|
||||
mockError
|
||||
);
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
|
@ -517,7 +531,9 @@ describe('useVODLogosStore', () => {
|
|||
const mockError = new Error('Failed to fetch count');
|
||||
api.getVODLogos.mockRejectedValue(mockError);
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const { result } = renderHook(() => useVODLogosStore());
|
||||
|
||||
|
|
@ -529,7 +545,10 @@ describe('useVODLogosStore', () => {
|
|||
}
|
||||
});
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch unused logos count:', mockError);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Failed to fetch unused logos count:',
|
||||
mockError
|
||||
);
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,9 +12,15 @@ const reduceChannels = (channels) => {
|
|||
return acc;
|
||||
}, {});
|
||||
return { channelsByUUID, channelsByID };
|
||||
}
|
||||
};
|
||||
|
||||
const showNotificationIfNewChannel = (currentStats, oldChannels, ch, channelsByUUID, channels) => {
|
||||
const showNotificationIfNewChannel = (
|
||||
currentStats,
|
||||
oldChannels,
|
||||
ch,
|
||||
channelsByUUID,
|
||||
channels
|
||||
) => {
|
||||
if (currentStats.channels) {
|
||||
if (oldChannels[ch.channel_id] === undefined) {
|
||||
// Add null checks to prevent accessing properties on undefined
|
||||
|
|
@ -30,7 +36,7 @@ const showNotificationIfNewChannel = (currentStats, oldChannels, ch, channelsByU
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const showNotificationIfNewClient = (currentStats, oldClients, client) => {
|
||||
// This check prevents the notifications if streams are active on page load
|
||||
|
|
@ -43,9 +49,15 @@ const showNotificationIfNewClient = (currentStats, oldClients, client) => {
|
|||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const showNotificationIfChannelStopped = (currentStats, oldChannels, newChannels, channelsByUUID, channels) => {
|
||||
const showNotificationIfChannelStopped = (
|
||||
currentStats,
|
||||
oldChannels,
|
||||
newChannels,
|
||||
channelsByUUID,
|
||||
channels
|
||||
) => {
|
||||
// This check prevents the notifications if streams are active on page load
|
||||
if (currentStats.channels) {
|
||||
for (const uuid in oldChannels) {
|
||||
|
|
@ -70,9 +82,13 @@ const showNotificationIfChannelStopped = (currentStats, oldChannels, newChannels
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const showNotificationIfClientStopped = (currentStats, oldClients, newClients) => {
|
||||
const showNotificationIfClientStopped = (
|
||||
currentStats,
|
||||
oldClients,
|
||||
newClients
|
||||
) => {
|
||||
if (currentStats.channels) {
|
||||
for (const clientId in oldClients) {
|
||||
if (newClients[clientId] === undefined) {
|
||||
|
|
@ -84,7 +100,7 @@ const showNotificationIfClientStopped = (currentStats, oldClients, newClients) =
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const useChannelsStore = create((set, get) => ({
|
||||
channels: [],
|
||||
|
|
@ -228,7 +244,7 @@ const useChannelsStore = create((set, get) => ({
|
|||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const { channelsByUUID, updatedChannels } = reduceChannels(channels);
|
||||
|
||||
set((state) => ({
|
||||
|
|
@ -383,24 +399,36 @@ const useChannelsStore = create((set, get) => ({
|
|||
channelsByUUID,
|
||||
} = state;
|
||||
const newClients = {};
|
||||
|
||||
|
||||
const newChannels = stats.channels.reduce((acc, ch) => {
|
||||
acc[ch.channel_id] = ch;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
stats.channels.forEach(ch => {
|
||||
showNotificationIfNewChannel(currentStats, oldChannels, ch, channelsByUUID, channels);
|
||||
stats.channels.forEach((ch) => {
|
||||
showNotificationIfNewChannel(
|
||||
currentStats,
|
||||
oldChannels,
|
||||
ch,
|
||||
channelsByUUID,
|
||||
channels
|
||||
);
|
||||
|
||||
ch.clients.forEach(client => {
|
||||
newClients[client.client_id] = client;
|
||||
showNotificationIfNewClient(currentStats, oldClients, client);
|
||||
});
|
||||
ch.clients.forEach((client) => {
|
||||
newClients[client.client_id] = client;
|
||||
showNotificationIfNewClient(currentStats, oldClients, client);
|
||||
});
|
||||
});
|
||||
|
||||
showNotificationIfChannelStopped(currentStats, oldChannels, newChannels, channelsByUUID, channels);
|
||||
showNotificationIfChannelStopped(
|
||||
currentStats,
|
||||
oldChannels,
|
||||
newChannels,
|
||||
channelsByUUID,
|
||||
channels
|
||||
);
|
||||
showNotificationIfClientStopped(currentStats, oldClients, newClients);
|
||||
|
||||
|
||||
return {
|
||||
stats,
|
||||
activeChannels: newChannels,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import api from '../api';
|
|||
const determineEPGStatus = (data, currentEpg) => {
|
||||
if (data.status) return data.status;
|
||||
if (data.action === 'downloading') return 'fetching';
|
||||
if (data.action === 'parsing_channels' || data.action === 'parsing_programs') return 'parsing';
|
||||
if (data.action === 'parsing_channels' || data.action === 'parsing_programs')
|
||||
return 'parsing';
|
||||
if (data.progress === 100) return 'success';
|
||||
return currentEpg?.status || 'idle';
|
||||
};
|
||||
|
|
@ -118,23 +119,26 @@ const useEPGsStore = create((set) => ({
|
|||
|
||||
// Only update epgs object if status or last_message actually changed
|
||||
// This prevents unnecessary re-renders on every progress update
|
||||
const lastMessage = data.status === 'error'
|
||||
? (data.error || 'Unknown error')
|
||||
: state.epgs[data.source]?.last_message;
|
||||
const lastMessage =
|
||||
data.status === 'error'
|
||||
? data.error || 'Unknown error'
|
||||
: state.epgs[data.source]?.last_message;
|
||||
|
||||
const currentEpg = state.epgs[data.source];
|
||||
const shouldUpdateEpg = currentEpg &&
|
||||
(currentEpg.status !== status || currentEpg.last_message !== lastMessage);
|
||||
const shouldUpdateEpg =
|
||||
currentEpg &&
|
||||
(currentEpg.status !== status ||
|
||||
currentEpg.last_message !== lastMessage);
|
||||
|
||||
const epgs = shouldUpdateEpg
|
||||
? {
|
||||
...state.epgs,
|
||||
[data.source]: {
|
||||
...currentEpg,
|
||||
status,
|
||||
last_message: lastMessage,
|
||||
},
|
||||
}
|
||||
...state.epgs,
|
||||
[data.source]: {
|
||||
...currentEpg,
|
||||
status,
|
||||
last_message: lastMessage,
|
||||
},
|
||||
}
|
||||
: state.epgs;
|
||||
|
||||
return { refreshProgress, epgs };
|
||||
|
|
|
|||
|
|
@ -2,10 +2,8 @@ import { create } from 'zustand';
|
|||
import api from '../api';
|
||||
|
||||
const getLogosArray = (response) => {
|
||||
return Array.isArray(response)
|
||||
? response
|
||||
: response.results || [];
|
||||
}
|
||||
return Array.isArray(response) ? response : response.results || [];
|
||||
};
|
||||
|
||||
const useLogosStore = create((set, get) => ({
|
||||
logos: {},
|
||||
|
|
|
|||
|
|
@ -38,4 +38,4 @@ export const usePluginStore = create((set, get) => ({
|
|||
set({ plugins: [] });
|
||||
get().fetchPlugins();
|
||||
},
|
||||
}));
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -7,8 +7,7 @@ const useStreamsTableStore = create((set) => ({
|
|||
sorting: [{ id: 'name', desc: false }],
|
||||
pagination: {
|
||||
pageIndex: 0,
|
||||
pageSize:
|
||||
JSON.parse(localStorage.getItem('streams-page-size')) || 50,
|
||||
pageSize: JSON.parse(localStorage.getItem('streams-page-size')) || 50,
|
||||
},
|
||||
selectedStreamIds: [],
|
||||
allQueryIds: [],
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ const getFetchContentParams = (state) => {
|
|||
params.append('category', state.filters.category);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
};
|
||||
|
||||
const getMovieDetails = (response, movieId) => {
|
||||
return {
|
||||
|
|
@ -35,7 +35,7 @@ const getMovieDetails = (response, movieId) => {
|
|||
imdb_id: response.imdb_id || '',
|
||||
m3u_account: response.m3u_account || '',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getMovieDetailsWithProvider = (response, movieId) => {
|
||||
return {
|
||||
|
|
@ -65,7 +65,7 @@ const getMovieDetailsWithProvider = (response, movieId) => {
|
|||
video: response.video || {},
|
||||
audio: response.audio || {},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getSeriesDetails = (response, seriesId) => {
|
||||
return {
|
||||
|
|
@ -92,7 +92,7 @@ const getSeriesDetails = (response, seriesId) => {
|
|||
m3u_account: response.m3u_account || '',
|
||||
youtube_trailer: response.custom_properties?.youtube_trailer || '',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getEpisodeDetails = (episode, seasonNumber, seriesInfo) => {
|
||||
return {
|
||||
|
|
@ -117,7 +117,7 @@ const getEpisodeDetails = (episode, seasonNumber, seriesInfo) => {
|
|||
tmdb_id: episode.tmdb_id || '',
|
||||
imdb_id: episode.imdb_id || '',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const useVODStore = create((set, get) => ({
|
||||
content: {}, // Store for individual content details (when fetching movie/series details)
|
||||
|
|
|
|||
|
|
@ -256,7 +256,7 @@ describe('dateTimeUtils', () => {
|
|||
const setTimeZone = vi.fn();
|
||||
useLocalStorage.mockReturnValue(['America/New_York', setTimeZone]);
|
||||
useSettingsStore.mockReturnValue({
|
||||
'system_settings': { value: { time_zone: 'America/Los_Angeles' } }
|
||||
system_settings: { value: { time_zone: 'America/Los_Angeles' } },
|
||||
});
|
||||
|
||||
renderHook(() => dateTimeUtils.useUserTimeZone());
|
||||
|
|
@ -321,18 +321,25 @@ describe('dateTimeUtils', () => {
|
|||
});
|
||||
|
||||
it('should start with Sunday', () => {
|
||||
expect(dateTimeUtils.RECURRING_DAY_OPTIONS[0]).toEqual({ value: 6, label: 'Sun' });
|
||||
expect(dateTimeUtils.RECURRING_DAY_OPTIONS[0]).toEqual({
|
||||
value: 6,
|
||||
label: 'Sun',
|
||||
});
|
||||
});
|
||||
|
||||
it('should include all weekdays', () => {
|
||||
const labels = dateTimeUtils.RECURRING_DAY_OPTIONS.map(opt => opt.label);
|
||||
const labels = dateTimeUtils.RECURRING_DAY_OPTIONS.map(
|
||||
(opt) => opt.label
|
||||
);
|
||||
expect(labels).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useDateTimeFormat', () => {
|
||||
it('should return 12h format and mdy date format by default', () => {
|
||||
useLocalStorage.mockReturnValueOnce(['12h', vi.fn()]).mockReturnValueOnce(['mdy', vi.fn()]);
|
||||
useLocalStorage
|
||||
.mockReturnValueOnce(['12h', vi.fn()])
|
||||
.mockReturnValueOnce(['mdy', vi.fn()]);
|
||||
|
||||
const { result } = renderHook(() => dateTimeUtils.useDateTimeFormat());
|
||||
|
||||
|
|
@ -341,7 +348,9 @@ describe('dateTimeUtils', () => {
|
|||
});
|
||||
|
||||
it('should return 24h format when set', () => {
|
||||
useLocalStorage.mockReturnValueOnce(['24h', vi.fn()]).mockReturnValueOnce(['mdy', vi.fn()]);
|
||||
useLocalStorage
|
||||
.mockReturnValueOnce(['24h', vi.fn()])
|
||||
.mockReturnValueOnce(['mdy', vi.fn()]);
|
||||
|
||||
const { result } = renderHook(() => dateTimeUtils.useDateTimeFormat());
|
||||
|
||||
|
|
@ -349,7 +358,9 @@ describe('dateTimeUtils', () => {
|
|||
});
|
||||
|
||||
it('should return dmy date format when set', () => {
|
||||
useLocalStorage.mockReturnValueOnce(['12h', vi.fn()]).mockReturnValueOnce(['dmy', vi.fn()]);
|
||||
useLocalStorage
|
||||
.mockReturnValueOnce(['12h', vi.fn()])
|
||||
.mockReturnValueOnce(['dmy', vi.fn()]);
|
||||
|
||||
const { result } = renderHook(() => dateTimeUtils.useDateTimeFormat());
|
||||
|
||||
|
|
@ -428,26 +439,28 @@ describe('dateTimeUtils', () => {
|
|||
it('should sort by offset then name', () => {
|
||||
const result = dateTimeUtils.buildTimeZoneOptions();
|
||||
for (let i = 1; i < result.length; i++) {
|
||||
expect(result[i].numericOffset).toBeGreaterThanOrEqual(result[i - 1].numericOffset);
|
||||
expect(result[i].numericOffset).toBeGreaterThanOrEqual(
|
||||
result[i - 1].numericOffset
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should include DST information when applicable', () => {
|
||||
const result = dateTimeUtils.buildTimeZoneOptions();
|
||||
const dstZone = result.find(opt => opt.label.includes('DST range'));
|
||||
const dstZone = result.find((opt) => opt.label.includes('DST range'));
|
||||
expect(dstZone).toBeDefined();
|
||||
});
|
||||
|
||||
it('should add preferred zone if not in list', () => {
|
||||
const preferredZone = 'Custom/Zone';
|
||||
const result = dateTimeUtils.buildTimeZoneOptions(preferredZone);
|
||||
const found = result.find(opt => opt.value === preferredZone);
|
||||
const found = result.find((opt) => opt.value === preferredZone);
|
||||
expect(found).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not duplicate existing zones', () => {
|
||||
const result = dateTimeUtils.buildTimeZoneOptions('UTC');
|
||||
const utcOptions = result.filter(opt => opt.value === 'UTC');
|
||||
const utcOptions = result.filter((opt) => opt.value === 'UTC');
|
||||
expect(utcOptions).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ describe('networkUtils', () => {
|
|||
expect(networkUtils.IPV4_CIDR_REGEX.test('10.0.0.0/8')).toBe(true);
|
||||
expect(networkUtils.IPV4_CIDR_REGEX.test('172.16.0.0/12')).toBe(true);
|
||||
expect(networkUtils.IPV4_CIDR_REGEX.test('0.0.0.0/0')).toBe(true);
|
||||
expect(networkUtils.IPV4_CIDR_REGEX.test('255.255.255.255/32')).toBe(true);
|
||||
expect(networkUtils.IPV4_CIDR_REGEX.test('255.255.255.255/32')).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('should not match invalid IPv4 CIDR notation', () => {
|
||||
|
|
@ -29,7 +31,11 @@ describe('networkUtils', () => {
|
|||
expect(networkUtils.IPV6_CIDR_REGEX.test('2001:db8::/32')).toBe(true);
|
||||
expect(networkUtils.IPV6_CIDR_REGEX.test('fe80::/10')).toBe(true);
|
||||
expect(networkUtils.IPV6_CIDR_REGEX.test('::/0')).toBe(true);
|
||||
expect(networkUtils.IPV6_CIDR_REGEX.test('2001:0db8:85a3:0000:0000:8a2e:0370:7334/64')).toBe(true);
|
||||
expect(
|
||||
networkUtils.IPV6_CIDR_REGEX.test(
|
||||
'2001:0db8:85a3:0000:0000:8a2e:0370:7334/64'
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should match compressed IPv6 CIDR notation', () => {
|
||||
|
|
@ -38,7 +44,9 @@ describe('networkUtils', () => {
|
|||
});
|
||||
|
||||
it('should match IPv6 with embedded IPv4', () => {
|
||||
expect(networkUtils.IPV6_CIDR_REGEX.test('::ffff:192.168.1.1/96')).toBe(true);
|
||||
expect(networkUtils.IPV6_CIDR_REGEX.test('::ffff:192.168.1.1/96')).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('should not match invalid IPv6 CIDR notation', () => {
|
||||
|
|
|
|||
|
|
@ -74,7 +74,10 @@ describe('notificationUtils', () => {
|
|||
|
||||
notificationUtils.updateNotification(notificationId, notificationObject);
|
||||
|
||||
expect(notifications.update).toHaveBeenCalledWith(notificationId, notificationObject);
|
||||
expect(notifications.update).toHaveBeenCalledWith(
|
||||
notificationId,
|
||||
notificationObject
|
||||
);
|
||||
expect(notifications.update).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
|
|
@ -82,7 +85,9 @@ describe('notificationUtils', () => {
|
|||
const mockReturnValue = { success: true };
|
||||
notifications.update.mockReturnValue(mockReturnValue);
|
||||
|
||||
const result = notificationUtils.updateNotification('id', { message: 'test' });
|
||||
const result = notificationUtils.updateNotification('id', {
|
||||
message: 'test',
|
||||
});
|
||||
|
||||
expect(result).toBe(mockReturnValue);
|
||||
});
|
||||
|
|
@ -98,7 +103,10 @@ describe('notificationUtils', () => {
|
|||
|
||||
notificationUtils.updateNotification(notificationId, updateObject);
|
||||
|
||||
expect(notifications.update).toHaveBeenCalledWith(notificationId, updateObject);
|
||||
expect(notifications.update).toHaveBeenCalledWith(
|
||||
notificationId,
|
||||
updateObject
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle loading to error transition', () => {
|
||||
|
|
@ -112,7 +120,10 @@ describe('notificationUtils', () => {
|
|||
|
||||
notificationUtils.updateNotification(notificationId, updateObject);
|
||||
|
||||
expect(notifications.update).toHaveBeenCalledWith(notificationId, updateObject);
|
||||
expect(notifications.update).toHaveBeenCalledWith(
|
||||
notificationId,
|
||||
updateObject
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle partial updates', () => {
|
||||
|
|
@ -123,7 +134,10 @@ describe('notificationUtils', () => {
|
|||
|
||||
notificationUtils.updateNotification(notificationId, updateObject);
|
||||
|
||||
expect(notifications.update).toHaveBeenCalledWith(notificationId, updateObject);
|
||||
expect(notifications.update).toHaveBeenCalledWith(
|
||||
notificationId,
|
||||
updateObject
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty notification id', () => {
|
||||
|
|
@ -139,7 +153,10 @@ describe('notificationUtils', () => {
|
|||
|
||||
notificationUtils.updateNotification(null, notificationObject);
|
||||
|
||||
expect(notifications.update).toHaveBeenCalledWith(null, notificationObject);
|
||||
expect(notifications.update).toHaveBeenCalledWith(
|
||||
null,
|
||||
notificationObject
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -89,4 +89,4 @@ export const getSeriesInfo = (customProps) => {
|
|||
const cp = customProps || {};
|
||||
const pr = cp.program || {};
|
||||
return { tvg_id: pr.tvg_id, title: pr.title };
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export const formatTime = (seconds) => {
|
|||
|
||||
export const getMovieDisplayTitle = (vodContent) => {
|
||||
return vodContent.content_name;
|
||||
}
|
||||
};
|
||||
|
||||
export const getEpisodeDisplayTitle = (metadata) => {
|
||||
const season = metadata.season_number
|
||||
|
|
@ -34,18 +34,18 @@ export const getEpisodeDisplayTitle = (metadata) => {
|
|||
? `E${metadata.episode_number.toString().padStart(2, '0')}`
|
||||
: 'E??';
|
||||
return `${metadata.series_name} - ${season}${episode}`;
|
||||
}
|
||||
};
|
||||
|
||||
export const getMovieSubtitle = (metadata) => {
|
||||
const parts = [];
|
||||
if (metadata.genre) parts.push(metadata.genre);
|
||||
// We'll handle rating separately as a badge now
|
||||
return parts;
|
||||
}
|
||||
};
|
||||
|
||||
export const getEpisodeSubtitle = (metadata) => {
|
||||
return [metadata.episode_name || 'Episode'];
|
||||
}
|
||||
};
|
||||
|
||||
export const calculateProgress = (connection, duration_secs) => {
|
||||
if (!connection || !duration_secs) {
|
||||
|
|
@ -92,7 +92,7 @@ export const calculateProgress = (connection, duration_secs) => {
|
|||
currentTime: Math.max(0, currentTime), // Don't go negative
|
||||
totalTime: totalSeconds,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const calculateConnectionDuration = (connection) => {
|
||||
// If duration is provided by API, use it
|
||||
|
|
@ -115,9 +115,12 @@ export const calculateConnectionDuration = (connection) => {
|
|||
}
|
||||
|
||||
return 'Unknown duration';
|
||||
}
|
||||
};
|
||||
|
||||
export const calculateConnectionStartTime = (connection, fullDateTimeFormat) => {
|
||||
export const calculateConnectionStartTime = (
|
||||
connection,
|
||||
fullDateTimeFormat
|
||||
) => {
|
||||
if (connection.connected_at) {
|
||||
return format(connection.connected_at * 1000, fullDateTimeFormat);
|
||||
}
|
||||
|
|
@ -136,4 +139,4 @@ export const calculateConnectionStartTime = (connection, fullDateTimeFormat) =>
|
|||
}
|
||||
|
||||
return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
getConfirmationDetails,
|
||||
} from '../PluginCardUtils';
|
||||
import { getConfirmationDetails } from '../PluginCardUtils';
|
||||
|
||||
describe('PluginCardUtils', () => {
|
||||
describe('getConfirmationDetails', () => {
|
||||
|
|
@ -13,7 +11,8 @@ describe('PluginCardUtils', () => {
|
|||
expect(result).toEqual({
|
||||
requireConfirm: true,
|
||||
confirmTitle: 'Run Test Action?',
|
||||
confirmMessage: 'You\'re about to run "Test Action" from "Test Plugin".',
|
||||
confirmMessage:
|
||||
'You\'re about to run "Test Action" from "Test Plugin".',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -156,7 +156,9 @@ describe('RecordingCardUtils', () => {
|
|||
const channel = { uuid: 'channel-123' };
|
||||
const result = getShowVideoUrl(channel, 'dev');
|
||||
|
||||
expect(result).toMatch(/^https?:\/\/.*:5656\/proxy\/ts\/stream\/channel-123$/);
|
||||
expect(result).toMatch(
|
||||
/^https?:\/\/.*:5656\/proxy\/ts\/stream\/channel-123$/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -208,7 +210,9 @@ describe('RecordingCardUtils', () => {
|
|||
|
||||
it('handles bulk remove error gracefully', async () => {
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation();
|
||||
API.bulkRemoveSeriesRecordings.mockRejectedValue(new Error('Bulk remove failed'));
|
||||
API.bulkRemoveSeriesRecordings.mockRejectedValue(
|
||||
new Error('Bulk remove failed')
|
||||
);
|
||||
API.deleteSeriesRule.mockResolvedValue();
|
||||
const seriesInfo = { tvg_id: 'series-123', title: 'Test Series' };
|
||||
|
||||
|
|
|
|||
|
|
@ -14,31 +14,43 @@ describe('StreamConnectionCardUtils', () => {
|
|||
describe('getBufferingSpeedThreshold', () => {
|
||||
it('should return parsed buffering_speed from proxy settings', () => {
|
||||
const proxySetting = {
|
||||
value: { buffering_speed: 2.5 }
|
||||
value: { buffering_speed: 2.5 },
|
||||
};
|
||||
expect(StreamConnectionCardUtils.getBufferingSpeedThreshold(proxySetting)).toBe(2.5);
|
||||
expect(
|
||||
StreamConnectionCardUtils.getBufferingSpeedThreshold(proxySetting)
|
||||
).toBe(2.5);
|
||||
});
|
||||
|
||||
it('should return 1.0 for invalid JSON', () => {
|
||||
const proxySetting = { value: { buffering_speed: 'invalid' } };
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
expect(StreamConnectionCardUtils.getBufferingSpeedThreshold(proxySetting)).toBe(1.0);
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
expect(
|
||||
StreamConnectionCardUtils.getBufferingSpeedThreshold(proxySetting)
|
||||
).toBe(1.0);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should return 1.0 when buffering_speed is not a number', () => {
|
||||
const proxySetting = {
|
||||
value: JSON.stringify({ buffering_speed: 'not a number' })
|
||||
value: JSON.stringify({ buffering_speed: 'not a number' }),
|
||||
};
|
||||
expect(StreamConnectionCardUtils.getBufferingSpeedThreshold(proxySetting)).toBe(1.0);
|
||||
expect(
|
||||
StreamConnectionCardUtils.getBufferingSpeedThreshold(proxySetting)
|
||||
).toBe(1.0);
|
||||
});
|
||||
|
||||
it('should return 1.0 when proxySetting is null', () => {
|
||||
expect(StreamConnectionCardUtils.getBufferingSpeedThreshold(null)).toBe(1.0);
|
||||
expect(StreamConnectionCardUtils.getBufferingSpeedThreshold(null)).toBe(
|
||||
1.0
|
||||
);
|
||||
});
|
||||
|
||||
it('should return 1.0 when value is missing', () => {
|
||||
expect(StreamConnectionCardUtils.getBufferingSpeedThreshold({})).toBe(1.0);
|
||||
expect(StreamConnectionCardUtils.getBufferingSpeedThreshold({})).toBe(
|
||||
1.0
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -60,17 +72,14 @@ describe('StreamConnectionCardUtils', () => {
|
|||
it('should create map from m3u accounts array', () => {
|
||||
const m3uAccounts = [
|
||||
{ id: 1, name: 'Account 1' },
|
||||
{ id: 2, name: 'Account 2' }
|
||||
{ id: 2, name: 'Account 2' },
|
||||
];
|
||||
const result = StreamConnectionCardUtils.getM3uAccountsMap(m3uAccounts);
|
||||
expect(result).toEqual({ 1: 'Account 1', 2: 'Account 2' });
|
||||
});
|
||||
|
||||
it('should handle accounts without id', () => {
|
||||
const m3uAccounts = [
|
||||
{ name: 'Account 1' },
|
||||
{ id: 2, name: 'Account 2' }
|
||||
];
|
||||
const m3uAccounts = [{ name: 'Account 1' }, { id: 2, name: 'Account 2' }];
|
||||
const result = StreamConnectionCardUtils.getM3uAccountsMap(m3uAccounts);
|
||||
expect(result).toEqual({ 2: 'Account 2' });
|
||||
});
|
||||
|
|
@ -100,7 +109,7 @@ describe('StreamConnectionCardUtils', () => {
|
|||
it('should find stream when channelUrl includes stream url', () => {
|
||||
const streamData = [
|
||||
{ id: 1, url: 'http://example.com/stream1' },
|
||||
{ id: 2, url: 'http://example.com/stream2' }
|
||||
{ id: 2, url: 'http://example.com/stream2' },
|
||||
];
|
||||
const result = StreamConnectionCardUtils.getMatchingStreamByUrl(
|
||||
streamData,
|
||||
|
|
@ -111,7 +120,7 @@ describe('StreamConnectionCardUtils', () => {
|
|||
|
||||
it('should find stream when stream url includes channelUrl', () => {
|
||||
const streamData = [
|
||||
{ id: 1, url: 'http://example.com/stream1/playlist.m3u8' }
|
||||
{ id: 1, url: 'http://example.com/stream1/playlist.m3u8' },
|
||||
];
|
||||
const result = StreamConnectionCardUtils.getMatchingStreamByUrl(
|
||||
streamData,
|
||||
|
|
@ -134,7 +143,7 @@ describe('StreamConnectionCardUtils', () => {
|
|||
it('should find stream by id as string', () => {
|
||||
const streams = [
|
||||
{ id: 1, name: 'Stream 1' },
|
||||
{ id: 2, name: 'Stream 2' }
|
||||
{ id: 2, name: 'Stream 2' },
|
||||
];
|
||||
const result = StreamConnectionCardUtils.getSelectedStream(streams, '2');
|
||||
expect(result).toEqual(streams[1]);
|
||||
|
|
@ -167,11 +176,20 @@ describe('StreamConnectionCardUtils', () => {
|
|||
dateTimeUtils.subtract.mockReturnValue(mockConnectedTime);
|
||||
dateTimeUtils.format.mockReturnValue('01/01/2024 10:00:00');
|
||||
|
||||
const accessor = StreamConnectionCardUtils.connectedAccessor('MM/DD/YYYY, HH:mm:ss');
|
||||
const accessor = StreamConnectionCardUtils.connectedAccessor(
|
||||
'MM/DD/YYYY, HH:mm:ss'
|
||||
);
|
||||
const result = accessor({ connected_since: 7200 });
|
||||
|
||||
expect(dateTimeUtils.subtract).toHaveBeenCalledWith(mockNow, 7200, 'second');
|
||||
expect(dateTimeUtils.format).toHaveBeenCalledWith(mockConnectedTime, 'MM/DD/YYYY, HH:mm:ss');
|
||||
expect(dateTimeUtils.subtract).toHaveBeenCalledWith(
|
||||
mockNow,
|
||||
7200,
|
||||
'second'
|
||||
);
|
||||
expect(dateTimeUtils.format).toHaveBeenCalledWith(
|
||||
mockConnectedTime,
|
||||
'MM/DD/YYYY, HH:mm:ss'
|
||||
);
|
||||
expect(result).toBe('01/01/2024 10:00:00');
|
||||
});
|
||||
|
||||
|
|
@ -181,7 +199,8 @@ describe('StreamConnectionCardUtils', () => {
|
|||
dateTimeUtils.initializeTime.mockReturnValue(mockTime);
|
||||
dateTimeUtils.format.mockReturnValue('01/01/2024 10:00:00');
|
||||
|
||||
const accessor = StreamConnectionCardUtils.connectedAccessor('MM/DD/YYYY');
|
||||
const accessor =
|
||||
StreamConnectionCardUtils.connectedAccessor('MM/DD/YYYY');
|
||||
const result = accessor({ connected_at: 1704103200 });
|
||||
|
||||
expect(dateTimeUtils.initializeTime).toHaveBeenCalledWith(1704103200000);
|
||||
|
|
@ -189,7 +208,8 @@ describe('StreamConnectionCardUtils', () => {
|
|||
});
|
||||
|
||||
it('should return Unknown when no time data available', () => {
|
||||
const accessor = StreamConnectionCardUtils.connectedAccessor('MM/DD/YYYY');
|
||||
const accessor =
|
||||
StreamConnectionCardUtils.connectedAccessor('MM/DD/YYYY');
|
||||
const result = accessor({});
|
||||
expect(result).toBe('Unknown');
|
||||
});
|
||||
|
|
@ -202,7 +222,10 @@ describe('StreamConnectionCardUtils', () => {
|
|||
const accessor = StreamConnectionCardUtils.durationAccessor();
|
||||
const result = accessor({ connected_since: 9000 });
|
||||
|
||||
expect(dateTimeUtils.toFriendlyDuration).toHaveBeenCalledWith(9000, 'seconds');
|
||||
expect(dateTimeUtils.toFriendlyDuration).toHaveBeenCalledWith(
|
||||
9000,
|
||||
'seconds'
|
||||
);
|
||||
expect(result).toBe('2h 30m');
|
||||
});
|
||||
|
||||
|
|
@ -212,7 +235,10 @@ describe('StreamConnectionCardUtils', () => {
|
|||
const accessor = StreamConnectionCardUtils.durationAccessor();
|
||||
const result = accessor({ connection_duration: 4500 });
|
||||
|
||||
expect(dateTimeUtils.toFriendlyDuration).toHaveBeenCalledWith(4500, 'seconds');
|
||||
expect(dateTimeUtils.toFriendlyDuration).toHaveBeenCalledWith(
|
||||
4500,
|
||||
'seconds'
|
||||
);
|
||||
expect(result).toBe('1h 15m');
|
||||
});
|
||||
|
||||
|
|
@ -226,15 +252,23 @@ describe('StreamConnectionCardUtils', () => {
|
|||
describe('getLogoUrl', () => {
|
||||
it('should return cache_url from logos map when logoId exists', () => {
|
||||
const logos = {
|
||||
'logo-123': { cache_url: '/api/logos/logo-123/cache/' }
|
||||
'logo-123': { cache_url: '/api/logos/logo-123/cache/' },
|
||||
};
|
||||
const result = StreamConnectionCardUtils.getLogoUrl('logo-123', logos, null);
|
||||
const result = StreamConnectionCardUtils.getLogoUrl(
|
||||
'logo-123',
|
||||
logos,
|
||||
null
|
||||
);
|
||||
expect(result).toBe('/api/logos/logo-123/cache/');
|
||||
});
|
||||
|
||||
it('should fallback to previewedStream logo_url when logoId not in map', () => {
|
||||
const previewedStream = { logo_url: 'http://example.com/logo.png' };
|
||||
const result = StreamConnectionCardUtils.getLogoUrl('logo-456', {}, previewedStream);
|
||||
const result = StreamConnectionCardUtils.getLogoUrl(
|
||||
'logo-456',
|
||||
{},
|
||||
previewedStream
|
||||
);
|
||||
expect(result).toBe('http://example.com/logo.png');
|
||||
});
|
||||
|
||||
|
|
@ -260,15 +294,18 @@ describe('StreamConnectionCardUtils', () => {
|
|||
it('should format stream options with account names from map', () => {
|
||||
const streams = [
|
||||
{ id: 1, name: 'Stream 1', m3u_account: 100 },
|
||||
{ id: 2, name: 'Stream 2', m3u_account: 200 }
|
||||
{ id: 2, name: 'Stream 2', m3u_account: 200 },
|
||||
];
|
||||
const accountsMap = { 100: 'Premium Account', 200: 'Basic Account' };
|
||||
|
||||
const result = StreamConnectionCardUtils.getStreamOptions(streams, accountsMap);
|
||||
const result = StreamConnectionCardUtils.getStreamOptions(
|
||||
streams,
|
||||
accountsMap
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ value: '1', label: 'Stream 1 [Premium Account]' },
|
||||
{ value: '2', label: 'Stream 2 [Basic Account]' }
|
||||
{ value: '2', label: 'Stream 2 [Basic Account]' },
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -284,7 +321,10 @@ describe('StreamConnectionCardUtils', () => {
|
|||
const streams = [{ id: 5, m3u_account: 100 }];
|
||||
const accountsMap = { 100: 'Account' };
|
||||
|
||||
const result = StreamConnectionCardUtils.getStreamOptions(streams, accountsMap);
|
||||
const result = StreamConnectionCardUtils.getStreamOptions(
|
||||
streams,
|
||||
accountsMap
|
||||
);
|
||||
|
||||
expect(result[0].label).toBe('Stream #5 [Account]');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ describe('VodConnectionCardUtils', () => {
|
|||
const metadata = {
|
||||
series_name: 'Breaking Bad',
|
||||
season_number: 1,
|
||||
episode_number: 5
|
||||
episode_number: 5,
|
||||
};
|
||||
const result = VodConnectionCardUtils.getEpisodeDisplayTitle(metadata);
|
||||
expect(result).toBe('Breaking Bad - S01E05');
|
||||
|
|
@ -106,7 +106,7 @@ describe('VodConnectionCardUtils', () => {
|
|||
const metadata = {
|
||||
series_name: 'The Office',
|
||||
season_number: 3,
|
||||
episode_number: 9
|
||||
episode_number: 9,
|
||||
};
|
||||
const result = VodConnectionCardUtils.getEpisodeDisplayTitle(metadata);
|
||||
expect(result).toBe('The Office - S03E09');
|
||||
|
|
@ -115,7 +115,7 @@ describe('VodConnectionCardUtils', () => {
|
|||
it('should use S?? when season_number is missing', () => {
|
||||
const metadata = {
|
||||
series_name: 'Lost',
|
||||
episode_number: 5
|
||||
episode_number: 5,
|
||||
};
|
||||
const result = VodConnectionCardUtils.getEpisodeDisplayTitle(metadata);
|
||||
expect(result).toBe('Lost - S??E05');
|
||||
|
|
@ -124,7 +124,7 @@ describe('VodConnectionCardUtils', () => {
|
|||
it('should use E?? when episode_number is missing', () => {
|
||||
const metadata = {
|
||||
series_name: 'Friends',
|
||||
season_number: 2
|
||||
season_number: 2,
|
||||
};
|
||||
const result = VodConnectionCardUtils.getEpisodeDisplayTitle(metadata);
|
||||
expect(result).toBe('Friends - S02E??');
|
||||
|
|
@ -167,7 +167,7 @@ describe('VodConnectionCardUtils', () => {
|
|||
it('should calculate progress from last_seek_percentage', () => {
|
||||
const connection = {
|
||||
last_seek_percentage: 50,
|
||||
last_seek_timestamp: 990 // 10 seconds ago
|
||||
last_seek_timestamp: 990, // 10 seconds ago
|
||||
};
|
||||
const result = VodConnectionCardUtils.calculateProgress(connection, 200);
|
||||
|
||||
|
|
@ -179,7 +179,7 @@ describe('VodConnectionCardUtils', () => {
|
|||
it('should cap currentTime at duration when seeking', () => {
|
||||
const connection = {
|
||||
last_seek_percentage: 95,
|
||||
last_seek_timestamp: 900 // 100 seconds ago
|
||||
last_seek_timestamp: 900, // 100 seconds ago
|
||||
};
|
||||
const result = VodConnectionCardUtils.calculateProgress(connection, 200);
|
||||
|
||||
|
|
@ -189,7 +189,7 @@ describe('VodConnectionCardUtils', () => {
|
|||
|
||||
it('should fallback to position_seconds when seek data unavailable', () => {
|
||||
const connection = {
|
||||
position_seconds: 75
|
||||
position_seconds: 75,
|
||||
};
|
||||
const result = VodConnectionCardUtils.calculateProgress(connection, 200);
|
||||
|
||||
|
|
@ -218,7 +218,7 @@ describe('VodConnectionCardUtils', () => {
|
|||
it('should ensure currentTime is not negative', () => {
|
||||
const connection = {
|
||||
last_seek_percentage: 10,
|
||||
last_seek_timestamp: 2000 // In the future somehow
|
||||
last_seek_timestamp: 2000, // In the future somehow
|
||||
};
|
||||
const result = VodConnectionCardUtils.calculateProgress(connection, 200);
|
||||
|
||||
|
|
@ -231,9 +231,13 @@ describe('VodConnectionCardUtils', () => {
|
|||
dateTimeUtils.toFriendlyDuration.mockReturnValue('1h 30m');
|
||||
const connection = { duration: 5400 };
|
||||
|
||||
const result = VodConnectionCardUtils.calculateConnectionDuration(connection);
|
||||
const result =
|
||||
VodConnectionCardUtils.calculateConnectionDuration(connection);
|
||||
|
||||
expect(dateTimeUtils.toFriendlyDuration).toHaveBeenCalledWith(5400, 'seconds');
|
||||
expect(dateTimeUtils.toFriendlyDuration).toHaveBeenCalledWith(
|
||||
5400,
|
||||
'seconds'
|
||||
);
|
||||
expect(result).toBe('1h 30m');
|
||||
});
|
||||
|
||||
|
|
@ -242,22 +246,28 @@ describe('VodConnectionCardUtils', () => {
|
|||
dateTimeUtils.toFriendlyDuration.mockReturnValue('45m');
|
||||
|
||||
const connection = { client_id: 'vod_900000_abc' };
|
||||
const result = VodConnectionCardUtils.calculateConnectionDuration(connection);
|
||||
const result =
|
||||
VodConnectionCardUtils.calculateConnectionDuration(connection);
|
||||
|
||||
expect(dateTimeUtils.toFriendlyDuration).toHaveBeenCalledWith(100, 'seconds');
|
||||
expect(dateTimeUtils.toFriendlyDuration).toHaveBeenCalledWith(
|
||||
100,
|
||||
'seconds'
|
||||
);
|
||||
expect(result).toBe('45m');
|
||||
});
|
||||
|
||||
it('should return Unknown duration when no data available', () => {
|
||||
const connection = {};
|
||||
const result = VodConnectionCardUtils.calculateConnectionDuration(connection);
|
||||
const result =
|
||||
VodConnectionCardUtils.calculateConnectionDuration(connection);
|
||||
|
||||
expect(result).toBe('Unknown duration');
|
||||
});
|
||||
|
||||
it('should return Unknown duration when client_id is invalid format', () => {
|
||||
const connection = { client_id: 'invalid_format' };
|
||||
const result = VodConnectionCardUtils.calculateConnectionDuration(connection);
|
||||
const result =
|
||||
VodConnectionCardUtils.calculateConnectionDuration(connection);
|
||||
|
||||
expect(result).toBe('Unknown duration');
|
||||
});
|
||||
|
|
@ -267,7 +277,8 @@ describe('VodConnectionCardUtils', () => {
|
|||
dateTimeUtils.toFriendlyDuration.mockReturnValue('45m');
|
||||
|
||||
const connection = { client_id: 'vod_invalid_abc' };
|
||||
const result = VodConnectionCardUtils.calculateConnectionDuration(connection);
|
||||
const result =
|
||||
VodConnectionCardUtils.calculateConnectionDuration(connection);
|
||||
|
||||
// If parseInt fails, the code should still handle it
|
||||
expect(result).toBe('45m'); // or 'Unknown duration' depending on implementation
|
||||
|
|
@ -279,9 +290,15 @@ describe('VodConnectionCardUtils', () => {
|
|||
dateTimeUtils.format.mockReturnValue('01/15/2024 14:30:00');
|
||||
|
||||
const connection = { connected_at: 1705329000 };
|
||||
const result = VodConnectionCardUtils.calculateConnectionStartTime(connection, 'MM/DD/YYYY, HH:mm:ss');
|
||||
const result = VodConnectionCardUtils.calculateConnectionStartTime(
|
||||
connection,
|
||||
'MM/DD/YYYY, HH:mm:ss'
|
||||
);
|
||||
|
||||
expect(dateTimeUtils.format).toHaveBeenCalledWith(1705329000000, 'MM/DD/YYYY, HH:mm:ss');
|
||||
expect(dateTimeUtils.format).toHaveBeenCalledWith(
|
||||
1705329000000,
|
||||
'MM/DD/YYYY, HH:mm:ss'
|
||||
);
|
||||
expect(result).toBe('01/15/2024 14:30:00');
|
||||
});
|
||||
|
||||
|
|
@ -289,22 +306,34 @@ describe('VodConnectionCardUtils', () => {
|
|||
dateTimeUtils.format.mockReturnValue('01/15/2024 13:00:00');
|
||||
|
||||
const connection = { client_id: 'vod_1705323600000_abc' };
|
||||
const result = VodConnectionCardUtils.calculateConnectionStartTime(connection, 'MM/DD/YYYY, HH:mm:ss');
|
||||
const result = VodConnectionCardUtils.calculateConnectionStartTime(
|
||||
connection,
|
||||
'MM/DD/YYYY, HH:mm:ss'
|
||||
);
|
||||
|
||||
expect(dateTimeUtils.format).toHaveBeenCalledWith(1705323600000, 'MM/DD/YYYY, HH:mm:ss');
|
||||
expect(dateTimeUtils.format).toHaveBeenCalledWith(
|
||||
1705323600000,
|
||||
'MM/DD/YYYY, HH:mm:ss'
|
||||
);
|
||||
expect(result).toBe('01/15/2024 13:00:00');
|
||||
});
|
||||
|
||||
it('should return Unknown when no timestamp data available', () => {
|
||||
const connection = {};
|
||||
const result = VodConnectionCardUtils.calculateConnectionStartTime(connection, 'MM/DD/YYYY, HH:mm:ss');
|
||||
const result = VodConnectionCardUtils.calculateConnectionStartTime(
|
||||
connection,
|
||||
'MM/DD/YYYY, HH:mm:ss'
|
||||
);
|
||||
|
||||
expect(result).toBe('Unknown');
|
||||
});
|
||||
|
||||
it('should return Unknown when client_id is invalid format', () => {
|
||||
const connection = { client_id: 'invalid_format' };
|
||||
const result = VodConnectionCardUtils.calculateConnectionStartTime(connection, 'MM/DD/YYYY');
|
||||
const result = VodConnectionCardUtils.calculateConnectionStartTime(
|
||||
connection,
|
||||
'MM/DD/YYYY'
|
||||
);
|
||||
|
||||
expect(result).toBe('Unknown');
|
||||
});
|
||||
|
|
@ -313,11 +342,13 @@ describe('VodConnectionCardUtils', () => {
|
|||
dateTimeUtils.format.mockReturnValue('01/15/2024 13:00:00');
|
||||
|
||||
const connection = { client_id: 'vod_notanumber_abc' };
|
||||
const result = VodConnectionCardUtils.calculateConnectionStartTime(connection, 'MM/DD/YYYY');
|
||||
const result = VodConnectionCardUtils.calculateConnectionStartTime(
|
||||
connection,
|
||||
'MM/DD/YYYY'
|
||||
);
|
||||
|
||||
// If parseInt succeeds on any number, format will be called
|
||||
expect(result).toBe('01/15/2024 13:00:00'); // or 'Unknown' depending on implementation
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@ export const format = (dateTime, formatStr) =>
|
|||
|
||||
export const getNow = () => dayjs();
|
||||
|
||||
export const toFriendlyDuration = (dateTime, unit) => dayjs.duration(dateTime, unit).humanize();
|
||||
export const toFriendlyDuration = (dateTime, unit) =>
|
||||
dayjs.duration(dateTime, unit).humanize();
|
||||
|
||||
export const fromNow = (dateTime) => dayjs(dateTime).fromNow();
|
||||
|
||||
|
|
@ -113,7 +114,8 @@ export const useDateTimeFormat = () => {
|
|||
const dateFormat = dateFormatSetting === 'mdy' ? 'MMM D' : 'D MMM';
|
||||
|
||||
// Full format strings for detailed date-time displays
|
||||
const fullDateFormat = dateFormatSetting === 'mdy' ? 'MM/DD/YYYY' : 'DD/MM/YYYY';
|
||||
const fullDateFormat =
|
||||
dateFormatSetting === 'mdy' ? 'MM/DD/YYYY' : 'DD/MM/YYYY';
|
||||
const fullTimeFormat = timeFormatSetting === '12h' ? 'h:mm:ss A' : 'HH:mm:ss';
|
||||
const fullDateTimeFormat = `${fullDateFormat}, ${fullTimeFormat}`;
|
||||
|
||||
|
|
@ -278,4 +280,4 @@ export const getDefaultTimeZone = () => {
|
|||
} catch (error) {
|
||||
return 'UTC';
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ const filterByUpcoming = (arr, tvid, titleKey, toUserTime, userNow) => {
|
|||
const st = toUserTime(r.start_time);
|
||||
return st.isAfter(userNow());
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const dedupeByProgram = (filtered) => {
|
||||
// Deduplicate by program.id if present, else by time+title
|
||||
|
|
@ -62,7 +62,7 @@ const dedupeByProgram = (filtered) => {
|
|||
deduped.push(r);
|
||||
}
|
||||
return deduped;
|
||||
}
|
||||
};
|
||||
|
||||
export const getUpcomingEpisodes = (
|
||||
isSeriesGroup,
|
||||
|
|
|
|||
|
|
@ -63,4 +63,4 @@ export const deleteRecurringRuleById = async (ruleId) => {
|
|||
|
||||
export const updateRecurringRuleEnabled = async (ruleId, checked) => {
|
||||
await API.updateRecurringRule(ruleId, { enabled: checked });
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ describe('RecordingDetailsModalUtils', () => {
|
|||
audio_codec: 'AAC',
|
||||
audio_channels: 2,
|
||||
sample_rate: 48000,
|
||||
audio_bitrate: 128
|
||||
audio_bitrate: 128,
|
||||
};
|
||||
|
||||
const result = RecordingDetailsModalUtils.getStatRows(stats);
|
||||
|
|
@ -28,49 +28,45 @@ describe('RecordingDetailsModalUtils', () => {
|
|||
['Audio Codec', 'AAC'],
|
||||
['Audio Channels', 2],
|
||||
['Sample Rate', '48000 Hz'],
|
||||
['Audio Bitrate', '128 kb/s']
|
||||
['Audio Bitrate', '128 kb/s'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should use width x height when resolution is not present', () => {
|
||||
const stats = {
|
||||
width: 1280,
|
||||
height: 720
|
||||
height: 720,
|
||||
};
|
||||
|
||||
const result = RecordingDetailsModalUtils.getStatRows(stats);
|
||||
|
||||
expect(result).toEqual([
|
||||
['Resolution', '1280x720']
|
||||
]);
|
||||
expect(result).toEqual([['Resolution', '1280x720']]);
|
||||
});
|
||||
|
||||
it('should prefer resolution over width/height', () => {
|
||||
const stats = {
|
||||
resolution: '1920x1080',
|
||||
width: 1280,
|
||||
height: 720
|
||||
height: 720,
|
||||
};
|
||||
|
||||
const result = RecordingDetailsModalUtils.getStatRows(stats);
|
||||
|
||||
expect(result).toEqual([
|
||||
['Resolution', '1920x1080']
|
||||
]);
|
||||
expect(result).toEqual([['Resolution', '1920x1080']]);
|
||||
});
|
||||
|
||||
it('should filter out null values', () => {
|
||||
const stats = {
|
||||
video_codec: 'H.264',
|
||||
resolution: null,
|
||||
source_fps: 30
|
||||
source_fps: 30,
|
||||
};
|
||||
|
||||
const result = RecordingDetailsModalUtils.getStatRows(stats);
|
||||
|
||||
expect(result).toEqual([
|
||||
['Video Codec', 'H.264'],
|
||||
['FPS', 30]
|
||||
['FPS', 30],
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -78,74 +74,68 @@ describe('RecordingDetailsModalUtils', () => {
|
|||
const stats = {
|
||||
video_codec: 'H.264',
|
||||
source_fps: undefined,
|
||||
audio_codec: 'AAC'
|
||||
audio_codec: 'AAC',
|
||||
};
|
||||
|
||||
const result = RecordingDetailsModalUtils.getStatRows(stats);
|
||||
|
||||
expect(result).toEqual([
|
||||
['Video Codec', 'H.264'],
|
||||
['Audio Codec', 'AAC']
|
||||
['Audio Codec', 'AAC'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should filter out empty strings', () => {
|
||||
const stats = {
|
||||
video_codec: '',
|
||||
audio_codec: 'AAC'
|
||||
audio_codec: 'AAC',
|
||||
};
|
||||
|
||||
const result = RecordingDetailsModalUtils.getStatRows(stats);
|
||||
|
||||
expect(result).toEqual([
|
||||
['Audio Codec', 'AAC']
|
||||
]);
|
||||
expect(result).toEqual([['Audio Codec', 'AAC']]);
|
||||
});
|
||||
|
||||
it('should handle missing width or height gracefully', () => {
|
||||
const stats = {
|
||||
width: 1920,
|
||||
video_codec: 'H.264'
|
||||
video_codec: 'H.264',
|
||||
};
|
||||
|
||||
const result = RecordingDetailsModalUtils.getStatRows(stats);
|
||||
|
||||
expect(result).toEqual([
|
||||
['Video Codec', 'H.264']
|
||||
]);
|
||||
expect(result).toEqual([['Video Codec', 'H.264']]);
|
||||
});
|
||||
|
||||
it('should format bitrates correctly', () => {
|
||||
const stats = {
|
||||
video_bitrate: 2500,
|
||||
audio_bitrate: 192
|
||||
audio_bitrate: 192,
|
||||
};
|
||||
|
||||
const result = RecordingDetailsModalUtils.getStatRows(stats);
|
||||
|
||||
expect(result).toEqual([
|
||||
['Video Bitrate', '2500 kb/s'],
|
||||
['Audio Bitrate', '192 kb/s']
|
||||
['Audio Bitrate', '192 kb/s'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should format sample rate correctly', () => {
|
||||
const stats = {
|
||||
sample_rate: 44100
|
||||
sample_rate: 44100,
|
||||
};
|
||||
|
||||
const result = RecordingDetailsModalUtils.getStatRows(stats);
|
||||
|
||||
expect(result).toEqual([
|
||||
['Sample Rate', '44100 Hz']
|
||||
]);
|
||||
expect(result).toEqual([['Sample Rate', '44100 Hz']]);
|
||||
});
|
||||
|
||||
it('should return empty array when no valid stats', () => {
|
||||
const stats = {
|
||||
video_codec: null,
|
||||
resolution: undefined,
|
||||
source_fps: ''
|
||||
source_fps: '',
|
||||
};
|
||||
|
||||
const result = RecordingDetailsModalUtils.getStatRows(stats);
|
||||
|
|
@ -193,7 +183,7 @@ describe('RecordingDetailsModalUtils', () => {
|
|||
it('should return rating from program custom_properties', () => {
|
||||
const customProps = {};
|
||||
const program = {
|
||||
custom_properties: { rating: 'TV-14' }
|
||||
custom_properties: { rating: 'TV-14' },
|
||||
};
|
||||
|
||||
const result = RecordingDetailsModalUtils.getRating(customProps, program);
|
||||
|
|
@ -204,7 +194,7 @@ describe('RecordingDetailsModalUtils', () => {
|
|||
it('should prefer customProps rating over program rating', () => {
|
||||
const customProps = { rating: 'TV-MA' };
|
||||
const program = {
|
||||
custom_properties: { rating: 'TV-14' }
|
||||
custom_properties: { rating: 'TV-14' },
|
||||
};
|
||||
|
||||
const result = RecordingDetailsModalUtils.getRating(customProps, program);
|
||||
|
|
@ -215,7 +205,7 @@ describe('RecordingDetailsModalUtils', () => {
|
|||
it('should prefer rating_value over program rating', () => {
|
||||
const customProps = { rating_value: 'PG-13' };
|
||||
const program = {
|
||||
custom_properties: { rating: 'TV-14' }
|
||||
custom_properties: { rating: 'TV-14' },
|
||||
};
|
||||
|
||||
const result = RecordingDetailsModalUtils.getRating(customProps, program);
|
||||
|
|
@ -294,16 +284,16 @@ describe('RecordingDetailsModalUtils', () => {
|
|||
start_time: '2024-01-02T12:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {
|
||||
program: { tvg_id: 'show1', title: 'Test Show' }
|
||||
}
|
||||
program: { tvg_id: 'show1', title: 'Test Show' },
|
||||
},
|
||||
},
|
||||
{
|
||||
start_time: '2024-01-02T13:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {
|
||||
program: { tvg_id: 'show2', title: 'Other Show' }
|
||||
}
|
||||
}
|
||||
program: { tvg_id: 'show2', title: 'Other Show' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = RecordingDetailsModalUtils.getUpcomingEpisodes(
|
||||
|
|
@ -325,16 +315,16 @@ describe('RecordingDetailsModalUtils', () => {
|
|||
start_time: '2023-12-31T12:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {
|
||||
program: { tvg_id: 'show1', title: 'Test Show' }
|
||||
}
|
||||
program: { tvg_id: 'show1', title: 'Test Show' },
|
||||
},
|
||||
},
|
||||
{
|
||||
start_time: '2024-01-02T12:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {
|
||||
program: { tvg_id: 'show1', title: 'Test Show' }
|
||||
}
|
||||
}
|
||||
program: { tvg_id: 'show1', title: 'Test Show' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = RecordingDetailsModalUtils.getUpcomingEpisodes(
|
||||
|
|
@ -358,8 +348,8 @@ describe('RecordingDetailsModalUtils', () => {
|
|||
custom_properties: {
|
||||
season: 1,
|
||||
episode: 5,
|
||||
program: { tvg_id: 'show1', title: 'Test Show' }
|
||||
}
|
||||
program: { tvg_id: 'show1', title: 'Test Show' },
|
||||
},
|
||||
},
|
||||
{
|
||||
start_time: '2024-01-02T18:00:00',
|
||||
|
|
@ -367,9 +357,9 @@ describe('RecordingDetailsModalUtils', () => {
|
|||
custom_properties: {
|
||||
season: 1,
|
||||
episode: 5,
|
||||
program: { tvg_id: 'show1', title: 'Test Show' }
|
||||
}
|
||||
}
|
||||
program: { tvg_id: 'show1', title: 'Test Show' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = RecordingDetailsModalUtils.getUpcomingEpisodes(
|
||||
|
|
@ -391,17 +381,17 @@ describe('RecordingDetailsModalUtils', () => {
|
|||
channel: 'ch1',
|
||||
custom_properties: {
|
||||
onscreen_episode: 'S01E05',
|
||||
program: { tvg_id: 'show1', title: 'Test Show' }
|
||||
}
|
||||
program: { tvg_id: 'show1', title: 'Test Show' },
|
||||
},
|
||||
},
|
||||
{
|
||||
start_time: '2024-01-02T18:00:00',
|
||||
channel: 'ch2',
|
||||
custom_properties: {
|
||||
onscreen_episode: 's01e05',
|
||||
program: { tvg_id: 'show1', title: 'Test Show' }
|
||||
}
|
||||
}
|
||||
program: { tvg_id: 'show1', title: 'Test Show' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = RecordingDetailsModalUtils.getUpcomingEpisodes(
|
||||
|
|
@ -425,9 +415,9 @@ describe('RecordingDetailsModalUtils', () => {
|
|||
program: {
|
||||
tvg_id: 'show1',
|
||||
title: 'Test Show',
|
||||
sub_title: 'The Beginning'
|
||||
}
|
||||
}
|
||||
sub_title: 'The Beginning',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
start_time: '2024-01-02T18:00:00',
|
||||
|
|
@ -436,10 +426,10 @@ describe('RecordingDetailsModalUtils', () => {
|
|||
program: {
|
||||
tvg_id: 'show1',
|
||||
title: 'Test Show',
|
||||
sub_title: 'The Beginning'
|
||||
}
|
||||
}
|
||||
}
|
||||
sub_title: 'The Beginning',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = RecordingDetailsModalUtils.getUpcomingEpisodes(
|
||||
|
|
@ -460,16 +450,16 @@ describe('RecordingDetailsModalUtils', () => {
|
|||
start_time: '2024-01-02T12:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {
|
||||
program: { tvg_id: 'show1', title: 'Test Show', id: 123 }
|
||||
}
|
||||
program: { tvg_id: 'show1', title: 'Test Show', id: 123 },
|
||||
},
|
||||
},
|
||||
{
|
||||
start_time: '2024-01-02T18:00:00',
|
||||
channel: 'ch2',
|
||||
custom_properties: {
|
||||
program: { tvg_id: 'show1', title: 'Test Show', id: 123 }
|
||||
}
|
||||
}
|
||||
program: { tvg_id: 'show1', title: 'Test Show', id: 123 },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = RecordingDetailsModalUtils.getUpcomingEpisodes(
|
||||
|
|
@ -491,25 +481,25 @@ describe('RecordingDetailsModalUtils', () => {
|
|||
end_time: '2024-01-03T13:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {
|
||||
program: { tvg_id: 'show1', title: 'Test Show', id: 3 }
|
||||
}
|
||||
program: { tvg_id: 'show1', title: 'Test Show', id: 3 },
|
||||
},
|
||||
},
|
||||
{
|
||||
start_time: '2024-01-02T12:00:00',
|
||||
end_time: '2024-01-02T13:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {
|
||||
program: { tvg_id: 'show1', title: 'Test Show', id: 1 }
|
||||
}
|
||||
program: { tvg_id: 'show1', title: 'Test Show', id: 1 },
|
||||
},
|
||||
},
|
||||
{
|
||||
start_time: '2024-01-04T12:00:00',
|
||||
end_time: '2024-01-04T13:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {
|
||||
program: { tvg_id: 'show1', title: 'Test Show', id: 4 }
|
||||
}
|
||||
}
|
||||
program: { tvg_id: 'show1', title: 'Test Show', id: 4 },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = RecordingDetailsModalUtils.getUpcomingEpisodes(
|
||||
|
|
@ -533,9 +523,9 @@ describe('RecordingDetailsModalUtils', () => {
|
|||
start_time: '2024-01-02T12:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {
|
||||
program: { tvg_id: 'show1', title: 'Test Show', id: 1 }
|
||||
}
|
||||
}
|
||||
program: { tvg_id: 'show1', title: 'Test Show', id: 1 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = RecordingDetailsModalUtils.getUpcomingEpisodes(
|
||||
|
|
@ -556,9 +546,9 @@ describe('RecordingDetailsModalUtils', () => {
|
|||
start_time: '2024-01-02T12:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {
|
||||
program: { tvg_id: 'show1', title: 'test show' }
|
||||
}
|
||||
}
|
||||
program: { tvg_id: 'show1', title: 'test show' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = RecordingDetailsModalUtils.getUpcomingEpisodes(
|
||||
|
|
@ -582,9 +572,9 @@ describe('RecordingDetailsModalUtils', () => {
|
|||
program: {
|
||||
tvg_id: 'show1',
|
||||
title: 'Test Show',
|
||||
custom_properties: { season: 2, episode: 3 }
|
||||
}
|
||||
}
|
||||
custom_properties: { season: 2, episode: 3 },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
start_time: '2024-01-02T18:00:00',
|
||||
|
|
@ -593,10 +583,10 @@ describe('RecordingDetailsModalUtils', () => {
|
|||
program: {
|
||||
tvg_id: 'show1',
|
||||
title: 'Test Show',
|
||||
custom_properties: { season: 2, episode: 3 }
|
||||
}
|
||||
}
|
||||
}
|
||||
custom_properties: { season: 2, episode: 3 },
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = RecordingDetailsModalUtils.getUpcomingEpisodes(
|
||||
|
|
@ -615,8 +605,8 @@ describe('RecordingDetailsModalUtils', () => {
|
|||
const recordings = [
|
||||
{
|
||||
start_time: '2024-01-02T12:00:00',
|
||||
channel: 'ch1'
|
||||
}
|
||||
channel: 'ch1',
|
||||
},
|
||||
];
|
||||
|
||||
const result = RecordingDetailsModalUtils.getUpcomingEpisodes(
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import dayjs from 'dayjs';
|
|||
vi.mock('../../../api.js', () => ({
|
||||
default: {
|
||||
updateRecurringRule: vi.fn(),
|
||||
deleteRecurringRule: vi.fn()
|
||||
}
|
||||
deleteRecurringRule: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('RecurringRuleModalUtils', () => {
|
||||
|
|
@ -20,7 +20,7 @@ describe('RecurringRuleModalUtils', () => {
|
|||
const channels = {
|
||||
ch1: { id: 1, channel_number: '10', name: 'ABC' },
|
||||
ch2: { id: 2, channel_number: '5', name: 'NBC' },
|
||||
ch3: { id: 3, channel_number: '15', name: 'CBS' }
|
||||
ch3: { id: 3, channel_number: '15', name: 'CBS' },
|
||||
};
|
||||
|
||||
const result = RecurringRuleModalUtils.getChannelOptions(channels);
|
||||
|
|
@ -28,7 +28,7 @@ describe('RecurringRuleModalUtils', () => {
|
|||
expect(result).toEqual([
|
||||
{ value: '2', label: 'NBC' },
|
||||
{ value: '1', label: 'ABC' },
|
||||
{ value: '3', label: 'CBS' }
|
||||
{ value: '3', label: 'CBS' },
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ describe('RecurringRuleModalUtils', () => {
|
|||
const channels = {
|
||||
ch1: { id: 1, channel_number: '10', name: 'ZBC' },
|
||||
ch2: { id: 2, channel_number: '10', name: 'ABC' },
|
||||
ch3: { id: 3, channel_number: '10', name: 'MBC' }
|
||||
ch3: { id: 3, channel_number: '10', name: 'MBC' },
|
||||
};
|
||||
|
||||
const result = RecurringRuleModalUtils.getChannelOptions(channels);
|
||||
|
|
@ -44,35 +44,35 @@ describe('RecurringRuleModalUtils', () => {
|
|||
expect(result).toEqual([
|
||||
{ value: '2', label: 'ABC' },
|
||||
{ value: '3', label: 'MBC' },
|
||||
{ value: '1', label: 'ZBC' }
|
||||
{ value: '1', label: 'ZBC' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle missing channel numbers', () => {
|
||||
const channels = {
|
||||
ch1: { id: 1, name: 'ABC' },
|
||||
ch2: { id: 2, channel_number: '5', name: 'NBC' }
|
||||
ch2: { id: 2, channel_number: '5', name: 'NBC' },
|
||||
};
|
||||
|
||||
const result = RecurringRuleModalUtils.getChannelOptions(channels);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ value: '1', label: 'ABC' },
|
||||
{ value: '2', label: 'NBC' }
|
||||
{ value: '2', label: 'NBC' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should use fallback label when name is missing', () => {
|
||||
const channels = {
|
||||
ch1: { id: 1, channel_number: '10' },
|
||||
ch2: { id: 2, channel_number: '5', name: '' }
|
||||
ch2: { id: 2, channel_number: '5', name: '' },
|
||||
};
|
||||
|
||||
const result = RecurringRuleModalUtils.getChannelOptions(channels);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ value: '2', label: 'Channel 2' },
|
||||
{ value: '1', label: 'Channel 1' }
|
||||
{ value: '1', label: 'Channel 1' },
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -96,7 +96,7 @@ describe('RecurringRuleModalUtils', () => {
|
|||
|
||||
it('should convert channel id to string value', () => {
|
||||
const channels = {
|
||||
ch1: { id: 123, channel_number: '10', name: 'ABC' }
|
||||
ch1: { id: 123, channel_number: '10', name: 'ABC' },
|
||||
};
|
||||
|
||||
const result = RecurringRuleModalUtils.getChannelOptions(channels);
|
||||
|
|
@ -108,7 +108,7 @@ describe('RecurringRuleModalUtils', () => {
|
|||
it('should handle non-numeric channel numbers', () => {
|
||||
const channels = {
|
||||
ch1: { id: 1, channel_number: 'HD1', name: 'ABC' },
|
||||
ch2: { id: 2, channel_number: '5', name: 'NBC' }
|
||||
ch2: { id: 2, channel_number: '5', name: 'NBC' },
|
||||
};
|
||||
|
||||
const result = RecurringRuleModalUtils.getChannelOptions(channels);
|
||||
|
|
@ -131,16 +131,16 @@ describe('RecurringRuleModalUtils', () => {
|
|||
const recordings = [
|
||||
{
|
||||
start_time: '2024-01-02T12:00:00',
|
||||
custom_properties: { rule: { id: 1 } }
|
||||
custom_properties: { rule: { id: 1 } },
|
||||
},
|
||||
{
|
||||
start_time: '2024-01-03T12:00:00',
|
||||
custom_properties: { rule: { id: 1 } }
|
||||
custom_properties: { rule: { id: 1 } },
|
||||
},
|
||||
{
|
||||
start_time: '2024-01-04T12:00:00',
|
||||
custom_properties: { rule: { id: 2 } }
|
||||
}
|
||||
custom_properties: { rule: { id: 2 } },
|
||||
},
|
||||
];
|
||||
|
||||
const result = RecurringRuleModalUtils.getUpcomingOccurrences(
|
||||
|
|
@ -159,12 +159,12 @@ describe('RecurringRuleModalUtils', () => {
|
|||
const recordings = [
|
||||
{
|
||||
start_time: '2023-12-31T12:00:00',
|
||||
custom_properties: { rule: { id: 1 } }
|
||||
custom_properties: { rule: { id: 1 } },
|
||||
},
|
||||
{
|
||||
start_time: '2024-01-02T12:00:00',
|
||||
custom_properties: { rule: { id: 1 } }
|
||||
}
|
||||
custom_properties: { rule: { id: 1 } },
|
||||
},
|
||||
];
|
||||
|
||||
const result = RecurringRuleModalUtils.getUpcomingOccurrences(
|
||||
|
|
@ -182,16 +182,16 @@ describe('RecurringRuleModalUtils', () => {
|
|||
const recordings = [
|
||||
{
|
||||
start_time: '2024-01-04T12:00:00',
|
||||
custom_properties: { rule: { id: 1 } }
|
||||
custom_properties: { rule: { id: 1 } },
|
||||
},
|
||||
{
|
||||
start_time: '2024-01-02T12:00:00',
|
||||
custom_properties: { rule: { id: 1 } }
|
||||
custom_properties: { rule: { id: 1 } },
|
||||
},
|
||||
{
|
||||
start_time: '2024-01-03T12:00:00',
|
||||
custom_properties: { rule: { id: 1 } }
|
||||
}
|
||||
custom_properties: { rule: { id: 1 } },
|
||||
},
|
||||
];
|
||||
|
||||
const result = RecurringRuleModalUtils.getUpcomingOccurrences(
|
||||
|
|
@ -211,12 +211,12 @@ describe('RecurringRuleModalUtils', () => {
|
|||
const recordings = {
|
||||
rec1: {
|
||||
start_time: '2024-01-02T12:00:00',
|
||||
custom_properties: { rule: { id: 1 } }
|
||||
custom_properties: { rule: { id: 1 } },
|
||||
},
|
||||
rec2: {
|
||||
start_time: '2024-01-03T12:00:00',
|
||||
custom_properties: { rule: { id: 1 } }
|
||||
}
|
||||
custom_properties: { rule: { id: 1 } },
|
||||
},
|
||||
};
|
||||
|
||||
const result = RecurringRuleModalUtils.getUpcomingOccurrences(
|
||||
|
|
@ -254,8 +254,8 @@ describe('RecurringRuleModalUtils', () => {
|
|||
it('should handle recordings without custom_properties', () => {
|
||||
const recordings = [
|
||||
{
|
||||
start_time: '2024-01-02T12:00:00'
|
||||
}
|
||||
start_time: '2024-01-02T12:00:00',
|
||||
},
|
||||
];
|
||||
|
||||
const result = RecurringRuleModalUtils.getUpcomingOccurrences(
|
||||
|
|
@ -272,8 +272,8 @@ describe('RecurringRuleModalUtils', () => {
|
|||
const recordings = [
|
||||
{
|
||||
start_time: '2024-01-02T12:00:00',
|
||||
custom_properties: {}
|
||||
}
|
||||
custom_properties: {},
|
||||
},
|
||||
];
|
||||
|
||||
const result = RecurringRuleModalUtils.getUpcomingOccurrences(
|
||||
|
|
@ -290,8 +290,8 @@ describe('RecurringRuleModalUtils', () => {
|
|||
const recordings = [
|
||||
{
|
||||
start_time: '2024-01-02T12:00:00',
|
||||
custom_properties: { rule: null }
|
||||
}
|
||||
custom_properties: { rule: null },
|
||||
},
|
||||
];
|
||||
|
||||
const result = RecurringRuleModalUtils.getUpcomingOccurrences(
|
||||
|
|
@ -315,7 +315,7 @@ describe('RecurringRuleModalUtils', () => {
|
|||
start_date: '2024-01-01',
|
||||
end_date: '2024-12-31',
|
||||
rule_name: 'My Rule',
|
||||
enabled: true
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
await RecurringRuleModalUtils.updateRecurringRule(1, values);
|
||||
|
|
@ -328,7 +328,7 @@ describe('RecurringRuleModalUtils', () => {
|
|||
start_date: '2024-01-01',
|
||||
end_date: '2024-12-31',
|
||||
name: 'My Rule',
|
||||
enabled: true
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -338,7 +338,7 @@ describe('RecurringRuleModalUtils', () => {
|
|||
days_of_week: ['0', '6'],
|
||||
start_time: '10:00',
|
||||
end_time: '11:00',
|
||||
enabled: false
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
await RecurringRuleModalUtils.updateRecurringRule(1, values);
|
||||
|
|
@ -351,7 +351,7 @@ describe('RecurringRuleModalUtils', () => {
|
|||
start_date: null,
|
||||
end_date: null,
|
||||
name: '',
|
||||
enabled: false
|
||||
enabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -360,7 +360,7 @@ describe('RecurringRuleModalUtils', () => {
|
|||
channel_id: '5',
|
||||
start_time: '10:00',
|
||||
end_time: '11:00',
|
||||
enabled: true
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
await RecurringRuleModalUtils.updateRecurringRule(1, values);
|
||||
|
|
@ -373,7 +373,7 @@ describe('RecurringRuleModalUtils', () => {
|
|||
start_date: null,
|
||||
end_date: null,
|
||||
name: '',
|
||||
enabled: true
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -385,7 +385,7 @@ describe('RecurringRuleModalUtils', () => {
|
|||
end_time: '11:00',
|
||||
start_date: dayjs('2024-06-15'),
|
||||
end_date: dayjs('2024-12-25'),
|
||||
enabled: true
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
await RecurringRuleModalUtils.updateRecurringRule(1, values);
|
||||
|
|
@ -398,7 +398,7 @@ describe('RecurringRuleModalUtils', () => {
|
|||
start_date: '2024-06-15',
|
||||
end_date: '2024-12-25',
|
||||
name: '',
|
||||
enabled: true
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -410,7 +410,7 @@ describe('RecurringRuleModalUtils', () => {
|
|||
end_time: '11:00',
|
||||
start_date: null,
|
||||
end_date: null,
|
||||
enabled: true
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
await RecurringRuleModalUtils.updateRecurringRule(1, values);
|
||||
|
|
@ -423,7 +423,7 @@ describe('RecurringRuleModalUtils', () => {
|
|||
start_date: null,
|
||||
end_date: null,
|
||||
name: '',
|
||||
enabled: true
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -434,7 +434,7 @@ describe('RecurringRuleModalUtils', () => {
|
|||
start_time: '10:00',
|
||||
end_time: '11:00',
|
||||
rule_name: ' Trimmed Name ',
|
||||
enabled: true
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
await RecurringRuleModalUtils.updateRecurringRule(1, values);
|
||||
|
|
@ -447,7 +447,7 @@ describe('RecurringRuleModalUtils', () => {
|
|||
start_date: null,
|
||||
end_date: null,
|
||||
name: 'Trimmed Name',
|
||||
enabled: true
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -457,7 +457,7 @@ describe('RecurringRuleModalUtils', () => {
|
|||
days_of_week: [],
|
||||
start_time: '10:00',
|
||||
end_time: '11:00',
|
||||
enabled: true
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
await RecurringRuleModalUtils.updateRecurringRule(1, values);
|
||||
|
|
@ -470,7 +470,7 @@ describe('RecurringRuleModalUtils', () => {
|
|||
start_date: null,
|
||||
end_date: null,
|
||||
name: '',
|
||||
enabled: true
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -480,7 +480,7 @@ describe('RecurringRuleModalUtils', () => {
|
|||
days_of_week: [],
|
||||
start_time: '10:00',
|
||||
end_time: '11:00',
|
||||
enabled: 'true'
|
||||
enabled: 'true',
|
||||
};
|
||||
|
||||
await RecurringRuleModalUtils.updateRecurringRule(1, values);
|
||||
|
|
@ -493,7 +493,7 @@ describe('RecurringRuleModalUtils', () => {
|
|||
start_date: null,
|
||||
end_date: null,
|
||||
name: '',
|
||||
enabled: true
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -518,7 +518,7 @@ describe('RecurringRuleModalUtils', () => {
|
|||
await RecurringRuleModalUtils.updateRecurringRuleEnabled(1, true);
|
||||
|
||||
expect(API.updateRecurringRule).toHaveBeenCalledWith(1, {
|
||||
enabled: true
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -526,7 +526,7 @@ describe('RecurringRuleModalUtils', () => {
|
|||
await RecurringRuleModalUtils.updateRecurringRuleEnabled(1, false);
|
||||
|
||||
expect(API.updateRecurringRule).toHaveBeenCalledWith(1, {
|
||||
enabled: false
|
||||
enabled: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ export const uploadComskipIni = async (file) => {
|
|||
|
||||
export const getDvrSettingsFormInitialValues = () => {
|
||||
return {
|
||||
'tv_template': '',
|
||||
'movie_template': '',
|
||||
'tv_fallback_template': '',
|
||||
'movie_fallback_template': '',
|
||||
'comskip_enabled': false,
|
||||
'comskip_custom_path': '',
|
||||
'pre_offset_minutes': 0,
|
||||
'post_offset_minutes': 0,
|
||||
tv_template: '',
|
||||
movie_template: '',
|
||||
tv_fallback_template: '',
|
||||
movie_fallback_template: '',
|
||||
comskip_enabled: false,
|
||||
comskip_custom_path: '',
|
||||
pre_offset_minutes: 0,
|
||||
post_offset_minutes: 0,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import { NETWORK_ACCESS_OPTIONS } from '../../../constants.js';
|
|||
import { IPV4_CIDR_REGEX, IPV6_CIDR_REGEX } from '../../networkUtils.js';
|
||||
|
||||
// Default CIDR ranges for M3U/EPG endpoints (local networks only)
|
||||
const M3U_EPG_DEFAULTS = '127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,::1/128,fc00::/7,fe80::/10';
|
||||
const M3U_EPG_DEFAULTS =
|
||||
'127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,::1/128,fc00::/7,fe80::/10';
|
||||
|
||||
export const getNetworkAccessFormInitialValues = () => {
|
||||
return Object.keys(NETWORK_ACCESS_OPTIONS).reduce((acc, key) => {
|
||||
|
|
@ -39,4 +40,4 @@ export const getNetworkAccessDefaults = () => {
|
|||
XC_API: '0.0.0.0/0,::/0',
|
||||
UI: '0.0.0.0/0,::/0',
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,4 +15,4 @@ export const getProxySettingDefaults = () => {
|
|||
channel_shutdown_delay: 0,
|
||||
channel_init_grace_period: 5,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,4 +16,4 @@ export const getStreamSettingsFormValidation = () => {
|
|||
default_stream_profile: isNotEmpty('Select a stream profile'),
|
||||
preferred_region: isNotEmpty('Select a region'),
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,4 +14,4 @@ export const saveTimeZoneSetting = async (tzValue, settings) => {
|
|||
value: newValue,
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ describe('DvrSettingsFormUtils', () => {
|
|||
it('should call API.getComskipConfig and return result', async () => {
|
||||
const mockConfig = {
|
||||
enabled: true,
|
||||
custom_path: '/path/to/comskip'
|
||||
custom_path: '/path/to/comskip',
|
||||
};
|
||||
API.getComskipConfig.mockResolvedValue(mockConfig);
|
||||
|
||||
|
|
@ -27,13 +27,17 @@ describe('DvrSettingsFormUtils', () => {
|
|||
const error = new Error('API Error');
|
||||
API.getComskipConfig.mockRejectedValue(error);
|
||||
|
||||
await expect(DvrSettingsFormUtils.getComskipConfig()).rejects.toThrow('API Error');
|
||||
await expect(DvrSettingsFormUtils.getComskipConfig()).rejects.toThrow(
|
||||
'API Error'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadComskipIni', () => {
|
||||
it('should call API.uploadComskipIni with file and return result', async () => {
|
||||
const mockFile = new File(['content'], 'comskip.ini', { type: 'text/plain' });
|
||||
const mockFile = new File(['content'], 'comskip.ini', {
|
||||
type: 'text/plain',
|
||||
});
|
||||
const mockResponse = { success: true };
|
||||
API.uploadComskipIni.mockResolvedValue(mockResponse);
|
||||
|
||||
|
|
@ -44,11 +48,15 @@ describe('DvrSettingsFormUtils', () => {
|
|||
});
|
||||
|
||||
it('should handle API errors', async () => {
|
||||
const mockFile = new File(['content'], 'comskip.ini', { type: 'text/plain' });
|
||||
const mockFile = new File(['content'], 'comskip.ini', {
|
||||
type: 'text/plain',
|
||||
});
|
||||
const error = new Error('Upload failed');
|
||||
API.uploadComskipIni.mockRejectedValue(error);
|
||||
|
||||
await expect(DvrSettingsFormUtils.uploadComskipIni(mockFile)).rejects.toThrow('Upload failed');
|
||||
await expect(
|
||||
DvrSettingsFormUtils.uploadComskipIni(mockFile)
|
||||
).rejects.toThrow('Upload failed');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -57,14 +65,14 @@ describe('DvrSettingsFormUtils', () => {
|
|||
const result = DvrSettingsFormUtils.getDvrSettingsFormInitialValues();
|
||||
|
||||
expect(result).toEqual({
|
||||
'tv_template': '',
|
||||
'movie_template': '',
|
||||
'tv_fallback_template': '',
|
||||
'movie_fallback_template': '',
|
||||
'comskip_enabled': false,
|
||||
'comskip_custom_path': '',
|
||||
'pre_offset_minutes': 0,
|
||||
'post_offset_minutes': 0,
|
||||
tv_template: '',
|
||||
movie_template: '',
|
||||
tv_fallback_template: '',
|
||||
movie_fallback_template: '',
|
||||
comskip_enabled: false,
|
||||
comskip_custom_path: '',
|
||||
pre_offset_minutes: 0,
|
||||
post_offset_minutes: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ import * as NetworkAccessFormUtils from '../NetworkAccessFormUtils';
|
|||
import * as constants from '../../../../constants.js';
|
||||
|
||||
vi.mock('../../../../constants.js', () => ({
|
||||
NETWORK_ACCESS_OPTIONS: {}
|
||||
NETWORK_ACCESS_OPTIONS: {},
|
||||
}));
|
||||
|
||||
vi.mock('../../../networkUtils.js', () => ({
|
||||
IPV4_CIDR_REGEX: /^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/,
|
||||
IPV6_CIDR_REGEX: /^([0-9a-fA-F:]+)\/\d{1,3}$/
|
||||
IPV6_CIDR_REGEX: /^([0-9a-fA-F:]+)\/\d{1,3}$/,
|
||||
}));
|
||||
|
||||
describe('NetworkAccessFormUtils', () => {
|
||||
|
|
@ -21,7 +21,7 @@ describe('NetworkAccessFormUtils', () => {
|
|||
vi.mocked(constants).NETWORK_ACCESS_OPTIONS = {
|
||||
'network-access-admin': 'Admin Access',
|
||||
'network-access-api': 'API Access',
|
||||
'network-access-streaming': 'Streaming Access'
|
||||
'network-access-streaming': 'Streaming Access',
|
||||
};
|
||||
|
||||
const result = NetworkAccessFormUtils.getNetworkAccessFormInitialValues();
|
||||
|
|
@ -29,7 +29,7 @@ describe('NetworkAccessFormUtils', () => {
|
|||
expect(result).toEqual({
|
||||
'network-access-admin': '0.0.0.0/0,::/0',
|
||||
'network-access-api': '0.0.0.0/0,::/0',
|
||||
'network-access-streaming': '0.0.0.0/0,::/0'
|
||||
'network-access-streaming': '0.0.0.0/0,::/0',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -43,11 +43,13 @@ describe('NetworkAccessFormUtils', () => {
|
|||
|
||||
it('should return a new object each time', () => {
|
||||
vi.mocked(constants).NETWORK_ACCESS_OPTIONS = {
|
||||
'network-access-admin': 'Admin Access'
|
||||
'network-access-admin': 'Admin Access',
|
||||
};
|
||||
|
||||
const result1 = NetworkAccessFormUtils.getNetworkAccessFormInitialValues();
|
||||
const result2 = NetworkAccessFormUtils.getNetworkAccessFormInitialValues();
|
||||
const result1 =
|
||||
NetworkAccessFormUtils.getNetworkAccessFormInitialValues();
|
||||
const result2 =
|
||||
NetworkAccessFormUtils.getNetworkAccessFormInitialValues();
|
||||
|
||||
expect(result1).toEqual(result2);
|
||||
expect(result1).not.toBe(result2);
|
||||
|
|
@ -58,20 +60,24 @@ describe('NetworkAccessFormUtils', () => {
|
|||
beforeEach(() => {
|
||||
vi.mocked(constants).NETWORK_ACCESS_OPTIONS = {
|
||||
'network-access-admin': 'Admin Access',
|
||||
'network-access-api': 'API Access'
|
||||
'network-access-api': 'API Access',
|
||||
};
|
||||
});
|
||||
|
||||
it('should return validation functions for all network access options', () => {
|
||||
const result = NetworkAccessFormUtils.getNetworkAccessFormValidation();
|
||||
|
||||
expect(Object.keys(result)).toEqual(['network-access-admin', 'network-access-api']);
|
||||
expect(Object.keys(result)).toEqual([
|
||||
'network-access-admin',
|
||||
'network-access-api',
|
||||
]);
|
||||
expect(typeof result['network-access-admin']).toBe('function');
|
||||
expect(typeof result['network-access-api']).toBe('function');
|
||||
});
|
||||
|
||||
it('should validate valid IPv4 CIDR ranges', () => {
|
||||
const validation = NetworkAccessFormUtils.getNetworkAccessFormValidation();
|
||||
const validation =
|
||||
NetworkAccessFormUtils.getNetworkAccessFormValidation();
|
||||
const validator = validation['network-access-admin'];
|
||||
|
||||
expect(validator('192.168.1.0/24')).toBeNull();
|
||||
|
|
@ -80,7 +86,8 @@ describe('NetworkAccessFormUtils', () => {
|
|||
});
|
||||
|
||||
it('should validate valid IPv6 CIDR ranges', () => {
|
||||
const validation = NetworkAccessFormUtils.getNetworkAccessFormValidation();
|
||||
const validation =
|
||||
NetworkAccessFormUtils.getNetworkAccessFormValidation();
|
||||
const validator = validation['network-access-admin'];
|
||||
|
||||
expect(validator('2001:db8::/32')).toBeNull();
|
||||
|
|
@ -88,7 +95,8 @@ describe('NetworkAccessFormUtils', () => {
|
|||
});
|
||||
|
||||
it('should validate multiple CIDR ranges separated by commas', () => {
|
||||
const validation = NetworkAccessFormUtils.getNetworkAccessFormValidation();
|
||||
const validation =
|
||||
NetworkAccessFormUtils.getNetworkAccessFormValidation();
|
||||
const validator = validation['network-access-admin'];
|
||||
|
||||
expect(validator('192.168.1.0/24,10.0.0.0/8')).toBeNull();
|
||||
|
|
@ -97,7 +105,8 @@ describe('NetworkAccessFormUtils', () => {
|
|||
});
|
||||
|
||||
it('should return error for invalid IPv4 CIDR ranges', () => {
|
||||
const validation = NetworkAccessFormUtils.getNetworkAccessFormValidation();
|
||||
const validation =
|
||||
NetworkAccessFormUtils.getNetworkAccessFormValidation();
|
||||
const validator = validation['network-access-admin'];
|
||||
|
||||
expect(validator('192.168.1.256.1/24')).toBe('Invalid CIDR range');
|
||||
|
|
@ -106,16 +115,20 @@ describe('NetworkAccessFormUtils', () => {
|
|||
});
|
||||
|
||||
it('should return error when any CIDR in comma-separated list is invalid', () => {
|
||||
const validation = NetworkAccessFormUtils.getNetworkAccessFormValidation();
|
||||
const validation =
|
||||
NetworkAccessFormUtils.getNetworkAccessFormValidation();
|
||||
const validator = validation['network-access-admin'];
|
||||
|
||||
expect(validator('192.168.1.0/24,invalid')).toBe('Invalid CIDR range');
|
||||
expect(validator('invalid,192.168.1.0/24')).toBe('Invalid CIDR range');
|
||||
expect(validator('192.168.1.0/24,10.0.0.0/8,invalid')).toBe('Invalid CIDR range');
|
||||
expect(validator('192.168.1.0/24,10.0.0.0/8,invalid')).toBe(
|
||||
'Invalid CIDR range'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty strings', () => {
|
||||
const validation = NetworkAccessFormUtils.getNetworkAccessFormValidation();
|
||||
const validation =
|
||||
NetworkAccessFormUtils.getNetworkAccessFormValidation();
|
||||
const validator = validation['network-access-admin'];
|
||||
|
||||
expect(validator('')).toBe('Invalid CIDR range');
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import * as ProxySettingsFormUtils from '../ProxySettingsFormUtils';
|
|||
import * as constants from '../../../../constants.js';
|
||||
|
||||
vi.mock('../../../../constants.js', () => ({
|
||||
PROXY_SETTINGS_OPTIONS: {}
|
||||
PROXY_SETTINGS_OPTIONS: {},
|
||||
}));
|
||||
|
||||
describe('ProxySettingsFormUtils', () => {
|
||||
|
|
@ -16,7 +16,7 @@ describe('ProxySettingsFormUtils', () => {
|
|||
vi.mocked(constants).PROXY_SETTINGS_OPTIONS = {
|
||||
'proxy-buffering-timeout': 'Buffering Timeout',
|
||||
'proxy-buffering-speed': 'Buffering Speed',
|
||||
'proxy-redis-chunk-ttl': 'Redis Chunk TTL'
|
||||
'proxy-redis-chunk-ttl': 'Redis Chunk TTL',
|
||||
};
|
||||
|
||||
const result = ProxySettingsFormUtils.getProxySettingsFormInitialValues();
|
||||
|
|
@ -24,7 +24,7 @@ describe('ProxySettingsFormUtils', () => {
|
|||
expect(result).toEqual({
|
||||
'proxy-buffering-timeout': '',
|
||||
'proxy-buffering-speed': '',
|
||||
'proxy-redis-chunk-ttl': ''
|
||||
'proxy-redis-chunk-ttl': '',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -38,11 +38,13 @@ describe('ProxySettingsFormUtils', () => {
|
|||
|
||||
it('should return a new object each time', () => {
|
||||
vi.mocked(constants).PROXY_SETTINGS_OPTIONS = {
|
||||
'proxy-setting': 'Proxy Setting'
|
||||
'proxy-setting': 'Proxy Setting',
|
||||
};
|
||||
|
||||
const result1 = ProxySettingsFormUtils.getProxySettingsFormInitialValues();
|
||||
const result2 = ProxySettingsFormUtils.getProxySettingsFormInitialValues();
|
||||
const result1 =
|
||||
ProxySettingsFormUtils.getProxySettingsFormInitialValues();
|
||||
const result2 =
|
||||
ProxySettingsFormUtils.getProxySettingsFormInitialValues();
|
||||
|
||||
expect(result1).toEqual(result2);
|
||||
expect(result1).not.toBe(result2);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import * as StreamSettingsFormUtils from '../StreamSettingsFormUtils';
|
|||
import { isNotEmpty } from '@mantine/form';
|
||||
|
||||
vi.mock('@mantine/form', () => ({
|
||||
isNotEmpty: vi.fn((message) => message)
|
||||
isNotEmpty: vi.fn((message) => message),
|
||||
}));
|
||||
|
||||
describe('StreamSettingsFormUtils', () => {
|
||||
|
|
@ -13,42 +13,49 @@ describe('StreamSettingsFormUtils', () => {
|
|||
|
||||
describe('getStreamSettingsFormInitialValues', () => {
|
||||
it('should return initial values with correct defaults', () => {
|
||||
const result = StreamSettingsFormUtils.getStreamSettingsFormInitialValues();
|
||||
const result =
|
||||
StreamSettingsFormUtils.getStreamSettingsFormInitialValues();
|
||||
|
||||
expect(result).toEqual({
|
||||
'default_user_agent': '',
|
||||
'default_stream_profile': '',
|
||||
'preferred_region': '',
|
||||
'auto_import_mapped_files': true,
|
||||
'm3u_hash_key': []
|
||||
default_user_agent: '',
|
||||
default_stream_profile: '',
|
||||
preferred_region: '',
|
||||
auto_import_mapped_files: true,
|
||||
m3u_hash_key: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return boolean true for auto-import-mapped-files', () => {
|
||||
const result = StreamSettingsFormUtils.getStreamSettingsFormInitialValues();
|
||||
const result =
|
||||
StreamSettingsFormUtils.getStreamSettingsFormInitialValues();
|
||||
|
||||
expect(result['auto_import_mapped_files']).toBe(true);
|
||||
expect(typeof result['auto_import_mapped_files']).toBe('boolean');
|
||||
});
|
||||
|
||||
it('should return empty array for m3u-hash-key', () => {
|
||||
const result = StreamSettingsFormUtils.getStreamSettingsFormInitialValues();
|
||||
const result =
|
||||
StreamSettingsFormUtils.getStreamSettingsFormInitialValues();
|
||||
|
||||
expect(result['m3u_hash_key']).toEqual([]);
|
||||
expect(Array.isArray(result['m3u_hash_key'])).toBe(true);
|
||||
});
|
||||
|
||||
it('should return a new object each time', () => {
|
||||
const result1 = StreamSettingsFormUtils.getStreamSettingsFormInitialValues();
|
||||
const result2 = StreamSettingsFormUtils.getStreamSettingsFormInitialValues();
|
||||
const result1 =
|
||||
StreamSettingsFormUtils.getStreamSettingsFormInitialValues();
|
||||
const result2 =
|
||||
StreamSettingsFormUtils.getStreamSettingsFormInitialValues();
|
||||
|
||||
expect(result1).toEqual(result2);
|
||||
expect(result1).not.toBe(result2);
|
||||
});
|
||||
|
||||
it('should return a new array instance for m3u-hash-key each time', () => {
|
||||
const result1 = StreamSettingsFormUtils.getStreamSettingsFormInitialValues();
|
||||
const result2 = StreamSettingsFormUtils.getStreamSettingsFormInitialValues();
|
||||
const result1 =
|
||||
StreamSettingsFormUtils.getStreamSettingsFormInitialValues();
|
||||
const result2 =
|
||||
StreamSettingsFormUtils.getStreamSettingsFormInitialValues();
|
||||
|
||||
expect(result1['m3u_hash_key']).not.toBe(result2['m3u_hash_key']);
|
||||
});
|
||||
|
|
@ -61,7 +68,7 @@ describe('StreamSettingsFormUtils', () => {
|
|||
expect(Object.keys(result)).toEqual([
|
||||
'default_user_agent',
|
||||
'default_stream_profile',
|
||||
'preferred_region'
|
||||
'preferred_region',
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -4,30 +4,35 @@ import * as SystemSettingsFormUtils from '../SystemSettingsFormUtils';
|
|||
describe('SystemSettingsFormUtils', () => {
|
||||
describe('getSystemSettingsFormInitialValues', () => {
|
||||
it('should return initial values with correct defaults', () => {
|
||||
const result = SystemSettingsFormUtils.getSystemSettingsFormInitialValues();
|
||||
const result =
|
||||
SystemSettingsFormUtils.getSystemSettingsFormInitialValues();
|
||||
|
||||
expect(result).toEqual({
|
||||
'max_system_events': 100
|
||||
max_system_events: 100,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return number value for max-system-events', () => {
|
||||
const result = SystemSettingsFormUtils.getSystemSettingsFormInitialValues();
|
||||
const result =
|
||||
SystemSettingsFormUtils.getSystemSettingsFormInitialValues();
|
||||
|
||||
expect(result['max_system_events']).toBe(100);
|
||||
expect(typeof result['max_system_events']).toBe('number');
|
||||
});
|
||||
|
||||
it('should return a new object each time', () => {
|
||||
const result1 = SystemSettingsFormUtils.getSystemSettingsFormInitialValues();
|
||||
const result2 = SystemSettingsFormUtils.getSystemSettingsFormInitialValues();
|
||||
const result1 =
|
||||
SystemSettingsFormUtils.getSystemSettingsFormInitialValues();
|
||||
const result2 =
|
||||
SystemSettingsFormUtils.getSystemSettingsFormInitialValues();
|
||||
|
||||
expect(result1).toEqual(result2);
|
||||
expect(result1).not.toBe(result2);
|
||||
});
|
||||
|
||||
it('should have max-system-events property', () => {
|
||||
const result = SystemSettingsFormUtils.getSystemSettingsFormInitialValues();
|
||||
const result =
|
||||
SystemSettingsFormUtils.getSystemSettingsFormInitialValues();
|
||||
|
||||
expect(result).toHaveProperty('max_system_events');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import * as SettingsUtils from '../../../pages/SettingsUtils.js';
|
|||
|
||||
vi.mock('../../../pages/SettingsUtils.js', () => ({
|
||||
createSetting: vi.fn(),
|
||||
updateSetting: vi.fn()
|
||||
updateSetting: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('UiSettingsFormUtils', () => {
|
||||
|
|
@ -16,12 +16,12 @@ describe('UiSettingsFormUtils', () => {
|
|||
it('should update existing setting when id is present', async () => {
|
||||
const tzValue = 'America/New_York';
|
||||
const settings = {
|
||||
'system_settings': {
|
||||
system_settings: {
|
||||
id: 123,
|
||||
key: 'system_settings',
|
||||
name: 'System Settings',
|
||||
value: { time_zone: 'UTC' }
|
||||
}
|
||||
value: { time_zone: 'UTC' },
|
||||
},
|
||||
};
|
||||
|
||||
await UiSettingsFormUtils.saveTimeZoneSetting(tzValue, settings);
|
||||
|
|
@ -31,7 +31,7 @@ describe('UiSettingsFormUtils', () => {
|
|||
id: 123,
|
||||
key: 'system_settings',
|
||||
name: 'System Settings',
|
||||
value: { time_zone: 'America/New_York' }
|
||||
value: { time_zone: 'America/New_York' },
|
||||
});
|
||||
expect(SettingsUtils.createSetting).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
@ -39,11 +39,11 @@ describe('UiSettingsFormUtils', () => {
|
|||
it('should create new setting when existing setting has no id', async () => {
|
||||
const tzValue = 'Europe/London';
|
||||
const settings = {
|
||||
'system_settings': {
|
||||
system_settings: {
|
||||
key: 'system_settings',
|
||||
name: 'System Settings',
|
||||
value: { time_zone: 'UTC' }
|
||||
}
|
||||
value: { time_zone: 'UTC' },
|
||||
},
|
||||
};
|
||||
|
||||
await UiSettingsFormUtils.saveTimeZoneSetting(tzValue, settings);
|
||||
|
|
@ -52,7 +52,7 @@ describe('UiSettingsFormUtils', () => {
|
|||
expect(SettingsUtils.createSetting).toHaveBeenCalledWith({
|
||||
key: 'system_settings',
|
||||
name: 'System Settings',
|
||||
value: { time_zone: 'Europe/London' }
|
||||
value: { time_zone: 'Europe/London' },
|
||||
});
|
||||
expect(SettingsUtils.updateSetting).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
@ -67,7 +67,7 @@ describe('UiSettingsFormUtils', () => {
|
|||
expect(SettingsUtils.createSetting).toHaveBeenCalledWith({
|
||||
key: 'system_settings',
|
||||
name: 'System Settings',
|
||||
value: { time_zone: 'Asia/Tokyo' }
|
||||
value: { time_zone: 'Asia/Tokyo' },
|
||||
});
|
||||
expect(SettingsUtils.updateSetting).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
@ -75,7 +75,7 @@ describe('UiSettingsFormUtils', () => {
|
|||
it('should create new setting when system_settings is null', async () => {
|
||||
const tzValue = 'Pacific/Auckland';
|
||||
const settings = {
|
||||
'system_settings': null
|
||||
system_settings: null,
|
||||
};
|
||||
|
||||
await UiSettingsFormUtils.saveTimeZoneSetting(tzValue, settings);
|
||||
|
|
@ -84,7 +84,7 @@ describe('UiSettingsFormUtils', () => {
|
|||
expect(SettingsUtils.createSetting).toHaveBeenCalledWith({
|
||||
key: 'system_settings',
|
||||
name: 'System Settings',
|
||||
value: { time_zone: 'Pacific/Auckland' }
|
||||
value: { time_zone: 'Pacific/Auckland' },
|
||||
});
|
||||
expect(SettingsUtils.updateSetting).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
@ -92,11 +92,11 @@ describe('UiSettingsFormUtils', () => {
|
|||
it('should create new setting when id is undefined', async () => {
|
||||
const tzValue = 'America/Los_Angeles';
|
||||
const settings = {
|
||||
'system_settings': {
|
||||
system_settings: {
|
||||
id: undefined,
|
||||
key: 'system_settings',
|
||||
value: { time_zone: 'UTC' }
|
||||
}
|
||||
value: { time_zone: 'UTC' },
|
||||
},
|
||||
};
|
||||
|
||||
await UiSettingsFormUtils.saveTimeZoneSetting(tzValue, settings);
|
||||
|
|
@ -108,13 +108,13 @@ describe('UiSettingsFormUtils', () => {
|
|||
it('should preserve existing properties when updating', async () => {
|
||||
const tzValue = 'UTC';
|
||||
const settings = {
|
||||
'system_settings': {
|
||||
system_settings: {
|
||||
id: 456,
|
||||
key: 'system_settings',
|
||||
name: 'System Settings',
|
||||
value: { time_zone: 'America/New_York', some_other_setting: 'value' },
|
||||
extraProp: 'should be preserved'
|
||||
}
|
||||
extraProp: 'should be preserved',
|
||||
},
|
||||
};
|
||||
|
||||
await UiSettingsFormUtils.saveTimeZoneSetting(tzValue, settings);
|
||||
|
|
@ -124,19 +124,19 @@ describe('UiSettingsFormUtils', () => {
|
|||
key: 'system_settings',
|
||||
name: 'System Settings',
|
||||
value: { time_zone: 'UTC', some_other_setting: 'value' },
|
||||
extraProp: 'should be preserved'
|
||||
extraProp: 'should be preserved',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty string timezone value', async () => {
|
||||
const tzValue = '';
|
||||
const settings = {
|
||||
'system_settings': {
|
||||
system_settings: {
|
||||
id: 789,
|
||||
key: 'system_settings',
|
||||
name: 'System Settings',
|
||||
value: { time_zone: 'America/New_York' }
|
||||
}
|
||||
value: { time_zone: 'America/New_York' },
|
||||
},
|
||||
};
|
||||
|
||||
await UiSettingsFormUtils.saveTimeZoneSetting(tzValue, settings);
|
||||
|
|
@ -145,7 +145,7 @@ describe('UiSettingsFormUtils', () => {
|
|||
id: 789,
|
||||
key: 'system_settings',
|
||||
name: 'System Settings',
|
||||
value: { time_zone: '' }
|
||||
value: { time_zone: '' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// IPv4 CIDR regex - validates IP address and prefix length (0-32)
|
||||
export const IPV4_CIDR_REGEX = /^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\/(3[0-2]|[12]?[0-9])$/;
|
||||
export const IPV4_CIDR_REGEX =
|
||||
/^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\/(3[0-2]|[12]?[0-9])$/;
|
||||
|
||||
// IPv6 CIDR regex - validates IPv6 address and prefix length (0-128)
|
||||
export const IPV6_CIDR_REGEX =
|
||||
|
|
@ -21,4 +22,4 @@ export function formatSpeed(bytes) {
|
|||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
|
||||
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@ export function showNotification(notificationObject) {
|
|||
|
||||
export function updateNotification(notificationId, notificationObject) {
|
||||
return notifications.update(notificationId, notificationObject);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ const dedupeById = (list, toUserTime, completed, now, inProgress, upcoming) => {
|
|||
else completed.push(rec);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const categorizeRecordings = (recordings, toUserTime, now) => {
|
||||
const inProgress = [];
|
||||
|
|
@ -87,4 +87,4 @@ export const categorizeRecordings = (recordings, toUserTime, now) => {
|
|||
upcoming: upcomingGrouped,
|
||||
completed,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -27,12 +27,39 @@ export const saveChangedSettings = async (settings, changedSettings) => {
|
|||
};
|
||||
|
||||
// Map of field prefixes to their groups
|
||||
const streamFields = ['default_user_agent', 'default_stream_profile', 'm3u_hash_key', 'preferred_region', 'auto_import_mapped_files'];
|
||||
const epgFields = ['epg_match_mode', 'epg_match_ignore_prefixes', 'epg_match_ignore_suffixes', 'epg_match_ignore_custom'];
|
||||
const dvrFields = ['tv_template', 'movie_template', 'tv_fallback_dir', 'tv_fallback_template', 'movie_fallback_template',
|
||||
'comskip_enabled', 'comskip_custom_path', 'pre_offset_minutes', 'post_offset_minutes', 'series_rules'];
|
||||
const backupFields = ['schedule_enabled', 'schedule_frequency', 'schedule_time', 'schedule_day_of_week',
|
||||
'retention_count', 'schedule_cron_expression'];
|
||||
const streamFields = [
|
||||
'default_user_agent',
|
||||
'default_stream_profile',
|
||||
'm3u_hash_key',
|
||||
'preferred_region',
|
||||
'auto_import_mapped_files',
|
||||
];
|
||||
const epgFields = [
|
||||
'epg_match_mode',
|
||||
'epg_match_ignore_prefixes',
|
||||
'epg_match_ignore_suffixes',
|
||||
'epg_match_ignore_custom',
|
||||
];
|
||||
const dvrFields = [
|
||||
'tv_template',
|
||||
'movie_template',
|
||||
'tv_fallback_dir',
|
||||
'tv_fallback_template',
|
||||
'movie_fallback_template',
|
||||
'comskip_enabled',
|
||||
'comskip_custom_path',
|
||||
'pre_offset_minutes',
|
||||
'post_offset_minutes',
|
||||
'series_rules',
|
||||
];
|
||||
const backupFields = [
|
||||
'schedule_enabled',
|
||||
'schedule_frequency',
|
||||
'schedule_time',
|
||||
'schedule_day_of_week',
|
||||
'retention_count',
|
||||
'schedule_cron_expression',
|
||||
];
|
||||
const systemFields = ['time_zone', 'max_system_events'];
|
||||
|
||||
for (const formKey in changedSettings) {
|
||||
|
|
@ -44,7 +71,11 @@ export const saveChangedSettings = async (settings, changedSettings) => {
|
|||
if (existing?.id) {
|
||||
await updateSetting({ ...existing, value });
|
||||
} else {
|
||||
await createSetting({ key: 'proxy_settings', name: 'Proxy Settings', value });
|
||||
await createSetting({
|
||||
key: 'proxy_settings',
|
||||
name: 'Proxy Settings',
|
||||
value,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
|
@ -54,7 +85,11 @@ export const saveChangedSettings = async (settings, changedSettings) => {
|
|||
if (existing?.id) {
|
||||
await updateSetting({ ...existing, value });
|
||||
} else {
|
||||
await createSetting({ key: 'network_access', name: 'Network Access', value });
|
||||
await createSetting({
|
||||
key: 'network_access',
|
||||
name: 'Network Access',
|
||||
value,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
|
@ -65,16 +100,29 @@ export const saveChangedSettings = async (settings, changedSettings) => {
|
|||
value = value.join(',');
|
||||
}
|
||||
|
||||
if (['default_user_agent', 'default_stream_profile'].includes(formKey) && value != null) {
|
||||
if (
|
||||
['default_user_agent', 'default_stream_profile'].includes(formKey) &&
|
||||
value != null
|
||||
) {
|
||||
value = parseInt(value, 10);
|
||||
}
|
||||
|
||||
const numericFields = ['pre_offset_minutes', 'post_offset_minutes', 'retention_count', 'schedule_day_of_week', 'max_system_events'];
|
||||
const numericFields = [
|
||||
'pre_offset_minutes',
|
||||
'post_offset_minutes',
|
||||
'retention_count',
|
||||
'schedule_day_of_week',
|
||||
'max_system_events',
|
||||
];
|
||||
if (numericFields.includes(formKey) && value != null) {
|
||||
value = typeof value === 'number' ? value : parseInt(value, 10);
|
||||
}
|
||||
|
||||
const booleanFields = ['comskip_enabled', 'schedule_enabled', 'auto_import_mapped_files'];
|
||||
const booleanFields = [
|
||||
'comskip_enabled',
|
||||
'schedule_enabled',
|
||||
'auto_import_mapped_files',
|
||||
];
|
||||
if (booleanFields.includes(formKey) && value != null) {
|
||||
value = typeof value === 'boolean' ? value : Boolean(value);
|
||||
}
|
||||
|
|
@ -107,8 +155,15 @@ export const saveChangedSettings = async (settings, changedSettings) => {
|
|||
throw new Error(`Failed to update ${groupKey}`);
|
||||
}
|
||||
} else {
|
||||
const name = groupKey.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
||||
const result = await createSetting({ key: groupKey, name: name, value: newValue });
|
||||
const name = groupKey
|
||||
.split('_')
|
||||
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
||||
.join(' ');
|
||||
const result = await createSetting({
|
||||
key: groupKey,
|
||||
name: name,
|
||||
value: newValue,
|
||||
});
|
||||
if (!result) {
|
||||
throw new Error(`Failed to create ${groupKey}`);
|
||||
}
|
||||
|
|
@ -120,7 +175,11 @@ export const getChangedSettings = (values, settings) => {
|
|||
const changedSettings = {};
|
||||
|
||||
// EPG fields that should be kept as arrays
|
||||
const epgFields = ['epg_match_ignore_prefixes', 'epg_match_ignore_suffixes', 'epg_match_ignore_custom'];
|
||||
const epgFields = [
|
||||
'epg_match_ignore_prefixes',
|
||||
'epg_match_ignore_suffixes',
|
||||
'epg_match_ignore_custom',
|
||||
];
|
||||
|
||||
for (const settingKey in values) {
|
||||
// Skip grouped settings that are handled by their own dedicated forms
|
||||
|
|
@ -181,8 +240,14 @@ export const parseSettings = (settings) => {
|
|||
const streamSettings = settings['stream_settings']?.value;
|
||||
if (streamSettings && typeof streamSettings === 'object') {
|
||||
// IDs must be strings for Select components
|
||||
parsed.default_user_agent = streamSettings.default_user_agent != null ? String(streamSettings.default_user_agent) : null;
|
||||
parsed.default_stream_profile = streamSettings.default_stream_profile != null ? String(streamSettings.default_stream_profile) : null;
|
||||
parsed.default_user_agent =
|
||||
streamSettings.default_user_agent != null
|
||||
? String(streamSettings.default_user_agent)
|
||||
: null;
|
||||
parsed.default_stream_profile =
|
||||
streamSettings.default_stream_profile != null
|
||||
? String(streamSettings.default_stream_profile)
|
||||
: null;
|
||||
parsed.preferred_region = streamSettings.preferred_region;
|
||||
parsed.auto_import_mapped_files = streamSettings.auto_import_mapped_files;
|
||||
|
||||
|
|
@ -200,9 +265,18 @@ export const parseSettings = (settings) => {
|
|||
// EPG settings - direct mapping with underscore keys
|
||||
const epgSettings = settings['epg_settings']?.value;
|
||||
// Always set EPG fields (even if settings don't exist yet)
|
||||
parsed.epg_match_ignore_prefixes = (epgSettings && Array.isArray(epgSettings.epg_match_ignore_prefixes)) ? epgSettings.epg_match_ignore_prefixes : [];
|
||||
parsed.epg_match_ignore_suffixes = (epgSettings && Array.isArray(epgSettings.epg_match_ignore_suffixes)) ? epgSettings.epg_match_ignore_suffixes : [];
|
||||
parsed.epg_match_ignore_custom = (epgSettings && Array.isArray(epgSettings.epg_match_ignore_custom)) ? epgSettings.epg_match_ignore_custom : [];
|
||||
parsed.epg_match_ignore_prefixes =
|
||||
epgSettings && Array.isArray(epgSettings.epg_match_ignore_prefixes)
|
||||
? epgSettings.epg_match_ignore_prefixes
|
||||
: [];
|
||||
parsed.epg_match_ignore_suffixes =
|
||||
epgSettings && Array.isArray(epgSettings.epg_match_ignore_suffixes)
|
||||
? epgSettings.epg_match_ignore_suffixes
|
||||
: [];
|
||||
parsed.epg_match_ignore_custom =
|
||||
epgSettings && Array.isArray(epgSettings.epg_match_ignore_custom)
|
||||
? epgSettings.epg_match_ignore_custom
|
||||
: [];
|
||||
|
||||
// DVR settings - direct mapping with underscore keys
|
||||
const dvrSettings = settings['dvr_settings']?.value;
|
||||
|
|
@ -212,29 +286,52 @@ export const parseSettings = (settings) => {
|
|||
parsed.tv_fallback_dir = dvrSettings.tv_fallback_dir;
|
||||
parsed.tv_fallback_template = dvrSettings.tv_fallback_template;
|
||||
parsed.movie_fallback_template = dvrSettings.movie_fallback_template;
|
||||
parsed.comskip_enabled = typeof dvrSettings.comskip_enabled === 'boolean' ? dvrSettings.comskip_enabled : Boolean(dvrSettings.comskip_enabled);
|
||||
parsed.comskip_enabled =
|
||||
typeof dvrSettings.comskip_enabled === 'boolean'
|
||||
? dvrSettings.comskip_enabled
|
||||
: Boolean(dvrSettings.comskip_enabled);
|
||||
parsed.comskip_custom_path = dvrSettings.comskip_custom_path;
|
||||
parsed.pre_offset_minutes = typeof dvrSettings.pre_offset_minutes === 'number' ? dvrSettings.pre_offset_minutes : parseInt(dvrSettings.pre_offset_minutes, 10) || 0;
|
||||
parsed.post_offset_minutes = typeof dvrSettings.post_offset_minutes === 'number' ? dvrSettings.post_offset_minutes : parseInt(dvrSettings.post_offset_minutes, 10) || 0;
|
||||
parsed.pre_offset_minutes =
|
||||
typeof dvrSettings.pre_offset_minutes === 'number'
|
||||
? dvrSettings.pre_offset_minutes
|
||||
: parseInt(dvrSettings.pre_offset_minutes, 10) || 0;
|
||||
parsed.post_offset_minutes =
|
||||
typeof dvrSettings.post_offset_minutes === 'number'
|
||||
? dvrSettings.post_offset_minutes
|
||||
: parseInt(dvrSettings.post_offset_minutes, 10) || 0;
|
||||
parsed.series_rules = dvrSettings.series_rules;
|
||||
}
|
||||
|
||||
// Backup settings - direct mapping with underscore keys
|
||||
const backupSettings = settings['backup_settings']?.value;
|
||||
if (backupSettings && typeof backupSettings === 'object') {
|
||||
parsed.schedule_enabled = typeof backupSettings.schedule_enabled === 'boolean' ? backupSettings.schedule_enabled : Boolean(backupSettings.schedule_enabled);
|
||||
parsed.schedule_enabled =
|
||||
typeof backupSettings.schedule_enabled === 'boolean'
|
||||
? backupSettings.schedule_enabled
|
||||
: Boolean(backupSettings.schedule_enabled);
|
||||
parsed.schedule_frequency = String(backupSettings.schedule_frequency || '');
|
||||
parsed.schedule_time = String(backupSettings.schedule_time || '');
|
||||
parsed.schedule_day_of_week = typeof backupSettings.schedule_day_of_week === 'number' ? backupSettings.schedule_day_of_week : parseInt(backupSettings.schedule_day_of_week, 10) || 0;
|
||||
parsed.retention_count = typeof backupSettings.retention_count === 'number' ? backupSettings.retention_count : parseInt(backupSettings.retention_count, 10) || 0;
|
||||
parsed.schedule_cron_expression = String(backupSettings.schedule_cron_expression || '');
|
||||
parsed.schedule_day_of_week =
|
||||
typeof backupSettings.schedule_day_of_week === 'number'
|
||||
? backupSettings.schedule_day_of_week
|
||||
: parseInt(backupSettings.schedule_day_of_week, 10) || 0;
|
||||
parsed.retention_count =
|
||||
typeof backupSettings.retention_count === 'number'
|
||||
? backupSettings.retention_count
|
||||
: parseInt(backupSettings.retention_count, 10) || 0;
|
||||
parsed.schedule_cron_expression = String(
|
||||
backupSettings.schedule_cron_expression || ''
|
||||
);
|
||||
}
|
||||
|
||||
// System settings - direct mapping with underscore keys
|
||||
const systemSettings = settings['system_settings']?.value;
|
||||
if (systemSettings && typeof systemSettings === 'object') {
|
||||
parsed.time_zone = String(systemSettings.time_zone || '');
|
||||
parsed.max_system_events = typeof systemSettings.max_system_events === 'number' ? systemSettings.max_system_events : parseInt(systemSettings.max_system_events, 10) || 100;
|
||||
parsed.max_system_events =
|
||||
typeof systemSettings.max_system_events === 'number'
|
||||
? systemSettings.max_system_events
|
||||
: parseInt(systemSettings.max_system_events, 10) || 100;
|
||||
}
|
||||
|
||||
// Proxy and network access are already grouped objects
|
||||
|
|
@ -246,4 +343,4 @@ export const parseSettings = (settings) => {
|
|||
}
|
||||
|
||||
return parsed;
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -24,9 +24,11 @@ export const getCurrentPrograms = async (channelHistory, channelsByUUID) => {
|
|||
try {
|
||||
// Get all active channel IDs that have actual channels (not just streams)
|
||||
const activeChannelIds = Object.values(channelHistory)
|
||||
.filter(ch => ch.name && channelsByUUID && channelsByUUID[ch.channel_id])
|
||||
.map(ch => channelsByUUID[ch.channel_id])
|
||||
.filter(id => id !== undefined);
|
||||
.filter(
|
||||
(ch) => ch.name && channelsByUUID && channelsByUUID[ch.channel_id]
|
||||
)
|
||||
.map((ch) => channelsByUUID[ch.channel_id])
|
||||
.filter((id) => id !== undefined);
|
||||
|
||||
if (activeChannelIds.length === 0) {
|
||||
return {};
|
||||
|
|
@ -37,7 +39,7 @@ export const getCurrentPrograms = async (channelHistory, channelsByUUID) => {
|
|||
// Convert array to map keyed by channel UUID for easy lookup
|
||||
const programsMap = {};
|
||||
if (programs && Array.isArray(programs)) {
|
||||
programs.forEach(program => {
|
||||
programs.forEach((program) => {
|
||||
// Find the channel UUID from the channel ID
|
||||
const channelEntry = Object.entries(channelsByUUID).find(
|
||||
([uuid, id]) => id === program.channel_id
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ describe('DVRUtils', () => {
|
|||
start_time: '2024-01-01T11:00:00',
|
||||
end_time: '2024-01-01T13:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {}
|
||||
}
|
||||
custom_properties: {},
|
||||
},
|
||||
];
|
||||
|
||||
const result = DVRUtils.categorizeRecordings(recordings, toUserTime, now);
|
||||
|
|
@ -39,8 +39,8 @@ describe('DVRUtils', () => {
|
|||
start_time: '2024-01-01T14:00:00',
|
||||
end_time: '2024-01-01T15:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {}
|
||||
}
|
||||
custom_properties: {},
|
||||
},
|
||||
];
|
||||
|
||||
const result = DVRUtils.categorizeRecordings(recordings, toUserTime, now);
|
||||
|
|
@ -58,8 +58,8 @@ describe('DVRUtils', () => {
|
|||
start_time: '2024-01-01T10:00:00',
|
||||
end_time: '2024-01-01T11:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: { status: 'completed' }
|
||||
}
|
||||
custom_properties: { status: 'completed' },
|
||||
},
|
||||
];
|
||||
|
||||
const result = DVRUtils.categorizeRecordings(recordings, toUserTime, now);
|
||||
|
|
@ -77,8 +77,8 @@ describe('DVRUtils', () => {
|
|||
start_time: '2024-01-01T11:00:00',
|
||||
end_time: '2024-01-01T13:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: { status: 'interrupted' }
|
||||
}
|
||||
custom_properties: { status: 'interrupted' },
|
||||
},
|
||||
];
|
||||
|
||||
const result = DVRUtils.categorizeRecordings(recordings, toUserTime, now);
|
||||
|
|
@ -95,8 +95,8 @@ describe('DVRUtils', () => {
|
|||
start_time: '2024-01-01T09:00:00',
|
||||
end_time: '2024-01-01T10:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {}
|
||||
}
|
||||
custom_properties: {},
|
||||
},
|
||||
];
|
||||
|
||||
const result = DVRUtils.categorizeRecordings(recordings, toUserTime, now);
|
||||
|
|
@ -114,8 +114,8 @@ describe('DVRUtils', () => {
|
|||
end_time: '2024-01-01T13:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {
|
||||
program: { id: 100 }
|
||||
}
|
||||
program: { id: 100 },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
|
|
@ -123,9 +123,9 @@ describe('DVRUtils', () => {
|
|||
end_time: '2024-01-01T13:00:00',
|
||||
channel: 'ch2',
|
||||
custom_properties: {
|
||||
program: { id: 100 }
|
||||
}
|
||||
}
|
||||
program: { id: 100 },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = DVRUtils.categorizeRecordings(recordings, toUserTime, now);
|
||||
|
|
@ -141,8 +141,8 @@ describe('DVRUtils', () => {
|
|||
end_time: '2024-01-01T13:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {
|
||||
program: { title: 'Show A' }
|
||||
}
|
||||
program: { title: 'Show A' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
|
|
@ -150,9 +150,9 @@ describe('DVRUtils', () => {
|
|||
end_time: '2024-01-01T13:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {
|
||||
program: { title: 'Show A' }
|
||||
}
|
||||
}
|
||||
program: { title: 'Show A' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = DVRUtils.categorizeRecordings(recordings, toUserTime, now);
|
||||
|
|
@ -168,8 +168,8 @@ describe('DVRUtils', () => {
|
|||
end_time: '2024-01-01T13:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {
|
||||
program: { title: 'Show A' }
|
||||
}
|
||||
program: { title: 'Show A' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
|
|
@ -177,9 +177,9 @@ describe('DVRUtils', () => {
|
|||
end_time: '2024-01-01T13:00:00',
|
||||
channel: 'ch2',
|
||||
custom_properties: {
|
||||
program: { title: 'Show A' }
|
||||
}
|
||||
}
|
||||
program: { title: 'Show A' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = DVRUtils.categorizeRecordings(recordings, toUserTime, now);
|
||||
|
|
@ -194,22 +194,22 @@ describe('DVRUtils', () => {
|
|||
start_time: '2024-01-01T10:00:00',
|
||||
end_time: '2024-01-01T13:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: { program: { id: 1 } }
|
||||
custom_properties: { program: { id: 1 } },
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
start_time: '2024-01-01T11:30:00',
|
||||
end_time: '2024-01-01T13:00:00',
|
||||
channel: 'ch2',
|
||||
custom_properties: { program: { id: 2 } }
|
||||
custom_properties: { program: { id: 2 } },
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
start_time: '2024-01-01T11:00:00',
|
||||
end_time: '2024-01-01T13:00:00',
|
||||
channel: 'ch3',
|
||||
custom_properties: { program: { id: 3 } }
|
||||
}
|
||||
custom_properties: { program: { id: 3 } },
|
||||
},
|
||||
];
|
||||
|
||||
const result = DVRUtils.categorizeRecordings(recordings, toUserTime, now);
|
||||
|
|
@ -227,8 +227,8 @@ describe('DVRUtils', () => {
|
|||
end_time: '2024-01-01T15:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {
|
||||
program: { tvg_id: 'show1', title: 'Show A' }
|
||||
}
|
||||
program: { tvg_id: 'show1', title: 'Show A' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
|
|
@ -236,8 +236,8 @@ describe('DVRUtils', () => {
|
|||
end_time: '2024-01-01T16:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {
|
||||
program: { tvg_id: 'show1', title: 'Show A' }
|
||||
}
|
||||
program: { tvg_id: 'show1', title: 'Show A' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
|
|
@ -245,9 +245,9 @@ describe('DVRUtils', () => {
|
|||
end_time: '2024-01-01T17:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {
|
||||
program: { tvg_id: 'show1', title: 'Show A' }
|
||||
}
|
||||
}
|
||||
program: { tvg_id: 'show1', title: 'Show A' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = DVRUtils.categorizeRecordings(recordings, toUserTime, now);
|
||||
|
|
@ -265,8 +265,8 @@ describe('DVRUtils', () => {
|
|||
end_time: '2024-01-01T15:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {
|
||||
program: { tvg_id: 'show1', title: 'Show A' }
|
||||
}
|
||||
program: { tvg_id: 'show1', title: 'Show A' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
|
|
@ -274,9 +274,9 @@ describe('DVRUtils', () => {
|
|||
end_time: '2024-01-01T16:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {
|
||||
program: { tvg_id: 'show1', title: 'show a' }
|
||||
}
|
||||
}
|
||||
program: { tvg_id: 'show1', title: 'show a' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = DVRUtils.categorizeRecordings(recordings, toUserTime, now);
|
||||
|
|
@ -293,8 +293,8 @@ describe('DVRUtils', () => {
|
|||
end_time: '2024-01-01T15:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {
|
||||
program: { tvg_id: 'show1', title: 'Show A' }
|
||||
}
|
||||
program: { tvg_id: 'show1', title: 'Show A' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
|
|
@ -302,9 +302,9 @@ describe('DVRUtils', () => {
|
|||
end_time: '2024-01-01T16:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {
|
||||
program: { tvg_id: 'show2', title: 'Show A' }
|
||||
}
|
||||
}
|
||||
program: { tvg_id: 'show2', title: 'Show A' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = DVRUtils.categorizeRecordings(recordings, toUserTime, now);
|
||||
|
|
@ -321,22 +321,28 @@ describe('DVRUtils', () => {
|
|||
start_time: '2024-01-01T16:00:00',
|
||||
end_time: '2024-01-01T17:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: { program: { id: 1, tvg_id: 'show1', title: 'Show A' } }
|
||||
custom_properties: {
|
||||
program: { id: 1, tvg_id: 'show1', title: 'Show A' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
start_time: '2024-01-01T14:00:00',
|
||||
end_time: '2024-01-01T15:00:00',
|
||||
channel: 'ch2',
|
||||
custom_properties: { program: { id: 2, tvg_id: 'show2', title: 'Show B' } }
|
||||
custom_properties: {
|
||||
program: { id: 2, tvg_id: 'show2', title: 'Show B' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
start_time: '2024-01-01T15:00:00',
|
||||
end_time: '2024-01-01T16:00:00',
|
||||
channel: 'ch3',
|
||||
custom_properties: { program: { id: 3, tvg_id: 'show3', title: 'Show C' } }
|
||||
}
|
||||
custom_properties: {
|
||||
program: { id: 3, tvg_id: 'show3', title: 'Show C' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = DVRUtils.categorizeRecordings(recordings, toUserTime, now);
|
||||
|
|
@ -346,7 +352,6 @@ describe('DVRUtils', () => {
|
|||
expect(result.upcoming[2].id).toBe(1);
|
||||
});
|
||||
|
||||
|
||||
it('should sort completed by end_time descending', () => {
|
||||
const recordings = [
|
||||
{
|
||||
|
|
@ -354,22 +359,22 @@ describe('DVRUtils', () => {
|
|||
start_time: '2024-01-01T08:00:00',
|
||||
end_time: '2024-01-01T09:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: { status: 'completed' }
|
||||
custom_properties: { status: 'completed' },
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
start_time: '2024-01-01T10:00:00',
|
||||
end_time: '2024-01-01T11:00:00',
|
||||
channel: 'ch2',
|
||||
custom_properties: { status: 'completed' }
|
||||
custom_properties: { status: 'completed' },
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
start_time: '2024-01-01T09:00:00',
|
||||
end_time: '2024-01-01T10:00:00',
|
||||
channel: 'ch3',
|
||||
custom_properties: { status: 'completed' }
|
||||
}
|
||||
custom_properties: { status: 'completed' },
|
||||
},
|
||||
];
|
||||
|
||||
const result = DVRUtils.categorizeRecordings(recordings, toUserTime, now);
|
||||
|
|
@ -386,8 +391,8 @@ describe('DVRUtils', () => {
|
|||
start_time: '2024-01-01T14:00:00',
|
||||
end_time: '2024-01-01T15:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {}
|
||||
}
|
||||
custom_properties: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = DVRUtils.categorizeRecordings(recordings, toUserTime, now);
|
||||
|
|
@ -418,15 +423,15 @@ describe('DVRUtils', () => {
|
|||
start_time: '2024-01-01T14:00:00',
|
||||
end_time: '2024-01-01T15:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {}
|
||||
custom_properties: {},
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
start_time: '2024-01-01T14:00:00',
|
||||
end_time: '2024-01-01T15:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {}
|
||||
}
|
||||
custom_properties: {},
|
||||
},
|
||||
];
|
||||
|
||||
const result = DVRUtils.categorizeRecordings(recordings, toUserTime, now);
|
||||
|
|
@ -440,8 +445,8 @@ describe('DVRUtils', () => {
|
|||
id: 1,
|
||||
start_time: '2024-01-01T11:00:00',
|
||||
end_time: '2024-01-01T13:00:00',
|
||||
channel: 'ch1'
|
||||
}
|
||||
channel: 'ch1',
|
||||
},
|
||||
];
|
||||
|
||||
const result = DVRUtils.categorizeRecordings(recordings, toUserTime, now);
|
||||
|
|
@ -456,8 +461,8 @@ describe('DVRUtils', () => {
|
|||
start_time: '2024-01-01T14:00:00',
|
||||
end_time: '2024-01-01T15:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {}
|
||||
}
|
||||
custom_properties: {},
|
||||
},
|
||||
];
|
||||
|
||||
const result = DVRUtils.categorizeRecordings(recordings, toUserTime, now);
|
||||
|
|
@ -472,8 +477,8 @@ describe('DVRUtils', () => {
|
|||
start_time: '2024-01-01T14:00:00',
|
||||
end_time: '2024-01-01T15:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {}
|
||||
}
|
||||
custom_properties: {},
|
||||
},
|
||||
];
|
||||
|
||||
const result = DVRUtils.categorizeRecordings(recordings, toUserTime, now);
|
||||
|
|
@ -489,8 +494,8 @@ describe('DVRUtils', () => {
|
|||
end_time: '2024-01-01T15:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {
|
||||
program: { id: 100, tvg_id: 'show1', title: 'Show A' }
|
||||
}
|
||||
program: { id: 100, tvg_id: 'show1', title: 'Show A' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
|
|
@ -498,8 +503,8 @@ describe('DVRUtils', () => {
|
|||
end_time: '2024-01-01T15:00:00',
|
||||
channel: 'ch2',
|
||||
custom_properties: {
|
||||
program: { id: 100, tvg_id: 'show1', title: 'Show A' }
|
||||
}
|
||||
program: { id: 100, tvg_id: 'show1', title: 'Show A' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
|
|
@ -507,9 +512,9 @@ describe('DVRUtils', () => {
|
|||
end_time: '2024-01-01T16:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {
|
||||
program: { id: 101, tvg_id: 'show1', title: 'Show A' }
|
||||
}
|
||||
}
|
||||
program: { id: 101, tvg_id: 'show1', title: 'Show A' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = DVRUtils.categorizeRecordings(recordings, toUserTime, now);
|
||||
|
|
@ -526,9 +531,9 @@ describe('DVRUtils', () => {
|
|||
end_time: '2024-01-01T15:00:00',
|
||||
channel: 'ch1',
|
||||
custom_properties: {
|
||||
program: { tvg_id: 'show1', title: 'Show A' }
|
||||
}
|
||||
}
|
||||
program: { tvg_id: 'show1', title: 'Show A' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = DVRUtils.categorizeRecordings(recordings, toUserTime, now);
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ vi.mock('../../../api.js', () => ({
|
|||
setPluginEnabled: vi.fn(),
|
||||
importPlugin: vi.fn(),
|
||||
reloadPlugins: vi.fn(),
|
||||
deletePlugin: vi.fn()
|
||||
}
|
||||
deletePlugin: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('PluginsUtils', () => {
|
||||
|
|
@ -66,7 +66,9 @@ describe('PluginsUtils', () => {
|
|||
|
||||
API.updatePluginSettings.mockRejectedValue(error);
|
||||
|
||||
await expect(PluginsUtils.updatePluginSettings(key, settings)).rejects.toThrow('API error');
|
||||
await expect(
|
||||
PluginsUtils.updatePluginSettings(key, settings)
|
||||
).rejects.toThrow('API error');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -109,7 +111,9 @@ describe('PluginsUtils', () => {
|
|||
|
||||
API.runPluginAction.mockRejectedValue(error);
|
||||
|
||||
await expect(PluginsUtils.runPluginAction(key, actionId)).rejects.toThrow('Action not found');
|
||||
await expect(PluginsUtils.runPluginAction(key, actionId)).rejects.toThrow(
|
||||
'Action not found'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -187,13 +191,17 @@ describe('PluginsUtils', () => {
|
|||
|
||||
API.setPluginEnabled.mockRejectedValue(error);
|
||||
|
||||
await expect(PluginsUtils.setPluginEnabled(key, next)).rejects.toThrow('Plugin not found');
|
||||
await expect(PluginsUtils.setPluginEnabled(key, next)).rejects.toThrow(
|
||||
'Plugin not found'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('importPlugin', () => {
|
||||
it('should call API importPlugin with importFile', async () => {
|
||||
const importFile = new File(['content'], 'plugin.zip', { type: 'application/zip' });
|
||||
const importFile = new File(['content'], 'plugin.zip', {
|
||||
type: 'application/zip',
|
||||
});
|
||||
|
||||
await PluginsUtils.importPlugin(importFile);
|
||||
|
||||
|
|
@ -202,7 +210,9 @@ describe('PluginsUtils', () => {
|
|||
});
|
||||
|
||||
it('should return API response', async () => {
|
||||
const importFile = new File(['content'], 'plugin.zip', { type: 'application/zip' });
|
||||
const importFile = new File(['content'], 'plugin.zip', {
|
||||
type: 'application/zip',
|
||||
});
|
||||
const mockResponse = { key: 'imported-plugin', success: true };
|
||||
|
||||
API.importPlugin.mockResolvedValue(mockResponse);
|
||||
|
|
@ -230,12 +240,16 @@ describe('PluginsUtils', () => {
|
|||
});
|
||||
|
||||
it('should propagate API errors', async () => {
|
||||
const importFile = new File(['content'], 'plugin.zip', { type: 'application/zip' });
|
||||
const importFile = new File(['content'], 'plugin.zip', {
|
||||
type: 'application/zip',
|
||||
});
|
||||
const error = new Error('Invalid plugin format');
|
||||
|
||||
API.importPlugin.mockRejectedValue(error);
|
||||
|
||||
await expect(PluginsUtils.importPlugin(importFile)).rejects.toThrow('Invalid plugin format');
|
||||
await expect(PluginsUtils.importPlugin(importFile)).rejects.toThrow(
|
||||
'Invalid plugin format'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ vi.mock('../../../api.js', () => ({
|
|||
checkSetting: vi.fn(),
|
||||
updateSetting: vi.fn(),
|
||||
createSetting: vi.fn(),
|
||||
rehashStreams: vi.fn()
|
||||
}
|
||||
rehashStreams: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('SettingsUtils', () => {
|
||||
|
|
@ -36,7 +36,11 @@ describe('SettingsUtils', () => {
|
|||
|
||||
describe('createSetting', () => {
|
||||
it('should call API createSetting with values', async () => {
|
||||
const values = { key: 'new-setting', name: 'New Setting', value: 'value' };
|
||||
const values = {
|
||||
key: 'new-setting',
|
||||
name: 'New Setting',
|
||||
value: 'value',
|
||||
};
|
||||
await SettingsUtils.createSetting(values);
|
||||
expect(API.createSetting).toHaveBeenCalledWith(values);
|
||||
expect(API.createSetting).toHaveBeenCalledTimes(1);
|
||||
|
|
@ -59,13 +63,13 @@ describe('SettingsUtils', () => {
|
|||
key: 'stream_settings',
|
||||
value: {
|
||||
default_user_agent: 5,
|
||||
m3u_hash_key: 'channel_name'
|
||||
}
|
||||
}
|
||||
m3u_hash_key: 'channel_name',
|
||||
},
|
||||
},
|
||||
};
|
||||
const changedSettings = {
|
||||
default_user_agent: 7,
|
||||
preferred_region: 'UK'
|
||||
preferred_region: 'UK',
|
||||
};
|
||||
|
||||
API.updateSetting.mockResolvedValue({});
|
||||
|
|
@ -78,8 +82,8 @@ describe('SettingsUtils', () => {
|
|||
value: {
|
||||
default_user_agent: 7,
|
||||
m3u_hash_key: 'channel_name',
|
||||
preferred_region: 'UK'
|
||||
}
|
||||
preferred_region: 'UK',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -88,11 +92,11 @@ describe('SettingsUtils', () => {
|
|||
stream_settings: {
|
||||
id: 1,
|
||||
key: 'stream_settings',
|
||||
value: {}
|
||||
}
|
||||
value: {},
|
||||
},
|
||||
};
|
||||
const changedSettings = {
|
||||
m3u_hash_key: ['channel_name', 'channel_number']
|
||||
m3u_hash_key: ['channel_name', 'channel_number'],
|
||||
};
|
||||
|
||||
API.updateSetting.mockResolvedValue({});
|
||||
|
|
@ -103,8 +107,8 @@ describe('SettingsUtils', () => {
|
|||
id: 1,
|
||||
key: 'stream_settings',
|
||||
value: {
|
||||
m3u_hash_key: 'channel_name,channel_number'
|
||||
}
|
||||
m3u_hash_key: 'channel_name,channel_number',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -113,12 +117,12 @@ describe('SettingsUtils', () => {
|
|||
stream_settings: {
|
||||
id: 1,
|
||||
key: 'stream_settings',
|
||||
value: {}
|
||||
}
|
||||
value: {},
|
||||
},
|
||||
};
|
||||
const changedSettings = {
|
||||
default_user_agent: '5',
|
||||
default_stream_profile: '3'
|
||||
default_stream_profile: '3',
|
||||
};
|
||||
|
||||
API.updateSetting.mockResolvedValue({});
|
||||
|
|
@ -130,8 +134,8 @@ describe('SettingsUtils', () => {
|
|||
key: 'stream_settings',
|
||||
value: {
|
||||
default_user_agent: 5,
|
||||
default_stream_profile: 3
|
||||
}
|
||||
default_stream_profile: 3,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -140,17 +144,17 @@ describe('SettingsUtils', () => {
|
|||
dvr_settings: {
|
||||
id: 2,
|
||||
key: 'dvr_settings',
|
||||
value: {}
|
||||
value: {},
|
||||
},
|
||||
stream_settings: {
|
||||
id: 1,
|
||||
key: 'stream_settings',
|
||||
value: {}
|
||||
}
|
||||
value: {},
|
||||
},
|
||||
};
|
||||
const changedSettings = {
|
||||
comskip_enabled: true,
|
||||
auto_import_mapped_files: false
|
||||
auto_import_mapped_files: false,
|
||||
};
|
||||
|
||||
API.updateSetting.mockResolvedValue({});
|
||||
|
|
@ -166,15 +170,15 @@ describe('SettingsUtils', () => {
|
|||
id: 5,
|
||||
key: 'proxy_settings',
|
||||
value: {
|
||||
buffering_speed: 1.0
|
||||
}
|
||||
}
|
||||
buffering_speed: 1.0,
|
||||
},
|
||||
},
|
||||
};
|
||||
const changedSettings = {
|
||||
proxy_settings: {
|
||||
buffering_speed: 2.5,
|
||||
buffering_timeout: 15
|
||||
}
|
||||
buffering_timeout: 15,
|
||||
},
|
||||
};
|
||||
|
||||
API.updateSetting.mockResolvedValue({});
|
||||
|
|
@ -186,8 +190,8 @@ describe('SettingsUtils', () => {
|
|||
key: 'proxy_settings',
|
||||
value: {
|
||||
buffering_speed: 2.5,
|
||||
buffering_timeout: 15
|
||||
}
|
||||
buffering_timeout: 15,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -195,8 +199,8 @@ describe('SettingsUtils', () => {
|
|||
const settings = {};
|
||||
const changedSettings = {
|
||||
proxy_settings: {
|
||||
buffering_speed: 2.5
|
||||
}
|
||||
buffering_speed: 2.5,
|
||||
},
|
||||
};
|
||||
|
||||
API.createSetting.mockResolvedValue({});
|
||||
|
|
@ -207,8 +211,8 @@ describe('SettingsUtils', () => {
|
|||
key: 'proxy_settings',
|
||||
name: 'Proxy Settings',
|
||||
value: {
|
||||
buffering_speed: 2.5
|
||||
}
|
||||
buffering_speed: 2.5,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -217,11 +221,11 @@ describe('SettingsUtils', () => {
|
|||
network_access: {
|
||||
id: 6,
|
||||
key: 'network_access',
|
||||
value: []
|
||||
}
|
||||
value: [],
|
||||
},
|
||||
};
|
||||
const changedSettings = {
|
||||
network_access: ['192.168.1.0/24', '10.0.0.0/8']
|
||||
network_access: ['192.168.1.0/24', '10.0.0.0/8'],
|
||||
};
|
||||
|
||||
API.updateSetting.mockResolvedValue({});
|
||||
|
|
@ -231,7 +235,7 @@ describe('SettingsUtils', () => {
|
|||
expect(API.updateSetting).toHaveBeenCalledWith({
|
||||
id: 6,
|
||||
key: 'network_access',
|
||||
value: ['192.168.1.0/24', '10.0.0.0/8']
|
||||
value: ['192.168.1.0/24', '10.0.0.0/8'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -239,7 +243,7 @@ describe('SettingsUtils', () => {
|
|||
describe('parseSettings', () => {
|
||||
it('should parse grouped settings correctly', () => {
|
||||
const mockSettings = {
|
||||
'stream_settings': {
|
||||
stream_settings: {
|
||||
id: 1,
|
||||
key: 'stream_settings',
|
||||
value: {
|
||||
|
|
@ -247,19 +251,19 @@ describe('SettingsUtils', () => {
|
|||
default_stream_profile: 3,
|
||||
m3u_hash_key: 'channel_name,channel_number',
|
||||
preferred_region: 'US',
|
||||
auto_import_mapped_files: true
|
||||
}
|
||||
auto_import_mapped_files: true,
|
||||
},
|
||||
},
|
||||
'dvr_settings': {
|
||||
dvr_settings: {
|
||||
id: 2,
|
||||
key: 'dvr_settings',
|
||||
value: {
|
||||
tv_template: '/media/tv/{show}/{season}/',
|
||||
comskip_enabled: false,
|
||||
pre_offset_minutes: 2,
|
||||
post_offset_minutes: 5
|
||||
}
|
||||
}
|
||||
post_offset_minutes: 5,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = SettingsUtils.parseSettings(mockSettings);
|
||||
|
|
@ -280,13 +284,13 @@ describe('SettingsUtils', () => {
|
|||
|
||||
it('should handle empty m3u_hash_key', () => {
|
||||
const mockSettings = {
|
||||
'stream_settings': {
|
||||
stream_settings: {
|
||||
id: 1,
|
||||
key: 'stream_settings',
|
||||
value: {
|
||||
m3u_hash_key: ''
|
||||
}
|
||||
}
|
||||
m3u_hash_key: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = SettingsUtils.parseSettings(mockSettings);
|
||||
|
|
@ -295,30 +299,30 @@ describe('SettingsUtils', () => {
|
|||
|
||||
it('should handle proxy_settings', () => {
|
||||
const mockSettings = {
|
||||
'proxy_settings': {
|
||||
proxy_settings: {
|
||||
id: 5,
|
||||
key: 'proxy_settings',
|
||||
value: {
|
||||
buffering_speed: 2.5,
|
||||
buffering_timeout: 15
|
||||
}
|
||||
}
|
||||
buffering_timeout: 15,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = SettingsUtils.parseSettings(mockSettings);
|
||||
expect(result.proxy_settings).toEqual({
|
||||
buffering_speed: 2.5,
|
||||
buffering_timeout: 15
|
||||
buffering_timeout: 15,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle network_access', () => {
|
||||
const mockSettings = {
|
||||
'network_access': {
|
||||
network_access: {
|
||||
id: 6,
|
||||
key: 'network_access',
|
||||
value: ['192.168.1.0/24', '10.0.0.0/8']
|
||||
}
|
||||
value: ['192.168.1.0/24', '10.0.0.0/8'],
|
||||
},
|
||||
};
|
||||
|
||||
const result = SettingsUtils.parseSettings(mockSettings);
|
||||
|
|
@ -331,12 +335,12 @@ describe('SettingsUtils', () => {
|
|||
const values = {
|
||||
time_zone: 'America/New_York',
|
||||
max_system_events: 2000,
|
||||
comskip_enabled: true
|
||||
comskip_enabled: true,
|
||||
};
|
||||
const settings = {
|
||||
time_zone: { value: 'UTC' },
|
||||
max_system_events: { value: 1000 },
|
||||
comskip_enabled: { value: false }
|
||||
comskip_enabled: { value: false },
|
||||
};
|
||||
|
||||
const changes = SettingsUtils.getChangedSettings(values, settings);
|
||||
|
|
@ -344,18 +348,18 @@ describe('SettingsUtils', () => {
|
|||
expect(changes).toEqual({
|
||||
time_zone: 'America/New_York',
|
||||
max_system_events: 2000,
|
||||
comskip_enabled: true
|
||||
comskip_enabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not detect unchanged values', () => {
|
||||
const values = {
|
||||
time_zone: 'UTC',
|
||||
max_system_events: 1000
|
||||
max_system_events: 1000,
|
||||
};
|
||||
const settings = {
|
||||
time_zone: { value: 'UTC' },
|
||||
max_system_events: { value: 1000 }
|
||||
max_system_events: { value: 1000 },
|
||||
};
|
||||
|
||||
const changes = SettingsUtils.getChangedSettings(values, settings);
|
||||
|
|
@ -364,10 +368,10 @@ describe('SettingsUtils', () => {
|
|||
|
||||
it('should preserve type of numeric values', () => {
|
||||
const values = {
|
||||
max_system_events: 2000
|
||||
max_system_events: 2000,
|
||||
};
|
||||
const settings = {
|
||||
max_system_events: { value: 1000 }
|
||||
max_system_events: { value: 1000 },
|
||||
};
|
||||
|
||||
const changes = SettingsUtils.getChangedSettings(values, settings);
|
||||
|
|
@ -377,16 +381,16 @@ describe('SettingsUtils', () => {
|
|||
|
||||
it('should detect changes in array values', () => {
|
||||
const values = {
|
||||
m3u_hash_key: ['channel_name', 'channel_number']
|
||||
m3u_hash_key: ['channel_name', 'channel_number'],
|
||||
};
|
||||
const settings = {
|
||||
m3u_hash_key: { value: 'channel_name' }
|
||||
m3u_hash_key: { value: 'channel_name' },
|
||||
};
|
||||
|
||||
const changes = SettingsUtils.getChangedSettings(values, settings);
|
||||
// Arrays are converted to comma-separated strings internally
|
||||
expect(changes).toEqual({
|
||||
m3u_hash_key: 'channel_name,channel_number'
|
||||
m3u_hash_key: 'channel_name,channel_number',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -394,12 +398,12 @@ describe('SettingsUtils', () => {
|
|||
const values = {
|
||||
time_zone: 'America/New_York',
|
||||
proxy_settings: {
|
||||
buffering_speed: 2.5
|
||||
buffering_speed: 2.5,
|
||||
},
|
||||
network_access: ['192.168.1.0/24']
|
||||
network_access: ['192.168.1.0/24'],
|
||||
};
|
||||
const settings = {
|
||||
time_zone: { value: 'UTC' }
|
||||
time_zone: { value: 'UTC' },
|
||||
};
|
||||
|
||||
const changes = SettingsUtils.getChangedSettings(values, settings);
|
||||
|
|
@ -456,8 +460,8 @@ describe('SettingsUtils', () => {
|
|||
epg_match_ignore_prefixes: [],
|
||||
epg_match_ignore_suffixes: [],
|
||||
epg_match_ignore_custom: [],
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
const changedSettings = {
|
||||
epg_match_mode: 'advanced',
|
||||
|
|
@ -476,7 +480,7 @@ describe('SettingsUtils', () => {
|
|||
epg_match_ignore_prefixes: ['HD:'],
|
||||
epg_match_ignore_suffixes: [],
|
||||
epg_match_ignore_custom: [],
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -497,7 +501,7 @@ describe('SettingsUtils', () => {
|
|||
value: {
|
||||
epg_match_mode: 'advanced',
|
||||
epg_match_ignore_prefixes: ['Sling:'],
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -511,8 +515,8 @@ describe('SettingsUtils', () => {
|
|||
epg_match_ignore_prefixes: ['HD:'],
|
||||
epg_match_ignore_suffixes: [' 4K'],
|
||||
epg_match_ignore_custom: ['Plus'],
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
const changedSettings = {
|
||||
epg_match_mode: 'default',
|
||||
|
|
@ -530,7 +534,7 @@ describe('SettingsUtils', () => {
|
|||
epg_match_ignore_prefixes: ['HD:'],
|
||||
epg_match_ignore_suffixes: [' 4K'],
|
||||
epg_match_ignore_custom: ['Plus'],
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ vi.mock('../../../api.js', () => ({
|
|||
stopClient: vi.fn(),
|
||||
stopVODClient: vi.fn(),
|
||||
fetchActiveChannelStats: vi.fn(),
|
||||
getVODStats: vi.fn()
|
||||
}
|
||||
getVODStats: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('StatsUtils', () => {
|
||||
|
|
@ -41,7 +41,9 @@ describe('StatsUtils', () => {
|
|||
|
||||
API.stopChannel.mockRejectedValue(error);
|
||||
|
||||
await expect(StatsUtils.stopChannel(id)).rejects.toThrow('Failed to stop channel');
|
||||
await expect(StatsUtils.stopChannel(id)).rejects.toThrow(
|
||||
'Failed to stop channel'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -72,7 +74,9 @@ describe('StatsUtils', () => {
|
|||
|
||||
API.stopClient.mockRejectedValue(error);
|
||||
|
||||
await expect(StatsUtils.stopClient(channelId, clientId)).rejects.toThrow('Failed to stop client');
|
||||
await expect(StatsUtils.stopClient(channelId, clientId)).rejects.toThrow(
|
||||
'Failed to stop client'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -100,7 +104,9 @@ describe('StatsUtils', () => {
|
|||
|
||||
API.stopVODClient.mockRejectedValue(error);
|
||||
|
||||
await expect(StatsUtils.stopVODClient(clientId)).rejects.toThrow('Failed to stop VOD client');
|
||||
await expect(StatsUtils.stopVODClient(clientId)).rejects.toThrow(
|
||||
'Failed to stop VOD client'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -122,7 +128,9 @@ describe('StatsUtils', () => {
|
|||
|
||||
API.fetchActiveChannelStats.mockRejectedValue(error);
|
||||
|
||||
await expect(StatsUtils.fetchActiveChannelStats()).rejects.toThrow('Failed to fetch stats');
|
||||
await expect(StatsUtils.fetchActiveChannelStats()).rejects.toThrow(
|
||||
'Failed to fetch stats'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -144,26 +152,29 @@ describe('StatsUtils', () => {
|
|||
|
||||
API.getVODStats.mockRejectedValue(error);
|
||||
|
||||
await expect(StatsUtils.getVODStats()).rejects.toThrow('Failed to fetch VOD stats');
|
||||
await expect(StatsUtils.getVODStats()).rejects.toThrow(
|
||||
'Failed to fetch VOD stats'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCombinedConnections', () => {
|
||||
it('should combine channel history and VOD connections', () => {
|
||||
const channelHistory = {
|
||||
'ch1': { channel_id: 'ch1', uptime: 100 }
|
||||
ch1: { channel_id: 'ch1', uptime: 100 },
|
||||
};
|
||||
const vodConnections = [
|
||||
{
|
||||
content_type: 'movie',
|
||||
content_uuid: 'uuid1',
|
||||
connections: [
|
||||
{ client_id: 'client1', connected_at: 50 }
|
||||
]
|
||||
}
|
||||
connections: [{ client_id: 'client1', connected_at: 50 }],
|
||||
},
|
||||
];
|
||||
|
||||
const result = StatsUtils.getCombinedConnections(channelHistory, vodConnections);
|
||||
const result = StatsUtils.getCombinedConnections(
|
||||
channelHistory,
|
||||
vodConnections
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].type).toBe('stream');
|
||||
|
|
@ -172,19 +183,20 @@ describe('StatsUtils', () => {
|
|||
|
||||
it('should sort by sortKey descending (newest first)', () => {
|
||||
const channelHistory = {
|
||||
'ch1': { channel_id: 'ch1', uptime: 50 }
|
||||
ch1: { channel_id: 'ch1', uptime: 50 },
|
||||
};
|
||||
const vodConnections = [
|
||||
{
|
||||
content_type: 'movie',
|
||||
content_uuid: 'uuid1',
|
||||
connections: [
|
||||
{ client_id: 'client1', connected_at: 100 }
|
||||
]
|
||||
}
|
||||
connections: [{ client_id: 'client1', connected_at: 100 }],
|
||||
},
|
||||
];
|
||||
|
||||
const result = StatsUtils.getCombinedConnections(channelHistory, vodConnections);
|
||||
const result = StatsUtils.getCombinedConnections(
|
||||
channelHistory,
|
||||
vodConnections
|
||||
);
|
||||
|
||||
expect(result[0].sortKey).toBe(100);
|
||||
expect(result[1].sortKey).toBe(50);
|
||||
|
|
@ -197,9 +209,9 @@ describe('StatsUtils', () => {
|
|||
content_uuid: 'uuid1',
|
||||
connections: [
|
||||
{ client_id: 'client1', connected_at: 100 },
|
||||
{ client_id: 'client2', connected_at: 200 }
|
||||
]
|
||||
}
|
||||
{ client_id: 'client2', connected_at: 200 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = StatsUtils.getCombinedConnections({}, vodConnections);
|
||||
|
|
@ -218,9 +230,9 @@ describe('StatsUtils', () => {
|
|||
content_uuid: 'uuid1',
|
||||
connections: [
|
||||
{ client_id: 'client1', connected_at: 100 },
|
||||
{ client_id: 'client2', connected_at: 200 }
|
||||
]
|
||||
}
|
||||
{ client_id: 'client2', connected_at: 200 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = StatsUtils.getCombinedConnections({}, vodConnections);
|
||||
|
|
@ -231,7 +243,7 @@ describe('StatsUtils', () => {
|
|||
|
||||
it('should use uptime for stream sortKey', () => {
|
||||
const channelHistory = {
|
||||
'ch1': { channel_id: 'ch1', uptime: 150 }
|
||||
ch1: { channel_id: 'ch1', uptime: 150 },
|
||||
};
|
||||
|
||||
const result = StatsUtils.getCombinedConnections(channelHistory, []);
|
||||
|
|
@ -241,7 +253,7 @@ describe('StatsUtils', () => {
|
|||
|
||||
it('should default to 0 for missing uptime', () => {
|
||||
const channelHistory = {
|
||||
'ch1': { channel_id: 'ch1' }
|
||||
ch1: { channel_id: 'ch1' },
|
||||
};
|
||||
|
||||
const result = StatsUtils.getCombinedConnections(channelHistory, []);
|
||||
|
|
@ -254,10 +266,8 @@ describe('StatsUtils', () => {
|
|||
{
|
||||
content_type: 'movie',
|
||||
content_uuid: 'uuid1',
|
||||
connections: [
|
||||
{ client_id: 'client1', connected_at: 250 }
|
||||
]
|
||||
}
|
||||
connections: [{ client_id: 'client1', connected_at: 250 }],
|
||||
},
|
||||
];
|
||||
|
||||
const result = StatsUtils.getCombinedConnections({}, vodConnections);
|
||||
|
|
@ -270,8 +280,8 @@ describe('StatsUtils', () => {
|
|||
{
|
||||
content_type: 'movie',
|
||||
content_uuid: 'uuid1',
|
||||
connections: []
|
||||
}
|
||||
connections: [],
|
||||
},
|
||||
];
|
||||
|
||||
const result = StatsUtils.getCombinedConnections({}, vodConnections);
|
||||
|
|
@ -290,8 +300,8 @@ describe('StatsUtils', () => {
|
|||
{
|
||||
content_type: 'movie',
|
||||
content_uuid: 'uuid1',
|
||||
connections: null
|
||||
}
|
||||
connections: null,
|
||||
},
|
||||
];
|
||||
|
||||
const result = StatsUtils.getCombinedConnections({}, vodConnections);
|
||||
|
|
@ -303,13 +313,10 @@ describe('StatsUtils', () => {
|
|||
describe('getClientStats', () => {
|
||||
it('should extract clients from channel stats', () => {
|
||||
const stats = {
|
||||
'ch1': {
|
||||
ch1: {
|
||||
channel_id: 'ch1',
|
||||
clients: [
|
||||
{ client_id: 'client1' },
|
||||
{ client_id: 'client2' }
|
||||
]
|
||||
}
|
||||
clients: [{ client_id: 'client1' }, { client_id: 'client2' }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = StatsUtils.getClientStats(stats);
|
||||
|
|
@ -321,13 +328,11 @@ describe('StatsUtils', () => {
|
|||
|
||||
it('should attach channel reference to each client', () => {
|
||||
const stats = {
|
||||
'ch1': {
|
||||
ch1: {
|
||||
channel_id: 'ch1',
|
||||
name: 'Channel 1',
|
||||
clients: [
|
||||
{ client_id: 'client1' }
|
||||
]
|
||||
}
|
||||
clients: [{ client_id: 'client1' }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = StatsUtils.getClientStats(stats);
|
||||
|
|
@ -335,14 +340,14 @@ describe('StatsUtils', () => {
|
|||
expect(result[0].channel).toEqual({
|
||||
channel_id: 'ch1',
|
||||
name: 'Channel 1',
|
||||
clients: [{ client_id: 'client1' }]
|
||||
clients: [{ client_id: 'client1' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle channels without clients array', () => {
|
||||
const stats = {
|
||||
'ch1': { channel_id: 'ch1' },
|
||||
'ch2': { channel_id: 'ch2', clients: null }
|
||||
ch1: { channel_id: 'ch1' },
|
||||
ch2: { channel_id: 'ch2', clients: null },
|
||||
};
|
||||
|
||||
const result = StatsUtils.getClientStats(stats);
|
||||
|
|
@ -352,10 +357,10 @@ describe('StatsUtils', () => {
|
|||
|
||||
it('should handle empty clients array', () => {
|
||||
const stats = {
|
||||
'ch1': {
|
||||
ch1: {
|
||||
channel_id: 'ch1',
|
||||
clients: []
|
||||
}
|
||||
clients: [],
|
||||
},
|
||||
};
|
||||
|
||||
const result = StatsUtils.getClientStats(stats);
|
||||
|
|
@ -365,14 +370,14 @@ describe('StatsUtils', () => {
|
|||
|
||||
it('should combine clients from multiple channels', () => {
|
||||
const stats = {
|
||||
'ch1': {
|
||||
ch1: {
|
||||
channel_id: 'ch1',
|
||||
clients: [{ client_id: 'client1' }]
|
||||
clients: [{ client_id: 'client1' }],
|
||||
},
|
||||
'ch2': {
|
||||
ch2: {
|
||||
channel_id: 'ch2',
|
||||
clients: [{ client_id: 'client2' }]
|
||||
}
|
||||
clients: [{ client_id: 'client2' }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = StatsUtils.getClientStats(stats);
|
||||
|
|
@ -392,9 +397,7 @@ describe('StatsUtils', () => {
|
|||
describe('getStatsByChannelId', () => {
|
||||
it('should create stats indexed by channel_id', () => {
|
||||
const channelStats = {
|
||||
channels: [
|
||||
{ channel_id: 'ch1', total_bytes: 1000 }
|
||||
]
|
||||
channels: [{ channel_id: 'ch1', total_bytes: 1000 }],
|
||||
};
|
||||
const prevChannelHistory = {};
|
||||
const channelsByUUID = {};
|
||||
|
|
@ -415,15 +418,13 @@ describe('StatsUtils', () => {
|
|||
|
||||
it('should calculate bitrates from previous history', () => {
|
||||
const channelStats = {
|
||||
channels: [
|
||||
{ channel_id: 'ch1', total_bytes: 2000 }
|
||||
]
|
||||
channels: [{ channel_id: 'ch1', total_bytes: 2000 }],
|
||||
};
|
||||
const prevChannelHistory = {
|
||||
'ch1': {
|
||||
ch1: {
|
||||
total_bytes: 1000,
|
||||
bitrates: [500]
|
||||
}
|
||||
bitrates: [500],
|
||||
},
|
||||
};
|
||||
|
||||
const result = StatsUtils.getStatsByChannelId(
|
||||
|
|
@ -440,15 +441,13 @@ describe('StatsUtils', () => {
|
|||
it('should limit bitrates array to 15 entries', () => {
|
||||
const prevBitrates = new Array(15).fill(100);
|
||||
const channelStats = {
|
||||
channels: [
|
||||
{ channel_id: 'ch1', total_bytes: 2000 }
|
||||
]
|
||||
channels: [{ channel_id: 'ch1', total_bytes: 2000 }],
|
||||
};
|
||||
const prevChannelHistory = {
|
||||
'ch1': {
|
||||
ch1: {
|
||||
total_bytes: 1000,
|
||||
bitrates: prevBitrates
|
||||
}
|
||||
bitrates: prevBitrates,
|
||||
},
|
||||
};
|
||||
|
||||
const result = StatsUtils.getStatsByChannelId(
|
||||
|
|
@ -466,15 +465,13 @@ describe('StatsUtils', () => {
|
|||
|
||||
it('should skip negative bitrates', () => {
|
||||
const channelStats = {
|
||||
channels: [
|
||||
{ channel_id: 'ch1', total_bytes: 500 }
|
||||
]
|
||||
channels: [{ channel_id: 'ch1', total_bytes: 500 }],
|
||||
};
|
||||
const prevChannelHistory = {
|
||||
'ch1': {
|
||||
ch1: {
|
||||
total_bytes: 1000,
|
||||
bitrates: []
|
||||
}
|
||||
bitrates: [],
|
||||
},
|
||||
};
|
||||
|
||||
const result = StatsUtils.getStatsByChannelId(
|
||||
|
|
@ -490,18 +487,16 @@ describe('StatsUtils', () => {
|
|||
|
||||
it('should merge channel data from channelsByUUID', () => {
|
||||
const channelStats = {
|
||||
channels: [
|
||||
{ channel_id: 'uuid1', total_bytes: 1000 }
|
||||
]
|
||||
channels: [{ channel_id: 'uuid1', total_bytes: 1000 }],
|
||||
};
|
||||
const channelsByUUID = {
|
||||
'uuid1': 'channel-key-1'
|
||||
uuid1: 'channel-key-1',
|
||||
};
|
||||
const channels = {
|
||||
'channel-key-1': {
|
||||
name: 'Channel 1',
|
||||
logo: 'logo.png'
|
||||
}
|
||||
logo: 'logo.png',
|
||||
},
|
||||
};
|
||||
|
||||
const result = StatsUtils.getStatsByChannelId(
|
||||
|
|
@ -518,13 +513,11 @@ describe('StatsUtils', () => {
|
|||
|
||||
it('should find and attach stream profile', () => {
|
||||
const channelStats = {
|
||||
channels: [
|
||||
{ channel_id: 'ch1', stream_profile: '1' }
|
||||
]
|
||||
channels: [{ channel_id: 'ch1', stream_profile: '1' }],
|
||||
};
|
||||
const streamProfiles = [
|
||||
{ id: 1, name: 'HD Profile' },
|
||||
{ id: 2, name: 'SD Profile' }
|
||||
{ id: 2, name: 'SD Profile' },
|
||||
];
|
||||
|
||||
const result = StatsUtils.getStatsByChannelId(
|
||||
|
|
@ -540,13 +533,9 @@ describe('StatsUtils', () => {
|
|||
|
||||
it('should default to Unknown for missing stream profile', () => {
|
||||
const channelStats = {
|
||||
channels: [
|
||||
{ channel_id: 'ch1', stream_profile: '999' }
|
||||
]
|
||||
channels: [{ channel_id: 'ch1', stream_profile: '999' }],
|
||||
};
|
||||
const streamProfiles = [
|
||||
{ id: 1, name: 'HD Profile' }
|
||||
];
|
||||
const streamProfiles = [{ id: 1, name: 'HD Profile' }];
|
||||
|
||||
const result = StatsUtils.getStatsByChannelId(
|
||||
channelStats,
|
||||
|
|
@ -561,9 +550,7 @@ describe('StatsUtils', () => {
|
|||
|
||||
it('should preserve stream_id from channel stats', () => {
|
||||
const channelStats = {
|
||||
channels: [
|
||||
{ channel_id: 'ch1', stream_id: 'stream-123' }
|
||||
]
|
||||
channels: [{ channel_id: 'ch1', stream_id: 'stream-123' }],
|
||||
};
|
||||
|
||||
const result = StatsUtils.getStatsByChannelId(
|
||||
|
|
@ -579,9 +566,7 @@ describe('StatsUtils', () => {
|
|||
|
||||
it('should set stream_id to null if missing', () => {
|
||||
const channelStats = {
|
||||
channels: [
|
||||
{ channel_id: 'ch1' }
|
||||
]
|
||||
channels: [{ channel_id: 'ch1' }],
|
||||
};
|
||||
|
||||
const result = StatsUtils.getStatsByChannelId(
|
||||
|
|
@ -600,8 +585,8 @@ describe('StatsUtils', () => {
|
|||
const channelStats = {
|
||||
channels: [
|
||||
{ total_bytes: 1000 },
|
||||
{ channel_id: 'ch1', total_bytes: 2000 }
|
||||
]
|
||||
{ channel_id: 'ch1', total_bytes: 2000 },
|
||||
],
|
||||
};
|
||||
|
||||
const result = StatsUtils.getStatsByChannelId(
|
||||
|
|
@ -614,7 +599,10 @@ describe('StatsUtils', () => {
|
|||
|
||||
expect(result).not.toHaveProperty('undefined');
|
||||
expect(result).toHaveProperty('ch1');
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Found channel without channel_id:', { total_bytes: 1000 });
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Found channel without channel_id:',
|
||||
{ total_bytes: 1000 }
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
|
@ -635,9 +623,7 @@ describe('StatsUtils', () => {
|
|||
|
||||
it('should initialize empty bitrates array for new channels', () => {
|
||||
const channelStats = {
|
||||
channels: [
|
||||
{ channel_id: 'ch1', total_bytes: 1000 }
|
||||
]
|
||||
channels: [{ channel_id: 'ch1', total_bytes: 1000 }],
|
||||
};
|
||||
|
||||
const result = StatsUtils.getStatsByChannelId(
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ describe('VODsUtils', () => {
|
|||
describe('getCategoryOptions', () => {
|
||||
it('should return all categories option plus formatted categories', () => {
|
||||
const categories = {
|
||||
'cat1': { name: 'Action', category_type: 'movie' },
|
||||
'cat2': { name: 'Drama', category_type: 'series' }
|
||||
cat1: { name: 'Action', category_type: 'movie' },
|
||||
cat2: { name: 'Drama', category_type: 'series' },
|
||||
};
|
||||
const filters = { type: 'all' };
|
||||
|
||||
|
|
@ -14,15 +14,21 @@ describe('VODsUtils', () => {
|
|||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0]).toEqual({ value: '', label: 'All Categories' });
|
||||
expect(result[1]).toEqual({ value: 'Action|movie', label: 'Action (movie)' });
|
||||
expect(result[2]).toEqual({ value: 'Drama|series', label: 'Drama (series)' });
|
||||
expect(result[1]).toEqual({
|
||||
value: 'Action|movie',
|
||||
label: 'Action (movie)',
|
||||
});
|
||||
expect(result[2]).toEqual({
|
||||
value: 'Drama|series',
|
||||
label: 'Drama (series)',
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter to only movies when type is movies', () => {
|
||||
const categories = {
|
||||
'cat1': { name: 'Action', category_type: 'movie' },
|
||||
'cat2': { name: 'Drama', category_type: 'series' },
|
||||
'cat3': { name: 'Comedy', category_type: 'movie' }
|
||||
cat1: { name: 'Action', category_type: 'movie' },
|
||||
cat2: { name: 'Drama', category_type: 'series' },
|
||||
cat3: { name: 'Comedy', category_type: 'movie' },
|
||||
};
|
||||
const filters = { type: 'movies' };
|
||||
|
||||
|
|
@ -36,9 +42,9 @@ describe('VODsUtils', () => {
|
|||
|
||||
it('should filter to only series when type is series', () => {
|
||||
const categories = {
|
||||
'cat1': { name: 'Action', category_type: 'movie' },
|
||||
'cat2': { name: 'Drama', category_type: 'series' },
|
||||
'cat3': { name: 'Sitcom', category_type: 'series' }
|
||||
cat1: { name: 'Action', category_type: 'movie' },
|
||||
cat2: { name: 'Drama', category_type: 'series' },
|
||||
cat3: { name: 'Sitcom', category_type: 'series' },
|
||||
};
|
||||
const filters = { type: 'series' };
|
||||
|
||||
|
|
@ -52,8 +58,8 @@ describe('VODsUtils', () => {
|
|||
|
||||
it('should show all categories when type is all', () => {
|
||||
const categories = {
|
||||
'cat1': { name: 'Action', category_type: 'movie' },
|
||||
'cat2': { name: 'Drama', category_type: 'series' }
|
||||
cat1: { name: 'Action', category_type: 'movie' },
|
||||
cat2: { name: 'Drama', category_type: 'series' },
|
||||
};
|
||||
const filters = { type: 'all' };
|
||||
|
||||
|
|
@ -74,7 +80,7 @@ describe('VODsUtils', () => {
|
|||
|
||||
it('should create value with name and category_type separated by pipe', () => {
|
||||
const categories = {
|
||||
'cat1': { name: 'Action', category_type: 'movie' }
|
||||
cat1: { name: 'Action', category_type: 'movie' },
|
||||
};
|
||||
const filters = { type: 'all' };
|
||||
|
||||
|
|
@ -85,8 +91,8 @@ describe('VODsUtils', () => {
|
|||
|
||||
it('should handle undefined type filter', () => {
|
||||
const categories = {
|
||||
'cat1': { name: 'Action', category_type: 'movie' },
|
||||
'cat2': { name: 'Drama', category_type: 'series' }
|
||||
cat1: { name: 'Action', category_type: 'movie' },
|
||||
cat2: { name: 'Drama', category_type: 'series' },
|
||||
};
|
||||
const filters = {};
|
||||
|
||||
|
|
@ -97,9 +103,9 @@ describe('VODsUtils', () => {
|
|||
|
||||
it('should filter out categories that do not match type', () => {
|
||||
const categories = {
|
||||
'cat1': { name: 'Action', category_type: 'movie' },
|
||||
'cat2': { name: 'Drama', category_type: 'series' },
|
||||
'cat3': { name: 'Comedy', category_type: 'movie' }
|
||||
cat1: { name: 'Action', category_type: 'movie' },
|
||||
cat2: { name: 'Drama', category_type: 'series' },
|
||||
cat3: { name: 'Comedy', category_type: 'movie' },
|
||||
};
|
||||
const filters = { type: 'series' };
|
||||
|
||||
|
|
@ -113,18 +119,14 @@ describe('VODsUtils', () => {
|
|||
describe('filterCategoriesToEnabled', () => {
|
||||
it('should return only categories with enabled m3u_accounts', () => {
|
||||
const allCategories = {
|
||||
'cat1': {
|
||||
cat1: {
|
||||
name: 'Action',
|
||||
m3u_accounts: [
|
||||
{ id: 1, enabled: true }
|
||||
]
|
||||
m3u_accounts: [{ id: 1, enabled: true }],
|
||||
},
|
||||
'cat2': {
|
||||
cat2: {
|
||||
name: 'Drama',
|
||||
m3u_accounts: [
|
||||
{ id: 2, enabled: false }
|
||||
]
|
||||
}
|
||||
m3u_accounts: [{ id: 2, enabled: false }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = VODsUtils.filterCategoriesToEnabled(allCategories);
|
||||
|
|
@ -135,14 +137,14 @@ describe('VODsUtils', () => {
|
|||
|
||||
it('should include category if any m3u_account is enabled', () => {
|
||||
const allCategories = {
|
||||
'cat1': {
|
||||
cat1: {
|
||||
name: 'Action',
|
||||
m3u_accounts: [
|
||||
{ id: 1, enabled: false },
|
||||
{ id: 2, enabled: true },
|
||||
{ id: 3, enabled: false }
|
||||
]
|
||||
}
|
||||
{ id: 3, enabled: false },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = VODsUtils.filterCategoriesToEnabled(allCategories);
|
||||
|
|
@ -152,13 +154,13 @@ describe('VODsUtils', () => {
|
|||
|
||||
it('should exclude category if all m3u_accounts are disabled', () => {
|
||||
const allCategories = {
|
||||
'cat1': {
|
||||
cat1: {
|
||||
name: 'Action',
|
||||
m3u_accounts: [
|
||||
{ id: 1, enabled: false },
|
||||
{ id: 2, enabled: false }
|
||||
]
|
||||
}
|
||||
{ id: 2, enabled: false },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = VODsUtils.filterCategoriesToEnabled(allCategories);
|
||||
|
|
@ -168,10 +170,10 @@ describe('VODsUtils', () => {
|
|||
|
||||
it('should exclude category with empty m3u_accounts array', () => {
|
||||
const allCategories = {
|
||||
'cat1': {
|
||||
cat1: {
|
||||
name: 'Action',
|
||||
m3u_accounts: []
|
||||
}
|
||||
m3u_accounts: [],
|
||||
},
|
||||
};
|
||||
|
||||
const result = VODsUtils.filterCategoriesToEnabled(allCategories);
|
||||
|
|
@ -181,13 +183,11 @@ describe('VODsUtils', () => {
|
|||
|
||||
it('should preserve original category data', () => {
|
||||
const allCategories = {
|
||||
'cat1': {
|
||||
cat1: {
|
||||
name: 'Action',
|
||||
category_type: 'movie',
|
||||
m3u_accounts: [
|
||||
{ id: 1, enabled: true }
|
||||
]
|
||||
}
|
||||
m3u_accounts: [{ id: 1, enabled: true }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = VODsUtils.filterCategoriesToEnabled(allCategories);
|
||||
|
|
@ -203,18 +203,18 @@ describe('VODsUtils', () => {
|
|||
|
||||
it('should filter multiple categories correctly', () => {
|
||||
const allCategories = {
|
||||
'cat1': {
|
||||
cat1: {
|
||||
name: 'Action',
|
||||
m3u_accounts: [{ id: 1, enabled: true }]
|
||||
m3u_accounts: [{ id: 1, enabled: true }],
|
||||
},
|
||||
'cat2': {
|
||||
cat2: {
|
||||
name: 'Drama',
|
||||
m3u_accounts: [{ id: 2, enabled: false }]
|
||||
m3u_accounts: [{ id: 2, enabled: false }],
|
||||
},
|
||||
'cat3': {
|
||||
cat3: {
|
||||
name: 'Comedy',
|
||||
m3u_accounts: [{ id: 3, enabled: true }]
|
||||
}
|
||||
m3u_accounts: [{ id: 3, enabled: true }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = VODsUtils.filterCategoriesToEnabled(allCategories);
|
||||
|
|
@ -227,10 +227,10 @@ describe('VODsUtils', () => {
|
|||
|
||||
it('should handle category with null m3u_accounts', () => {
|
||||
const allCategories = {
|
||||
'cat1': {
|
||||
cat1: {
|
||||
name: 'Action',
|
||||
m3u_accounts: null
|
||||
}
|
||||
m3u_accounts: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
|
|
@ -240,13 +240,13 @@ describe('VODsUtils', () => {
|
|||
|
||||
it('should handle truthy enabled values', () => {
|
||||
const allCategories = {
|
||||
'cat1': {
|
||||
cat1: {
|
||||
name: 'Action',
|
||||
m3u_accounts: [
|
||||
{ id: 1, enabled: 1 },
|
||||
{ id: 2, enabled: false }
|
||||
]
|
||||
}
|
||||
{ id: 2, enabled: false },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = VODsUtils.filterCategoriesToEnabled(allCategories);
|
||||
|
|
@ -256,12 +256,10 @@ describe('VODsUtils', () => {
|
|||
|
||||
it('should only match strict true for enabled', () => {
|
||||
const allCategories = {
|
||||
'cat1': {
|
||||
cat1: {
|
||||
name: 'Action',
|
||||
m3u_accounts: [
|
||||
{ id: 1, enabled: 'true' }
|
||||
]
|
||||
}
|
||||
m3u_accounts: [{ id: 1, enabled: 'true' }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = VODsUtils.filterCategoriesToEnabled(allCategories);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue