From 6ee88be5499a9ba4d0b63b09996f89d701d65b60 Mon Sep 17 00:00:00 2001 From: arkon Date: Mon, 8 Jun 2026 18:01:20 +0200 Subject: [PATCH] test: fix title tests for new host constructor arg + async renderIndexHtml The WebServer constructor now takes `host` as the 4th positional arg (titleHostname shifted to 5th), and renderIndexHtml became async (it reads settings.json for the gesture bundle) and cache-busts asset URLs. Update the two title tests accordingly: - pass '127.0.0.1' as the bind host so the title value lands in the 5th titleHostname slot (server-index-title + push-payload-host-title) - await renderIndexHtml and make the cases async - strip ?v= cache-bust params before the byte-identical assertion Co-Authored-By: Claude Opus 4.8 (1M context) --- test/push-payload-host-title.test.ts | 3 +- test/server-index-title.test.ts | 44 +++++++++++++++------------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/test/push-payload-host-title.test.ts b/test/push-payload-host-title.test.ts index 2ce1e7f5..b5c39d85 100644 --- a/test/push-payload-host-title.test.ts +++ b/test/push-payload-host-title.test.ts @@ -47,7 +47,8 @@ interface PushPayload { function makeServerWithHost(host: string): WebServer { // Constructor only assigns fields — no network/disk activity until start(). - const server = new WebServer(0, false, true, host); + // 4th arg is the bind host; the title hostname is the 5th arg. + const server = new WebServer(0, false, true, '127.0.0.1', host); // Stub push store: one subscription with all events enabled. const fakeSub = { endpoint: 'https://push.example.com/abc', diff --git a/test/server-index-title.test.ts b/test/server-index-title.test.ts index 85deba6c..76cd4f02 100644 --- a/test/server-index-title.test.ts +++ b/test/server-index-title.test.ts @@ -32,36 +32,38 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const indexHtmlPath = join(__dirname, '..', 'src', 'web', 'public', 'index.html'); const rawTemplate = readFileSync(indexHtmlPath, 'utf-8'); -function render(host?: string): string { - const server = new WebServer(0, false, true, host); - return (server as unknown as { renderIndexHtml: () => string }).renderIndexHtml(); +async function render(host?: string): Promise { + // 4th arg is the bind host; the title hostname is the 5th arg. + const server = new WebServer(0, false, true, '127.0.0.1', host); + // renderIndexHtml is async (it reads settings.json for the gesture bundle). + return (server as unknown as { renderIndexHtml: () => Promise }).renderIndexHtml(); } describe('WebServer index.html templating (#82)', () => { - it('substitutes the bare <title>Codeman with codeman:', () => { - const html = render('laptop'); + it('substitutes the bare Codeman with codeman:', async () => { + const html = await render('laptop'); expect(html).toContain('codeman:laptop'); expect(html).not.toContain('Codeman'); }); - it('defaults to os.hostname() when no titleHostname is supplied', () => { - const html = render(); + it('defaults to os.hostname() when no titleHostname is supplied', async () => { + const html = await render(); const expected = `codeman:${osHostname()}`; expect(html).toContain(expected); }); - it('treats an empty-string titleHostname as "not supplied" and falls back to os.hostname()', () => { + it('treats an empty-string titleHostname as "not supplied" and falls back to os.hostname()', async () => { // CLI normally guarantees a non-empty string, but the constructor's // `titleHostname || getHostname()` guard makes empty fall through — // pin that behavior so a future refactor doesn't accidentally ship // a `codeman:` to users. - const html = render(''); + const html = await render(''); expect(html).toMatch(/codeman:.+<\/title>/); expect(html).not.toContain('<title>codeman:'); }); - it('HTML-escapes < > & in the hostname so it cannot break out of the title tag', () => { - const html = render(''); + it('HTML-escapes < > & in the hostname so it cannot break out of the title tag', async () => { + const html = await render(''); expect(html).toContain('codeman:<script>alert(1)</script>'); // The raw closing from the injected payload must NOT appear // outside the actual title element — escape-then-substitute prevents @@ -69,16 +71,18 @@ describe('WebServer index.html templating (#82)', () => { expect(html).not.toContain('<script>alert(1)</script>'); }); - it('escapes an ampersand without double-encoding existing entities', () => { + it('escapes an ampersand without double-encoding existing entities', async () => { // The escaper replaces & first, then < and >. A hostname that already // contains a literal `&` should render as `&` once, not `&amp;`. - const html = render('a&b'); + const html = await render('a&b'); expect(html).toContain('codeman:a&b'); expect(html).not.toContain('&amp;'); }); - it('only substitutes the tag — the rest of the template is byte-for-byte identical', () => { - const html = render('laptop'); + it('only substitutes the <title> tag — the rest of the template is identical (modulo asset cache-busting)', async () => { + // renderIndexHtml also appends ?v=<mtime> cache-bust params to same-origin + // .js/.css refs; strip them so the title remains the only other change. + const html = (await render('laptop')).replace(/(\.(?:js|css))\?v=[^"]*/g, '$1'); const beforeTitle = rawTemplate.split('<title>Codeman')[0]; const afterTitle = rawTemplate.split('Codeman')[1]; expect(html.startsWith(beforeTitle)).toBe(true); @@ -88,8 +92,8 @@ describe('WebServer index.html templating (#82)', () => { expect(html.length - rawTemplate.length).toBe(expectedDelta); }); - it('replaces the <title> placeholder exactly once', () => { - const html = render('laptop'); + it('replaces the <title> placeholder exactly once', async () => { + const html = await render('laptop'); // Defense against a future regression where the template gains a // second `<title>Codeman` (e.g. inside a