import { test, expect } from '@playwright/test'; import * as path from 'path'; const HTML_PATH = path.resolve('landing/dist/index.html'); const FILE_URL = 'file://' + HTML_PATH; // The landing page fetches its containing directory with Accept: application/json // to get the project list. Chromium refuses fetch() on file:// URLs entirely, so // page.route() can't help — we monkey-patch window.fetch in an init script and // drive the response from window.__mockProjects, set per-test. const FETCH_MOCK = ` window.__mockProjects = []; const realFetch = window.fetch; window.fetch = async function(url, opts) { const accept = opts && opts.headers && opts.headers.Accept; if (accept && String(accept).includes('json')) { return new Response(JSON.stringify(window.__mockProjects), { status: 200, headers: { 'Content-Type': 'application/json' }, }); } return realFetch.apply(this, arguments); }; `; const SAMPLE_PROJECTS = [ { name: '176109', url: '/176109/', title: 'Greenfield Substation' }, { name: '197072', url: '/197072/', title: 'Brownfield Tap' }, { name: '210045', url: '/210045/', title: '' }, ]; async function loadLandingWithProjects(page, projects) { await page.addInitScript({ content: FETCH_MOCK }); await page.addInitScript(p => { window.__mockProjects = p; }, projects); await page.goto(FILE_URL, { waitUntil: 'domcontentloaded' }); } test.describe('Landing page', () => { test('renders the welcome hero and a project table when projects come back', async ({ page }) => { await loadLandingWithProjects(page, SAMPLE_PROJECTS); await page.waitForSelector('.project-table', { timeout: 5000 }); await expect(page.locator('.landing-hero h1')).toContainText(/Welcome/i); const rowCount = await page.locator('.project-table tbody tr').count(); expect(rowCount).toBe(3); // Title column is shown because at least one project has a title. await expect(page.locator('th.project-table-title-col')).toBeVisible(); await expect(page.locator('.project-table tbody')).toContainText('Greenfield Substation'); await expect(page.locator('.project-table tbody')).toContainText('Brownfield Tap'); // 210045 has no title — should render a dash placeholder, not the empty string. await expect(page.locator('.project-table-no-title')).toHaveCount(1); }); test('column filters narrow the table; URL is updated', async ({ page }) => { await loadLandingWithProjects(page, SAMPLE_PROJECTS); await page.waitForSelector('.project-table', { timeout: 5000 }); await page.locator('input.column-filter[data-column="pn"]').fill('176'); await page.waitForTimeout(150); const rowsAfterPn = await page.locator('.project-table tbody tr').count(); expect(rowsAfterPn).toBe(1); await expect(page.locator('.project-table tbody')).toContainText('176109'); const search = await page.evaluate(() => location.search); expect(search).toContain('pn=176'); await page.locator('input.column-filter[data-column="pn"]').fill(''); await page.locator('input.column-filter[data-column="pt"]').fill('Brown'); await page.waitForTimeout(150); const rowsAfterPt = await page.locator('.project-table tbody tr').count(); expect(rowsAfterPt).toBe(1); await expect(page.locator('.project-table tbody')).toContainText('Brownfield Tap'); }); test('selecting projects enables Open Archive and writes ?projects=', async ({ page }) => { await loadLandingWithProjects(page, SAMPLE_PROJECTS); await page.waitForSelector('.project-table', { timeout: 5000 }); await expect(page.locator('#openArchiveBtn')).toBeDisabled(); await page.locator('.project-table-row[data-name="176109"]').click(); await expect(page.locator('#openArchiveBtn')).toBeEnabled(); await expect(page.locator('#selectionSummary')).toContainText('1 project selected'); const search = await page.evaluate(() => location.search); expect(search).toContain('projects=176109'); }); test('shows a friendly empty state when the server returns no projects', async ({ page }) => { await loadLandingWithProjects(page, []); await page.waitForSelector('.project-list-empty', { timeout: 5000 }); await expect(page.locator('.project-list-empty')).toContainText(/No projects to show/); await expect(page.locator('.project-list-empty')).toContainText(/access/i); await expect(page.locator('#openArchiveBtn')).toBeDisabled(); }); test('save / load named preset round-trips selection + filters', async ({ page }) => { await loadLandingWithProjects(page, SAMPLE_PROJECTS); await page.waitForSelector('.project-table', { timeout: 5000 }); // Set up state: pick 176109 and filter title by "Green". await page.locator('.project-table-row[data-name="176109"]').click(); await page.locator('input.column-filter[data-column="pt"]').fill('Green'); await page.waitForTimeout(150); // Save preset (open the split-button caret menu). await page.locator('#openArchiveMenuBtn').click(); await page.locator('button:has-text("Save current as preset")').click(); await page.locator('#presetNameInput').fill('My View'); await page.locator('button:has-text("Save")').click(); // Clear state so we can verify preset application restores it. // (Manual clear: unclick the row and empty the title filter.) await page.locator('.project-table-row[data-name="176109"]').click(); await page.locator('input.column-filter[data-column="pt"]').fill(''); await page.waitForTimeout(150); // Sanity: nothing selected, no filter. const cleared = await page.evaluate(() => ({ selectedRows: document.querySelectorAll('.project-table-row.is-selected').length, ptValue: document.querySelector('input.column-filter[data-column="pt"]').value, })); expect(cleared.selectedRows).toBe(0); expect(cleared.ptValue).toBe(''); // Open the menu and click "Load" on the preset (apply-stay variant — // clicking the preset name itself would navigate to archive.html). await page.locator('#openArchiveMenuBtn').click(); await page.waitForTimeout(100); await page.locator('.preset-menu-item:has(.preset-menu-item-name:has-text("My View")) .preset-load-btn').click(); await page.waitForTimeout(150); await expect(page.locator('.project-table-row[data-name="176109"]')).toHaveClass(/is-selected/); const ptVal = await page.locator('input.column-filter[data-column="pt"]').inputValue(); expect(ptVal).toBe('Green'); }); });