Add TV guide utility tests and vitest setup

This commit is contained in:
Jim McBride 2025-09-21 09:38:55 -05:00
parent 00b8119b81
commit 323f1d5c05
No known key found for this signature in database
GPG key ID: 3BA456686730E580
7 changed files with 2443 additions and 751 deletions

File diff suppressed because it is too large Load diff

View file

@ -7,7 +7,9 @@
"dev": "vite --host",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
"preview": "vite preview",
"test": "vitest --run",
"test:watch": "vitest"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
@ -43,6 +45,9 @@
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react-swc": "^3.8.0",
@ -50,7 +55,9 @@
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
"jsdom": "^27.0.0",
"prettier": "^3.5.3",
"vite": "^6.2.0"
"vite": "^6.2.0",
"vitest": "^3.2.4"
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,100 @@
import { describe, it, expect } from 'vitest';
import dayjs from 'dayjs';
import {
PROGRAM_HEIGHT,
EXPANDED_PROGRAM_HEIGHT,
buildChannelIdMap,
mapProgramsByChannel,
computeRowHeights,
} from '../guideUtils.js';
describe('guideUtils', () => {
describe('buildChannelIdMap', () => {
it('maps tvg ids from epg records and falls back to channel uuid', () => {
const channels = [
{ id: 1, epg_data_id: 'epg-1', uuid: 'uuid-1' },
{ id: 2, epg_data_id: null, uuid: 'uuid-2' },
];
const tvgsById = {
'epg-1': { tvg_id: 'alpha' },
};
const map = buildChannelIdMap(channels, tvgsById);
expect(map.get('alpha')).toBe(1);
expect(map.get('uuid-2')).toBe(2);
});
});
describe('mapProgramsByChannel', () => {
it('groups programs by channel and sorts them by start time', () => {
const programs = [
{
id: 10,
tvg_id: 'alpha',
start_time: dayjs('2025-01-01T02:00:00Z').toISOString(),
end_time: dayjs('2025-01-01T03:00:00Z').toISOString(),
title: 'Late Show',
},
{
id: 11,
tvg_id: 'alpha',
start_time: dayjs('2025-01-01T01:00:00Z').toISOString(),
end_time: dayjs('2025-01-01T02:00:00Z').toISOString(),
title: 'Evening News',
},
{
id: 20,
tvg_id: 'beta',
start_time: dayjs('2025-01-01T00:00:00Z').toISOString(),
end_time: dayjs('2025-01-01T01:00:00Z').toISOString(),
title: 'Morning Show',
},
];
const channelIdByTvgId = new Map([
['alpha', 1],
['beta', 2],
]);
const map = mapProgramsByChannel(programs, channelIdByTvgId);
expect(map.get(1)).toHaveLength(2);
expect(map.get(1)?.map((item) => item.id)).toEqual([11, 10]);
expect(map.get(2)).toHaveLength(1);
expect(map.get(2)?.[0].startMs).toBeTypeOf('number');
expect(map.get(2)?.[0].endMs).toBeTypeOf('number');
});
});
describe('computeRowHeights', () => {
it('returns program heights with expanded rows when needed', () => {
const filteredChannels = [
{ id: 1 },
{ id: 2 },
];
const programsByChannel = new Map([
[1, [{ id: 10 }, { id: 11 }]],
[2, [{ id: 20 }]],
]);
const collapsed = computeRowHeights(
filteredChannels,
programsByChannel,
null
);
expect(collapsed).toEqual([PROGRAM_HEIGHT, PROGRAM_HEIGHT]);
const expanded = computeRowHeights(
filteredChannels,
programsByChannel,
10
);
expect(expanded).toEqual([
EXPANDED_PROGRAM_HEIGHT,
PROGRAM_HEIGHT,
]);
});
});
});

View file

@ -0,0 +1,71 @@
import dayjs from 'dayjs';
export const PROGRAM_HEIGHT = 90;
export const EXPANDED_PROGRAM_HEIGHT = 180;
export function buildChannelIdMap(channels, tvgsById) {
const map = new Map();
channels.forEach((channel) => {
const tvgRecord = channel.epg_data_id
? tvgsById[channel.epg_data_id]
: null;
const tvgId = tvgRecord?.tvg_id ?? channel.uuid;
if (tvgId) {
map.set(String(tvgId), channel.id);
}
});
return map;
}
export function mapProgramsByChannel(programs, channelIdByTvgId) {
if (!programs?.length || !channelIdByTvgId?.size) {
return new Map();
}
const map = new Map();
programs.forEach((program) => {
const channelId = channelIdByTvgId.get(String(program.tvg_id));
if (!channelId) {
return;
}
if (!map.has(channelId)) {
map.set(channelId, []);
}
const startMs = program.startMs ?? dayjs(program.start_time).valueOf();
const endMs = program.endMs ?? dayjs(program.end_time).valueOf();
map.get(channelId).push({
...program,
startMs,
endMs,
});
});
map.forEach((list) => {
list.sort((a, b) => a.startMs - b.startMs);
});
return map;
}
export function computeRowHeights(
filteredChannels,
programsByChannelId,
expandedProgramId,
defaultHeight = PROGRAM_HEIGHT,
expandedHeight = EXPANDED_PROGRAM_HEIGHT
) {
if (!filteredChannels?.length) {
return [];
}
return filteredChannels.map((channel) => {
const channelPrograms = programsByChannelId.get(channel.id) || [];
const expanded = channelPrograms.some(
(program) => program.id === expandedProgramId
);
return expanded ? expandedHeight : defaultHeight;
});
}

View file

@ -0,0 +1,42 @@
import '@testing-library/jest-dom/vitest';
import { afterEach, vi } from 'vitest';
import { cleanup } from '@testing-library/react';
afterEach(() => {
cleanup();
});
if (typeof window !== 'undefined' && !window.matchMedia) {
window.matchMedia = vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
}));
}
if (typeof window !== 'undefined' && !window.ResizeObserver) {
class ResizeObserver {
constructor(callback) {
this.callback = callback;
}
observe() {}
unobserve() {}
disconnect() {}
}
window.ResizeObserver = ResizeObserver;
}
if (typeof window !== 'undefined') {
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = (cb) => setTimeout(cb, 16);
}
if (!window.cancelAnimationFrame) {
window.cancelAnimationFrame = (id) => clearTimeout(id);
}
}

View file

@ -26,4 +26,10 @@ export default defineConfig({
// },
// },
},
test: {
environment: 'jsdom',
setupFiles: ['./src/test/setupTests.js'],
globals: true,
},
});