aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/prototypes/widgets.js164
-rw-r--r--tests/gallery-probes/probe.mjs84
2 files changed, 213 insertions, 35 deletions
diff --git a/docs/prototypes/widgets.js b/docs/prototypes/widgets.js
index dc37bd4..bd5c79f 100644
--- a/docs/prototypes/widgets.js
+++ b/docs/prototypes/widgets.js
@@ -3746,7 +3746,17 @@ DUPRE.edgeMeter = function (host, opts = {}) {
};
/* R17 round CRT scope — pale phosphor face; the trace is the instrument's own
- animation, so it lives here with its reduced-motion gate */
+ animation, so it lives here with its reduced-motion gate.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: onChange(amp, 'Vpp N') fires on every animation tick, including
+ the initial paint — the demo waveform is instrument-owned.
+ handle: el, tick() — one animation step (the reduced-motion path paints
+ once and never ticks again on its own).
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block
+ of DUPRE_CSS; gradients register in the shared defs plate, except the
+ face gradient, which reads screen-family vars from this instrument's
+ subtree and so lives in a local defs. */
DUPRE.roundCrt = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const s = stageSvg(host, 'rsvg', 110, 104);
@@ -3797,7 +3807,16 @@ DUPRE.roundCrt = function (host, opts = {}) {
return { el: s, tick };
};
-/* R43 attitude indicator — sky/ground roll+shift behind a fixed miniature aircraft; 2D drag */
+/* R43 attitude indicator — sky/ground roll+shift behind a fixed miniature aircraft; 2D drag.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: bank (initial degrees, -60..60, default 0); pitch (initial
+ degrees, -20..20, default 0); onChange({bank, pitch}, 'BANK …
+ · PITCH …') fires on every set, including the initial paint.
+ handle: el, get() ({bank, pitch}), set(bank, pitch) — both clamp to
+ their ranges; the stage itself is a 2D drag surface.
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block
+ of DUPRE_CSS; gradients register in the shared defs plate. */
DUPRE.attitudeIndicator = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const s = stageSvg(host, 'rsvg', 130, 130);
@@ -3853,7 +3872,16 @@ DUPRE.attitudeIndicator = function (host, opts = {}) {
};
/* R44 heading bug + servo needle — drag parks the command; the needle chases
- with honest servo lag (instrument-owned animation, reduced-motion gated) */
+ with honest servo lag (instrument-owned animation, reduced-motion gated).
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: value (initial commanded heading in degrees, default 90 —
+ normalized to 0..360); onChange({cmd, act}, 'CMD … · ACT …')
+ fires on every set and on every servo step.
+ handle: el, get() ({cmd, act}), set(v) — v normalizes to 0..360;
+ dragging the stage moves the command bug, the needle chases.
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block
+ of DUPRE_CSS; gradients register in the shared defs plate. */
DUPRE.headingBug = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const s = stageSvg(host, 'rsvg', 130, 130);
@@ -3884,7 +3912,7 @@ DUPRE.headingBug = function (host, opts = {}) {
fill: '#bfc4d0', stroke: '#4a4e58', 'stroke-width': .6
});
svgEl(s, 'circle', { cx, cy, r: 4.5, fill: '#3a3631', stroke: '#060505', 'stroke-width': 1 });
- let cmd = opts.value ?? 90, act = cmd;
+ let cmd = ((opts.value ?? 90) % 360 + 360) % 360, act = cmd;
const draw = () => {
bug.setAttribute('transform', `rotate(${cmd.toFixed(1)},${cx},${cy})`);
needle.setAttribute('transform', `rotate(${act.toFixed(1)},${cx},${cy})`);
@@ -3904,7 +3932,15 @@ DUPRE.headingBug = function (host, opts = {}) {
};
/* R53 circular chart recorder — a day per revolution; the pen draws the cycle
- (instrument-owned clock, reduced-motion paints the full day once); click for fresh paper */
+ (instrument-owned clock, reduced-motion paints the full day once); click for fresh paper.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: onChange(hour, 'HH:MM · N%') fires on every pen step — the demo
+ day-cycle signal is instrument-owned.
+ handle: el, get() (current hour, 0..24), reset() — fresh paper, pen to
+ 00:00; clicking the stage does the same.
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block
+ of DUPRE_CSS; gradients register in the shared defs plate. */
DUPRE.chartRecorder = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const s = stageSvg(host, 'rsvg press', 130, 130);
@@ -3954,7 +3990,16 @@ DUPRE.chartRecorder = function (host, opts = {}) {
return { el: s, get: () => hour, reset };
};
-/* R54 vertical tape instrument — the scale scrolls behind a fixed index; drag to drive */
+/* R54 vertical tape instrument — the scale scrolls behind a fixed index; drag to drive.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: value (initial units, 5..35 on the printed RPM x100 scale,
+ default 24); onChange(value, 'RPM N') fires on every set,
+ including the initial paint.
+ handle: el, get() (units), set(v) — v clamps 5..35; the stage drags
+ vertically.
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block
+ of DUPRE_CSS; gradients register in the shared defs plate. */
DUPRE.verticalTape = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const s = stageSvg(host, 'rsvg', 70, 130);
@@ -3996,7 +4041,16 @@ DUPRE.verticalTape = function (host, opts = {}) {
return { el: s, get: () => v, set };
};
-/* R55 twin-needle gauge — mirrored half-scales, one hub, two independent needles */
+/* R55 twin-needle gauge — mirrored half-scales, one hub, two independent needles.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: fuel (initial, 0..4, default 2.4); oil (initial, 0..4, default
+ 3.1); onChange([fuel, oil], 'FUEL n.n · OIL n.n') fires on every
+ set, including the initial paint.
+ handle: el, get() ([fuel, oil]), set(fuel, oil) — both clamp 0..4;
+ each half of the stage drags its own needle vertically.
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block
+ of DUPRE_CSS; gradients register in the shared defs plate. */
DUPRE.twinNeedle = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const s = stageSvg(host, 'rsvg', 120, 110);
@@ -4045,7 +4099,7 @@ DUPRE.twinNeedle = function (host, opts = {}) {
};
const nF = needle('#e0523a'), nO = needle('#bfc4d0');
svgEl(s, 'circle', { cx, cy, r: 5, fill: '#3a3631', stroke: '#060505', 'stroke-width': 1 });
- let vF = opts.fuel ?? 2.4, vO = opts.oil ?? 3.1;
+ let vF, vO;
const draw = () => {
nF.setAttribute('transform', `rotate(${angL(vF).toFixed(1)},${cx},${cy})`);
nO.setAttribute('transform', `rotate(${angR(vO).toFixed(1)},${cx},${cy})`);
@@ -4060,11 +4114,21 @@ DUPRE.twinNeedle = function (host, opts = {}) {
};
half(0, () => vF, v => vF = v);
half(60, () => vO, v => vO = v);
- draw();
+ set(opts.fuel ?? 2.4, opts.oil ?? 3.1);
return { el: s, get: () => [vF, vO], set };
};
-/* R56 comfort-zone crossed needles — temp and humidity cross over printed verdicts */
+/* R56 comfort-zone crossed needles — temp and humidity cross over printed verdicts.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: temp (initial °F, 40..100, default 72); humidity (initial %,
+ 0..100, default 45); onChange([temp, humidity], 'NF · N% RH ·
+ VERDICT') fires on every set, including the initial paint.
+ handle: el, get() ([temp, humidity]), set(temp, humidity) — both clamp
+ to their scales; the left half of the stage drags temp, the
+ right half humidity.
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block
+ of DUPRE_CSS; gradients register in the shared defs plate. */
DUPRE.comfortMeter = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const s = stageSvg(host, 'rsvg', 130, 122);
@@ -4137,7 +4201,7 @@ DUPRE.comfortMeter = function (host, opts = {}) {
const nT = needle(), nH = needle();
nT.setAttribute('transform', `translate(${PT[0]},${PT[1]})`);
nH.setAttribute('transform', `translate(${PH[0]},${PH[1]})`);
- let vT = opts.temp ?? 72, vH = opts.humidity ?? 45;
+ let vT, vH;
const verdict = () => vT > 78 ? 'warm' : vT < 62 ? 'cold' : vH > 60 ? 'humid' : vH < 30 ? 'dry' : 'right';
const draw = () => {
nT.setAttribute('transform', `translate(${PT[0]},${PT[1]}) rotate(${angT(vT).toFixed(1)})`);
@@ -4154,7 +4218,7 @@ DUPRE.comfortMeter = function (host, opts = {}) {
};
half(0, () => vT, v => vT = v, 40, 100);
half(65, () => vH, v => vH = v, 0, 100);
- draw();
+ set(opts.temp ?? 72, opts.humidity ?? 45);
return { el: s, get: () => [vT, vH], set };
};
@@ -4192,15 +4256,25 @@ DUPRE.statusLamps = function (host, opts = {}) {
return { el: wrap, get: () => states.slice(), set };
};
-/* 19 badges — labelled flags; click a badge to cycle its variant */
+/* 19 badges — labelled flags; click a badge to cycle its variant.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: items (array of [label, variant] pairs, variant 0 gold · 1 red ·
+ 2 ghost, default a three-badge demo set); onChange(variants array
+ copy, label) fires per change and once at build with a count
+ summary.
+ handle: el, get() (variants copy), set(i, v) — v wraps modulo 3.
+ CSS lives in the .dupre-badge block of DUPRE_CSS; no other styles
+ involved. */
DUPRE.badges = function (host, opts = {}) {
const onChange = opts.onChange || noop;
- const CLS = ['badge', 'badge red', 'badge ghost'];
+ const CLS = ['dupre-badge', 'dupre-badge dupre-red', 'dupre-badge dupre-ghost'];
+ const norm = v => (((v | 0) % CLS.length) + CLS.length) % CLS.length;
const items = opts.items || [['TUNNEL', 0], ['LOW BATT', 1], ['2.4G', 2]];
- const vars = items.map(it => it[1]);
+ const vars = items.map(it => norm(it[1]));
const wrap = document.createElement('span');
- const els = items.map(([txt, v], i) => {
- const b = document.createElement('span'); b.className = CLS[v]; b.textContent = txt; b.style.cursor = 'pointer';
+ const els = items.map(([txt], i) => {
+ const b = document.createElement('span'); b.className = CLS[vars[i]]; b.textContent = txt; b.style.cursor = 'pointer';
b.addEventListener('click', () => set(i, vars[i] + 1));
wrap.appendChild(b);
if (i < items.length - 1) wrap.appendChild(document.createTextNode(' '));
@@ -4208,7 +4282,7 @@ DUPRE.badges = function (host, opts = {}) {
});
host.appendChild(wrap);
function set(i, v) {
- vars[i] = ((v % CLS.length) + CLS.length) % CLS.length;
+ vars[i] = norm(v);
els[i].className = CLS[vars[i]];
onChange(vars.slice(), items[i][0]);
}
@@ -4217,13 +4291,24 @@ DUPRE.badges = function (host, opts = {}) {
};
/* 20 tabular readout — mm:ss countdown; builder owns the state, the page
- drives tick() on its own clock (tick contract); click pauses/resumes */
+ drives tick() on its own clock (tick contract); click pauses/resumes.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: secs (initial seconds, wraps modulo 3600, default 24:10); run
+ (initial running state, default true); unit (caption under the
+ digits, default 'timer'); onChange(secs, 'running'/'paused')
+ fires on every set, including the initial paint.
+ handle: el, get() (secs), set(v) — v wraps modulo 3600; tick() — one
+ second down when running (the page's clock drives it); clicking
+ the digits pauses/resumes.
+ CSS lives in the .dupre-readout block of DUPRE_CSS; no other styles
+ involved. */
DUPRE.tabularReadout = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const wrap = document.createElement('div'); wrap.style.textAlign = 'center';
- wrap.innerHTML = '<div class="readout"></div><span class="u"></span>';
- const out = wrap.querySelector('.readout');
- wrap.querySelector('.u').textContent = opts.unit || 'timer';
+ wrap.innerHTML = '<div class="dupre-readout"></div><span class="dupre-unit"></span>';
+ const out = wrap.querySelector('.dupre-readout');
+ wrap.querySelector('.dupre-unit').textContent = opts.unit || 'timer';
host.appendChild(wrap);
let secs, run = opts.run !== undefined ? !!opts.run : true;
const draw = () => {
@@ -4236,12 +4321,21 @@ DUPRE.tabularReadout = function (host, opts = {}) {
return { el: wrap, get: () => secs, set, tick: () => { if (run) set(secs - 1); } };
};
-/* 21 engraved label — hairline-flanked caps label with a count; click bumps 1-9 */
+/* 21 engraved label — hairline-flanked caps label with a count; click bumps 1-9.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: label (caption text, default 'outputs'); count (initial count,
+ default 3); onChange(count, 'count N') fires on every set,
+ including the initial paint.
+ handle: el, get() (count), set(v); clicking the label cycles the count
+ 1..9.
+ CSS lives in the .dupre-engrave block of DUPRE_CSS; no other styles
+ involved. */
DUPRE.engravedLabel = function (host, opts = {}) {
const onChange = opts.onChange || noop;
- const e = document.createElement('span'); e.className = 'engrave';
+ const e = document.createElement('span'); e.className = 'dupre-engrave';
e.append(opts.label || 'outputs');
- const c = document.createElement('span'); c.className = 'cnt'; e.appendChild(c);
+ const c = document.createElement('span'); c.className = 'dupre-cnt'; e.appendChild(c);
host.appendChild(e);
let n;
const set = v => { n = v; c.textContent = '· ' + n; onChange(n, 'count ' + n); };
@@ -5420,9 +5514,9 @@ const DUPRE_CSS = `
.dupre-chip{color:var(--dim);cursor:pointer;border-bottom:1px dotted var(--wash);font-size:12px}
.dupre-chip.dupre-on{color:var(--chip-on,var(--gold));border-color:var(--chip-on,var(--gold))}
-.badge{font-size:.62rem;letter-spacing:.18em;color:var(--panel);background:var(--gold);border-radius:4px;padding:1px 6px}
-.badge.red{background:var(--fail);color:var(--cream)}
-.badge.ghost{background:transparent;border:1px solid var(--slate);color:var(--silver)}
+.dupre-badge{font-size:.62rem;letter-spacing:.18em;color:var(--panel);background:var(--gold);border-radius:4px;padding:1px 6px}
+.dupre-badge.dupre-red{background:var(--fail);color:var(--cream)}
+.dupre-badge.dupre-ghost{background:transparent;border:1px solid var(--slate);color:var(--silver)}
/* fader */
.dupre-fader{width:150px;height:16px;position:relative;cursor:pointer;touch-action:none}
@@ -5489,9 +5583,9 @@ const DUPRE_CSS = `
.dupre-ring b{position:relative;color:var(--cream);font-size:12px;font-weight:700;font-variant-numeric:tabular-nums}
/* tabular readout */
-.readout{color:var(--cream);font-size:24px;font-weight:700;font-variant-numeric:tabular-nums;letter-spacing:.04em;cursor:pointer}
-.readout small{color:var(--dim);font-size:12px;font-weight:400}
-.readout .u{color:var(--steel);font-size:.6rem;letter-spacing:.2em;display:block;text-align:center;margin-top:2px}
+.dupre-readout{color:var(--cream);font-size:24px;font-weight:700;font-variant-numeric:tabular-nums;letter-spacing:.04em;cursor:pointer}
+.dupre-readout small{color:var(--dim);font-size:12px;font-weight:400}
+.dupre-unit{color:var(--steel);font-size:.6rem;letter-spacing:.2em;display:block;text-align:center;margin-top:2px}
/* sparkline */
.dupre-spark{width:170px;height:44px}
@@ -5515,11 +5609,11 @@ const DUPRE_CSS = `
.dupre-seg button.dupre-on{background:var(--seg-on-bg,linear-gradient(180deg,var(--amber-grad-top),var(--gold)));color:var(--seg-on-ink,var(--panel));font-weight:700}
/* engraved section label */
-.engrave{width:180px;color:var(--steel);font-size:.62rem;letter-spacing:.3em;text-transform:uppercase;cursor:pointer;
+.dupre-engrave{width:180px;color:var(--steel);font-size:.62rem;letter-spacing:.3em;text-transform:uppercase;cursor:pointer;
display:flex;align-items:center;gap:9px}
-.engrave::before,.engrave::after{content:"";height:1px;background:var(--wash);flex:1}
-.engrave::before{max-width:10px}
-.engrave .cnt{color:var(--dim);letter-spacing:.1em;text-transform:none}
+.dupre-engrave::before,.dupre-engrave::after{content:"";height:1px;background:var(--wash);flex:1}
+.dupre-engrave::before{max-width:10px}
+.dupre-engrave .dupre-cnt{color:var(--dim);letter-spacing:.1em;text-transform:none}
/* waveform strip */
.dupre-wave{width:170px;height:38px}
diff --git a/tests/gallery-probes/probe.mjs b/tests/gallery-probes/probe.mjs
index 0539da0..81ca016 100644
--- a/tests/gallery-probes/probe.mjs
+++ b/tests/gallery-probes/probe.mjs
@@ -1402,6 +1402,90 @@ try {
fwdT: 'rotate(-42deg)', rflT: 'rotate(42deg)', thH: '75%', bdT: 'rotate(0deg)',
cmT: 'rotate(0deg)', penT: '3px' }), b8dom);
+ // batch-9 domain gates on detached hosts: the flight instruments clamp (or
+ // normalize) both set() and their build opts, badges coerces junk variants,
+ // the readout wraps its clock.
+ const clamps9 = await evl(`(() => {
+ const ai = DUPRE.attitudeIndicator(document.createElement('div'), { bank: -999, pitch: 5 });
+ const aiB = ai.get().bank;
+ ai.set(999, 999); const aiHi = ai.get();
+ const hb = DUPRE.headingBug(document.createElement('div'), { value: 450 });
+ const hbB = hb.get().cmd;
+ const hbNdl = hb.el.querySelector('g polygon').parentNode.getAttribute('transform');
+ hb.set(-30); const hbSet = hb.get().cmd;
+ const vt = DUPRE.verticalTape(document.createElement('div'), { value: 99 });
+ const vtB = vt.get(); vt.set(0); const vtLo = vt.get();
+ const vtT = vt.el.querySelector('g g').getAttribute('transform');
+ const tn = DUPRE.twinNeedle(document.createElement('div'), { fuel: 99, oil: -5 });
+ const tnB = tn.get();
+ let cmTxt; const cm = DUPRE.comfortMeter(document.createElement('div'),
+ { temp: 999, humidity: -40, onChange: (v, t) => cmTxt = t });
+ const cmB = cm.get();
+ const bg = DUPRE.badges(document.createElement('div'), { items: [['X', 7], ['Y', 'junk']] });
+ const bgVars = bg.get();
+ const tr = DUPRE.tabularReadout(document.createElement('div'), { secs: -10 });
+ const trB = tr.get();
+ return JSON.stringify({ aiB, aiHi, hbB, hbNdl, hbSet, vtB, vtLo, vtT, tnB, cmB, cmTxt, bgVars, trB });
+ })()`);
+ ok('batch-9 domain gates: flight-instrument clamps at build and set, badges variant coercion, readout wrap',
+ clamps9 === JSON.stringify({ aiB: -60, aiHi: { bank: 60, pitch: 20 }, hbB: 90,
+ hbNdl: 'rotate(90.0,65,65)', hbSet: 330, vtB: 35, vtLo: 5, vtT: 'translate(0,92.5)',
+ tnB: [4, 0], cmB: [100, 0], cmTxt: '100F · 0% RH · TOO WARM', bgVars: [1, 0], trB: 3590 }), clamps9);
+
+ // batch-9 state through the renamed DOM (catches rename stragglers): badges
+ // swap variant classes, the readout counts down through dupre-readout and
+ // carries a styled dupre-unit (the old .readout .u rule was dead — .u is a
+ // sibling, so the caption rendered unstyled), the engraved label counts
+ // through dupre-cnt, and the SVG flight instruments track set().
+ const b9dom = await evl(`(() => {
+ const bg = DUPRE.badges(document.createElement('div'));
+ const nBadge = bg.el.querySelectorAll('.dupre-badge').length;
+ const red0 = bg.el.querySelectorAll('.dupre-badge.dupre-red').length;
+ const ghost0 = bg.el.querySelectorAll('.dupre-badge.dupre-ghost').length;
+ bg.set(0, 1);
+ const red1 = bg.el.querySelectorAll('.dupre-badge.dupre-red').length;
+ const trHost = document.createElement('div'); document.body.appendChild(trHost);
+ const tr = DUPRE.tabularReadout(trHost);
+ const trTxt = tr.el.querySelector('.dupre-readout').textContent;
+ const unit = tr.el.querySelector('.dupre-unit');
+ const unitTxt = unit.textContent;
+ const unitDisp = getComputedStyle(unit).display;
+ tr.tick(); const trTick = tr.el.querySelector('.dupre-readout').textContent;
+ tr.el.querySelector('.dupre-readout').click(); tr.tick();
+ const trPaused = tr.el.querySelector('.dupre-readout').textContent;
+ trHost.remove();
+ const el = DUPRE.engravedLabel(document.createElement('div'));
+ const cnt0 = el.el.querySelector('.dupre-cnt').textContent;
+ el.set(7); const cnt7 = el.el.querySelector('.dupre-cnt').textContent;
+ el.el.click(); const cnt8 = el.el.querySelector('.dupre-cnt').textContent;
+ const tn = DUPRE.twinNeedle(document.createElement('div'));
+ tn.set(0, 4);
+ const tnF = tn.el.querySelector('polygon[fill="#e0523a"]').parentNode.getAttribute('transform');
+ const tnO = tn.el.querySelector('polygon[fill="#bfc4d0"]').parentNode.getAttribute('transform');
+ const cm = DUPRE.comfortMeter(document.createElement('div'));
+ cm.set(70, 80);
+ const humid = [...cm.el.querySelectorAll('text')].find(t => t.textContent === 'TOO HUMID').getAttribute('fill');
+ const ai = DUPRE.attitudeIndicator(document.createElement('div'));
+ ai.set(30, -10);
+ const aiT = ai.el.querySelector('g g').getAttribute('transform');
+ const rc = DUPRE.roundCrt(document.createElement('div'));
+ const p1 = rc.el.querySelector('polyline[stroke="#f4fcf4"]').getAttribute('points');
+ rc.tick();
+ const p2 = rc.el.querySelector('polyline[stroke="#f4fcf4"]').getAttribute('points');
+ const cr = DUPRE.chartRecorder(document.createElement('div'));
+ cr.reset();
+ const crH = cr.get();
+ const crD = cr.el.querySelector('path[stroke="#8f2416"]').getAttribute('d');
+ return JSON.stringify({ nBadge, red0, ghost0, red1, trTxt, unitTxt, unitDisp, trTick, trPaused,
+ cnt0, cnt7, cnt8, tnF, tnO, humid, aiT, crtMoves: p1 !== p2 && !p2.includes('NaN'), crH, crD });
+ })()`);
+ ok('batch-9 renamed DOM tracks state: badge variants, readout clock + styled unit, engraved count, instruments follow set()',
+ b9dom === JSON.stringify({ nBadge: 3, red0: 1, ghost0: 1, red1: 2, trTxt: '24:10',
+ unitTxt: 'timer', unitDisp: 'block', trTick: '24:09', trPaused: '24:09',
+ cnt0: '· 3', cnt7: '· 7', cnt8: '· 8', tnF: 'rotate(-170.0,60,62)', tnO: 'rotate(10.0,60,62)',
+ humid: '#c23a28', aiT: 'rotate(-30.0,65,65) translate(0,-16.0)',
+ crtMoves: true, crH: 0, crD: '' }), b9dom);
+
// late exceptions from interactions
const errs2 = events.filter(e => e.method === 'Runtime.exceptionThrown');
ok('no exceptions after interaction', errs2.length === 0);