Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions test/helpers/issue_form.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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']);

Expand Down
83 changes: 52 additions & 31 deletions test/scripts/direct_issue_body.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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('<!doctype html><body>' + FIXTURE + '</body>', {
Expand All @@ -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',
});
Expand All @@ -69,34 +97,27 @@ 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_'));
assert.deepEqual(blank, ['Slide deck or one-pager (PDF)\n\n_No response_']);
});

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/);
});
Expand Down
6 changes: 4 additions & 2 deletions test/scripts/signin_url.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Loading