a97c2a4034
`skillsText` is written into the page with `set:html`, because its copy carries a `<code>.agents/skills/</code>`. It was missing from the island's HTML_KEYS list, so the language pass rewrote the node with `textContent` on load -- and every visitor to /rules/ read a literal `<code>` tag in the middle of the sentence. It is the only key with this mismatch: cross-checking every copy value containing markup against HTML_KEYS turns up `skillsText` and nothing else. Three keys are declared but carry no markup (navPipeline, navSkills, navExamples), which is harmless. Also teaches rendered-text-diff.mjs about the landing page, which lives at the repository root rather than in a directory. It was requesting /index/index.html and diffing against a 404, which reported a clean four spans. With the path fixed the landing page really is clean, 36 of 36. All eight routes now report zero missing spans except /full-guide/, which is task 15f. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
97 lines
3.7 KiB
JavaScript
97 lines
3.7 KiB
JavaScript
#!/usr/bin/env node
|
|
// Compare the *rendered* text of a legacy page against its Astro replacement.
|
|
//
|
|
// node .agents/scripts/rendered-text-diff.mjs full-guide
|
|
//
|
|
// Why this exists: scripts/verify.mjs reads the legacy files, so a migrated
|
|
// page can drop half its content and still pass the gate. Task 15d shipped
|
|
// /full-guide/ missing 86 rendered spans -- the entire verification section,
|
|
// the hands-on exercise brief, and both "Clone from Gitea" links -- and every
|
|
// check was green.
|
|
//
|
|
// Static HTML comparison is useless here: the guide's tab panels are injected
|
|
// by an island at runtime, so half the legacy page's markup has no static
|
|
// counterpart. This walks the live DOM instead and skips anything the browser
|
|
// is not painting -- which also drops the hidden Portuguese half of each
|
|
// bilingual pair, so the two sides line up.
|
|
//
|
|
// Requires playwright (devDependency) and two static servers; it starts both.
|
|
import { spawn } from 'node:child_process';
|
|
import { cpSync, mkdtempSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { chromium } from 'playwright';
|
|
|
|
// `index` is the landing page: it lives at the repository root, not in a
|
|
// directory of its own, so it needs a different path on the legacy side.
|
|
const route = process.argv[2];
|
|
if (!route) {
|
|
console.error('usage: rendered-text-diff.mjs <route> e.g. full-guide, or index');
|
|
process.exit(2);
|
|
}
|
|
const legacyPath = route === 'index' ? 'index.html' : `${route}/index.html`;
|
|
const astroPath = route === 'index' ? '' : `${route}/`;
|
|
|
|
// The built site expects to be served under the configured base path.
|
|
const staging = mkdtempSync(join(tmpdir(), 'af-rtd-'));
|
|
cpSync('dist', join(staging, 'ai-for-dummies'), { recursive: true });
|
|
|
|
const serve = (dir, port) =>
|
|
spawn('python3', ['-m', 'http.server', String(port), '-d', dir], { stdio: 'ignore' });
|
|
const servers = [serve('.', 4197), serve(staging, 4196)];
|
|
const stop = () => {
|
|
servers.forEach((s) => s.kill());
|
|
rmSync(staging, { recursive: true, force: true });
|
|
};
|
|
|
|
// Visible text nodes, in document order, whitespace collapsed.
|
|
const visibleText = () => {
|
|
const out = [];
|
|
const walk = (node) => {
|
|
for (const child of node.childNodes) {
|
|
if (child.nodeType === Node.TEXT_NODE) {
|
|
const text = child.textContent.replace(/\s+/g, ' ').trim();
|
|
if (text) out.push(text);
|
|
continue;
|
|
}
|
|
if (child.nodeType !== Node.ELEMENT_NODE) continue;
|
|
if (child.tagName === 'SCRIPT' || child.tagName === 'STYLE') continue;
|
|
const style = getComputedStyle(child);
|
|
if (child.hidden || style.display === 'none' || style.visibility === 'hidden') continue;
|
|
walk(child);
|
|
}
|
|
};
|
|
walk(document.body);
|
|
return out;
|
|
};
|
|
|
|
try {
|
|
await new Promise((r) => setTimeout(r, 1500));
|
|
const browser = await chromium.launch();
|
|
const grab = async (url) => {
|
|
const page = await browser.newPage({ viewport: { width: 1400, height: 1000 } });
|
|
await page.goto(url, { waitUntil: 'load' });
|
|
// The islands hydrate and render their initial panel on load; without this
|
|
// every panel's copy reads as missing.
|
|
await page.waitForTimeout(1200);
|
|
const spans = await page.evaluate(visibleText);
|
|
await page.close();
|
|
return spans;
|
|
};
|
|
|
|
const legacy = await grab(`http://localhost:4197/${legacyPath}`);
|
|
const astro = await grab(`http://localhost:4196/ai-for-dummies/${astroPath}`);
|
|
await browser.close();
|
|
|
|
const rendered = new Set(astro);
|
|
const missing = legacy.filter((span) => !rendered.has(span));
|
|
|
|
console.log(
|
|
`legacy ${legacy.length} spans · astro ${astro.length} spans · missing ${missing.length}`,
|
|
);
|
|
for (const span of missing) console.log(` - ${span}`);
|
|
process.exitCode = missing.length === 0 ? 0 : 1;
|
|
} finally {
|
|
stop();
|
|
}
|