Spaces:
Running
Running
| ; | |
| 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); | |
| }); | |
| }); | |