// Verify the plugin-facing loop API: setLoop / clearLoop / getLoop on // window.feedBack, the input validation in setLoop, and the // loadSavedLoop refactor that funnels through setLoop. // // Same isolation strategy as loop_restart.test.js — extract relevant // functions by brace-matching and run them in a vm sandbox. const { test } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const vm = require('node:vm'); // The A-B loop was carved out of app.js into its own module (R3a). The // window.feedBack API surface it is published through stayed in app.js. const LOOPS_JS = path.join(__dirname, '..', '..', 'static', 'js', 'loops.js'); const APP_JS = path.join(__dirname, '..', '..', 'static', 'app.js'); function extractFunction(rawSrc, signature) { // loops.js is an ES module; the vm sandbox evaluates plain script text. const src = rawSrc.replace(/^export /gm, ''); const start = src.indexOf(signature); if (start === -1) throw new Error(`extractFunction: '${signature}' not found in static/js/loops.js`); let scan = start + signature.length; if (src[scan] === '(') { let parenDepth = 1; scan++; while (scan < src.length && parenDepth > 0) { const ch = src[scan]; if (ch === '(') parenDepth++; else if (ch === ')') parenDepth--; scan++; } } const openBrace = src.indexOf('{', scan); let depth = 1; let i = openBrace + 1; while (i < src.length && depth > 0) { const ch = src[i]; if (ch === '{') depth++; else if (ch === '}') depth--; i++; } if (depth !== 0) throw new Error(`extractFunction: unbalanced braces after '${signature}'`); return src.slice(start, i); } function buildSandbox() { const seekCalls = []; const sectionPracticeModeCalls = []; const transportEvents = []; // clearLoop() used to zero section-practice's three selection scalars by hand. // They now live in static/js/section-practice.js, which owns them, so clearLoop // calls its exported resetSelection() instead. This is a SPY, not a stub — the // test below still asserts the reset happens, it just asserts it through the // seam rather than by reaching into someone else's state. const resetSelectionCalls = []; const sandbox = { seekCalls, sectionPracticeModeCalls, transportEvents, resetSelectionCalls, resetSelection: () => resetSelectionCalls.push(true), // Mutable state (declared as `var` in eval prelude so it lives on // the sandbox global and the extracted functions can read/write). // The actual values are set below. // DOM stub: every getElementById returns the same stand-in object. // Writes to className/textContent/classList are absorbed silently; // we don't assert on them here (the runtime would catch a missing // element, the unit test cares about loop bookkeeping). document: { getElementById: () => ({ className: '', textContent: '', value: '', // _syncSavedLoopSelection iterates over