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, seedRepo, seedBranch, seedSubdir, } from './helpers.ts'; let browser: Browser; let server: Awaited>; beforeAll(async () => { await setupTestEnv(); server = await spawnServer(); browser = await chromium.launch(); // Create my-repo and seed it const adminCtx = await browser.newContext(); const adminPage = await adminCtx.newPage(); try { await login(adminPage); await adminPage.goto(`${BASE}/new`); await adminPage.fill('[name=name]', 'my-repo'); await adminPage.click('form[action="/new"] button[type=submit]'); await adminPage.waitForURL(`${BASE}/my-repo`); } finally { await adminCtx.close(); } await seedRepo('my-repo'); }); 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; } // ─── Branch selector ────────────────────────────────────────────────────────── describe('branch selector', () => { let adminCtx: BrowserContext; beforeAll(async () => { adminCtx = await loggedInContext(); // Add a second branch so the selector is meaningful await seedBranch('my-repo', 'dev'); }); afterAll(async () => { await adminCtx.close(); }); test('branch selector appears on repo home', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/my-repo`); expect(await page.locator('.branch-selector').isVisible()).toBe(true); expect(await page.locator('.branch-select').inputValue()).toBe('main'); } finally { await page.close(); } }); test('branch selector shows all branches on repo home', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/my-repo`); const options = await page.locator('.branch-select option').allTextContents(); expect(options).toContain('main'); expect(options).toContain('dev'); } finally { await page.close(); } }); test('branch selector appears on file tree with current ref selected', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/my-repo/tree/main`); expect(await page.locator('.branch-selector').isVisible()).toBe(true); expect(await page.locator('.branch-select').inputValue()).toBe('main'); } finally { await page.close(); } }); test('branch selector appears on commit log with current ref selected', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/my-repo/commits/main`); expect(await page.locator('.branch-selector').isVisible()).toBe(true); expect(await page.locator('.branch-select').inputValue()).toBe('main'); } finally { await page.close(); } }); test('branch selector appears on file blob', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/my-repo/blob/main/README.md`); expect(await page.locator('.branch-selector').isVisible()).toBe(true); expect(await page.locator('.branch-select').inputValue()).toBe('main'); } finally { await page.close(); } }); test('switching branch on commit log navigates to the selected branch', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/my-repo/commits/main`); await page.locator('.branch-select').selectOption('dev'); await page.locator('form.branch-selector').evaluate((f: any) => f.submit()); await page.waitForURL(`${BASE}/my-repo/commits/dev`); expect(page.url()).toContain('/commits/dev'); } finally { await page.close(); } }); test('switching branch on file tree navigates to the selected branch', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/my-repo/tree/main`); await page.locator('.branch-select').selectOption('dev'); await page.locator('form.branch-selector').evaluate((f: any) => f.submit()); await page.waitForURL(`${BASE}/my-repo/tree/dev`); expect(page.url()).toContain('/tree/dev'); } finally { await page.close(); } }); test('branch-switch route preserves subpath when switching tree', async () => { const page = await adminCtx.newPage(); try { const resp = await page.request.get( `${BASE}/my-repo/branch-switch?view=tree&rev=dev&path=src/foo`, { maxRedirects: 0 }, ).catch(r => r); // 302 redirect to /my-repo/tree/dev/src/foo const loc = (resp as any).headers()?.['location'] ?? ''; expect(loc).toContain('/tree/dev/src/foo'); } finally { await page.close(); } }); test('branch-switch route redirects commits view correctly', async () => { const page = await adminCtx.newPage(); try { const resp = await page.request.get( `${BASE}/my-repo/branch-switch?view=commits&rev=dev`, { maxRedirects: 0 }, ).catch(r => r); const loc = (resp as any).headers()?.['location'] ?? ''; expect(loc).toContain('/commits/dev'); } finally { await page.close(); } }); test('branch-switch route redirects blob view correctly', async () => { const page = await adminCtx.newPage(); try { const resp = await page.request.get( `${BASE}/my-repo/branch-switch?view=blob&rev=dev&path=README.md`, { maxRedirects: 0 }, ).catch(r => r); const loc = (resp as any).headers()?.['location'] ?? ''; expect(loc).toContain('/blob/dev/README.md'); } finally { await page.close(); } }); }); // ─── Default branch settings ────────────────────────────────────────────────── describe('default branch settings', () => { // Runs after 'branch selector', so my-repo already has both main and dev branches. let adminCtx: BrowserContext; beforeAll(async () => { adminCtx = await loggedInContext(); }); afterAll(async () => { await adminCtx.close(); }); test('settings page shows default branch select', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/my-repo/settings`); expect(await page.locator('#default_branch').isVisible()).toBe(true); const options = await page.locator('#default_branch option').allTextContents(); expect(options).toContain('main'); expect(options).toContain('dev'); } finally { await page.close(); } }); test('current default branch is pre-selected', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/my-repo/settings`); expect(await page.locator('#default_branch').inputValue()).toBe('main'); } finally { await page.close(); } }); test('changing default branch saves and is reflected in the repo home', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/my-repo/settings`); await page.locator('#default_branch').selectOption('dev'); await page.click('form[action$="/settings"] button[type=submit]'); expect(await page.locator('.form-success').isVisible()).toBe(true); // The select now shows dev as current expect(await page.locator('#default_branch').inputValue()).toBe('dev'); // Repo home branch selector should reflect the new default await page.goto(`${BASE}/my-repo`); expect(await page.locator('.branch-select').inputValue()).toBe('dev'); } finally { await page.close(); } }); test('commit log link in repo nav uses the new default branch', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/my-repo`); const commitsHref = await page.locator('.repo-tab[href*="/commits/"]').getAttribute('href'); expect(commitsHref).toContain('/commits/dev'); } finally { await page.close(); } }); test('changing default branch back to main restores original state', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/my-repo/settings`); await page.locator('#default_branch').selectOption('main'); await page.click('form[action$="/settings"] button[type=submit]'); expect(await page.locator('.form-success').isVisible()).toBe(true); expect(await page.locator('#default_branch').inputValue()).toBe('main'); } finally { await page.close(); } }); test('settings page shows hint instead of select when repo has no branches', async () => { // Create an empty repo (no commits → no branches) const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/new`); await page.fill('[name=name]', 'empty-for-branch-test'); await page.click('form[action="/new"] button[type=submit]'); await page.waitForURL(`${BASE}/empty-for-branch-test`); await page.goto(`${BASE}/empty-for-branch-test/settings`); expect(await page.locator('#default_branch').count()).toBe(0); expect(await page.locator('.form-hint').isVisible()).toBe(true); } finally { await page.close(); } }); }); // ─── File browser ───────────────────────────────────────────────────────────── describe('file browser', () => { // my-repo already has README.md + index.js from the file-level beforeAll. // We add a subdirectory here so we can test directory navigation. let adminCtx: BrowserContext; beforeAll(async () => { adminCtx = await loggedInContext(); await seedSubdir('my-repo', 'src', { 'app.ts': 'export {};\n', 'README.md': '# src readme\n', }); }); afterAll(async () => { await adminCtx.close(); }); test('repo home shows file tree instead of recent commits', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/my-repo`); expect(await page.locator('.file-tree').isVisible()).toBe(true); expect(await page.locator('.repo-commits-section').count()).toBe(0); } finally { await page.close(); } }); test('repo home file tree lists files and directories', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/my-repo`); const names = await page.locator('.file-name a').allTextContents(); expect(names).toContain('README.md'); expect(names).toContain('index.js'); expect(names).toContain('src'); } finally { await page.close(); } }); test('directories appear before files in file tree', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/my-repo`); const names = await page.locator('.file-name a').allTextContents(); const srcIdx = names.indexOf('src'); const readmeIdx = names.indexOf('README.md'); expect(srcIdx).toBeGreaterThanOrEqual(0); expect(readmeIdx).toBeGreaterThanOrEqual(0); expect(srcIdx).toBeLessThan(readmeIdx); } finally { await page.close(); } }); test('no ".." entry at repository root', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/my-repo`); const names = await page.locator('.file-name a').allTextContents(); expect(names).not.toContain('..'); } finally { await page.close(); } }); test('clicking directory navigates into it', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/my-repo`); await page.locator('.file-name a', { hasText: 'src' }).click(); await page.waitForURL(`${BASE}/my-repo/tree/main/src`); expect(page.url()).toContain('/tree/main/src'); } finally { await page.close(); } }); test('".." entry appears in subdirectory', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/my-repo/tree/main/src`); const names = await page.locator('.file-name a').allTextContents(); expect(names).toContain('..'); } finally { await page.close(); } }); test('".." at one level deep links to tree root', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/my-repo/tree/main/src`); const upHref = await page.locator('.file-name a', { hasText: '..' }).getAttribute('href'); expect(upHref).toBe('/my-repo/tree/main'); } finally { await page.close(); } }); test('files in subdirectory show plain names, not full paths', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/my-repo/tree/main/src`); const names = await page.locator('.file-name a').allTextContents(); expect(names).toContain('app.ts'); // Must NOT contain the full path with prefix expect(names).not.toContain('src/app.ts'); expect(names).not.toContain('src/README.md'); } finally { await page.close(); } }); test('readme is shown below file tree on repo home', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/my-repo`); const treeBox = await page.locator('.file-tree').boundingBox(); const readmeBox = await page.locator('.readme-section').boundingBox(); expect(treeBox).not.toBeNull(); expect(readmeBox).not.toBeNull(); expect(readmeBox!.y).toBeGreaterThan(treeBox!.y + treeBox!.height - 1); } finally { await page.close(); } }); test('readme in subdirectory is shown when present', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/my-repo/tree/main/src`); expect(await page.locator('.readme-section').isVisible()).toBe(true); expect(await page.locator('.readme-section .markdown-body').innerHTML()) .toContain('src readme'); } finally { await page.close(); } }); test('file tree on /tree/:ref also shows readme', async () => { const page = await adminCtx.newPage(); try { await page.goto(`${BASE}/my-repo/tree/main`); expect(await page.locator('.file-tree').isVisible()).toBe(true); expect(await page.locator('.readme-section').isVisible()).toBe(true); } finally { await page.close(); } }); });