culture-compass / tests /app.test.js
guicybercode's picture
Launch Culture Compass static explorer
0fbbe82 verified
Raw
History Blame Contribute Delete
40.8 kB
"use strict";
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const SPACE_ROOT = path.resolve(__dirname, "..");
const README_PATH = path.join(SPACE_ROOT, "README.md");
const HTML_PATH = path.join(SPACE_ROOT, "index.html");
const CSS_PATH = path.join(SPACE_ROOT, "styles.css");
const APP_PATH = path.join(SPACE_ROOT, "app.js");
const FAVICON_PATH = path.join(SPACE_ROOT, "favicon.svg");
const SOURCE_URLS = {
japan: "https://huggingface.co/datasets/guicybercode/japan-math-philosophy-prompts",
iceland: "https://huggingface.co/datasets/guicybercode/iceland-tech-christian-ethics-prompts",
};
const DATA_URLS = {
japan: `${SOURCE_URLS.japan}/resolve/0b472efffdcd5b0b6a166cab21ca318108711629/data/train.jsonl`,
iceland: `${SOURCE_URLS.iceland}/resolve/99a46bd5df9d6af3d7226e4836cb28d4d754f3b7/data/train.jsonl`,
};
const read = (filePath) => fs.readFileSync(filePath, "utf8");
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function frontmatter(markdown) {
const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
assert.ok(match, "README.md must start with YAML frontmatter");
const result = Object.create(null);
let activeList = null;
for (const rawLine of match[1].split(/\r?\n/)) {
const line = rawLine.trimEnd();
const item = line.match(/^\s*-\s+(.+)$/);
if (item && activeList) {
result[activeList].push(item[1].trim());
continue;
}
const property = line.match(/^([A-Za-z_][\w-]*):(?:\s*(.*))?$/);
if (!property) continue;
const [, key, value = ""] = property;
if (value.trim()) {
result[key] = value.trim().replace(/^(["'])(.*)\1$/, "$2");
activeList = null;
} else {
result[key] = [];
activeList = key;
}
}
return result;
}
function openingTagById(html, id) {
const match = html.match(
new RegExp(`<([a-z][\\w:-]*)\\b[^>]*\\bid=["']${escapeRegExp(id)}["'][^>]*>`, "i"),
);
assert.ok(match, `expected an element with id="${id}"`);
return match[0];
}
function attribute(tag, name) {
const match = tag.match(
new RegExp(`\\b${escapeRegExp(name)}\\s*=\\s*(["'])(.*?)\\1`, "i"),
);
return match ? match[2] : null;
}
function cssRuleBlocks(css, selector) {
const blocks = [];
const rulePattern = /([^{}]+)\{([^{}]*)\}/g;
for (const match of css.matchAll(rulePattern)) {
const selectors = match[1]
.split(",")
.map((candidate) => candidate.trim())
.filter(Boolean);
if (selectors.includes(selector)) blocks.push(match[2]);
}
return blocks;
}
function minimumHeightPx(css, selector) {
const values = cssRuleBlocks(css, selector).flatMap((block) =>
[...block.matchAll(/\bmin-height\s*:\s*([\d.]+)px\b/gi)].map((match) => Number(match[1])),
);
assert.ok(values.length > 0, `${selector} must declare a pixel min-height`);
return Math.max(...values);
}
function cssVariable(block, name) {
const match = block.match(new RegExp(`${escapeRegExp(name)}\\s*:\\s*(#[0-9a-f]{6})\\b`, "i"));
assert.ok(match, `${name} must be declared as a six-digit hex color`);
return match[1];
}
function relativeLuminance(hexColor) {
const channels = hexColor
.slice(1)
.match(/.{2}/g)
.map((channel) => Number.parseInt(channel, 16) / 255)
.map((channel) => (channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4));
return (0.2126 * channels[0]) + (0.7152 * channels[1]) + (0.0722 * channels[2]);
}
function contrastRatio(first, second) {
const values = [relativeLuminance(first), relativeLuminance(second)].sort((a, b) => b - a);
return (values[0] + 0.05) / (values[1] + 0.05);
}
function loadApplicationApi() {
delete require.cache[require.resolve(APP_PATH)];
return require(APP_PATH);
}
function japanRecord(overrides = {}) {
return {
id: "jmp-test-pt-br",
localization_group_id: "jmp-test",
language: "pt-BR",
title: "Três formas, uma quantidade",
math_topic: "rational-equivalence",
philosophical_focus: "Identidade e representação",
prompt: "Some 0,5, 1/2 e 50% e explique a representação.",
learning_objectives: ["Reconhecer representações equivalentes."],
expected_response: {
short_answer: "3/2",
reflection_criteria: ["Distingue valor e notação."],
},
facilitation_note: "Aceite argumentos coerentes.",
japan_reference: {
boundary_note: "Rótulo editorial ficcional; não representa uma tradição japonesa real.",
},
provenance: {
creation_mode: "model_generated_unreviewed",
human_review_status: "not_reviewed",
},
license: "CC-BY-4.0",
...overrides,
};
}
function icelandRecord(overrides = {}) {
return {
id: "ice-test-pt-br",
pair_id: "ice-test",
language: "pt-BR",
scenario_title: "A microrrede no vale",
technology_domain: "emergency-energy-allocation",
landscape_motif: "geothermal-valley",
topic_tags: ["ai-governance", "privacy"],
setting: "Um vale geotérmico inteiramente inventado enfrenta uma tempestade.",
discussion_question: "Como a cooperativa deveria distribuir energia?",
perspectives: [
{
id: "a",
title: "Bem comum temporário",
argument: "Uma regra temporária pode proteger serviços vitais.",
questions: ["Quem define os serviços vitais?"],
},
{
id: "b",
title: "Consentimento",
argument: "Alternativas não coercivas preservam dignidade e confiança.",
questions: ["Como preservar a privacidade?"],
},
],
christian_ethics_concepts: ["stewardship", "human-dignity"],
pluralism: {
required: true,
safeguard: "A lente cristã é opcional; convide também argumentos seculares e de outras tradições.",
},
fictionality: {
is_fictional: true,
claims_real_icelandic_or_christian_practices: false,
notice: "Cenário ficcional; não descreve práticas islandesas ou cristãs reais.",
},
creation: {
method: "ai-generated-original-draft",
human_review_status: "required-before-curated-release-or-use",
},
license: "CC-BY-4.0",
...overrides,
};
}
class FakeElement {
constructor(tagName = "div", id = "") {
this.tagName = tagName.toUpperCase();
this.id = id;
this.className = "";
this.dataset = {};
this.children = [];
this.attributes = new Map();
this.listeners = new Map();
this.hidden = false;
this.disabled = false;
this.value = "";
this.lang = "";
this.href = "";
this.ownerDocument = null;
this.focusCalls = 0;
this._textContent = "";
}
get textContent() {
return this._textContent + this.children.map((child) => child.textContent || "").join("");
}
set textContent(value) {
this._textContent = String(value ?? "");
this.children = [];
}
get options() {
return this.children;
}
append(...nodes) {
this.children.push(...nodes);
}
replaceChildren(...nodes) {
this._textContent = "";
this.children = [...nodes];
}
setAttribute(name, value) {
this.attributes.set(name, String(value));
}
getAttribute(name) {
return this.attributes.get(name) ?? null;
}
addEventListener(type, listener) {
const listeners = this.listeners.get(type) || [];
listeners.push(listener);
this.listeners.set(type, listeners);
}
dispatch(type) {
for (const listener of this.listeners.get(type) || []) listener({ target: this, type });
}
focus() {
this.focusCalls += 1;
if (this.ownerDocument) this.ownerDocument.activeElement = this;
}
}
class FakeDocument {
constructor() {
this.documentElement = { lang: "pt-BR" };
this.title = "";
this.activeElement = null;
this.elements = new Map();
this.listeners = new Map();
this.translatedNodes = [];
}
addElement(id, tagName = "div", value = "") {
const element = new FakeElement(tagName, id);
element.ownerDocument = this;
element.value = value;
this.elements.set(id, element);
return element;
}
getElementById(id) {
return this.elements.get(id) || null;
}
createElement(tagName) {
const element = new FakeElement(tagName);
element.ownerDocument = this;
return element;
}
addEventListener(type, listener) {
this.listeners.set(type, listener);
}
dispatch(type) {
const listener = this.listeners.get(type);
if (listener) listener({ type, target: this });
}
querySelectorAll(selector) {
if (selector === "[data-i18n]") return this.translatedNodes;
return [];
}
}
function explorerDocument() {
const document = new FakeDocument();
const definitions = [
["ui-language", "select", "pt-BR"],
["dataset-select", "select", "japan"],
["content-language", "select", "pt-BR"],
["topic-select", "select", "all"],
["random-button", "button", ""],
["retry-button", "button", ""],
["state-panel", "div", ""],
["state-title", "h3", ""],
["state-message", "p", ""],
["result-region", "div", ""],
["live-status", "p", ""],
["activity-card", "article", ""],
["activity-title", "h3", ""],
["activity-context", "p", ""],
["record-position", "p", ""],
["activity-body", "div", ""],
["source-link", "a", ""],
];
definitions.forEach((definition) => document.addElement(...definition));
document.getElementById("activity-card").hidden = true;
const translated = new FakeElement("p");
translated.dataset.i18n = "heroTitle";
document.translatedNodes.push(translated);
return { document, translated };
}
function descendants(element) {
return element.children.flatMap((child) => [child, ...descendants(child)]);
}
async function waitFor(predicate, message, attempts = 50) {
for (let attempt = 0; attempt < attempts; attempt += 1) {
if (predicate()) return;
await new Promise((resolve) => setImmediate(resolve));
}
assert.fail(message);
}
async function withExplorer(fetchImplementation, exercise) {
const previousDocument = global.document;
const previousFetch = global.fetch;
const fixture = explorerDocument();
global.document = fixture.document;
global.fetch = fetchImplementation;
try {
loadApplicationApi();
fixture.document.dispatch("DOMContentLoaded");
await exercise(fixture);
} finally {
delete require.cache[require.resolve(APP_PATH)];
if (previousDocument === undefined) delete global.document;
else global.document = previousDocument;
if (previousFetch === undefined) delete global.fetch;
else global.fetch = previousFetch;
}
}
test("README declares a static Hugging Face Space and both source datasets", () => {
const metadata = frontmatter(read(README_PATH));
assert.equal(metadata.sdk, "static");
assert.equal(metadata.app_file, "index.html");
assert.deepEqual(metadata.datasets, [
"guicybercode/japan-math-philosophy-prompts",
"guicybercode/iceland-tech-christian-ethics-prompts",
]);
});
test("HTML exposes semantic landmarks and a keyboard skip target", () => {
const html = read(HTML_PATH);
const skipLink = html.match(/<a\b[^>]*class=["'][^"']*\bskip-link\b[^"']*["'][^>]*>/i);
assert.match(html, /<!doctype html>/i);
assert.match(html, /<header\b/i);
assert.match(html, /<main\b/i);
assert.match(html, /<footer\b/i);
assert.match(html, /<nav\b[^>]*aria-label=/i);
assert.match(html, /<aside\b[^>]*(?:aria-label|aria-labelledby)=/i);
assert.ok(skipLink, "expected a visible-on-focus skip link");
assert.equal(attribute(skipLink[0], "href"), "#main-content");
const main = openingTagById(html, "main-content");
assert.match(main, /^<main\b/i);
assert.equal(attribute(main, "tabindex"), "-1");
});
test("every explorer control has a persistent associated label", () => {
const html = read(HTML_PATH);
const controlIds = ["ui-language", "dataset-select", "content-language", "topic-select"];
for (const id of controlIds) {
const control = openingTagById(html, id);
assert.match(control, /^<select\b/i, `${id} must be a native select`);
assert.match(
html,
new RegExp(`<label\\b[^>]*\\bfor=["']${escapeRegExp(id)}["'][^>]*>`, "i"),
`${id} must have a label with a matching for attribute`,
);
}
for (const id of ["random-button", "retry-button"]) {
const button = openingTagById(html, id);
assert.match(button, /^<button\b/i);
assert.equal(attribute(button, "type"), "button");
}
});
test("dynamic status uses an atomic polite live region and exposes loading state", () => {
const html = read(HTML_PATH);
const status = openingTagById(html, "live-status");
const result = openingTagById(html, "result-region");
const stateTitle = openingTagById(html, "state-title");
const activityTitle = openingTagById(html, "activity-title");
assert.equal(attribute(status, "role"), "status");
assert.equal(attribute(status, "aria-live"), "polite");
assert.equal(attribute(status, "aria-atomic"), "true");
assert.equal(attribute(result, "aria-busy"), "true");
assert.equal(attribute(stateTitle, "tabindex"), "-1");
assert.equal(attribute(activityTitle, "tabindex"), "-1");
assert.match(html, /<article\b[^>]*\bid=["']activity-card["']/i);
});
test("static dataset, language, and English link labels declare their natural language", () => {
const html = read(HTML_PATH);
const datasetOptions = html.match(/<select\b[^>]*id=["']dataset-select["'][^>]*>([\s\S]*?)<\/select>/i);
const contentOptions = html.match(/<select\b[^>]*id=["']content-language["'][^>]*>([\s\S]*?)<\/select>/i);
assert.ok(datasetOptions);
assert.ok(contentOptions);
for (const value of ["japan", "iceland"]) {
assert.match(
datasetOptions[1],
new RegExp(`<option\\b(?=[^>]*\\bvalue=["']${value}["'])(?=[^>]*\\blang=["']en["'])[^>]*>`, "i"),
);
}
for (const language of ["pt-BR", "en", "ja"]) {
assert.match(
contentOptions[1],
new RegExp(`<option\\b(?=[^>]*\\bvalue=["']${escapeRegExp(language)}["'])(?=[^>]*\\blang=["']${escapeRegExp(language)}["'])[^>]*>`, "i"),
);
}
const footer = html.match(/<footer\b[^>]*class=["'][^"']*site-footer[^"']*["'][^>]*>([\s\S]*?)<\/footer>/i);
assert.ok(footer);
const footerSourceLinks = [...footer[1].matchAll(/<a\b[^>]*href=["']https:\/\/huggingface\.co\/datasets\/[^"']+["'][^>]*>/gi)];
assert.equal(footerSourceLinks.length, 2);
for (const link of footerSourceLinks) assert.equal(attribute(link[0], "lang"), "en");
});
test("HTML loads only local app assets and external links use fixed HTTPS destinations", () => {
const html = read(HTML_PATH);
const localAssetUrls = ["app.js", "favicon.svg", "styles.css"];
const allowedRemoteUrls = new Set([
SOURCE_URLS.japan,
SOURCE_URLS.iceland,
]);
const assetUrls = [...html.matchAll(/<(?:link|script)\b[^>]*(?:href|src)=["']([^"']+)["'][^>]*>/gi)]
.map((match) => match[1]);
assert.deepEqual(assetUrls.sort(), localAssetUrls.sort());
const remoteLinks = [...html.matchAll(/<a\b[^>]*href=["'](https?:\/\/[^"']+)["'][^>]*>/gi)]
.map((match) => match[1]);
assert.ok(remoteLinks.length >= 2, "expected links to both source repositories");
for (const url of remoteLinks) {
assert.ok(allowedRemoteUrls.has(url), `unexpected external link: ${url}`);
}
assert.doesNotMatch(html, /\son[a-z]+\s*=/i, "inline event handlers are not allowed");
assert.doesNotMatch(html, /\b(?:src|href)=["']\/\//i, "protocol-relative resources are not allowed");
});
test("local SVG favicon is self-contained and contains no executable or remote content", () => {
assert.ok(fs.existsSync(FAVICON_PATH), "favicon.svg must exist beside index.html");
const svg = read(FAVICON_PATH);
assert.match(svg, /^\s*<svg\b/i);
assert.match(svg, /\bxmlns=["']http:\/\/www\.w3\.org\/2000\/svg["']/i);
assert.match(svg, /\bviewBox=["']0 0 64 64["']/i);
assert.match(svg, /<\/svg>\s*$/i);
assert.doesNotMatch(svg, /<(?:script|foreignObject|iframe|image|use)\b/i);
assert.doesNotMatch(svg, /\son[a-z]+\s*=/i);
assert.doesNotMatch(svg, /\b(?:href|xlink:href)\s*=/i);
assert.doesNotMatch(svg, /\burl\s*\(/i);
});
test("CSS keeps every interactive control family at least 44px tall", () => {
const css = read(CSS_PATH);
const interactiveSelectors = [
".skip-link",
"select",
".primary-button",
".secondary-button",
".source-link",
".footer-grid a",
];
for (const selector of interactiveSelectors) {
assert.ok(
minimumHeightPx(css, selector) >= 44,
`${selector} must meet the 44px minimum target contract`,
);
}
assert.match(css, /:focus-visible\s*\{[^}]*\boutline\s*:/s);
assert.match(css, /@media\s*\(prefers-reduced-motion:\s*reduce\)/i);
assert.match(css, /@media\s*\(max-width:\s*23\.4375rem\)/i, "expected a 375px breakpoint");
});
test("dark-surface tokens retain AAA contrast in light and dark color schemes", () => {
const css = read(CSS_PATH);
const lightRoot = css.match(/^:root\s*\{([^}]*)\}/);
const darkRoot = css.match(/@media\s*\(prefers-color-scheme:\s*dark\)\s*\{\s*:root\s*\{([^}]*)\}/i);
assert.ok(lightRoot, "expected light :root tokens");
assert.ok(darkRoot, "expected dark-scheme :root token overrides");
for (const [scheme, block] of [["light", lightRoot[1]], ["dark", darkRoot[1]]]) {
const surface = cssVariable(block, "--color-dark-surface");
const foreground = cssVariable(block, "--color-on-dark-surface");
assert.ok(
contrastRatio(surface, foreground) >= 7,
`${scheme} dark-surface pair must meet WCAG AAA contrast`,
);
}
for (const selector of [".controls", ".site-footer"]) {
const blocks = cssRuleBlocks(css, selector);
assert.ok(blocks.some((block) => /background\s*:\s*var\(--color-dark-surface\)/.test(block)));
assert.ok(blocks.some((block) => /color\s*:\s*var\(--color-on-dark-surface\)/.test(block)));
}
assert.ok(
cssRuleBlocks(css, ".footer-grid a").some((block) => /color\s*:\s*var\(--color-on-dark-surface\)/.test(block)),
);
});
test("app source avoids HTML injection and executable-string APIs", () => {
assert.ok(fs.existsSync(APP_PATH), "app.js must exist");
const source = read(APP_PATH);
assert.doesNotMatch(source, /\b(?:innerHTML|outerHTML)\b/);
assert.doesNotMatch(source, /\bdocument\s*\.\s*write(?:ln)?\s*\(/);
assert.doesNotMatch(source, /\beval\s*\(/);
assert.doesNotMatch(source, /\bnew\s+Function\s*\(/);
assert.doesNotMatch(source, /\bhttp:\/\//i);
});
test("app source pins both fetch and repository URLs to HTTPS", () => {
assert.ok(fs.existsSync(APP_PATH), "app.js must exist");
const source = read(APP_PATH);
const expectedUrls = [
SOURCE_URLS.japan,
DATA_URLS.japan,
SOURCE_URLS.iceland,
DATA_URLS.iceland,
];
for (const url of expectedUrls) assert.ok(source.includes(url), `missing fixed URL: ${url}`);
const literalUrls = [...source.matchAll(/https?:\/\/[^\s"'`)]+/g)].map((match) => match[0]);
for (const url of literalUrls) {
assert.ok(expectedUrls.includes(url), `unexpected URL literal in app.js: ${url}`);
}
});
test("app exposes a small pure API that can be tested without a browser", () => {
const api = loadApplicationApi();
const functions = [
"parseJsonl",
"readLimitedText",
"groupId",
"validateRecords",
"buildActivities",
"filterActivities",
"selectRandom",
"getAvailableLanguages",
"getAvailableTopics",
"normalizeJapanRecord",
"normalizeIcelandRecord",
];
for (const name of functions) assert.equal(typeof api[name], "function", `${name} must be exported`);
assert.equal(typeof api.TRANSLATIONS, "object");
assert.equal(typeof api.DATASETS, "object");
});
test("limited response reader preserves UTF-8 characters split across stream chunks", async () => {
const { readLimitedText } = loadApplicationApi();
const expected = "A🌋漢字 — matemática";
const encoded = new TextEncoder().encode(expected);
const chunks = [
encoded.slice(0, 3),
encoded.slice(3, 6),
encoded.slice(6, 10),
encoded.slice(10),
];
let reads = 0;
let cancelCalls = 0;
let releaseCalls = 0;
const reader = {
async read() {
const value = chunks[reads];
reads += 1;
return value ? { done: false, value } : { done: true, value: undefined };
},
async cancel() {
cancelCalls += 1;
},
releaseLock() {
releaseCalls += 1;
},
};
const actual = await readLimitedText({ body: { getReader: () => reader } }, encoded.byteLength);
assert.equal(actual, expected);
assert.equal(cancelCalls, 0);
assert.equal(releaseCalls, 1);
assert.equal(reads, chunks.length + 1);
});
test("limited response reader cancels and rejects as soon as streamed bytes exceed the limit", async () => {
const { readLimitedText } = loadApplicationApi();
const chunks = [new TextEncoder().encode("1234"), new TextEncoder().encode("56"), new TextEncoder().encode("ignored")];
const cancelReasons = [];
let reads = 0;
let releaseCalls = 0;
const reader = {
async read() {
const value = chunks[reads];
reads += 1;
return value ? { done: false, value } : { done: true, value: undefined };
},
async cancel(reason) {
cancelReasons.push(reason);
},
releaseLock() {
releaseCalls += 1;
},
};
await assert.rejects(
readLimitedText({ body: { getReader: () => reader } }, 5),
/exceeds size limit/i,
);
assert.equal(reads, 2, "the reader must stop before consuming subsequent chunks");
assert.ok(cancelReasons.length >= 1, "the oversized stream must be cancelled");
assert.ok(cancelReasons.includes("Dataset response exceeds size limit"));
assert.equal(releaseCalls, 1);
});
test("limited response reader falls back to response.text and still enforces UTF-8 byte limits", async () => {
const { readLimitedText } = loadApplicationApi();
const source = "哲学";
let textCalls = 0;
const response = {
body: null,
async text() {
textCalls += 1;
return source;
},
};
const bytes = new TextEncoder().encode(source).byteLength;
assert.equal(await readLimitedText(response, bytes), source);
await assert.rejects(readLimitedText(response, bytes - 1), /exceeds size limit/i);
assert.equal(textCalls, 2);
});
test("JSONL parser accepts final newline, no final newline, blank lines, CRLF, and Unicode", () => {
const { parseJsonl } = loadApplicationApi();
assert.deepEqual(parseJsonl('{"id":"one"}\n'), [{ id: "one" }]);
assert.deepEqual(parseJsonl('{"id":"one"}'), [{ id: "one" }]);
assert.deepEqual(
parseJsonl('{"id":"um","title":"Matemática"}\r\n\r\n{"id":"二","title":"哲学"}\r\n'),
[
{ id: "um", title: "Matemática" },
{ id: "二", title: "哲学" },
],
);
assert.deepEqual(parseJsonl("\n\r\n \n"), []);
});
test("JSONL parser rejects malformed and non-object records with the physical line number", () => {
const { parseJsonl } = loadApplicationApi();
assert.throws(
() => parseJsonl('{"id":"one"}\n\n{"broken":}\n'),
/line 3/i,
"blank lines must not distort diagnostics",
);
assert.throws(() => parseJsonl("[]\n"), /line 1/i);
});
test("record validation enforces stable identity, language, and unique ids", () => {
const { groupId, validateRecords } = loadApplicationApi();
const first = japanRecord();
const input = [first];
assert.equal(groupId(first), "jmp-test");
assert.equal(groupId(icelandRecord()), "ice-test");
assert.equal(groupId({ id: "ungrouped" }), null);
assert.equal(validateRecords(input), input);
assert.throws(() => validateRecords([{ id: "missing-group", language: "en" }]), /identity|language/i);
assert.throws(
() => validateRecords([first, { ...first }]),
/duplicate record id/i,
);
});
test("all UI copy exists in Brazilian Portuguese, English, and Simplified Chinese", () => {
const { TRANSLATIONS } = loadApplicationApi();
const locales = ["pt-BR", "en", "zh-CN"];
const referenceKeys = Object.keys(TRANSLATIONS["pt-BR"]).sort();
assert.ok(referenceKeys.length >= 35, "translation contract should cover the full interface");
for (const locale of locales) {
assert.deepEqual(Object.keys(TRANSLATIONS[locale]).sort(), referenceKeys, `${locale} must have every UI key`);
for (const key of referenceKeys) {
assert.equal(typeof TRANSLATIONS[locale][key], "string");
assert.ok(TRANSLATIONS[locale][key].trim(), `${locale}.${key} must not be blank`);
}
}
for (const key of ["heroTitle", "draftText", "emptyTitle", "errorTitle", "retryButton"]) {
assert.notEqual(TRANSLATIONS["pt-BR"][key], TRANSLATIONS.en[key], `${key} must be translated to English`);
assert.notEqual(TRANSLATIONS.en[key], TRANSLATIONS["zh-CN"][key], `${key} must be translated to Chinese`);
assert.match(TRANSLATIONS["zh-CN"][key], /[\u3400-\u9fff]/u, `${key} should contain Chinese text`);
}
for (const locale of locales) {
assert.doesNotMatch(
TRANSLATIONS[locale].selected,
/\{title\}/,
`${locale}.selected must not echo untrusted remote titles in the live region`,
);
}
});
test("dataset configuration keeps interface and content languages independent", () => {
const { DATASETS } = loadApplicationApi();
assert.deepEqual(DATASETS.japan.languages, ["pt-BR", "en", "ja"]);
assert.deepEqual(DATASETS.iceland.languages, ["pt-BR", "en"]);
assert.ok(!DATASETS.japan.languages.includes("zh-CN"), "Chinese is UI copy, not Japan dataset content");
assert.ok(!DATASETS.iceland.languages.includes("zh-CN"), "Chinese is UI copy, not Iceland dataset content");
});
test("normalization and filters preserve dataset-specific fields and topic semantics", () => {
const {
buildActivities,
filterActivities,
getAvailableLanguages,
getAvailableTopics,
normalizeJapanRecord,
normalizeIcelandRecord,
} = loadApplicationApi();
const japanPt = japanRecord();
const japanEn = japanRecord({
id: "jmp-test-en",
language: "en",
title: "Three forms, one quantity",
philosophical_focus: "Identity and representation",
});
const japanGeometry = japanRecord({
id: "jmp-geometry-pt-br",
localization_group_id: "jmp-geometry",
title: "Área e perspectiva",
math_topic: "geometry",
});
const icelandPt = icelandRecord();
const normalizedJapan = normalizeJapanRecord(japanPt);
assert.equal(normalizedJapan.kind, "japan");
assert.equal(normalizedJapan.title, japanPt.title);
assert.equal(normalizedJapan.raw.prompt, japanPt.prompt);
assert.equal(normalizedJapan.raw.japan_reference.boundary_note, japanPt.japan_reference.boundary_note);
assert.deepEqual(
normalizedJapan.facets.map(({ kind, value }) => [kind, value]),
[
["philosophy", "Identidade e representação"],
["math", "rational-equivalence"],
],
);
const normalizedIceland = normalizeIcelandRecord(icelandPt);
assert.equal(normalizedIceland.kind, "iceland");
assert.equal(normalizedIceland.title, icelandPt.scenario_title);
assert.equal(normalizedIceland.raw.perspectives.length, 2);
assert.equal(normalizedIceland.raw.pluralism.safeguard, icelandPt.pluralism.safeguard);
assert.equal(normalizedIceland.raw.fictionality.notice, icelandPt.fictionality.notice);
assert.deepEqual(buildActivities([japanPt], "japan").map((item) => item.kind), ["japan"]);
assert.deepEqual(buildActivities([icelandPt], "iceland").map((item) => item.kind), ["iceland"]);
const japanActivities = [japanPt, japanEn, japanGeometry];
assert.deepEqual(getAvailableLanguages(japanActivities, "japan"), ["pt-BR", "en"]);
assert.deepEqual(filterActivities(japanActivities, "en").map((item) => item.id), ["jmp-test-en"]);
assert.deepEqual(
filterActivities(japanActivities, "pt-BR", "math:geometry").map((item) => item.id),
["jmp-geometry-pt-br"],
);
assert.deepEqual(filterActivities(japanActivities, "ja", "all"), []);
const ptTopics = getAvailableTopics(japanActivities, "japan", "pt-BR");
assert.ok(ptTopics.some((topic) => topic.kind === "math" && topic.value === "geometry"));
assert.ok(!ptTopics.some((topic) => topic.value === "Identity and representation"));
const iceTopics = getAvailableTopics([icelandPt], "iceland", "pt-BR");
assert.ok(iceTopics.some((topic) => topic.kind === "technology" && topic.value === icelandPt.technology_domain));
assert.ok(iceTopics.some((topic) => topic.kind === "tag" && topic.value === "privacy"));
});
test("random selection handles zero, one, and many activities deterministically", () => {
const { selectRandom } = loadApplicationApi();
const records = [{ id: "a" }, { id: "b" }, { id: "c" }];
assert.equal(selectRandom([], null, () => 0), null);
assert.equal(selectRandom([records[0]], "a", () => 0), records[0]);
assert.equal(selectRandom(records, "a", () => 0), records[1]);
assert.equal(selectRandom(records, "a", () => 0.999), records[2]);
assert.notEqual(selectRandom(records, "b", () => 0).id, "b");
});
test("browser flow loads Japan, switches languages and datasets, renders required notices, and localizes UI", async () => {
const japanRecords = [
japanRecord(),
japanRecord({
id: "jmp-test-en",
language: "en",
title: "Three forms, one quantity",
prompt: "Add 0.5, 1/2, and 50%, then discuss representation.",
japan_reference: { boundary_note: "Fictional editorial label; not a real Japanese tradition." },
}),
];
const icelandRecords = [
icelandRecord(),
icelandRecord({
id: "ice-test-en",
language: "en",
scenario_title: "The Microgrid in the Valley",
setting: "A wholly invented geothermal valley faces a storm.",
discussion_question: "How should the cooperative distribute energy?",
}),
];
const requestedUrls = [];
const fetchStub = async (url) => {
requestedUrls.push(url);
const records = url.includes("iceland-tech") ? icelandRecords : japanRecords;
return { ok: true, status: 200, text: async () => `${records.map(JSON.stringify).join("\r\n")}\r\n` };
};
await withExplorer(fetchStub, async ({ document, translated }) => {
const card = document.getElementById("activity-card");
const body = document.getElementById("activity-body");
await waitFor(() => card.hidden === false, "Japan activity should render after loading");
assert.equal(requestedUrls[0], DATA_URLS.japan);
assert.equal(document.getElementById("result-region").getAttribute("aria-busy"), "false");
assert.match(body.textContent, /Some 0,5, 1\/2 e 50%/);
assert.match(body.textContent, /3\/2/);
assert.match(body.textContent, /não representa uma tradição japonesa real/);
assert.equal(document.getElementById("activity-context").lang, "en");
assert.equal(document.getElementById("live-status").textContent, "Atividade pronta para leitura.");
assert.doesNotMatch(document.getElementById("live-status").textContent, /Três formas, uma quantidade/);
const contentLanguage = document.getElementById("content-language");
assert.deepEqual(contentLanguage.options.map((option) => [option.value, option.lang]), [
["pt-BR", "pt-BR"],
["en", "en"],
]);
let topicOptions = document.getElementById("topic-select").options;
assert.equal(topicOptions[0].value, "all");
assert.equal(topicOptions[0].lang, "pt-BR");
assert.ok(topicOptions.slice(1).every((option) => option.lang === "pt-BR"));
contentLanguage.value = "en";
contentLanguage.dispatch("change");
assert.equal(document.getElementById("activity-title").textContent, "Three forms, one quantity");
assert.equal(card.lang, "pt-BR", "card chrome follows the interface locale");
assert.equal(document.getElementById("activity-title").lang, "en");
assert.ok(descendants(body).some((node) => node.lang === "en"), "remote English fields declare their language");
topicOptions = document.getElementById("topic-select").options;
assert.equal(topicOptions[0].lang, "pt-BR", "the all-topics label follows the interface language");
assert.ok(topicOptions.slice(1).every((option) => option.lang === "en"));
assert.equal(document.getElementById("live-status").textContent, "Atividade pronta para leitura.");
assert.doesNotMatch(document.getElementById("live-status").textContent, /Three forms, one quantity/);
const uiLanguage = document.getElementById("ui-language");
uiLanguage.value = "zh-CN";
uiLanguage.dispatch("change");
assert.equal(document.documentElement.lang, "zh-CN");
assert.match(document.title, /[\u3400-\u9fff]/u);
assert.match(translated.textContent, /[\u3400-\u9fff]/u);
assert.equal(document.getElementById("activity-title").textContent, "Three forms, one quantity");
assert.equal(card.lang, "zh-CN");
assert.equal(document.getElementById("activity-title").lang, "en");
topicOptions = document.getElementById("topic-select").options;
assert.equal(topicOptions[0].lang, "zh-CN");
assert.ok(topicOptions.slice(1).every((option) => option.lang === "en"));
assert.equal(document.getElementById("live-status").textContent, "活动已可阅读。");
assert.doesNotMatch(document.getElementById("live-status").textContent, /Three forms, one quantity/);
contentLanguage.value = "pt-BR";
contentLanguage.dispatch("change");
const dataset = document.getElementById("dataset-select");
dataset.value = "iceland";
dataset.dispatch("change");
await waitFor(
() => document.getElementById("activity-title").textContent === icelandRecords[0].scenario_title,
"Iceland activity should render after dataset selection",
);
assert.equal(requestedUrls.at(-1), DATA_URLS.iceland);
assert.deepEqual(document.getElementById("content-language").options.map((option) => option.value), ["pt-BR", "en"]);
assert.match(body.textContent, /Bem comum temporário/);
assert.match(body.textContent, /Consentimento/);
assert.match(body.textContent, /lente cristã é opcional/);
assert.match(body.textContent, /não descreve práticas islandesas ou cristãs reais/);
assert.equal(card.lang, "zh-CN");
assert.equal(document.getElementById("activity-title").lang, "pt-BR");
assert.ok(descendants(body).some((node) => node.lang === "pt-BR"), "remote Portuguese fields declare their language");
topicOptions = document.getElementById("topic-select").options;
assert.equal(topicOptions[0].lang, "zh-CN");
assert.ok(topicOptions.slice(1).every((option) => option.lang === "pt-BR"));
assert.equal(document.getElementById("activity-context").lang, "en");
assert.equal(document.getElementById("live-status").textContent, "活动已可阅读。");
assert.doesNotMatch(document.getElementById("live-status").textContent, /A microrrede no vale/);
assert.equal(document.getElementById("source-link").href, SOURCE_URLS.iceland);
});
});
test("browser flow exposes empty results without offering a misleading retry", async () => {
const records = [japanRecord()];
const fetchStub = async () => ({
ok: true,
status: 200,
text: async () => `${records.map(JSON.stringify).join("\n")}\n`,
});
await withExplorer(fetchStub, async ({ document }) => {
await waitFor(() => document.getElementById("activity-card").hidden === false, "activity should render");
const contentLanguage = document.getElementById("content-language");
contentLanguage.value = "ja";
contentLanguage.dispatch("change");
assert.equal(document.getElementById("state-panel").dataset.state, "empty");
assert.equal(document.getElementById("state-panel").hidden, false);
assert.equal(document.getElementById("activity-card").hidden, true);
assert.equal(document.getElementById("retry-button").hidden, true);
assert.equal(document.getElementById("result-region").getAttribute("aria-busy"), "false");
assert.match(document.getElementById("live-status").textContent, /Nenhuma atividade encontrada/);
});
});
test("browser flow exposes an error and retry performs a fresh successful request", async () => {
let attempts = 0;
const fetchStub = async () => {
attempts += 1;
if (attempts === 1) return { ok: false, status: 503, text: async () => "" };
return { ok: true, status: 200, text: async () => `${JSON.stringify(japanRecord())}\n` };
};
await withExplorer(fetchStub, async ({ document }) => {
const panel = document.getElementById("state-panel");
const retry = document.getElementById("retry-button");
await waitFor(() => panel.dataset.state === "error", "failed request should expose the error state");
assert.equal(retry.hidden, false);
assert.equal(document.getElementById("result-region").getAttribute("aria-busy"), "false");
assert.match(document.getElementById("live-status").textContent, /Não foi possível carregar/);
retry.dispatch("click");
assert.equal(document.activeElement, document.getElementById("state-title"));
assert.equal(document.getElementById("state-title").focusCalls, 1);
await waitFor(() => document.getElementById("activity-card").hidden === false, "retry should render data");
assert.equal(attempts, 2);
assert.equal(retry.hidden, true);
assert.equal(document.getElementById("activity-title").textContent, japanRecord().title);
assert.equal(document.activeElement, document.getElementById("activity-title"));
assert.equal(document.getElementById("activity-title").focusCalls, 1);
assert.equal(document.getElementById("live-status").textContent, "Atividade pronta para leitura.");
assert.doesNotMatch(document.getElementById("live-status").textContent, /Três formas, uma quantidade/);
});
});
test("a late response from an obsolete dataset cannot replace or poison the active dataset cache", async () => {
const staleJapan = japanRecord({ title: "Resposta tardia do Japão" });
const freshJapan = japanRecord({ id: "jmp-fresh-pt-br", title: "Resposta nova do Japão" });
const iceland = icelandRecord({ scenario_title: "Islândia permanece ativa" });
const calls = [];
let resolveStaleJapan;
const staleJapanResponse = new Promise((resolve) => {
resolveStaleJapan = resolve;
});
const responseFor = (record) => ({
ok: true,
status: 200,
headers: { get: () => null },
text: async () => `${JSON.stringify(record)}\n`,
});
const fetchStub = (url, options) => {
calls.push({ url, signal: options.signal });
const japanCalls = calls.filter((call) => call.url === DATA_URLS.japan).length;
if (url === DATA_URLS.japan && japanCalls === 1) return staleJapanResponse;
if (url === DATA_URLS.iceland) return Promise.resolve(responseFor(iceland));
if (url === DATA_URLS.japan && japanCalls === 2) return Promise.resolve(responseFor(freshJapan));
throw new Error(`unexpected fetch: ${url}`);
};
await withExplorer(fetchStub, async ({ document }) => {
await waitFor(() => calls.length === 1, "initial Japan request should start");
const firstSignal = calls[0].signal;
const dataset = document.getElementById("dataset-select");
dataset.value = "iceland";
dataset.dispatch("change");
await waitFor(
() => document.getElementById("activity-title").textContent === iceland.scenario_title,
"Iceland should render while Japan remains pending",
);
assert.equal(firstSignal.aborted, true, "switching datasets must abort the obsolete request");
resolveStaleJapan(responseFor(staleJapan));
await new Promise((resolve) => setImmediate(resolve));
await new Promise((resolve) => setImmediate(resolve));
assert.equal(document.getElementById("activity-title").textContent, iceland.scenario_title);
assert.equal(document.getElementById("source-link").href, SOURCE_URLS.iceland);
assert.deepEqual(document.getElementById("content-language").options.map((option) => option.value), ["pt-BR"]);
assert.match(document.getElementById("activity-body").textContent, /lente cristã é opcional/);
assert.doesNotMatch(document.getElementById("activity-body").textContent, /Some 0,5/);
dataset.value = "japan";
dataset.dispatch("change");
await waitFor(
() => document.getElementById("activity-title").textContent === freshJapan.title,
"returning to Japan must fetch a fresh response instead of using the obsolete response",
);
assert.equal(calls.filter((call) => call.url === DATA_URLS.japan).length, 2);
assert.equal(calls.filter((call) => call.url === DATA_URLS.iceland).length, 1);
assert.equal(document.getElementById("source-link").href, SOURCE_URLS.japan);
});
});