From 70a571be8657f50571e4915083411757ac4c54f7 Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Fri, 21 Aug 2026 08:19:09 -0400 Subject: [PATCH] fix(pbt): follow Bombadil 0.7's click schema The nightly property run has failed every night since 2026-08-14, when Dependabot took Bombadil from 0.6.1 to 0.7.0 (#1409). Two schema changes, one visible only after the first: `Click` now carries a `fingerprint` of the element rather than a `name`, so a replay can tell whether the thing it is about to click is still the thing the original run clicked. Built with the library's own `getFingerprint` rather than by hand, so it keeps matching whatever the schema asks for next. With that fixed the run got as far as its first scroll and then failed on `value must not be negative`: bounding rects are relative to the viewport, so anything scrolled past reports negative coordinates, which 0.7 rejects. Only what is on screen is offered now. Scrolling is its own action, and what it reveals becomes clickable in the next state. Verified locally: a 60-second exploration completes and reports no violations, having clicked buttons, inputs and spans throughout. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XDxiHpQJkRKWctS9BfQmbY Entire-Checkpoint: ecb4082e1016 --- ts/pbt/spec.ts | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/ts/pbt/spec.ts b/ts/pbt/spec.ts index 58af9e948..66a80f6f2 100644 --- a/ts/pbt/spec.ts +++ b/ts/pbt/spec.ts @@ -7,7 +7,7 @@ */ /* eslint-disable no-underscore-dangle -- window.__lcmHarness is the harness's fixed extractor surface */ import { always, eventually, now } from '@antithesishq/bombadil'; -import { actions, extract } from '@antithesishq/bombadil/browser'; +import { actions, extract, getFingerprint } from '@antithesishq/bombadil/browser'; export * from '@antithesishq/bombadil/browser/defaults'; @@ -117,24 +117,38 @@ export const suspendedStateEventuallyShowsBanner = always(() => ) ); -/** Click chaos-panel buttons and card-internal (shadow DOM) buttons. */ +/** + * Click chaos-panel buttons and card-internal (shadow DOM) buttons. + * + * A click carries a fingerprint of the element it came from, not just a + * point, so a replay can tell whether the thing it is about to click is + * still the thing the run originally clicked. Built with the library's own + * ``getFingerprint`` rather than by hand, so it keeps matching whatever the + * schema asks for next. + */ const clickablePoints = extract((state) => { - const points: { name: string; x: number; y: number }[] = []; + const points: { fingerprint: ReturnType; x: number; y: number }[] = []; + const view = state.document.defaultView; + const width = view?.innerWidth ?? 0; + const height = view?.innerHeight ?? 0; for (const el of deepQueryAll(state.document, 'button')) { const rect = el.getBoundingClientRect(); - if (rect.width > 0 && rect.height > 0) { - points.push({ - name: el.id || el.textContent?.trim().slice(0, 24) || 'button', - x: rect.left + rect.width / 2, - y: rect.top + rect.height / 2 - }); + const x = rect.left + rect.width / 2; + const y = rect.top + rect.height / 2; + // Rects are relative to the viewport, so anything scrolled past has + // negative coordinates -- which the action schema rejects outright. + // Offer only what is on screen right now; scrolling is its own + // action, and what it reveals becomes clickable on the next state. + const onScreen = x >= 0 && y >= 0 && x < width && y < height; + if (rect.width > 0 && rect.height > 0 && onScreen) { + points.push({ fingerprint: getFingerprint(el), x, y }); } } return points; }); export const clickButtons = actions(() => - clickablePoints.current.map(({ name, x, y }) => { - return { Click: { name, point: { x, y } } }; + clickablePoints.current.map(({ fingerprint, x, y }) => { + return { Click: { fingerprint, point: { x, y } } }; }) );