// @ts-check /** * PromptJS v6 — Static Adapter Branch & Edge Coverage (S-12) * ============================================================================ * * Target: src/engine/adapters/static.js * Baseline gap (full suite): branch 88.88%, uncovered lines 72, 75, * 320-321, 338. * * The existing v0.8 suite only puts HTML at the TOP level of dist, so the * recursive descent in findHtmlFiles (lines 320-321) and the non-index route * derivation in deriveRouteFromHtml (line 338) never run. injectMetaTags is * also never given ogImage/ogType (lines 72, 75). This suite covers all of * those via a NESTED html layout + a full meta object. */ import fs from 'fs'; import path from 'path'; import { describe, it, expect, afterEach } from 'vitest'; const AdapterStatic = require('../src/engine/adapters/static'); const tmpDirs = []; function makeTempDir() { const dir = path.join('/tmp', 'pjs-v6-static-' + Math.random().toString(36).slice(2, 8)); fs.mkdirSync(dir, { recursive: true }); tmpDirs.push(dir); return dir; } afterEach(() => { for (const dir of tmpDirs) { try { fs.rmSync(dir, { recursive: true, force: true }); } catch {} } tmpDirs.length = 0; }); // ── injectMetaTags ogImage / ogType branches (lines 72, 75) ──────────────── describe('v6 — static.injectMetaTags full meta', () => { it('injects og:image and og:type when present in meta', () => { const html = ''; const result = AdapterStatic.injectMetaTags(html, { title: 'Full', description: 'desc', ogImage: 'https://cdn.example.com/cover.png', ogType: 'article', }); expect(result).toContain('og:image'); expect(result).toContain('https://cdn.example.com/cover.png'); expect(result).toContain('og:type'); expect(result).toContain('article'); }); it('escapes attribute-breaking characters in meta values', () => { const html = ''; const result = AdapterStatic.injectMetaTags(html, { title: 'A "quoted" & title', }); // Raw double-quote / angle brackets must be escaped, not passed through. expect(result).not.toContain('"quoted"'); expect(result).toContain('"'); }); it('only ogImage (no title/desc) still injects the image tag', () => { const html = ''; const result = AdapterStatic.injectMetaTags(html, { ogImage: '/cover.jpg' }); expect(result).toContain('og:image'); expect(result).not.toContain('og:title'); }); }); // ── findHtmlFiles recursion + deriveRouteFromHtml non-index (320-321, 338) ── describe('v6 — static.runStaticAdapter with NESTED html', () => { function seedNestedDist() { const dir = makeTempDir(); const distDir = path.join(dir, 'dist'); fs.mkdirSync(path.join(distDir, 'blog'), { recursive: true }); const js = 'var __x = document.createElement("h1");'; const css = 'h1{color:red}'; const topHtml = '' + ''; // NESTED page in a subdirectory — forces findHtmlFiles to recurse and // deriveRouteFromHtml to take the non-index ('/blog/post') branch. const nestedHtml = '' + ''; fs.writeFileSync(path.join(distDir, 'prompt.js'), js); fs.writeFileSync(path.join(distDir, 'prompt.css'), css); fs.writeFileSync(path.join(distDir, 'index.html'), topHtml); fs.writeFileSync(path.join(distDir, 'blog', 'post.html'), nestedHtml); return distDir; } it('hashes assets and rewrites references in BOTH top-level and nested HTML', () => { const distDir = seedNestedDist(); const result = AdapterStatic.runStaticAdapter({ outDir: distDir, isSPA: false, routes: ['/', '/blog/post'], meta: { title: 'Nested Site', description: 'has nested pages', ogImage: '/og.png', ogType: 'website', }, siteUrl: 'https://example.com', }); expect(result.hashedAssets.js).toMatch(/^prompt\.[0-9a-f]{8}\.js$/); expect(result.hashedAssets.css).toMatch(/^prompt\.[0-9a-f]{8}\.css$/); // Nested page must have its asset refs rewritten (proves recursion ran). const nested = fs.readFileSync(path.join(distDir, 'blog', 'post.html'), 'utf-8'); expect(nested).toContain(result.hashedAssets.js); expect(nested).toContain(result.hashedAssets.css); // og tags injected into nested page too. expect(nested).toContain('og:image'); expect(nested).toContain('og:type'); // Canonical URL for the nested page uses the DERIVED non-index route. expect(nested).toContain('https://example.com/blog/post'); // Top page canonical uses '/' (index → '/'). const top = fs.readFileSync(path.join(distDir, 'index.html'), 'utf-8'); expect(top).toContain('rel="canonical"'); expect(top).toContain('https://example.com/'); // MPA mode → standalone 404 generated. expect(fs.existsSync(path.join(distDir, '404.html'))).toBe(true); const fourOhFour = fs.readFileSync(path.join(distDir, '404.html'), 'utf-8'); expect(fourOhFour).toContain('404'); }); it('SPA mode generates a 404 that reuses the index shell', () => { const dir = makeTempDir(); const distDir = path.join(dir, 'dist'); fs.mkdirSync(distDir, { recursive: true }); const shell = '
'; fs.writeFileSync(path.join(distDir, 'index.html'), shell); AdapterStatic.runStaticAdapter({ outDir: distDir, isSPA: true }); expect(fs.existsSync(path.join(distDir, '404.html'))).toBe(true); const fourOhFour = fs.readFileSync(path.join(distDir, '404.html'), 'utf-8'); // SPA 404 == index shell (reused), so it contains the app mount point. expect(fourOhFour).toContain('id="app"'); }); it('runs cleanly when no prompt.js/prompt.css exist (skips hashing branches)', () => { const dir = makeTempDir(); const distDir = path.join(dir, 'dist'); fs.mkdirSync(distDir, { recursive: true }); // Only an index.html — no prompt.js / prompt.css. The existsSync(jsPath) // and existsSync(cssPath) guards take their FALSE branch. fs.writeFileSync(path.join(distDir, 'index.html'), ''); const result = AdapterStatic.runStaticAdapter({ outDir: distDir, isSPA: false }); expect(result.hashedAssets.js).toBeUndefined(); expect(result.hashedAssets.css).toBeUndefined(); expect(result.errors.length).toBe(0); // MPA 404 still generated. expect(fs.existsSync(path.join(distDir, '404.html'))).toBe(true); }); it('runStaticAdapter() with no args defaults opts to {} (default-arg branch)', () => { const dir = makeTempDir(); const prevCwd = process.cwd(); try { process.chdir(dir); fs.mkdirSync('dist', { recursive: true }); fs.writeFileSync(path.join('dist', 'index.html'), ''); const result = AdapterStatic.runStaticAdapter(); expect(result.errors.length).toBe(0); expect(result.nonce).toBeNull(); } finally { process.chdir(prevCwd); } }); it('CSP injection rewrites every (nested) HTML file and returns a nonce', () => { const distDir = seedNestedDist(); const result = AdapterStatic.runStaticAdapter({ outDir: distDir, csp: true }); expect(typeof result.nonce).toBe('string'); expect(result.nonce.length).toBeGreaterThan(10); const nested = fs.readFileSync(path.join(distDir, 'blog', 'post.html'), 'utf-8'); expect(nested).toContain('Content-Security-Policy'); expect(nested).toContain(result.nonce); }); });