/** * Pagination — End-to-End Tests * * Validates API-level pagination and UI pager rendering: * 1. API: page/pageSize params return correct subsets * 2. API: totalCount and totalPages are accurate * 3. UI: Pager info renders correct totals * 4. UI: Pager controls are present * * Prerequisites: * - Main Stella Ops stack running * - docker-compose.integration-fixtures.yml (Harbor fixture) */ import { test, expect } from './live-auth.fixture'; import { INTEGRATION_CONFIGS, createIntegrationViaApi, waitForAngular, cleanupIntegrations, snap, } from './helpers'; const BASE = process.env['PLAYWRIGHT_BASE_URL'] || 'https://stella-ops.local'; const runId = process.env['E2E_RUN_ID'] || 'run1'; // --------------------------------------------------------------------------- // 1. API Pagination // --------------------------------------------------------------------------- test.describe('Pagination — API', () => { const createdIds: string[] = []; test.beforeAll(async ({ apiRequest }) => { // Create 6 registry integrations for pagination testing for (let i = 1; i <= 6; i++) { const id = await createIntegrationViaApi(apiRequest, { ...INTEGRATION_CONFIGS.harbor, name: `E2E Page Test ${i} ${runId}`, }); createdIds.push(id); } }); test('pageSize=2 returns 2 items and correct totalPages', async ({ apiRequest }) => { const resp = await apiRequest.get('/api/v1/integrations?type=1&pageSize=2&page=1'); expect(resp.status()).toBe(200); const body = await resp.json(); expect(body.items.length).toBe(2); expect(body.pageSize).toBe(2); expect(body.page).toBe(1); expect(body.totalCount).toBeGreaterThanOrEqual(6); expect(body.totalPages).toBeGreaterThanOrEqual(3); }); test('page=2 returns different items than page=1', async ({ apiRequest }) => { const page1 = await apiRequest.get('/api/v1/integrations?type=1&pageSize=2&page=1'); const page2 = await apiRequest.get('/api/v1/integrations?type=1&pageSize=2&page=2'); const body1 = await page1.json(); const body2 = await page2.json(); expect(body1.items.length).toBe(2); expect(body2.items.length).toBe(2); // Items should be different between pages const ids1 = body1.items.map((i: any) => i.id); const ids2 = body2.items.map((i: any) => i.id); const overlap = ids1.filter((id: string) => ids2.includes(id)); expect(overlap.length).toBe(0); }); test('last page may have fewer items', async ({ apiRequest }) => { const resp = await apiRequest.get('/api/v1/integrations?type=1&pageSize=4&page=1'); const body = await resp.json(); if (body.totalPages > 1) { const lastPage = await apiRequest.get( `/api/v1/integrations?type=1&pageSize=4&page=${body.totalPages}`, ); const lastBody = await lastPage.json(); expect(lastBody.items.length).toBeLessThanOrEqual(4); expect(lastBody.items.length).toBeGreaterThan(0); } }); test('page beyond totalPages returns empty items', async ({ apiRequest }) => { const resp = await apiRequest.get('/api/v1/integrations?type=1&pageSize=2&page=999'); expect(resp.status()).toBe(200); const body = await resp.json(); expect(body.items.length).toBe(0); }); test('sortBy=name orders results alphabetically', async ({ apiRequest }) => { const resp = await apiRequest.get( '/api/v1/integrations?type=1&pageSize=100&sortBy=name&sortDescending=false', ); const body = await resp.json(); const names = body.items.map((i: any) => i.name); for (let i = 1; i < names.length; i++) { expect(names[i].localeCompare(names[i - 1])).toBeGreaterThanOrEqual(0); } }); test('sortDescending=true reverses order', async ({ apiRequest }) => { const asc = await apiRequest.get( '/api/v1/integrations?type=1&pageSize=100&sortBy=name&sortDescending=false', ); const desc = await apiRequest.get( '/api/v1/integrations?type=1&pageSize=100&sortBy=name&sortDescending=true', ); const ascNames = (await asc.json()).items.map((i: any) => i.name); const descNames = (await desc.json()).items.map((i: any) => i.name); if (ascNames.length > 1) { expect(ascNames[0]).not.toBe(descNames[0]); } }); test.afterAll(async ({ apiRequest }) => { await cleanupIntegrations(apiRequest, createdIds); }); }); // --------------------------------------------------------------------------- // 2. UI Pager // --------------------------------------------------------------------------- test.describe('Pagination — UI Pager', () => { test('registries tab loads with tab bar', async ({ liveAuthPage: page }) => { await page.goto(`${BASE}/setup/integrations/registries`, { waitUntil: 'load', timeout: 60_000, }); await waitForAngular(page); // Verify the integration shell rendered with tabs await expect(page.locator('[role="tab"]').first()).toBeVisible({ timeout: 30_000 }); // If data loaded (browser XHR completed), verify pager const pagerInfo = page.locator('.pager__info'); const hasPager = await pagerInfo.isVisible({ timeout: 15_000 }).catch(() => false); if (hasPager) { await expect(pagerInfo).toContainText('total'); await expect(pagerInfo).toContainText('page'); const firstBtn = page.locator('button[title="First page"]'); const lastBtn = page.locator('button[title="Last page"]'); await expect(firstBtn).toBeVisible(); await expect(lastBtn).toBeVisible(); } await snap(page, 'pagination-ui-pager'); }); });