fix(634): page the rerun-collection picker to completeness #645
@@ -180,12 +180,48 @@ describe('SchedulesScreen — load', () => {
|
||||
// clamped to 100 server-side that skipped the first 100 rows — so the picker showed nothing at
|
||||
// all for the ordinary case of <=100 rerun collections. Assert the offset, not just the URL
|
||||
// shape: `pageNum=1` here is a silent empty picker, never an error.
|
||||
for (const request of rerunRequests) {
|
||||
const pageNum = new URL(request.url, 'http://localhost').searchParams.get('pageNum');
|
||||
expect(pageNum).toBe('0');
|
||||
}
|
||||
//
|
||||
// Assert only the FIRST request. Since #634 the loader legitimately walks pageNum 1, 2, … to
|
||||
// page to completeness, so "every request is page 0" is an invariant the CORRECT code violates.
|
||||
// It holds today only because this fixture's totalCount fits in one page — raising that default
|
||||
// would fail this test with a misleading "requested page 1" signal for what is proper paging.
|
||||
const firstPageNum = new URL(rerunRequests[0].url, 'http://localhost').searchParams.get('pageNum');
|
||||
expect(firstPageNum).toBe('0');
|
||||
});
|
||||
|
||||
it('pages the rerun-collection picker to completeness when totalCount exceeds one page (#634)', async () => {
|
||||
// Server clamps pageSize to 100 (MaxPageSize) regardless of what's requested, so a single
|
||||
// request only ever returns 100 rows. With 150 rerun collections the picker must issue a
|
||||
// second page (pageNum=1) and end up with all 150 — not silently stop at 100.
|
||||
const page0 = Array.from({ length: 100 }, (_, i) => ({ id: i + 1, name: `Rerun ${i + 1}` }));
|
||||
const page1 = Array.from({ length: 50 }, (_, i) => ({ id: i + 101, name: `Rerun ${i + 101}` }));
|
||||
|
||||
const handle = await renderReady({
|
||||
items: [responseItem({ collectionType: 'RerunFirstRun', collectionId: null, rerunCollectionId: null })],
|
||||
onRequest: (url) => {
|
||||
if (!url.startsWith('/api/v1/rerun-collections')) {
|
||||
return null;
|
||||
}
|
||||
const pageNum = new URL(url, 'http://localhost').searchParams.get('pageNum');
|
||||
if (pageNum === '0') {
|
||||
return jsonResponse({ totalCount: 150, page: page0 });
|
||||
}
|
||||
if (pageNum === '1') {
|
||||
return jsonResponse({ totalCount: 150, page: page1 });
|
||||
}
|
||||
return jsonResponse({ totalCount: 150, page: [] });
|
||||
}
|
||||
});
|
||||
|
||||
const rerunRequests = handle.requests.filter((r) => r.url.startsWith('/api/v1/rerun-collections'));
|
||||
expect(rerunRequests.length).toBe(2);
|
||||
|
||||
const select = screen.getByLabelText('Rerun collection') as HTMLSelectElement;
|
||||
// '(none)' + 150 rerun collections — the exact expected set, not a truthy/non-empty check.
|
||||
const values = Array.from(select.options).map((o) => o.value);
|
||||
expect(values).toEqual(['', ...Array.from({ length: 150 }, (_, i) => `${i + 1}`)]);
|
||||
}, 15000);
|
||||
|
||||
it('renders the "Active schedule" selector non-full-width so it cannot overflow the header (#463)', async () => {
|
||||
await renderReady();
|
||||
const select = screen.getByLabelText('Active schedule');
|
||||
|
||||
@@ -22,7 +22,8 @@ import {
|
||||
getWatermarks,
|
||||
messageFromScheduleError,
|
||||
type ProgramSchedule,
|
||||
replaceScheduleItems
|
||||
replaceScheduleItems,
|
||||
type RerunCollection
|
||||
} from '../api';
|
||||
import { registerNavigationGuard } from '../navigationGuard';
|
||||
import { usePrimaryAction } from '../primaryAction';
|
||||
@@ -41,18 +42,51 @@ import { ScheduleItemInspector, type SchedulePickerData } from '../schedules/Sch
|
||||
|
||||
const DIRTY_PROMPT = 'You have unsaved schedule changes. Discard them?';
|
||||
|
||||
// Server clamps pageSize to MaxPageSize=100 (RerunCollectionController) regardless of what's
|
||||
// requested, so a single `pageSize: 1000` request only ever returns the first 100 rows. Page to
|
||||
// completeness against totalCount instead of raising the server cap (api.search-allitems-paging
|
||||
// precedent — the client pages, the server stays bounded). Mirrors CollectionsScreen.enterReorder.
|
||||
//
|
||||
// `pageNum` is 0-BASED here and everywhere on /api/v1 (api.paging-zero-based): the first page is 0,
|
||||
// so the loop below starts its follow-up requests at 1. Before ersatztv#616 this call site passed
|
||||
// `pageNum: 1`, which skipped the first page entirely and showed nothing at all for the ordinary
|
||||
// case of <=100 rerun collections — the same call site, one defect earlier.
|
||||
const RERUN_PAGE_SIZE = 100;
|
||||
|
||||
async function loadAllRerunCollections(): Promise<RerunCollection[]> {
|
||||
const first = await getRerunCollections({ pageNum: 0, pageSize: RERUN_PAGE_SIZE });
|
||||
let all = first.page ?? [];
|
||||
const totalCount = first.totalCount ?? all.length;
|
||||
let pageNum = 1;
|
||||
|
||||
while (all.length < totalCount) {
|
||||
const next = await getRerunCollections({ pageNum, pageSize: RERUN_PAGE_SIZE });
|
||||
const nextPage = next.page ?? [];
|
||||
|
||||
if (nextPage.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
all = [...all, ...nextPage];
|
||||
pageNum += 1;
|
||||
}
|
||||
|
||||
return all;
|
||||
}
|
||||
|
||||
type BootState =
|
||||
| { status: 'loading' }
|
||||
| { status: 'error'; error: string }
|
||||
| { status: 'ready'; schedules: ProgramSchedule[]; pickers: SchedulePickerData };
|
||||
|
||||
// Other picker fetches below (getPlaylistGroups, getWatermarks, getGraphicsElements, getLanguages,
|
||||
// getFillerPresetsByKind) hit endpoints that return a plain, unpaged array server-side — no
|
||||
// pageNum/pageSize params, no clamp — so they aren't subject to the same silent-truncation defect
|
||||
// as rerun collections and don't need the same treatment (ersatztv#634).
|
||||
async function loadPickerData(): Promise<SchedulePickerData> {
|
||||
const [rerun, playlistGroups, watermarks, graphicsElements, languages, pre, mid, post, tail, fallback] =
|
||||
const [rerunCollections, playlistGroups, watermarks, graphicsElements, languages, pre, mid, post, tail, fallback] =
|
||||
await Promise.all([
|
||||
// pageNum is 0-BASED (api.paging-zero-based). This asked for page 1, which skipped the first
|
||||
// page entirely: pageSize is clamped to 100 server-side, so the picker was served rows 101+
|
||||
// and showed nothing at all for the normal case of <=100 rerun collections (ersatztv#616).
|
||||
getRerunCollections({ pageNum: 0, pageSize: 1000 }),
|
||||
loadAllRerunCollections(),
|
||||
getPlaylistGroups(),
|
||||
getWatermarks(),
|
||||
getGraphicsElements(),
|
||||
@@ -65,7 +99,7 @@ async function loadPickerData(): Promise<SchedulePickerData> {
|
||||
]);
|
||||
|
||||
return {
|
||||
rerunCollections: rerun.page ?? [],
|
||||
rerunCollections,
|
||||
playlistGroups,
|
||||
watermarks,
|
||||
graphicsElements,
|
||||
|
||||
Reference in New Issue
Block a user