diff --git a/CHANGELOG.md b/CHANGELOG.md index 438f86e..b4fc3c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,21 @@ major version, and each entry says so when it happens. ## [Unreleased] +### Fixed + +- Three tests added in rc.9 failed inside a real deployment's PHCT update, + which runs the template's suite on the deployment's own files. + `direct_issue_body.test.mjs` drove the template's `/submit/` snapshot but + compared against the repository's own issue template and ran the scaffolder + on its own `_data/schema.yml`, so a deployment that relabels a question + (BCHC's "Role of AI" for "How AI is involved") failed it. It now generates the + issue form and the scaffolder's schema from the snapshot's own questions, the + way `submit_form.test.mjs` already did. The byte-for-byte check on the + repository's own schema stays in `test/build/variants.test.mjs`. + `signin_url.test.mjs` required `_data/site.yml` to carry a blank + `submit.signin_url`, which a deployment's own `site.yml` need not have; it now + sets the key itself. The submit page and its issue body were not at fault. + ## [1.11.0-rc.9] — 2026-10-13 Ninth candidate. rc.8's live BCHC update passed and deployed. This candidate diff --git a/test/helpers/issue_form.mjs b/test/helpers/issue_form.mjs index 1a0486a..d1fdbff 100644 --- a/test/helpers/issue_form.mjs +++ b/test/helpers/issue_form.mjs @@ -9,6 +9,34 @@ * left blank — with a blank line between sections. */ +/** + * The schema a booted /submit/ page was built from, read back off the page: + * each question's key, label and type, whether it is required, and a choice + * question's options, in page order. A test driving the committed snapshot + * (test/fixtures/submit-form.html) generates its issue form and runs the + * scaffolder from this, never from the repository's own _data/schema.yml or + * .github/ISSUE_TEMPLATE/new-entry.yml: a deployment's update runs these tests + * on its own schema, whose questions and labels are not the snapshot's. + * @param {HTMLFormElement} form + * @returns {{entry: object, fields: object[]}} + */ +export function pageSchema(form) { + const fields = Array.from(form.querySelectorAll('[data-field]'), (wrap) => ({ + key: wrap.dataset.field, + label: wrap.dataset.label, + type: wrap.dataset.type, + required: wrap.dataset.required === 'true', + ...(wrap.dataset.type === 'select' || wrap.dataset.type === 'multiselect' + ? { + options: Array.from(wrap.querySelectorAll('input[value], option'), (node) => node.value).filter( + Boolean + ), + } + : {}), + })); + return { entry: { singular: 'Entry', path: 'catalog' }, fields }; +} + /** Issue-form elements that write a section into the issue body. */ const ANSWERED = new Set(['input', 'textarea', 'dropdown', 'checkboxes', 'upload']); diff --git a/test/scripts/direct_issue_body.test.mjs b/test/scripts/direct_issue_body.test.mjs index 52f0aef..1eed88a 100644 --- a/test/scripts/direct_issue_body.test.mjs +++ b/test/scripts/direct_issue_body.test.mjs @@ -3,15 +3,21 @@ * assets/js/submit/handoff.js) against the body GitHub's own form writes for * the same answers, and what the entry scaffolder reads back from each. * - * GitHub's side is modelled from the generated form itself - * (.github/ISSUE_TEMPLATE/new-entry.yml), filled with exactly what the link - * hand-off would have prefilled — so "the same answers" is the hand-off's own - * definition. The page is the Liquid snapshot submit_form.test.mjs uses; the - * byte-for-byte comparison against a freshly built page, where every schema - * question is on the page, is in test/build/variants.test.mjs. + * The page is the Liquid snapshot submit_form.test.mjs uses, and everything + * compared against it comes from that page's own questions (pageSchema in + * test/helpers/issue_form.mjs): GitHub's side is the form the generator + * (assets/js/configurator/issue-template.js) writes for them, filled with + * exactly what the link hand-off would have prefilled — so "the same answers" + * is the hand-off's own definition — and the scaffolder runs in a throwaway + * checkout holding that schema. Never this repository's own schema or issue + * template: a deployment's update runs this file on its own questions, which + * are not the snapshot's. The byte-for-byte comparison on the repository's own + * schema, from a freshly built page and the form generate.mjs wrote, is in + * test/build/variants.test.mjs. */ import assert from 'node:assert/strict'; import fs from 'node:fs'; +import os from 'node:os'; import path from 'node:path'; import test from 'node:test'; import { spawnSync } from 'node:child_process'; @@ -20,20 +26,28 @@ import { fileURLToPath } from 'node:url'; import { JSDOM } from 'jsdom'; import * as yaml from 'js-yaml'; -import { fillEveryField, githubFormBody } from '../helpers/issue_form.mjs'; +import { issueTemplateFromSchema } from '../../assets/js/configurator/issue-template.js'; +import { fillEveryField, githubFormBody, pageSchema } from '../helpers/issue_form.mjs'; const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); const FIXTURE = fs.readFileSync(path.join(ROOT, 'test', 'fixtures', 'submit-form.html'), 'utf8'); const SCRIPTS = yaml .load(fs.readFileSync(path.join(ROOT, 'submit', 'index.md'), 'utf8').split(/^---$/m)[1]) .scripts.map((src) => src.replace(/^\//, '')); -const TEMPLATE = yaml.load( - fs.readFileSync(path.join(ROOT, '.github', 'ISSUE_TEMPLATE', 'new-entry.yml'), 'utf8') -); const SCAFFOLDER = path.join(ROOT, 'scripts', 'new_entry_from_issue.mjs'); +// The throwaway checkout the scaffolder runs in: one file, _data/schema.yml, +// removed file by file when this file finishes. +const CHECKOUT = fs.mkdtempSync(path.join(os.tmpdir(), 'direct-body-')); +const CHECKOUT_SCHEMA = path.join(CHECKOUT, '_data', 'schema.yml'); + const booted = []; -test.after(() => booted.forEach((dom) => dom.window.close())); +test.after(() => { + booted.forEach((dom) => dom.window.close()); + if (fs.existsSync(CHECKOUT_SCHEMA)) fs.unlinkSync(CHECKOUT_SCHEMA); + if (fs.existsSync(path.dirname(CHECKOUT_SCHEMA))) fs.rmdirSync(path.dirname(CHECKOUT_SCHEMA)); + fs.rmdirSync(CHECKOUT); +}); async function filledPage() { const dom = new JSDOM('' + FIXTURE + '', { @@ -48,17 +62,31 @@ async function filledPage() { }); SCRIPTS.forEach((rel) => window.eval(fs.readFileSync(path.join(ROOT, rel), 'utf8'))); const form = window.document.querySelector('[data-submit-form]'); + const schema = pageSchema(form); + const template = yaml.load(issueTemplateFromSchema(schema)); const title = fillEveryField(window, form); const ns = window.SubmitForm; const fields = ns.readFields(form); const params = new URL(ns.issueUrl(form, fields, title)).searchParams; - return { window, form, ns, fields, title, params, issueTitle: form.dataset.titlePrefix + title }; + return { + window, + form, + ns, + fields, + title, + params, + schema, + template, + issueTitle: form.dataset.titlePrefix + title, + }; } -/** The scaffolder's dry run for one issue body: the entry file it would write. */ -function scaffold(body, title) { +/** The scaffolder's dry run for one issue body against `schema`: the entry file it would write. */ +function scaffold(body, title, schema) { + fs.mkdirSync(path.dirname(CHECKOUT_SCHEMA), { recursive: true }); + fs.writeFileSync(CHECKOUT_SCHEMA, yaml.dump(schema), 'utf8'); const result = spawnSync(process.execPath, [SCAFFOLDER, '--dry-run'], { - cwd: ROOT, + cwd: CHECKOUT, env: { ...process.env, ISSUE_BODY: body, ISSUE_TITLE: title, ISSUE_NUMBER: '7', GITHUB_OUTPUT: '' }, encoding: 'utf8', }); @@ -69,19 +97,9 @@ function scaffold(body, title) { } test('the direct body is GitHub’s form body for the same answers, question for question', async () => { - const { fields, ns, params } = await filledPage(); + const { fields, ns, params, template } = await filledPage(); const direct = ns.issueFormBody(fields); - // Only the questions this snapshot has: it predates a schema question or - // two, and the build test compares the whole of a freshly built page. - const onPage = new Set(fields.map((field) => field.label)); - const github = githubFormBody( - { - body: TEMPLATE.body.filter( - (element) => element.type === 'markdown' || onPage.has(element.attributes.label) - ), - }, - (id) => params.get(id) ?? '' - ); + const github = githubFormBody(template, (id) => params.get(id) ?? ''); assert.equal(direct, github); // Every answer the page sends travels: nothing blank but the file question. const blank = direct.split('\n\n### ').filter((section) => section.endsWith('\n\n_No response_')); @@ -89,14 +107,17 @@ test('the direct body is GitHub’s form body for the same answers, question for }); test('the scaffolder reads the direct body exactly as it reads the form’s, every field', async () => { - const { fields, ns, params, issueTitle } = await filledPage(); - const direct = scaffold(ns.issueFormBody(fields), issueTitle); + const { fields, ns, params, schema, template, issueTitle } = await filledPage(); + const direct = scaffold(ns.issueFormBody(fields), issueTitle, schema); const github = scaffold( - githubFormBody(TEMPLATE, (id) => params.get(id) ?? ''), - issueTitle + githubFormBody(template, (id) => params.get(id) ?? ''), + issueTitle, + schema ); assert.deepEqual(direct, github); assert.equal(direct.front.title, 'Water routing'); + // Every choice answer matched one of its question's options. + assert.doesNotMatch(direct.page, /Answers to fix/); // The write-up's own "### " line stayed prose, not a section. assert.match(direct.page, /### Not a real heading/); }); diff --git a/test/scripts/signin_url.test.mjs b/test/scripts/signin_url.test.mjs index 8af23ca..03ef9bf 100644 --- a/test/scripts/signin_url.test.mjs +++ b/test/scripts/signin_url.test.mjs @@ -48,9 +48,11 @@ test('anything the page would mangle or that would carry the token unencrypted i }); test('a hand-set signin_url survives the configurators, which never ask about it', () => { + // Set by hand here rather than read from this repository's _data/site.yml, + // which a deployment owns: it may leave the key out or point it at its own + // service. const base = defaultConfig(); - assert.equal(base.site.submit.signin_url, '', 'the shipped site.yml carries the key, blank'); - base.site.submit.signin_url = 'https://catalog-signin.vercel.app'; + base.site.submit = { ...base.site.submit, signin_url: 'https://catalog-signin.vercel.app' }; const config = applyAnswers(base, { ...answersFromConfig(base), siteName: 'Renamed' }); const site = jsYaml.load(renderFiles(config)['_data/site.yml']); assert.equal(site.submit.signin_url, 'https://catalog-signin.vercel.app');