From 29cb481f787261ea0daccffdce1810ae6affb04b Mon Sep 17 00:00:00 2001 From: fezster <97789007+fezster@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:29:35 +0000 Subject: [PATCH 1/2] feat: When recording from EPG, use the explicit channel number selected rather than relying on the findChannelByTvgId() function to select the channel - of which there can be many and it would default to the first channel in the list, which may not be the same stream you want to record. --- frontend/src/pages/Guide.jsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/frontend/src/pages/Guide.jsx b/frontend/src/pages/Guide.jsx index 81171637..09f6239f 100644 --- a/frontend/src/pages/Guide.jsx +++ b/frontend/src/pages/Guide.jsx @@ -114,6 +114,7 @@ export default function TVChannelGuide({ startDate, endDate }) { const [recordingForProgram, setRecordingForProgram] = useState(null); const [recordChoiceOpen, setRecordChoiceOpen] = useState(false); const [recordChoiceProgram, setRecordChoiceProgram] = useState(null); + const [recordChoiceChannel, setRecordChoiceChannel] = useState(null); const [existingRuleMode, setExistingRuleMode] = useState(null); const [rulesOpen, setRulesOpen] = useState(false); const [rules, setRules] = useState([]); @@ -708,8 +709,9 @@ export default function TVChannelGuide({ startDate, endDate }) { ); const openRecordChoice = useCallback( - async (program) => { + async (program, channel) => { setRecordChoiceProgram(program); + setRecordChoiceChannel(channel); setRecordChoiceOpen(true); try { const rules = await fetchRules(); @@ -725,8 +727,7 @@ export default function TVChannelGuide({ startDate, endDate }) { ); const recordOne = useCallback( - async (program) => { - const channel = findChannelByTvgId(program.tvg_id); + async (program, channel) => { if (!channel) { showNotification({ title: 'Unable to schedule recording', @@ -739,7 +740,7 @@ export default function TVChannelGuide({ startDate, endDate }) { await createRecording(channel, program); showNotification({ title: 'Recording scheduled' }); }, - [findChannelByTvgId] + [] ); const saveSeriesRule = useCallback(async (program, mode) => { @@ -1436,7 +1437,7 @@ export default function TVChannelGuide({ startDate, endDate }) { program={recordChoiceProgram} recording={recordingForProgram} existingRuleMode={existingRuleMode} - onRecordOne={() => recordOne(recordChoiceProgram)} + onRecordOne={() => recordOne(recordChoiceProgram, recordChoiceChannel)} onRecordSeriesAll={() => saveSeriesRule(recordChoiceProgram, 'all') } @@ -1473,7 +1474,7 @@ export default function TVChannelGuide({ startDate, endDate }) { recording={recordingForProgram} opened={!!selectedProgram} onClose={handleCloseModal} - onRecord={openRecordChoice} + onRecord={(program) => openRecordChoice(program, selectedChannel)} /> From fcbf79494722d4e1edc86ceaee8c09da544a3409 Mon Sep 17 00:00:00 2001 From: fezster <97789007+fezster@users.noreply.github.com> Date: Wed, 25 Mar 2026 09:36:59 +0000 Subject: [PATCH 2/2] feat: Update Guide component tests to use selected channel for recording and enhance program selection functionality --- frontend/src/pages/__tests__/Guide.test.jsx | 120 ++++++++++++++++---- 1 file changed, 97 insertions(+), 23 deletions(-) diff --git a/frontend/src/pages/__tests__/Guide.test.jsx b/frontend/src/pages/__tests__/Guide.test.jsx index 07ef1eeb..2145a2a0 100644 --- a/frontend/src/pages/__tests__/Guide.test.jsx +++ b/frontend/src/pages/__tests__/Guide.test.jsx @@ -153,7 +153,7 @@ vi.mock('react-window', () => ({ {children({ index: i, style: {}, - data: itemData.filteredChannels[i], + data: itemData, })} ))} @@ -162,10 +162,42 @@ vi.mock('react-window', () => ({ })); vi.mock('../../components/GuideRow', () => ({ - default: ({ data }) => ( -
GuideRow for {data?.name}
- ), + default: ({ index, data }) => { + const channel = data?.filteredChannels?.[index]; + const channelPrograms = data?.programsByChannelId?.get(channel?.id) || []; + const program = channelPrograms[0] || { + id: 'prog-1', + tvg_id: 'tvg-1', + title: 'Test Program 1', + channel_id: channel?.id, + start_time: '2024-01-15T12:00:00Z', + end_time: '2024-01-15T13:00:00Z', + programStart: dayjs('2024-01-15T12:00:00Z'), + programEnd: dayjs('2024-01-15T13:00:00Z'), + startMs: dayjs('2024-01-15T12:00:00Z').valueOf(), + endMs: dayjs('2024-01-15T13:00:00Z').valueOf(), + }; + + return ( +
+ GuideRow for {channel?.name} + +
+ ); + }, })); + vi.mock('../../components/HourTimeline', () => ({ default: ({ hourTimeline }) => (
@@ -554,43 +586,85 @@ describe('Guide', () => { }); describe('Recording Functionality', () => { - it('opens Series Rules modal when button is clicked', async () => { + it('opens Program Recording modal when Record One is clicked', async () => { vi.useRealTimers(); const user = userEvent.setup(); render(); - const rulesButton = await screen.findByText('Series Rules'); - await user.click(rulesButton); + const selectButtons = await screen.findAllByTestId('guide-row-select'); + await user.click(selectButtons[0]); + + await waitFor(() => + expect(screen.getByTestId('program-detail-modal')).toBeInTheDocument() + ); + + fireEvent.click(screen.getByText('Record')); + + await waitFor(() => + expect(screen.getByTestId('program-recording-modal')).toBeInTheDocument() + ); + + fireEvent.click(screen.getByText('Record One')); await waitFor(() => { - expect( - screen.getByTestId('series-recording-modal') - ).toBeInTheDocument(); + expect(guideUtils.createRecording).toHaveBeenCalled(); }); - - vi.useFakeTimers(); - vi.setSystemTime(new Date('2024-01-15T12:00:00Z')); }); - it('fetches rules when opening Series Rules modal', async () => { + it('uses selected channel for recordOne instead of tvg_id fallback', async () => { vi.useRealTimers(); - const mockRules = [{ id: 1, title: 'Test Rule' }]; - guideUtils.fetchRules.mockResolvedValue(mockRules); + API.getAllChannelIds.mockResolvedValue(['channel-1']); + API.getChannelsSummary.mockResolvedValue([mockChannelsState.channels['channel-1']]); + guideUtils.filterGuideChannels.mockImplementation((channels) => + Array.isArray(channels) ? channels : Object.values(channels) + ); + + const program = { + id: 'prog-1', + tvg_id: 'tvg-1', + title: 'Test Program 1', + channel_id: 'channel-1', + start_time: now.toISOString(), + end_time: now.add(1, 'hour').toISOString(), + programStart: now, + programEnd: now.add(1, 'hour'), + startMs: now.valueOf(), + endMs: now.add(1, 'hour').valueOf(), + }; + + guideUtils.fetchPrograms.mockResolvedValue([program]); - const user = userEvent.setup(); render(); - const rulesButton = await screen.findByText('Series Rules'); - await user.click(rulesButton); + await waitFor(() => + expect(screen.getAllByTestId('guide-row').length).toBeGreaterThan(0) + ); + + // Use userEvent instead of fireEvent so microtasks (Suspense lazy resolution) are flushed + const user = userEvent.setup({ delay: null }); + + await user.click(screen.getByTestId('guide-row-select')); + + await waitFor(() => + expect(screen.getByTestId('program-detail-modal')).toBeInTheDocument() + ); + + await user.click(screen.getByText('Record')); + + await waitFor(() => + expect(screen.getByTestId('program-recording-modal')).toBeInTheDocument() + ); + + await user.click(screen.getByText('Record One')); await waitFor(() => { - expect(guideUtils.fetchRules).toHaveBeenCalled(); + expect(guideUtils.createRecording).toHaveBeenCalledWith( + expect.objectContaining({ id: 'channel-1' }), + expect.objectContaining({ id: 'prog-1' }) + ); }); - - vi.useFakeTimers(); - vi.setSystemTime(new Date('2024-01-15T12:00:00Z')); }); });