import { describe, test, expect, beforeAll, afterAll } from 'bun:test'; import { chromium } from 'playwright'; import type { Browser, BrowserContext } from 'playwright'; import { BASE, ADMIN_PASS, setupTestEnv, spawnServer, killServer, login, } from './helpers.ts'; let browser: Browser; let server: Awaited>; beforeAll(async () => { await setupTestEnv(); server = await spawnServer(); browser = await chromium.launch(); }); afterAll(async () => { await browser.close(); await killServer(server); }); async function loggedInContext(username = 'admin', password = ADMIN_PASS) { const ctx = await browser.newContext(); const page = await ctx.newPage(); await login(page, username, password); await page.close(); return ctx; } // Helper: create N issues in a repo using the server API (no browser rendering) async function bulkCreateIssues(ctx: BrowserContext, repo: string, count: number) { for (let i = 1; i <= count; i++) { await ctx.request.fetch(`${BASE}/${repo}/issues`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: `title=Issue+number+${i}&body=`, maxRedirects: 0, }).catch(() => {}); // 302 redirect throws; that's fine } } // Helper: create N patches in a repo using the server API async function bulkCreatePatches(ctx: BrowserContext, repo: string, count: number) { const VALID_PATCH = [ 'From a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2 Mon Sep 17 00:00:00 2001', 'From: Test User ', 'Date: Mon, 01 Jan 2024 12:00:00 +0000', 'Subject: [PATCH] Add f.txt', '', '---', 'diff --git a/f.txt b/f.txt', 'new file mode 100644', 'index 0000000..9daeafb', '--- /dev/null', '+++ b/f.txt', '@@ -0,0 +1 @@', '+x', '', ].join('\n'); for (let i = 1; i <= count; i++) { await ctx.request.fetch(`${BASE}/${repo}/patches`, { method: 'POST', multipart: { title: `Patch number ${i}`, patch_file: { name: 'bulk.patch', mimeType: 'text/plain', buffer: Buffer.from(VALID_PATCH) }, }, maxRedirects: 0, }).catch(() => {}); } } // ─── Pagination ─────────────────────────────────────────────────────────────── describe('pagination', () => { let adminCtx: BrowserContext; beforeAll(async () => { adminCtx = await loggedInContext(); // Create a repo dedicated to pagination testing const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/new`); await page.fill('[name=name]', 'paged-repo'); await page.click('form[action="/new"] button[type=submit]'); await page.waitForURL(`${BASE}/paged-repo`); } finally { await page.close(); } // Create 21 issues via the API (triggers page 2 at 20 per page) await bulkCreateIssues(adminCtx, 'paged-repo', 21); // Create 21 patches via browser (patch upload requires multipart) await bulkCreatePatches(adminCtx, 'paged-repo', 21); }); afterAll(async () => { await adminCtx.close(); }); // ── Repo list pagination ────────────────────────────────────────────────── test('repo list page 1 shows repos and no pagination when few repos', async () => { // With only a handful of test repos (< 20), there should be no pagination nav const page = await adminCtx.newPage(); try { await page.goto(BASE); // Repos are shown expect(await page.locator('.repo-name').count()).toBeGreaterThan(0); } finally { await page.close(); } }); // ── Issue pagination ────────────────────────────────────────────────────── test('issue list page 1 shows at most 20 items', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/paged-repo/issues`); expect(await page.locator('.issue-item').count()).toBeLessThanOrEqual(20); } finally { await page.close(); } }); test('issue list pagination nav appears when more than 20 issues', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/paged-repo/issues`); expect(await page.locator('.pagination').isVisible()).toBe(true); } finally { await page.close(); } }); test('issue list page 2 shows remaining issues', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/paged-repo/issues?page=2`); const count = await page.locator('.issue-item').count(); expect(count).toBeGreaterThan(0); expect(count).toBeLessThanOrEqual(20); } finally { await page.close(); } }); test('issue list page 2 prev link goes to page 1', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/paged-repo/issues?page=2`); const prevHref = await page.locator('.pagination-prev .pagination-btn').getAttribute('href'); expect(prevHref).toContain('page=1'); } finally { await page.close(); } }); test('issue list page 1 next link goes to page 2', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/paged-repo/issues`); const nextHref = await page.locator('.pagination-next .pagination-btn').getAttribute('href'); expect(nextHref).toContain('page=2'); } finally { await page.close(); } }); // ── Patch pagination ────────────────────────────────────────────────────── test('patch list page 1 shows at most 20 items', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/paged-repo/patches`); expect(await page.locator('.issue-item').count()).toBeLessThanOrEqual(20); } finally { await page.close(); } }); test('patch list pagination nav appears when more than 20 patches', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/paged-repo/patches`); expect(await page.locator('.pagination').isVisible()).toBe(true); } finally { await page.close(); } }); test('patch list page 2 shows remaining patches', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/paged-repo/patches?page=2`); const count = await page.locator('.issue-item').count(); expect(count).toBeGreaterThan(0); } finally { await page.close(); } }); // ── Commit log pagination ───────────────────────────────────────────────── test('commit log with few commits shows no cursor nav', async () => { // paged-repo has no commits (empty repo) — there's no commit log to paginate // We verify the page loads without pagination controls const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/paged-repo`); expect(await page.locator('.commit-cursor-nav').count()).toBe(0); } finally { await page.close(); } }); });