| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046 |
- /**
- * DOM-walk + forms-module scenarios for the Statum client.
- *
- * Each scenario runs in a FRESH QuickJS context (run.sh invokes this script
- * once per scenario) with a minimal DOM shim (shim.js) that supports the
- * render walk and the forms module end to end, including capture-phase
- * events, form reset defaults and a scriptable fetch stub.
- *
- * State enters through the real machinery: a `set` directive returned by the
- * fetch stub and applied by bridge.loadEntrypoint() (which re-renders through
- * applyDirectives -> renderAll). Signatures vary per step so the idempotency
- * guard never short-circuits a deliberate re-render.
- *
- * 1 walk: stm-if/stm-else chain, stm-text, stm-class.*
- * 2 stm-for keyed: node identity preserved per key, removal honored
- * 3 stm-for unkeyed: instances rebuilt (all identities change)
- * 4 stm-prefill: input + textarea property writes (no attribute)
- * 5 stm-prefill: select single + multiple (array)
- * 6 stm-prefill: checkbox truthy/falsy/null
- * 7 stm-prefill: radio group, same expression per member
- * 8 dirty skip: dirty input keeps user value, clean sibling re-fills
- * 9 dirty is per element within a form
- * 10 reset event un-dirties; prefill re-applies after reset
- * 11 stm-clear-on-success: success resets + un-dirties the form
- * 12 stm-clear-on-success: failure keeps values and dirty state
- * 13 keyed loop of forms: clearing instance 2 leaves 1 and 3 alone
- * 14 two sibling forms: clearing A leaves B alone
- * 15 statum.document.reset: resets exactly the given form
- * 16 handler order: stm-prefill registered before stm-class.*
- * 17 reenter: action triggers an in-place entrypoint refresh (fetched twice)
- * 18 reenter ordering: refresh before navigate, both execute
- * 19 reenter failure: logged only; navigate still runs; promise resolves
- * 20 stm-on-success: applied on success, cleared on the next attempt
- * 21 stm-on-error/on-success mirroring: failure clears success + vice versa
- * 22 stm-clear-on-error: a failing action resets the form
- * 23 declined stm-confirm does NOT clear (clear-on-error present)
- * 24 headless core: stubbed fetch applies state, no walk, no crash
- * 25 stm-local basics: subtree scoping, evaluation through stm-text
- * 26 stm-local laziness: once per pass across two reads; twice across passes
- * 27 stm-global: page-wide visibility incl. before its declaring element
- * 28 stm-global duplicates: last declaration wins, one warn per pass
- * 29 variable cycles: logged, read undefined, walk survives
- * 30 from-attribute: observer redraws on real change only (same value: none)
- * 31 from-value: the declare/username example at the 3-char boundary
- * 32 from-checked on a checkbox
- * 33 declare-without-binder reads undefined; slot shadowing both ways
- * 34 stm-local replaces stm-def (demo's stamp pattern; stm-def inert)
- * 35 several -from- bindings on ONE element all update (review V1)
- *
- * Usage: qjs tests/dom/scenarios.js <1..35>
- */
- import * as std from 'std';
- import { installShim } from './shim.js';
- // Repo root as a CWD-relative path; run.sh cds to the repo root first (same
- // convention as tests/markdown/spec-test.js).
- var ROOT = '.';
- var scenario = scriptArgs.length > 1 ? scriptArgs[1] : '';
- function fail(msg) {
- std.err.puts('FAIL [' + scenario + '] ' + msg + '\n');
- std.exit(1);
- }
- function check(cond, msg) {
- if (!cond) fail(msg);
- }
- function load(name) {
- (0, eval)(std.loadFile(ROOT + '/js/' + name));
- }
- function loadAll() {
- load('statum-core.js');
- load('statum-document.js');
- load('statum-markdown.js');
- }
- /** Yield to the job queue n times so promise chains settle. */
- async function spin(n) {
- for (var i = 0; i < (n || 30); i++) await Promise.resolve();
- }
- var sigCounter = 0;
- /**
- * Put `public` data into a slot type through a set directive, then re-render.
- * Uses loadEntrypoint (public bridge machinery) so the exact production path
- * (fetch -> handleResponse -> applyDirectives -> renderAll) runs.
- */
- async function setState(typeName, publicData) {
- sigCounter++;
- var snapshot = { slot: { key: 'k-' + typeName, type: typeName, scope: 'page' }, public: publicData };
- globalThis.__shim.setFetch(function () {
- return globalThis.__shim.okResponse([{
- type: 'set',
- snapshot_frame: { content: JSON.stringify(snapshot), signature: 'sig-' + sigCounter }
- }]);
- });
- await globalThis.statum._bridge.loadEntrypoint();
- await spin();
- }
- /** Fire a real (bubbling, non-cancelable-by-default) event on an element. */
- function fire(el, type) {
- var evt = globalThis.__shim.makeEvent(type);
- el.dispatchEvent(evt);
- return evt;
- }
- function el(tag) { return globalThis.__shim.makeElement(tag); }
- function input(type, name, value) {
- var i = el('input');
- if (type) i.setAttribute('type', type);
- if (name) i.setAttribute('name', name);
- if (value !== undefined) i.setAttribute('value', value);
- return i;
- }
- async function main() {
- installShim();
- var log = globalThis.__shim.log;
- var body = globalThis.__shim.body;
- // Scenario 24 runs HEADLESS: core alone, no document module.
- if (scenario !== '24') loadAll();
- else load('statum-core.js');
- // Binding-module scenarios additionally load it (after document).
- if (scenario >= 25) load('statum-binding.js');
- var s = globalThis.statum;
- check(s && s._bridge, 'statum did not load');
- if (scenario === '24') {
- // Headless core: stub the entrypoint fetch, boot, and prove state was
- // applied with no walk and no crash.
- check(s.registerAttribute === undefined,
- 'headless core must not expose registerAttribute (it is document API)');
- check(s.document === undefined, 'headless core must not expose statum.document');
- sigCounter++;
- var snap = { slot: { key: 'k-h', type: 'h', scope: 'page' }, public: { hello: 'world' } };
- globalThis.__shim.setFetch(function () {
- return globalThis.__shim.okResponse([{
- type: 'set',
- snapshot_frame: { content: JSON.stringify(snap), signature: 'sig-h-' + sigCounter }
- }]);
- });
- await s.init();
- await spin(50);
- check(s.read('h') && s.read('h').hello === 'world',
- 'headless boot did not apply the entrypoint state: ' + JSON.stringify(s.state()));
- check(JSON.stringify(s.state()) === '{"h":{"hello":"world"}}',
- 'unexpected state: ' + JSON.stringify(s.state()));
- // A render cycle with no driver and no hooks must not crash or warn.
- s._bridge.renderAll();
- await spin(20);
- check(log.errors.length === 0, 'headless core errored: ' + JSON.stringify(log.errors));
- check(log.warns.length === 0, 'headless core warned: ' + JSON.stringify(log.warns));
- print('scenario ' + scenario + ': OK');
- return;
- }
- check(s.document && typeof s.document.reset === 'function', 'statum.document.reset missing');
- check(typeof s.document.markDirty === 'function' && typeof s.document.isDirty === 'function',
- 'statum.document.markDirty/isDirty missing');
- check(s.forms === undefined, 'statum.forms must be gone (merged into statum.document)');
- check(s._bridge.formsDirty instanceof WeakSet, 'bridge.formsDirty missing');
- check(typeof s._bridge.markDirty === 'function' && typeof s._bridge.isDirty === 'function',
- 'bridge.markDirty/isDirty missing');
- if (scenario === '1') {
- // if/else-if/else chain + stm-text + stm-class.*
- var w = el('div');
- var a = el('p'); a.setAttribute('stm-if', "m.mood === 'up'"); a.textContent = 'up';
- var b = el('p'); b.setAttribute('stm-else-if', "m.mood === 'mid'"); b.textContent = 'mid';
- var c = el('p'); c.setAttribute('stm-else', ''); c.textContent = 'down';
- w.appendChild(a); w.appendChild(b); w.appendChild(c);
- var t = el('span'); t.setAttribute('stm-text', 'm.mood');
- var cl = el('span'); cl.setAttribute('stm-class.hi', "m.mood === 'up'");
- w.appendChild(t); w.appendChild(cl);
- body.appendChild(w);
- await setState('m', { mood: 'up' });
- check(!a.hasAttribute('hidden') && b.hasAttribute('hidden') && c.hasAttribute('hidden'),
- 'branch up not exclusively shown');
- check(t.textContent === 'up', 'stm-text wrote ' + JSON.stringify(t.textContent));
- check(cl.classList.contains('hi'), 'stm-class.hi not applied');
- await setState('m', { mood: 'mid' });
- check(a.hasAttribute('hidden') && !b.hasAttribute('hidden') && c.hasAttribute('hidden'),
- 'branch mid not exclusively shown');
- check(!cl.classList.contains('hi'), 'stm-class.hi not removed');
- check(t.textContent === 'mid', 'stm-text not updated');
- await setState('m', { mood: 'zzz' });
- check(a.hasAttribute('hidden') && b.hasAttribute('hidden') && !c.hasAttribute('hidden'),
- 'else branch not shown');
- check(log.errors.length === 0, 'errors: ' + JSON.stringify(log.errors));
- } else if (scenario === '2') {
- // Keyed loop: identity per key preserved; removal honored.
- var holder = el('div');
- holder.setAttribute('stm-for-item-in', 'list.items');
- holder.setAttribute('stm-key', 'item.id');
- var row = el('p');
- row.setAttribute('stm-text', 'item.name');
- holder.appendChild(row);
- body.appendChild(holder);
- await setState('list', { items: [{ id: 1, name: 'a' }, { id: 2, name: 'b' }, { id: 3, name: 'c' }] });
- var insts = holder.parentNode.children.filter(function (n) { return n !== holder; });
- // holder is children[0]; instances follow it in order.
- var kids = body.children.filter(function (n) { return n !== holder; });
- check(kids.length === 3, 'expected 3 instances, got ' + kids.length);
- var k1 = kids[0], k2 = kids[1], k3 = kids[2];
- // The stm-text binding is on the template's child, so read the child.
- check(k1.children[0].textContent === 'a' && k2.children[0].textContent === 'b' &&
- k3.children[0].textContent === 'c',
- 'instance texts: ' + kids.map(function (n) { return n.children[0] && n.children[0].textContent; }).join(','));
- await setState('list', { items: [{ id: 1, name: 'a' }, { id: 3, name: 'c3' }] });
- var kids2 = body.children.filter(function (n) { return n !== holder; });
- check(kids2.length === 2, 'expected 2 instances after removal, got ' + kids2.length);
- check(kids2[0] === k1, 'key 1 node identity not preserved');
- check(kids2[1] === k3, 'key 3 node identity not preserved');
- check(kids2[1].children[0].textContent === 'c3', 'key 3 text not refreshed');
- check(k2.parentNode === null, 'removed instance still attached');
- check(log.errors.length === 0, 'errors: ' + JSON.stringify(log.errors));
- } else if (scenario === '3') {
- // Unkeyed loop: rebuild wholesale.
- var holder = el('div');
- holder.setAttribute('stm-for-item-in', 'list.items');
- var row = el('p');
- row.setAttribute('stm-text', 'item.name');
- holder.appendChild(row);
- body.appendChild(holder);
- await setState('list', { items: [{ name: 'a' }, { name: 'b' }] });
- var kids = body.children.filter(function (n) { return n !== holder; });
- check(kids.length === 2 && kids[0].children[0].textContent === 'a' &&
- kids[1].children[0].textContent === 'b', 'initial render');
- await setState('list', { items: [{ name: 'a' }, { name: 'b' }] });
- var kids2 = body.children.filter(function (n) { return n !== holder; });
- check(kids2.length === 2, 'instance count changed');
- check(kids2[0] !== kids[0] && kids2[1] !== kids[1], 'unkeyed loop reused nodes');
- check(log.errors.length === 0, 'errors: ' + JSON.stringify(log.errors));
- } else if (scenario === '4') {
- // Prefill writes the value PROPERTY, never the value attribute.
- var f = el('form');
- var ti = input('text', 'who'); ti.setAttribute('stm-prefill', 'p.who');
- var ta = el('textarea'); ta.setAttribute('name', 'bio'); ta.setAttribute('stm-prefill', 'p.bio');
- f.appendChild(ti); f.appendChild(ta);
- body.appendChild(f);
- await setState('p', { who: 'ada', bio: 'hi' });
- check(ti.value === 'ada', 'text input value = ' + JSON.stringify(ti.value));
- check(ta.value === 'hi', 'textarea value = ' + JSON.stringify(ta.value));
- check(ti.getAttribute('value') === null, 'text input value ATTRIBUTE was written');
- check(ta.getAttribute('stm-prefill') === 'p.bio', 'stm-prefill attribute consumed');
- await setState('p', { who: null, bio: undefined });
- check(ti.value === '', 'null should prefill as empty string, got ' + JSON.stringify(ti.value));
- check(ta.value === 'hi', 'undefined must leave the element untouched');
- await setState('p', { who: 42, bio: 'x' });
- check(ti.value === '42', 'number stringified: ' + JSON.stringify(ti.value));
- check(log.errors.length === 0, 'errors: ' + JSON.stringify(log.errors));
- } else if (scenario === '5') {
- // Select single + multiple.
- var f = el('form');
- var sel = el('select'); sel.setAttribute('stm-prefill', 'p.pick');
- var o1 = el('option'); o1.setAttribute('value', 'a'); o1.textContent = 'A';
- var o2 = el('option'); o2.setAttribute('value', 'b'); o2.textContent = 'B';
- sel.appendChild(o1); sel.appendChild(o2);
- var multi = el('select'); multi.setAttribute('multiple', ''); multi.setAttribute('stm-prefill', 'p.set');
- var m1 = el('option'); m1.setAttribute('value', 'x');
- var m2 = el('option'); m2.setAttribute('value', 'y');
- var m3 = el('option'); m3.setAttribute('value', 'z');
- multi.appendChild(m1); multi.appendChild(m2); multi.appendChild(m3);
- f.appendChild(sel); f.appendChild(multi);
- body.appendChild(f);
- await setState('p', { pick: 'b', set: ['x', 'z'] });
- check(sel.value === 'b', 'single select value = ' + JSON.stringify(sel.value));
- check(o2.selected === true && o1.selected === false, 'single select option states wrong');
- check(m1.selected === true && m2.selected === false && m3.selected === true,
- 'multiple select option states wrong');
- await setState('p', { pick: 'nope', set: 'y' });
- check(sel.value === '', 'unmatched single select should yield empty value');
- check(m1.selected === false && m2.selected === true && m3.selected === false,
- 'non-array multiple value must behave like single');
- check(log.errors.length === 0, 'errors: ' + JSON.stringify(log.errors));
- } else if (scenario === '6') {
- // Checkbox: truthy -> checked, falsy/null -> unchecked.
- var f = el('form');
- var cb = input('checkbox', 'ok'); cb.setAttribute('stm-prefill', 'p.ok');
- f.appendChild(cb);
- body.appendChild(f);
- await setState('p', { ok: true });
- check(cb.checked === true, 'truthy did not check');
- await setState('p', { ok: false });
- check(cb.checked === false, 'falsy did not uncheck');
- await setState('p', { ok: null });
- check(cb.checked === false, 'null did not uncheck');
- await setState('p', { ok: 'yes' });
- check(cb.checked === true, 'non-empty string should check');
- await setState('p', {}); // ok undefined -> untouched
- check(cb.checked === true, 'undefined must leave checked untouched');
- check(log.errors.length === 0, 'errors: ' + JSON.stringify(log.errors));
- } else if (scenario === '7') {
- // Radio group: every member carries the same expression; the member whose
- // own value matches the resolved value gets checked.
- var f = el('form');
- var mk = function (v) {
- var r = input('radio', 'g', v);
- r.setAttribute('stm-prefill', 'p.choice');
- f.appendChild(r);
- return r;
- };
- var ra = mk('a'), rb = mk('b'), rc = mk('c');
- body.appendChild(f);
- await setState('p', { choice: 'b' });
- check(ra.checked === false && rb.checked === true && rc.checked === false,
- 'state b checked the wrong member(s)');
- await setState('p', { choice: 'd' });
- check(ra.checked === false && rb.checked === false && rc.checked === false,
- 'state d (no match) must leave all unchecked');
- await setState('p', { choice: null });
- check(rb.checked === false, 'null must uncheck (String(null) matches nothing)');
- check(log.errors.length === 0, 'errors: ' + JSON.stringify(log.errors));
- } else if (scenario === '8') {
- // A dirty element is never overwritten; a clean sibling still fills.
- var f = el('form');
- var i1 = input('text', 'one'); i1.setAttribute('stm-prefill', 'p.one');
- var i2 = input('text', 'two'); i2.setAttribute('stm-prefill', 'p.two');
- f.appendChild(i1); f.appendChild(i2);
- body.appendChild(f);
- await setState('p', { one: 'x', two: 'x' });
- check(i1.value === 'x' && i2.value === 'x', 'initial fill failed');
- fire(i1, 'input'); // user edits input 1
- check(s.document.isDirty(i1), 'input event did not mark the element dirty');
- await setState('p', { one: 'y', two: 'y' });
- check(i1.value === 'x', 'dirty element was overwritten: ' + JSON.stringify(i1.value));
- check(i2.value === 'y', 'clean sibling not re-filled');
- check(log.errors.length === 0, 'errors: ' + JSON.stringify(log.errors));
- } else if (scenario === '9') {
- // Dirtiness is per element.
- var f = el('form');
- var i1 = input('text', 'one'); i1.setAttribute('stm-prefill', 'p.v');
- var i2 = input('text', 'two'); i2.setAttribute('stm-prefill', 'p.v');
- f.appendChild(i1); f.appendChild(i2);
- body.appendChild(f);
- await setState('p', { v: 'a' });
- fire(i1, 'change');
- await setState('p', { v: 'b' });
- check(i1.value === 'a', 'dirty element overwritten');
- check(i2.value === 'b', 'clean element not filled');
- check(s.document.isDirty(i1) && !s.document.isDirty(i2), 'dirty flags wrong');
- check(log.errors.length === 0, 'errors: ' + JSON.stringify(log.errors));
- } else if (scenario === '10') {
- // Reset restores defaults, un-dirties, and prefill re-applies.
- var f = el('form');
- var i1 = input('text', 'one', 'init'); i1.setAttribute('stm-prefill', 'p.v');
- f.appendChild(i1);
- body.appendChild(f);
- await setState('p', { v: 'state' });
- check(i1.value === 'state', 'prefill did not apply');
- fire(i1, 'input');
- check(s.document.isDirty(i1), 'not dirty after edit');
- f.reset();
- check(i1.value === 'init', 'reset did not restore the attribute default: ' + JSON.stringify(i1.value));
- check(!s.document.isDirty(i1), 'reset did not un-dirty');
- await setState('p', { v: 'zzz' });
- check(i1.value === 'zzz', 'prefill did not re-apply after reset');
- check(log.errors.length === 0, 'errors: ' + JSON.stringify(log.errors));
- } else if (scenario === '11') {
- // Clear-on-success: action resolves -> form resets and un-dirties.
- var f = el('form');
- f.setAttribute('stm-action', 'p.act');
- f.setAttribute('stm-clear-on-success', '');
- var i1 = input('text', 'one', 'dflt'); i1.setAttribute('stm-prefill', 'p.v');
- f.appendChild(i1);
- body.appendChild(f);
- await setState('p', { v: 'typed', act: { uri: '/save', method: 'POST' } });
- check(i1.value === 'typed', 'prefill did not apply');
- fire(i1, 'input'); // user edits -> dirty; reset must still win via clear
- globalThis.__shim.setFetch(function () { return globalThis.__shim.okResponse([]); });
- fire(f, 'submit');
- await spin(50);
- check(globalThis.__shim.fetchCalls.some(function (c) { return c.url === '/save'; }),
- 'the action request never fired');
- check(i1.value === 'dflt', 'form not reset after success: ' + JSON.stringify(i1.value));
- check(!s.document.isDirty(i1), 'form not un-dirtied after success');
- check(log.errors.length === 0, 'errors: ' + JSON.stringify(log.errors));
- } else if (scenario === '12') {
- // Clear-on-success: failure keeps values and dirty state.
- var f = el('form');
- f.setAttribute('stm-action', 'p.act');
- f.setAttribute('stm-clear-on-success', '');
- var i1 = input('text', 'one', 'dflt'); i1.setAttribute('stm-prefill', 'p.v');
- f.appendChild(i1);
- body.appendChild(f);
- await setState('p', { v: 'typed', act: { uri: '/save', method: 'POST' } });
- fire(i1, 'input');
- i1.value = 'edited';
- globalThis.__shim.setFetch(function () { return globalThis.__shim.failResponse(500); });
- fire(f, 'submit');
- await spin(50);
- check(i1.value === 'edited', 'form was reset despite failure: ' + JSON.stringify(i1.value));
- check(s.document.isDirty(i1), 'dirty state lost on failure');
- check(log.errors.length === 0, 'errors: ' + JSON.stringify(log.errors));
- // Error-directive variant (statum response carrying an error directive).
- globalThis.__shim.setFetch(function () {
- return globalThis.__shim.okResponse([{ type: 'error', message: 'nope' }]);
- });
- fire(f, 'submit');
- await spin(50);
- check(i1.value === 'edited', 'form was reset after an error directive');
- // Core logs error directives by design; nothing else may error.
- check(log.errors.length === 1 && log.errors[0].indexOf('error directive') !== -1,
- 'unexpected console.error: ' + JSON.stringify(log.errors));
- log.errors.length = 0;
- } else if (scenario === '13') {
- // Keyed loop of forms: clearing one instance leaves the others intact.
- var holder = el('div');
- holder.setAttribute('stm-for-item-in', 'p.rows');
- holder.setAttribute('stm-key', 'item.id');
- var row = el('form');
- row.setAttribute('stm-action', 'item.act');
- row.setAttribute('stm-clear-on-success', '');
- var ri = input('text', 'val', 'blank');
- ri.setAttribute('stm-prefill', 'item.val');
- row.appendChild(ri);
- holder.appendChild(row);
- body.appendChild(holder);
- var mkAct = function (uri) { return { uri: uri, method: 'POST' }; };
- await setState('p', {
- rows: [
- { id: 1, val: 'one', act: mkAct('/save1') },
- { id: 2, val: 'two', act: mkAct('/save2') },
- { id: 3, val: 'three', act: mkAct('/save3') }
- ]
- });
- var forms = body.querySelectorAll('form').filter(function (n) {
- // The loop template lives inside the (hidden) holder; instances sit in
- // their own DIVs outside it.
- var p = n.parentNode;
- return p !== holder && p.tagName === 'DIV';
- });
- check(forms.length === 3, 'expected 3 instance forms, got ' + forms.length);
- check(forms[0].children[0].value === 'one', 'instance 1 prefill failed');
- // Dirty instance 2's input by editing it.
- var i2 = forms[1].children[0];
- i2.value = 'user2';
- fire(i2, 'input');
- // Instance 1 and 3 inputs are clean (state-filled).
- globalThis.__shim.setFetch(function () { return globalThis.__shim.okResponse([]); });
- fire(forms[1], 'submit');
- await spin(50);
- check(forms[1].children[0].value === 'blank', 'instance 2 not reset: ' + JSON.stringify(forms[1].children[0].value));
- check(!s.document.isDirty(forms[1].children[0]), 'instance 2 not un-dirtied');
- check(forms[0].children[0].value === 'one', 'instance 1 was touched');
- check(forms[2].children[0].value === 'three', 'instance 3 was touched');
- var saved = globalThis.__shim.fetchCalls.filter(function (c) { return c.method === 'POST'; })
- .map(function (c) { return c.url; });
- check(saved.indexOf('/save2') !== -1 && saved.indexOf('/save1') === -1 && saved.indexOf('/save3') === -1,
- 'wrong action fired: ' + JSON.stringify(saved));
- check(log.errors.length === 0, 'errors: ' + JSON.stringify(log.errors));
- } else if (scenario === '14') {
- // Two sibling forms: clear-on-success on A only.
- var fa = el('form');
- fa.setAttribute('stm-action', 'p.act');
- fa.setAttribute('stm-clear-on-success', '');
- var ia = input('text', 'a', 'dflt-a');
- ia.setAttribute('stm-prefill', 'p.va');
- fa.appendChild(ia);
- var fb = el('form');
- fb.setAttribute('stm-action', 'p.act');
- var ib = input('text', 'b', 'dflt-b');
- ib.setAttribute('stm-prefill', 'p.vb');
- fb.appendChild(ib);
- body.appendChild(fa); body.appendChild(fb);
- await setState('p', { va: 'A', vb: 'B', act: { uri: '/save', method: 'POST' } });
- fire(ib, 'input'); // B becomes dirty
- globalThis.__shim.setFetch(function () { return globalThis.__shim.okResponse([]); });
- fire(fa, 'submit');
- await spin(50);
- check(ia.value === 'dflt-a', 'form A not reset');
- check(!s.document.isDirty(ia), 'form A not un-dirtied');
- check(ib.value === 'B', 'form B value was touched');
- check(s.document.isDirty(ib), 'form B dirty state was touched');
- check(log.errors.length === 0, 'errors: ' + JSON.stringify(log.errors));
- } else if (scenario === '15') {
- // statum.forms.reset resets exactly the given form.
- var fa = el('form');
- var ia = input('text', 'a', 'dflt-a');
- fa.appendChild(ia);
- var fb = el('form');
- var ib = input('text', 'b', 'dflt-b');
- fb.appendChild(ib);
- body.appendChild(fa); body.appendChild(fb);
- ia.value = 'x'; fire(ia, 'input');
- ib.value = 'y'; fire(ib, 'input');
- s.document.reset(fa);
- check(ia.value === 'dflt-a', 'form A not reset');
- check(!s.document.isDirty(ia), 'form A not un-dirtied');
- check(ib.value === 'y', 'form B was touched');
- check(s.document.isDirty(ib), 'form B dirty state was touched');
- // Passing a control inside a form resolves the owning form.
- ib.value = 'z';
- s.document.reset(ib);
- check(ib.value === 'dflt-b', 'forms.reset(el) did not resolve the owning form');
- check(log.errors.length === 0, 'errors: ' + JSON.stringify(log.errors));
- } else if (scenario === '16') {
- // Handler order: stm-prefill first (property writes feed later predicates).
- var names = s._bridge.attributeHandlers.map(function (h) { return h.name; });
- check(names.indexOf('stm-prefill') === 0, 'stm-prefill is not first: ' + JSON.stringify(names));
- check(names.indexOf('stm-class.') === 1, 'stm-class. is not second: ' + JSON.stringify(names));
- check(names.indexOf('stm-markdown') === names.indexOf('stm-html') + 1, 'markdown ordering broke');
- check(names[names.length - 1] === 'stm-h-demotion', 'last handler changed: ' + JSON.stringify(names));
- check(log.warns.length === 0, 'warnings: ' + JSON.stringify(log.warns));
- check(log.errors.length === 0, 'errors: ' + JSON.stringify(log.errors));
- } else if (scenario === '17') {
- // reenter: an action response re-fetches the entrypoint in place.
- var t = el('span'); t.setAttribute('stm-text', 'm.mood');
- body.appendChild(t);
- sigCounter++;
- var snap1 = { slot: { key: 'k-m', type: 'm', scope: 'page' }, public: { mood: 'first' } };
- var snap2 = { slot: { key: 'k-m', type: 'm', scope: 'page' }, public: { mood: 'second' } };
- var entrypointCount = 0;
- globalThis.__shim.setFetch(function (url) {
- if (String(url).indexOf('/_statum/entrypoint') !== -1) {
- entrypointCount++;
- return globalThis.__shim.okResponse([{
- type: 'set',
- snapshot_frame: {
- content: JSON.stringify(entrypointCount === 1 ? snap1 : snap2),
- signature: 'sig-r17-' + entrypointCount
- }
- }]);
- }
- // The action request: reply with a reenter directive.
- return globalThis.__shim.okResponse([{ type: 'reenter' }]);
- });
- await s._bridge.loadEntrypoint();
- await spin();
- check(t.textContent === 'first', 'initial entrypoint state not applied: ' + JSON.stringify(t.textContent));
- var initialFetches = globalThis.__shim.fetchCalls.length;
- await s._bridge.performAction({ uri: '/save', method: 'POST' }, {});
- await spin(50);
- var entryFetches = globalThis.__shim.fetchCalls.filter(function (c) {
- return c.url.indexOf('/_statum/entrypoint') !== -1;
- });
- check(entryFetches.length === 2, 'entrypoint fetched ' + entryFetches.length + ' time(s), expected 2 (initial + reenter)');
- check(globalThis.__shim.fetchCalls.length === initialFetches + 2,
- 'exactly two new requests were expected (action + refresh), got ' +
- (globalThis.__shim.fetchCalls.length - initialFetches));
- check(t.textContent === 'second', 'reentered entrypoint state not applied: ' + JSON.stringify(t.textContent));
- check(globalThis.location.href === 'http://localhost/', 'a bare reenter must not navigate');
- check(log.errors.length === 0, 'errors: ' + JSON.stringify(log.errors));
- } else if (scenario === '18') {
- // reenter + navigate in one response: refresh first, then navigate wins.
- var t = el('span'); t.setAttribute('stm-text', 'm.mood');
- body.appendChild(t);
- sigCounter++;
- var snap1 = { slot: { key: 'k-m', type: 'm', scope: 'page' }, public: { mood: 'first' } };
- var snap2 = { slot: { key: 'k-m', type: 'm', scope: 'page' }, public: { mood: 'second' } };
- var entrypointCount = 0;
- var order = []; // shared log: reenter's fetch vs the location change
- globalThis.__shim.setFetch(function (url) {
- if (String(url).indexOf('/_statum/entrypoint') !== -1) {
- entrypointCount++;
- order.push('entrypoint:' + entrypointCount);
- return globalThis.__shim.okResponse([{
- type: 'set',
- snapshot_frame: {
- content: JSON.stringify(entrypointCount === 1 ? snap1 : snap2),
- signature: 'sig-r18-' + entrypointCount
- }
- }]);
- }
- return globalThis.__shim.okResponse([{ type: 'reenter' }, { type: 'navigate', uri: '/after' }]);
- });
- // Record the navigation in the shared order log.
- var originalHref = globalThis.location.href;
- Object.defineProperty(globalThis.location, 'href', {
- configurable: true,
- get: function () { return originalHref; },
- set: function (v) { order.push('navigate:' + v); }
- });
- await s._bridge.loadEntrypoint();
- await spin();
- check(t.textContent === 'first', 'initial entrypoint state not applied');
- await s._bridge.performAction({ uri: '/save', method: 'POST' }, {});
- await spin(50);
- check(globalThis.__shim.fetchCalls.filter(function (c) {
- return c.url.indexOf('/_statum/entrypoint') !== -1;
- }).length === 2, 'reenter did not refresh the entrypoint');
- check(order.indexOf('navigate:/after') !== -1, 'navigate did not run');
- check(order.indexOf('entrypoint:2') < order.indexOf('navigate:/after'),
- 'reenter must run before navigate, order was ' + JSON.stringify(order));
- check(t.textContent === 'second', 'refresh state not applied before navigating');
- check(log.errors.length === 0, 'errors: ' + JSON.stringify(log.errors));
- } else if (scenario === '19') {
- // A failing reenter refresh: logged, swallowed, navigate still runs, and
- // the action promise resolves.
- var t = el('span'); t.setAttribute('stm-text', 'm.mood');
- body.appendChild(t);
- sigCounter++;
- var snap1 = { slot: { key: 'k-m', type: 'm', scope: 'page' }, public: { mood: 'first' } };
- var entrypointCount = 0;
- globalThis.__shim.setFetch(function (url) {
- if (String(url).indexOf('/_statum/entrypoint') !== -1) {
- entrypointCount++;
- if (entrypointCount === 1) {
- return globalThis.__shim.okResponse([{
- type: 'set',
- snapshot_frame: { content: JSON.stringify(snap1), signature: 'sig-r19-1' }
- }]);
- }
- return Promise.reject(new Error('network down'));
- }
- return globalThis.__shim.okResponse([{ type: 'reenter' }, { type: 'navigate', uri: '/after' }]);
- });
- var navigatedTo = null;
- Object.defineProperty(globalThis.location, 'href', {
- configurable: true,
- get: function () { return 'http://localhost/'; },
- set: function (v) { navigatedTo = v; }
- });
- await s._bridge.loadEntrypoint();
- await spin();
- var resolved = false;
- var rejected = null;
- await s._bridge.performAction({ uri: '/save', method: 'POST' }, {}).then(function () {
- resolved = true;
- }, function (e) {
- rejected = e;
- });
- await spin(50);
- check(resolved && rejected === null, 'action promise should resolve after a failed reenter, rejected=' + String(rejected));
- check(log.errors.some(function (m) { return m.indexOf('[statum] reenter: entrypoint refresh failed') !== -1; }),
- 'reenter failure was not logged: ' + JSON.stringify(log.errors));
- check(navigatedTo === '/after', 'navigate did not run after the failed reenter, navigatedTo=' + JSON.stringify(navigatedTo));
- check(log.errors.length === 1, 'exactly one console.error expected: ' + JSON.stringify(log.errors));
- // Core logs the swallowed reenter failure by design; nothing else may error.
- log.errors.length = 0;
- } else if (scenario === '20') {
- // stm-on-success: applied on success, cleared on the next attempt.
- var b = el('button');
- b.setAttribute('stm-action', 'p.act');
- b.setAttribute('stm-on-success', 'saved ok');
- body.appendChild(b);
- await setState('p', { act: { uri: '/save', method: 'POST' } });
- globalThis.__shim.setFetch(function () { return globalThis.__shim.okResponse([]); });
- fire(b, 'click');
- await spin(50);
- check(b.classList.contains('saved') && b.classList.contains('ok'),
- 'success classes not applied: ' + JSON.stringify(b.classList));
- // A new attempt clears them at attempt start. The fetch stub below never
- // resolves, freezing the attempt mid-flight so the cleared state (before
- // any outcome re-applies classes) is observable.
- var hang = new Promise(function () {});
- globalThis.__shim.setFetch(function () { return hang; });
- fire(b, 'click');
- await spin(20);
- check(!b.classList.contains('saved') && !b.classList.contains('ok'),
- 'success classes not cleared on a new attempt');
- check(log.errors.length === 0, 'errors: ' + JSON.stringify(log.errors));
- } else if (scenario === '21') {
- // Mirroring: a failure applies stm-on-error and clears stm-on-success;
- // a later success applies stm-on-success and clears stm-on-error.
- var b = el('button');
- b.setAttribute('stm-action', 'p.act');
- b.setAttribute('stm-on-error', 'boom');
- b.setAttribute('stm-on-success', 'yay');
- body.appendChild(b);
- await setState('p', { act: { uri: '/save', method: 'POST' } });
- globalThis.__shim.setFetch(function () { return globalThis.__shim.failResponse(500); });
- fire(b, 'click');
- await spin(50);
- check(b.classList.contains('boom'), 'error classes not applied');
- check(!b.classList.contains('yay'), 'success classes survived a failure');
- log.errors.length = 0;
- globalThis.__shim.setFetch(function () { return globalThis.__shim.okResponse([]); });
- fire(b, 'click');
- await spin(50);
- check(b.classList.contains('yay'), 'success classes not applied after a failure');
- check(!b.classList.contains('boom'), 'error classes survived a success');
- // And an error-directive failure mirrors the HTTP failure exactly.
- globalThis.__shim.setFetch(function () {
- return globalThis.__shim.okResponse([{ type: 'error', message: 'nope' }]);
- });
- fire(b, 'click');
- await spin(50);
- check(b.classList.contains('boom'), 'error classes not applied (error directive)');
- check(!b.classList.contains('yay'), 'success classes survived an error directive');
- log.errors.length = 0;
- check(log.errors.length === 0, 'errors: ' + JSON.stringify(log.errors));
- } else if (scenario === '22') {
- // stm-clear-on-error: a failing action resets the form (values and dirty).
- var f = el('form');
- f.setAttribute('stm-action', 'p.act');
- f.setAttribute('stm-clear-on-error', '');
- var i1 = input('text', 'a', 'dflt');
- i1.setAttribute('stm-prefill', 'p.v');
- f.appendChild(i1);
- body.appendChild(f);
- await setState('p', { v: 'from state', act: { uri: '/save', method: 'POST' } });
- i1.value = 'user typed';
- fire(i1, 'input');
- check(s.document.isDirty(i1), 'input not dirty after edit');
- globalThis.__shim.setFetch(function () { return globalThis.__shim.failResponse(500); });
- fire(f, 'submit');
- await spin(50);
- check(i1.value === 'dflt', 'form was not reset on failure: ' + JSON.stringify(i1.value));
- check(!s.document.isDirty(i1), 'form was not un-dirtied on failure');
- check(log.errors.length === 0, 'errors: ' + JSON.stringify(log.errors));
- // A success must NOT reset a stm-clear-on-error form.
- i1.value = 'typed again';
- fire(i1, 'input');
- globalThis.__shim.setFetch(function () { return globalThis.__shim.okResponse([]); });
- fire(f, 'submit');
- await spin(50);
- check(i1.value === 'typed again', 'stm-clear-on-error fired on success: ' + JSON.stringify(i1.value));
- check(log.errors.length === 0, 'errors: ' + JSON.stringify(log.errors));
- } else if (scenario === '23') {
- // A declined stm-confirm never clears: decline is cancellation, not error.
- var f = el('form');
- f.setAttribute('stm-action', 'p.act');
- f.setAttribute('stm-confirm', 'sure?');
- f.setAttribute('stm-clear-on-error', '');
- var i1 = input('text', 'a', 'dflt');
- f.appendChild(i1);
- body.appendChild(f);
- await setState('p', { act: { uri: '/save', method: 'POST' } });
- i1.value = 'user typed';
- fire(i1, 'input');
- // Decline the confirm (custom handler returning false).
- s.onConfirmHandler = function () { return false; };
- fire(f, 'submit');
- await spin(50);
- check(i1.value === 'user typed', 'declined confirm cleared the form: ' + JSON.stringify(i1.value));
- check(s.document.isDirty(i1), 'declined confirm un-dirtied the form');
- var posts = globalThis.__shim.fetchCalls.filter(function (c) { return c.method === 'POST'; });
- check(posts.length === 0, 'a declined confirm still fired a request: ' + JSON.stringify(posts));
- check(log.errors.length === 0, 'errors: ' + JSON.stringify(log.errors));
- // The error-directive path of clear-on-error: failure with a Statum body.
- s.onConfirmHandler = null; // window.confirm fallback: shim returns true
- i1.value = 'typed more';
- fire(i1, 'input');
- globalThis.__shim.setFetch(function () {
- return globalThis.__shim.okResponse([{ type: 'error', message: 'nope' }]);
- });
- fire(f, 'submit');
- await spin(50);
- check(i1.value === 'dflt', 'error directive did not reset a clear-on-error form: ' + JSON.stringify(i1.value));
- log.errors.length = 0;
- } else if (scenario === '25') {
- // stm-local basic: subtree scoping + evaluation through stm-text.
- var holder = el('div');
- holder.setAttribute('stm-local-x', '1 + 1');
- var span = el('span'); span.setAttribute('stm-text', 'x');
- holder.appendChild(span);
- body.appendChild(holder);
- var outside = el('span'); outside.setAttribute('stm-text', 'typeof x');
- body.appendChild(outside);
- await s._bridge.renderAll(); await spin();
- check(span.textContent === '2', 'stm-local-x did not resolve: ' + span.textContent);
- check(outside.textContent === 'undefined', 'local leaked outside its subtree');
- } else if (scenario === '26') {
- // Lazy evaluation: once per pass even when read many times.
- var calls = 0;
- globalThis.__count = function () { calls++; return calls; };
- var holder2 = el('div');
- holder2.setAttribute('stm-local-n', '__count()');
- var a2 = el('span'); a2.setAttribute('stm-text', 'n');
- var b2 = el('span'); b2.setAttribute('stm-text', 'n + n');
- holder2.appendChild(a2); holder2.appendChild(b2);
- body.appendChild(holder2);
- await s._bridge.renderAll(); await spin();
- check(a2.textContent === '1' && b2.textContent === '2', 'lazy value wrong: ' + a2.textContent + '/' + b2.textContent);
- check(calls === 1, 'expression evaluated ' + calls + ' times in one pass (must be 1)');
- await s._bridge.renderAll(); await spin();
- check(calls === 2, 'second pass did not re-evaluate (calls=' + calls + ')');
- } else if (scenario === '27') {
- // stm-global: visible page-wide on the FIRST pass (the pass-start sweep
- // exposes lazy getters; a reader before the declaring element resolves
- // the same binding on demand), and stable across passes.
- var early = el('span'); early.setAttribute('stm-text', 'typeof g');
- body.appendChild(early);
- var late = el('div'); late.setAttribute('stm-global-g', '41 + 1');
- var lateSpan = el('span'); lateSpan.setAttribute('stm-text', 'g');
- late.appendChild(lateSpan);
- body.appendChild(late);
- await s._bridge.renderAll(); await spin();
- check(early.textContent === 'number', 'global not visible before its declaration on pass 1');
- await s._bridge.renderAll(); await spin();
- check(early.textContent === 'number', 'global lost on second pass');
- // Prove the VALUE is reachable, not just the type, via an if-predicate.
- var gate = el('div'); gate.setAttribute('stm-if', 'g === 42');
- body.appendChild(gate);
- await s._bridge.renderAll(); await spin();
- check(!gate.hasAttribute('hidden'), 'g === 42 did not evaluate true');
- } else if (scenario === '28') {
- // Duplicate stm-global: last in document order wins, one warn per pass.
- var g1 = el('div'); g1.setAttribute('stm-global-dup', '1');
- var g2 = el('div'); g2.setAttribute('stm-global-dup', '2');
- var rd = el('span'); rd.setAttribute('stm-text', 'dup');
- body.appendChild(g1); body.appendChild(g2); body.appendChild(rd);
- log.warns.length = 0;
- await s._bridge.renderAll(); await spin();
- check(rd.textContent === '2', 'last duplicate did not win: ' + rd.textContent);
- check(log.warns.length === 1, 'expected exactly one duplicate warn, got ' + log.warns.length);
- log.warns.length = 0;
- await s._bridge.renderAll(); await spin();
- check(log.warns.length === 1, 'warn must fire once per pass');
- } else if (scenario === '29') {
- // Cycles: logged as an error, variables read undefined, walk survives.
- var ca = el('div'); ca.setAttribute('stm-global-a', 'b');
- var cb = el('div'); cb.setAttribute('stm-global-b', 'a');
- var ra = el('span'); ra.setAttribute('stm-text', 'typeof a');
- body.appendChild(ca); body.appendChild(cb); body.appendChild(ra);
- await s._bridge.renderAll(); await spin();
- check(log.errors.length >= 1 && /variable cycle/.test(log.errors[0]),
- 'cycle not logged: ' + JSON.stringify(log.errors));
- check(ra.textContent === 'undefined', 'cyclic variable must read undefined');
- log.errors.length = 0;
- } else if (scenario === '30') {
- // from-attribute (non-control): observer redraws on real change only.
- var host = el('div'); host.setAttribute('title', 'first');
- host.setAttribute('stm-local-t', 't'); // reads via the from binding below
- host.setAttribute('stm-local-t-from-title', '');
- // ^ both spellings would conflict; use only the from- form:
- host.removeAttribute('stm-local-t');
- var outT = el('span'); outT.setAttribute('stm-text', 't');
- host.appendChild(outT);
- body.appendChild(host);
- var cycles = 0;
- s.afterEvaluate(function () { cycles++; });
- await s._bridge.renderAll(); await spin();
- check(outT.textContent === 'first', 'from-title not read: ' + outT.textContent);
- cycles = 0;
- host.setAttribute('title', 'first'); // same value: no redraw
- await spin(10);
- check(cycles === 0, 'same-value attribute write triggered a redraw');
- host.setAttribute('title', 'second'); // real change: redraw
- await spin(10);
- check(cycles >= 1, 'changed attribute did not redraw');
- check(outT.textContent === 'second', 'variable not updated after change: ' + outT.textContent);
- } else if (scenario === '31') {
- // The operator's example: declare + local-from-value + stm-if.
- var box = el('div'); box.setAttribute('stm-declare-username', '');
- var f31 = el('form');
- var inp = input('text', 'username', '');
- inp.setAttribute('stm-local-username-from-value', '');
- f31.appendChild(inp);
- var alertBox = el('div'); alertBox.setAttribute('stm-if', 'username.length < 3');
- alertBox.textContent = 'Username is too short';
- var okBox = el('div'); okBox.setAttribute('stm-if', 'username.length >= 3');
- var okName = el('span'); okName.setAttribute('stm-text', 'username');
- okBox.appendChild(okName);
- box.appendChild(f31); box.appendChild(alertBox); box.appendChild(okBox);
- body.appendChild(box);
- await s._bridge.renderAll(); await spin();
- check(!alertBox.hasAttribute('hidden'), 'alert must show with empty username');
- check(okBox.hasAttribute('hidden'), 'ok box must hide with empty username');
- inp.value = 'ab'; fire(inp, 'input'); await spin(10);
- check(!alertBox.hasAttribute('hidden'), 'alert must show for "ab"');
- inp.value = 'abc'; fire(inp, 'input'); await spin(10);
- check(alertBox.hasAttribute('hidden'), 'alert must hide for "abc"');
- check(!okBox.hasAttribute('hidden') && okName.textContent === 'abc',
- 'ok box wrong: hidden=' + okBox.hasAttribute('hidden') + ' text=' + okName.textContent);
- } else if (scenario === '32') {
- // from-checked on a checkbox.
- var box32 = el('div'); box32.setAttribute('stm-declare-agree', '');
- var cbx = input('checkbox', 'agree', '');
- cbx.setAttribute('stm-local-agree-from-checked', '');
- var lab = el('span'); lab.setAttribute('stm-text', 'agree ? "yes" : "no"');
- box32.appendChild(cbx); box32.appendChild(lab);
- body.appendChild(box32);
- await s._bridge.renderAll(); await spin();
- check(lab.textContent === 'no', 'unchecked must read false: ' + lab.textContent);
- cbx.checked = true; fire(cbx, 'change'); await spin(10);
- check(lab.textContent === 'yes', 'checked must read true: ' + lab.textContent);
- } else if (scenario === '33') {
- // Declare without binder: undefined, page alive; shadowing.
- var box33 = el('div'); box33.setAttribute('stm-declare-later', '');
- var probe = el('span'); probe.setAttribute('stm-text', 'typeof later');
- box33.appendChild(probe);
- body.appendChild(box33);
- await s._bridge.renderAll(); await spin();
- check(probe.textContent === 'undefined', 'declared-but-unbound must read undefined');
- // Shadowing: a variable named like a slot type wins in its scope.
- await setState('counter', { n: 5 });
- var sh = el('div'); sh.setAttribute('stm-local-counter', '99');
- var shOut = el('span'); shOut.setAttribute('stm-text', 'typeof counter');
- sh.appendChild(shOut); body.appendChild(sh);
- var plain = el('span'); plain.setAttribute('stm-text', 'counter.n');
- body.appendChild(plain);
- await s._bridge.renderAll(); await spin();
- check(shOut.textContent === 'number', 'shadowing variable must hide the slot type: ' + shOut.textContent);
- check(plain.textContent === '5', 'slot type must still read outside the shadow');
- } else if (scenario === '34') {
- // stm-local replaces stm-def: the demo's stamp pattern, plus confirm the
- // old attribute is inert (documented removal).
- var box34 = el('div'); box34.setAttribute('stm-local-stamp', "'updated '" + ' + counter.n');
- var out34 = el('span'); out34.setAttribute('stm-text', 'stamp');
- box34.appendChild(out34); body.appendChild(box34);
- await setState('counter', { n: 7 });
- check(out34.textContent === 'updated 7', 'stamp local wrong: ' + out34.textContent);
- var legacy = el('div'); legacy.setAttribute('stm-def-old-as', '1');
- var legacyOut = el('span'); legacyOut.setAttribute('stm-text', 'typeof old');
- legacy.appendChild(legacyOut); body.appendChild(legacy);
- await s._bridge.renderAll(); await spin();
- check(legacyOut.textContent === 'undefined', 'stm-def must be inert after removal');
- } else if (scenario === '35') {
- // Several -from- bindings on ONE element (review finding: the plumbing
- // was keyed per element, so the second binding never updated).
- var box35 = el('div'); box35.setAttribute('stm-declare-v', '');
- box35.setAttribute('stm-declare-w', '');
- var inp35 = input('text', 'dual', '');
- inp35.setAttribute('stm-local-v-from-value', '');
- inp35.setAttribute('stm-local-w-from-value', '');
- var outV = el('span'); outV.setAttribute('stm-text', 'v');
- var outW = el('span'); outW.setAttribute('stm-text', 'w');
- box35.appendChild(inp35); box35.appendChild(outV); box35.appendChild(outW);
- body.appendChild(box35);
- await s._bridge.renderAll(); await spin();
- check(outV.textContent === '' && outW.textContent === '',
- 'empty input must read "" for both (got ' + outV.textContent + '/' + outW.textContent + ')');
- inp35.value = 'hello'; fire(inp35, 'input'); await spin(10);
- check(outV.textContent === 'hello', 'first from-value binding stuck: ' + outV.textContent);
- check(outW.textContent === 'hello', 'SECOND from-value binding on the same element stuck: ' + outW.textContent);
- // Same for two attribute observers on one element.
- var host35 = el('div'); host35.setAttribute('title', 'T1'); host35.setAttribute('lang', 'L1');
- host35.setAttribute('stm-global-a', 'a'); host35.removeAttribute('stm-global-a');
- host35.setAttribute('stm-global-t-from-title', '');
- host35.setAttribute('stm-global-l-from-lang', '');
- var outT35 = el('span'); outT35.setAttribute('stm-text', 't');
- var outL35 = el('span'); outL35.setAttribute('stm-text', 'l');
- body.appendChild(host35); body.appendChild(outT35); body.appendChild(outL35);
- await s._bridge.renderAll(); await spin();
- check(outT35.textContent === 'T1' && outL35.textContent === 'L1',
- 'attribute forms did not read initially: ' + outT35.textContent + '/' + outL35.textContent);
- host35.setAttribute('lang', 'L2'); await spin(10);
- check(outL35.textContent === 'L2', 'SECOND attribute observer on the same element stuck: ' + outL35.textContent);
- host35.setAttribute('title', 'T2'); await spin(10);
- check(outT35.textContent === 'T2', 'first attribute observer stuck: ' + outT35.textContent);
- } else {
- fail('unknown scenario: ' + scenario);
- }
- check(log.errors.length === 0, 'unexpected console.error: ' + JSON.stringify(log.errors));
- print('scenario ' + scenario + ': OK');
- }
- main().catch(function (e) {
- std.err.puts('FAIL [' + scenario + '] ' + (e && e.stack || e) + '\n');
- std.exit(1);
- });
|