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 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); 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-no-title')).toHaveCount(1); }); test('default mode has no project checkbox column; click row opens that single project', async ({ page }) => { await loadLandingWithProjects(page, SAMPLE_PROJECTS); await page.waitForSelector('.project-table', { timeout: 5000 }); // Default mode: no checkbox column on rows. await expect(page.locator('.project-table tbody td.project-table-checkbox-col')).toHaveCount(0); // Stub navigation so the click doesn't navigate the test page. await page.evaluate(() => { window.__navTo = null; window.LandingApp._setNavigate(url => { window.__navTo = url; }); }); await page.locator('.project-table-row[data-name="197072"]').click(); const navTo = await page.evaluate(() => window.__navTo); expect(navTo).toContain('archive.html?projects=197072'); }); test('column filters narrow the table; filters persist in URL', 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); await expect(page.locator('.project-table tbody')).toContainText('Brownfield Tap'); }); 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); }); test('"+ New group" enters select-mode: checkboxes appear, action bar shows', async ({ page }) => { await loadLandingWithProjects(page, SAMPLE_PROJECTS); await page.waitForSelector('.project-table', { timeout: 5000 }); await expect(page.locator('#selectActionBar')).toBeHidden(); await expect(page.locator('.project-table tbody td.project-table-checkbox-col')).toHaveCount(0); await page.locator('#newGroupBtn').click(); await expect(page.locator('#selectActionBar')).toBeVisible(); await expect(page.locator('.project-table tbody td.project-table-checkbox-col')).toHaveCount(3); await expect(page.locator('#groupNameInput')).toBeFocused(); }); test('Save group writes to localStorage and renders in the groups table', async ({ page }) => { await loadLandingWithProjects(page, SAMPLE_PROJECTS); await page.waitForSelector('.project-table', { timeout: 5000 }); await page.locator('#newGroupBtn').click(); // Click two project rows to check them. await page.locator('.project-table-row[data-name="176109"]').click(); await page.locator('.project-table-row[data-name="197072"]').click(); await page.locator('#groupNameInput').fill('Greenfield Sites'); await page.locator('#saveGroupBtn').click(); // Action bar closed, group rendered in groups table. await expect(page.locator('#selectActionBar')).toBeHidden(); await expect(page.locator('.groups-table')).toContainText('Greenfield Sites'); await expect(page.locator('.groups-row[data-name="Greenfield Sites"] .groups-row-count')).toContainText('2 projects'); const stored = await page.evaluate(() => localStorage.getItem('zddc_landing_groups')); expect(stored).toContain('Greenfield Sites'); expect(stored).toContain('176109'); expect(stored).toContain('197072'); }); test('clicking a group row opens archive with that group\'s projects', async ({ page }) => { await loadLandingWithProjects(page, SAMPLE_PROJECTS); await page.waitForSelector('.project-table', { timeout: 5000 }); // Seed a group via storage and reload. await page.evaluate(() => { localStorage.setItem('zddc_landing_groups', JSON.stringify([ { name: 'Both', projects: ['176109', '197072'] } ])); }); await page.reload({ waitUntil: 'domcontentloaded' }); await page.waitForSelector('.groups-row', { timeout: 5000 }); await page.evaluate(() => { window.__navTo = null; window.LandingApp._setNavigate(url => { window.__navTo = url; }); }); await page.locator('.groups-row[data-name="Both"]').click(); const navTo = await page.evaluate(() => window.__navTo); expect(navTo).toMatch(/archive\.html\?projects=176109,197072|archive\.html\?projects=197072,176109/); }); test('delete button removes a group', async ({ page }) => { await loadLandingWithProjects(page, SAMPLE_PROJECTS); await page.evaluate(() => { localStorage.setItem('zddc_landing_groups', JSON.stringify([ { name: 'ToGo', projects: ['176109'] } ])); }); await page.reload({ waitUntil: 'domcontentloaded' }); await page.waitForSelector('.groups-row', { timeout: 5000 }); // Auto-confirm the confirm() dialog. page.once('dialog', d => d.accept()); await page.locator('.groups-row[data-name="ToGo"] .groups-btn-delete').click(); await expect(page.locator('.groups-row[data-name="ToGo"]')).toHaveCount(0); const stored = await page.evaluate(() => localStorage.getItem('zddc_landing_groups')); expect(stored).not.toContain('ToGo'); }); test('edit button enters select-mode pre-populated with that group\'s projects', async ({ page }) => { await loadLandingWithProjects(page, SAMPLE_PROJECTS); await page.evaluate(() => { localStorage.setItem('zddc_landing_groups', JSON.stringify([ { name: 'OnlyOne', projects: ['176109'] } ])); }); await page.reload({ waitUntil: 'domcontentloaded' }); await page.waitForSelector('.groups-row', { timeout: 5000 }); await page.locator('.groups-row[data-name="OnlyOne"] .groups-btn-edit').click(); await expect(page.locator('#selectActionBar')).toBeVisible(); await expect(page.locator('#groupNameInput')).toHaveValue('OnlyOne'); // Pre-checked: 176109 only. const checked = page.locator('.project-table tbody input[type="checkbox"]:checked'); await expect(checked).toHaveCount(1); const checkedValue = await checked.getAttribute('value'); expect(checkedValue).toBe('176109'); }); test('"Open selected" excludes filtered-out checked projects', async ({ page }) => { await loadLandingWithProjects(page, SAMPLE_PROJECTS); await page.waitForSelector('.project-table', { timeout: 5000 }); await page.locator('#newGroupBtn').click(); // Check two: 176109 and 197072. await page.locator('.project-table-row[data-name="176109"]').click(); await page.locator('.project-table-row[data-name="197072"]').click(); // Filter to hide 197072 (its name doesn't contain "176"). await page.locator('input.column-filter[data-column="pn"]').fill('176'); await page.waitForTimeout(150); // Stub navigation. await page.evaluate(() => { window.__navTo = null; window.LandingApp._setNavigate(url => { window.__navTo = url; }); }); await page.locator('#openSelectedBtn').click(); const navTo = await page.evaluate(() => window.__navTo); expect(navTo).toContain('archive.html?projects=176109'); expect(navTo).not.toContain('197072'); }); test('legacy presets are migrated to groups on first load', async ({ page }) => { await loadLandingWithProjects(page, SAMPLE_PROJECTS); // Seed legacy presets and clear the new key. await page.evaluate(() => { localStorage.removeItem('zddc_landing_groups'); localStorage.setItem('zddc_landing_presets', JSON.stringify([ { name: 'Old Preset', state: { projects: ['176109', '210045'], pn: 'foo', sort: 'name' } } ])); }); await page.reload({ waitUntil: 'domcontentloaded' }); await page.waitForSelector('.groups-row', { timeout: 5000 }); await expect(page.locator('.groups-row[data-name="Old Preset"]')).toBeVisible(); const stored = await page.evaluate(() => localStorage.getItem('zddc_landing_groups')); expect(stored).toContain('Old Preset'); expect(stored).toContain('176109'); expect(stored).toContain('210045'); // Filter/sort metadata should NOT carry over. expect(stored).not.toContain('foo'); }); });