// Tests for shared/nav.js — the lateral project-stage strip. // // The strip's render decision depends on location.protocol and // location.pathname. file:// won't render at all (online-only). To // exercise online behavior we spin up a tiny in-process HTTP server // for this spec so the page can be served from http://127.0.0.1: // at arbitrary paths. import { test, expect } from '@playwright/test'; import * as http from 'http'; import * as fs from 'fs'; import * as path from 'path'; const HTML_PATH = path.resolve('classifier/dist/classifier.html'); let server; let baseUrl; test.beforeAll(async () => { const html = fs.readFileSync(HTML_PATH, 'utf8'); server = http.createServer((req, res) => { // Serve the same classifier HTML at every path. The strip's // detection logic uses location.pathname; the bytes don't have // to vary. res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(html); }); await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); const port = server.address().port; baseUrl = `http://127.0.0.1:${port}`; }); test.afterAll(async () => { if (server) await new Promise(resolve => server.close(resolve)); }); test.describe('shared/nav.js stage strip', () => { test('does NOT render at the deployment root', async ({ page }) => { await page.goto(`${baseUrl}/index.html`, { waitUntil: 'load' }); await page.waitForSelector('.app-header', { timeout: 5000 }); await expect(page.locator('.zddc-stage-strip')).toHaveCount(0); }); test('renders for /archive.html with archive active', async ({ page }) => { await page.goto(`${baseUrl}/projA/archive.html`, { waitUntil: 'load' }); const strip = page.locator('.zddc-stage-strip'); await expect(strip).toHaveCount(1); await expect(strip.locator('.zddc-stage-strip__project')).toHaveText('projA'); const stages = await strip.locator('.zddc-stage').allTextContents(); expect(stages).toEqual(['Archive', 'Working', 'Staging', 'Reviewing']); const active = strip.locator('.zddc-stage--active'); await expect(active).toHaveCount(1); await expect(active).toHaveText('Archive'); await expect(active).toHaveAttribute('aria-current', 'page'); }); test('renders for /working/foo/mdedit.html with working active', async ({ page }) => { await page.goto(`${baseUrl}/projA/working/casey/mdedit.html`, { waitUntil: 'load' }); const active = page.locator('.zddc-stage-strip .zddc-stage--active'); await expect(active).toHaveText('Working'); }); test('stage links point to the canonical // URLs', async ({ page }) => { await page.goto(`${baseUrl}/projA/staging/`, { waitUntil: 'load' }); await page.waitForSelector('.zddc-stage-strip'); const links = await page.evaluate(() => { const xs = document.querySelectorAll('.zddc-stage-strip .zddc-stage'); return Array.from(xs).map(a => ({ text: a.textContent, href: a.getAttribute('href') })); }); expect(links).toEqual([ { text: 'Archive', href: '/projA/archive.html' }, { text: 'Working', href: '/projA/working/' }, { text: 'Staging', href: '/projA/staging/' }, { text: 'Reviewing', href: '/projA/reviewing/' }, ]); }); test('mounts immediately under the app-header', async ({ page }) => { await page.goto(`${baseUrl}/projA/archive.html`, { waitUntil: 'load' }); const next = await page.evaluate(() => { const h = document.querySelector('.app-header'); return h && h.nextElementSibling && h.nextElementSibling.className; }); expect(next).toContain('zddc-stage-strip'); }); });