aboutsummaryrefslogtreecommitdiff
path: root/tests/gallery-probes
diff options
context:
space:
mode:
Diffstat (limited to 'tests/gallery-probes')
-rw-r--r--tests/gallery-probes/probe.mjs48
-rw-r--r--tests/gallery-probes/test_dupre_merge.py53
2 files changed, 93 insertions, 8 deletions
diff --git a/tests/gallery-probes/probe.mjs b/tests/gallery-probes/probe.mjs
index 3db09a5..9d230c0 100644
--- a/tests/gallery-probes/probe.mjs
+++ b/tests/gallery-probes/probe.mjs
@@ -52,6 +52,36 @@ async function drag(x1, y1, x2, y2) {
await send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: x2, y: y2, button: 'left', clickCount: 1 });
}
+// The page sets `html{scroll-behavior:smooth}`, so a plain scrollIntoView
+// animates and a rect read mid-flight is stale by the time the drag dispatches.
+// The press then lands off the widget and the check reports a widget bug. Scroll
+// instantly instead: the probe has no reason to animate, and this removes the
+// race rather than waiting it out.
+const scrollTo = expr =>
+ evl(`(${expr}).scrollIntoView({block:'center', behavior:'instant'}); ''`);
+
+// Belt and braces after the instant scroll: a zoom or column relayout can still
+// move things for a frame or two, so read the geometry until it stops moving.
+// Takes any numeric tuple (a full rect, or a centre point) and requires a stable
+// value twice in a row AND a y inside the viewport, because two matching samples
+// taken before a relayout starts would otherwise look settled.
+async function settledGeom(expr, { tries = 40, gap = 25 } = {}) {
+ const height = await evl(`window.innerHeight`);
+ let prev = null;
+ for (let i = 0; i < tries; i++) {
+ const now = await evl(expr);
+ const onscreen = now[1] >= 0 && now[1] < height;
+ if (prev && onscreen && now.every((v, j) => Math.abs(v - prev[j]) < 0.5)) return now;
+ prev = now;
+ await sleep(gap);
+ }
+ return prev;
+}
+
+// A press that misses reports as a dead widget, so name the real failure.
+const hits = (x, y, sel) =>
+ evl(`!!document.elementFromPoint(${x}, ${y})?.closest('${sel}')`);
+
const fails = [];
const ok = (name, cond, detail = '') => { console.log(`${cond ? 'PASS' : 'FAIL'} ${name}${detail ? ' — ' + detail : ''}`); if (!cond) fails.push(name); };
@@ -67,7 +97,7 @@ try {
// 1. card count (the instruments/row default is checked in 1h)
const cards = await evl(`document.querySelectorAll('.card').length`);
- ok('111 cards', cards === 111, `got ${cards}`);
+ ok('112 cards', cards === 112, `got ${cards}`);
// 1b. Every card carries a spec sheet. R58 shipped without one and nothing
// noticed — the sheet is the card's actual specification, so a card without
@@ -195,19 +225,21 @@ try {
// not 1/row: at one-per-row the card is wide enough that its controls fall
// outside the 1600px probe window and the dispatched mouse events miss.
await evl(`document.querySelector('.szbar .dupre-key[data-cols="2"]').click()`);
- await evl(`document.querySelectorAll('.card')[2].scrollIntoView({block:'center'}); ''`);
- await sleep(200);
- const fr = await evl(`(()=>{const c=document.querySelectorAll('.card')[2];const f=c.querySelector('.dupre-fader');const r=f.getBoundingClientRect();return [r.left,r.top,r.width,r.height];})()`);
+ await scrollTo(`document.querySelectorAll('.card')[2]`);
+ const fr = await settledGeom(`(()=>{const c=document.querySelectorAll('.card')[2];const f=c.querySelector('.dupre-fader');const r=f.getBoundingClientRect();return [r.left,r.top,r.width,r.height];})()`);
+ const fx = fr[0] + fr[2] * 0.2, fy = fr[1] + fr[3] / 2;
+ ok('fader press lands on the fader', await hits(fx, fy, '.dupre-fader'), `at ${Math.round(fx)},${Math.round(fy)}`);
const before = await evl(`document.getElementById('rd-03').textContent`);
- await drag(fr[0] + fr[2] * 0.2, fr[1] + fr[3] / 2, fr[0] + fr[2] * 0.9, fr[1] + fr[3] / 2);
+ await drag(fx, fy, fr[0] + fr[2] * 0.9, fy);
await sleep(150);
const after = await evl(`document.getElementById('rd-03').textContent`);
ok('fader drag tracks at 2/row', before !== after && after !== '—', `'${before}' -> '${after}'`);
// 4. behavioral, zoomed: toggle click on card 01
- await evl(`document.querySelectorAll('.card')[0].scrollIntoView({block:'center'}); ''`);
- await sleep(200);
- const sw = await evl(`(()=>{const c=document.querySelectorAll('.card')[0];const s=c.querySelector('.switch')||c.querySelector('.stagew > *');const r=s.getBoundingClientRect();return [r.left+r.width/2,r.top+r.height/2];})()`);
+ await scrollTo(`document.querySelectorAll('.card')[0]`);
+ const sw = await settledGeom(`(()=>{const c=document.querySelectorAll('.card')[0];const s=c.querySelector('.switch')||c.querySelector('.stagew > *');const r=s.getBoundingClientRect();return [r.left+r.width/2,r.top+r.height/2];})()`);
+ ok('toggle press lands on the switch', await hits(sw[0], sw[1], '.card'),
+ `at ${Math.round(sw[0])},${Math.round(sw[1])}`);
const t0 = await evl(`document.getElementById('rd-01').textContent`);
await send('Input.dispatchMouseEvent', { type: 'mousePressed', x: sw[0], y: sw[1], button: 'left', clickCount: 1 });
await send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: sw[0], y: sw[1], button: 'left', clickCount: 1 });
diff --git a/tests/gallery-probes/test_dupre_merge.py b/tests/gallery-probes/test_dupre_merge.py
new file mode 100644
index 0000000..891b789
--- /dev/null
+++ b/tests/gallery-probes/test_dupre_merge.py
@@ -0,0 +1,53 @@
+"""The settings-casting widget candidates live in the canonical Dupre kit."""
+
+import os
+import unittest
+
+
+ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
+
+
+def source(name):
+ with open(os.path.join(ROOT, "docs", "prototypes", name)) as handle:
+ return handle.read()
+
+
+class DupreMergeTests(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls.widgets = source("widgets.js")
+ cls.additions = source("dupre-kit-additions.js")
+ cls.gallery = source("panel-widget-gallery.html")
+
+ def test_three_candidates_are_owned_only_by_the_canonical_kit(self):
+ for name in ("detentFader", "drumRoller", "guardedToggle"):
+ declaration = f"DUPRE.{name} = function"
+ self.assertEqual(self.widgets.count(declaration), 1, name)
+ self.assertNotIn(declaration, self.additions, name)
+
+ def test_detent_fader_brings_its_styles_and_policy(self):
+ self.assertIn(".dupre-dfader{", self.widgets)
+ self.assertIn("DUPRE.detentFader.POLICY", self.widgets)
+
+ def test_drum_supports_any_channel_count_and_configurable_range(self):
+ for fragment in (
+ "opts.min !== undefined",
+ "opts.max !== undefined",
+ "opts.height !== undefined",
+ "chans.forEach((c, i) => set(i, c.v))",
+ ):
+ self.assertIn(fragment, self.widgets)
+
+ def test_guarded_toggle_throws_through_the_viewer_plane(self):
+ self.assertIn("lever.style.transformBox = 'view-box'", self.widgets)
+ self.assertIn("'rotateX(180deg)'", self.widgets)
+
+ def test_gallery_specifies_detent_and_multichannel_drum(self):
+ self.assertIn("card(C,'A1','Detent fader'", self.gallery)
+ self.assertIn("channels:[{name:'HIGH'", self.gallery)
+ self.assertIn("{name:'MID'", self.gallery)
+ self.assertIn("{name:'LOW'", self.gallery)
+
+
+if __name__ == "__main__":
+ unittest.main()