import assert from "node:assert/strict" import { test } from "node:test" import { mkdtemp, mkdir, readFile, readdir, rm, symlink, writeFile } from "node:fs/promises" import { tmpdir } from "node:os" import { dirname, join, resolve } from "node:path" import { fileURLToPath } from "node:url" import { runInNewContext } from "node:vm" import { execFile } from "node:child_process" import { promisify } from "node:util" import { parse as parseYaml } from "yaml" import { buildHelp, HELP_BASE_URL, languageRedirectScript, readDownloads, readFunding, readSite, readChromeStrings, SECTION_IDS, TOUR_MEDIA, TOUR_URL, TRANSLATION_PROBLEMS, validateFunding, validateRelease } from "./build-help.mjs" import { fetchReleases, nextPage, run as resolveRelease, selectRelease } from "./resolve-help-release.mjs" import "./test-help-download.mjs" // Help builds and checks are offline: any fetch fails the call and the final test. const fetchCalls = [] globalThis.fetch = async (...request) => { fetchCalls.push(request); throw new Error("help tests must not use the network") } const root = fileURLToPath(new URL("../", import.meta.url)) const args = process.argv.slice(2) function option(name, fallback) { const index = args.indexOf(name) if (index === -1) return fallback const [value] = args.splice(index, 2).slice(1) assert.ok(value, `${name} needs a value`) return value } const content = resolve(option("--content", resolve(root, "docs/help"))) const onlyLocale = option("--locale", process.env.CODENOMAD_HELP_LOCALE) assert.deepEqual(args, [], "unknown help test options") const LOCALES = ["en", "fr", "de", "es", "he", "ja", "ne", "ru", "tr", "zh-Hans"] const sections = (pages) => SECTION_IDS.map((id, index) => ({ id, pages: index === 0 ? pages : [] })) async function temporary(ctx) { const temporaryRoot = process.platform === "win32" ? join(tmpdir(), "opencode") : tmpdir() await mkdir(temporaryRoot, { recursive: true }) const directory = await mkdtemp(join(temporaryRoot, "codenomad-help-")) ctx.after(() => rm(directory, { recursive: true, force: true })) return directory } async function fixture(ctx) { const directory = await temporary(ctx) const source = join(directory, "source") await mkdir(source) const manifest = JSON.parse(await readFile(resolve(root, "docs/help/site.json"), "utf8")) await writeFile(join(source, "site.json"), JSON.stringify(manifest)) // A Windows temp directory may be on another drive; a junction keeps the // Markdown image relative while still reading the real repository asset. await symlink(resolve(root, "images"), join(source, "images"), process.platform === "win32" ? "junction" : "dir") const image = "../images/CodeNomad-Icon.png" for (const locale of LOCALES) { await mkdir(join(source, locale)) for (const page of manifest.sections.flatMap((section) => section.pages)) { await writeFile(join(source, locale, `${page}.md`), `# ${locale} ${page === "index" ? "Guide" : page}\n\n[Conversations](conversations.md#requests) · [Current heading](#start) · [OpenCode](https://opencode.ai/v2/docs/)\n\n## Start {#start}\n\n![Logo](${image})\n\n### Details {#details}\n\nText.\n\n## Requests {#requests}\n\n[Home](index.md#start)\n`) } } return { source, output: join(directory, "output") } } const unescape = (value) => value.replace(/&(?:amp|quot|#39|lt|gt);/g, (entity) => ({ "&": "&", """: '"', "'": "'", "<": "<", ">": ">" })[entity]) const escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]) const CSP = `` const funding = JSON.parse(await readFile(resolve(root, "docs/help/funding.json"), "utf8")) const SPONSORSHIP = /github\.com\/sponsors|opencollective\.com|liberapay\.com|polar\.sh|thanks\.dev|paypal\.|patreon\.com|buymeacoffee\.com|ko-fi\.com/i // The {{funding}} block: one link per channel, in funding.json order, naming its recipient. const fundingBlock = (chrome, channels) => `` // The one media element of the site: the home page's silent tour, whose source applies only to // wide screens without reduced motion, with the poster as the image everywhere else. function checkTour(html, chrome) { assert.equal(TOUR_MEDIA, "(prefers-reduced-motion: no-preference) and (min-width: 721px)") assert.deepEqual(html.match(/
[\s\S]*?<\/figure>/g), [`
${escapeHtml(chrome.tour.poster)}
${escapeHtml(chrome.tour.caption)}
`]) } async function checkSearch(output, locale) { const entries = JSON.parse(await readFile(join(output, locale, "search-index.json"), "utf8")) assert.ok(entries.length, "search index must contain headings") for (const entry of entries) { assert.ok(entry.title && entry.heading && typeof entry.excerpt === "string") assert.match(entry.href, /^[a-z0-9-]+\.html#[a-z0-9-]+$/) const [page, id] = entry.href.split("#") assert.ok((await readFile(join(output, locale, page), "utf8")).includes(`id="${id}"`), `invalid search target: ${entry.href}`) } } async function checkHtml(output, locale, page, locales, channels = funding) { const file = join(output, locale, `${page}.html`) const html = await readFile(file, "utf8") const chromeFile = (name) => readFile(resolve(root, `docs/help/i18n/${name}.json`), "utf8").then(JSON.parse) const chrome = await chromeFile(locale) assert.match(html, new RegExp(``)) assert.equal((html.match(/)/g) ?? []).length, 1, file) assert.equal((html.match(/aria-current="page"/g) ?? []).length, 2, "one current link in each CSS-exclusive navigation") assert.match(html, new RegExp(`href="${page}\.html" aria-current="page"`)) assert.ok(!/\{#[^}]*\}/.test(html), "heading suffixes must not be visible") const scripts = ["help-search", ...(page === "download" ? ["help-download"] : [])].map((name) => ``) // HTML tag names are case-insensitive: every script start counts, and only the exact owned tags pass. assert.deepEqual(html.match(/]*>[\s\S]*?<\/script>|]*>/gi) ?? [] if (media.length) { assert.equal(page, "index", "only the home page shows media") assert.equal(media.length, 2, "one video with one source") assert.match(media[0], /^