"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(/]*class=["'][^"']*\bskip-link\b[^"']*["'][^>]*>/i); assert.match(html, //i); assert.match(html, /]*aria-label=/i); assert.match(html, /]*(?: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, /^ { 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, /^]*\\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, /^ { 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, /]*\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(/]*id=["']dataset-select["'][^>]*>([\s\S]*?)<\/select>/i); const contentOptions = html.match(/]*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(`]*\\bvalue=["']${value}["'])(?=[^>]*\\blang=["']en["'])[^>]*>`, "i"), ); } for (const language of ["pt-BR", "en", "ja"]) { assert.match( contentOptions[1], new RegExp(`]*\\bvalue=["']${escapeRegExp(language)}["'])(?=[^>]*\\blang=["']${escapeRegExp(language)}["'])[^>]*>`, "i"), ); } const footer = html.match(/]*class=["'][^"']*site-footer[^"']*["'][^>]*>([\s\S]*?)<\/footer>/i); assert.ok(footer); const footerSourceLinks = [...footer[1].matchAll(/]*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(/]*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*\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); }); });