Merge origin/master into llm-recording-and-analysis

Brings in #73, which landed web fact-parity work overlapping this branch:
selector-tagged ax handles, aria-disabled in `enabled`, shadow-DOM traversal
and a `scrollable`/`editable` dump, plus a third folio property and two new
testTags on HomeScreen.

Six files conflicted. The option-carrying ones took the union of both sides'
fields, so `--label-source` and `--exit-on-violation` both reach the pipeline.
In web-runtime.ts both sides changed how an element is described: master gave
`elementHandle` a selector to tag handles with, this branch gave it raw
attribute names and a field hint. Both survive, and `enabled` now answers
through master's isEnabled while `editable` stays.

Claude-Session: https://claude.ai/code/session_01A5KmftdEJ49A9z5mF5ESrX
This commit is contained in:
pj committed 2026-08-15 13:28:11 +05:30
commit d987526e47
73 files changed
+6639 -457

No files matched your search

+3
View File
@@ -65,6 +65,7 @@ export default function Tabs({ tabs, defaultTabId, ariaLabel }: TabsProps) {
<div className="tabs">
<div
className="tabs-header"
data-testid="tab-strip"
role="tablist"
aria-label={ariaLabel}
onKeyDown={handleKeyDown}
@@ -82,6 +83,8 @@ export default function Tabs({ tabs, defaultTabId, ariaLabel }: TabsProps) {
}
}}
type="button"
data-testid="tab"
data-tab-id={tab.id}
role="tab"
id={`${panelId}-tab-${tab.id}`}
className="tabs-tab"
+2
View File
@@ -170,6 +170,8 @@ export default function ActionList({
}
}}
className="action-list-item"
data-testid="step-row"
data-step={step.index}
role="option"
tabIndex={isActive ? 0 : -1}
aria-selected={isActive}
+8
View File
@@ -9,6 +9,12 @@ export interface ScreenshotProps {
deviceHeight?: number;
}
// stepOfSource reads the step out of the screenshot URL the panel is rendering,
// so a spec can compare what this panel shows against what the toolbar claims.
function stepOfSource(src: string): string | undefined {
return /step-0*(\d+)\.png/.exec(src)?.[1];
}
const DEFAULT_WIDTH = 1080;
const DEFAULT_HEIGHT = 1920;
@@ -58,6 +64,8 @@ export default function Screenshot({ src, action, deviceWidth, deviceHeight }: S
>
<img
className="screenshot-image"
data-testid="screenshot"
data-step={stepOfSource(src)}
src={src}
alt="device screenshot"
onLoad={(event) => {
+12 -2
View File
@@ -130,13 +130,18 @@ export default function ViolationsPanel({
}
return (
<section className="violations-panel">
<section
className="violations-panel"
data-testid="violations-panel"
data-violation-count={rows.filter((row) => row.status === "violated").length}
>
{violationsOnly ? null : (
<header className="violations-panel-header">
<h2 className="violations-panel-title">properties</h2>
<button
type="button"
className="violations-panel-jump"
data-testid="jump-to-first-violation"
onClick={onJumpToFirstViolation}
disabled={!hasFirstViolation}
>
@@ -149,7 +154,12 @@ export default function ViolationsPanel({
const residual = residuals?.[name];
const witness = status === "violated" ? witnesses?.[name] : undefined;
return (
<li key={name} className="violations-panel-row" data-status={status}>
<li
key={name}
className="violations-panel-row"
data-testid="property-row"
data-status={status}
>
<div className="violations-panel-row-head">
<span
className="violations-panel-badge"
+9 -5
View File
@@ -172,7 +172,7 @@ export default function RunDetail() {
label: "Violations",
badge:
stepViolations.length > 0 ? (
<span className="tabs-badge" data-kind="violation">
<span className="tabs-badge" data-testid="violations-badge" data-kind="violation">
{stepViolations.length}
</span>
) : undefined,
@@ -239,7 +239,7 @@ export default function RunDetail() {
label: "Violations",
badge:
stepViolations.length > 0 ? (
<span className="tabs-badge" data-kind="violation">
<span className="tabs-badge" data-testid="violations-badge" data-kind="violation">
{stepViolations.length}
</span>
) : undefined,
@@ -267,7 +267,11 @@ export default function RunDetail() {
<span>
<strong title={run.spec_path}>{basename(run.spec_path)}</strong> seed={run.seed}
</span>
<span>
<span
data-testid="step-indicator"
data-step={stepIndex}
data-step-count={stepCount ?? 0}
>
step {stepIndex} / {stepCount ?? 0}
</span>
</div>
@@ -319,14 +323,14 @@ export default function RunDetail() {
</div>
</aside>
<section className="detail-state-before detail-panel">
<section className="detail-state-before detail-panel" data-testid="state-before">
<h2>state before</h2>
<div className="detail-panel-body">
<Tabs tabs={beforeTabs} defaultTabId="screenshot" ariaLabel="state before" />
</div>
</section>
<section className="detail-state-after detail-panel">
<section className="detail-state-after detail-panel" data-testid="state-after">
<h2>state after</h2>
<div className="detail-panel-body">
<Tabs tabs={afterTabs} defaultTabId="screenshot" ariaLabel="state after" />
+1 -1
View File
@@ -8,7 +8,7 @@
--text-muted: #6b6b6b;
--accent-violation: #d32f2f;
--accent-positive: #2e7d32;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
:root[data-theme="dark"],
-24
View File
@@ -1,27 +1,3 @@
@font-face {
font-family: "JetBrains Mono";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("/fonts/JetBrainsMono-Regular.woff2") format("woff2");
}
@font-face {
font-family: "JetBrains Mono";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("/fonts/JetBrainsMono-Medium.woff2") format("woff2");
}
@font-face {
font-family: "JetBrains Mono";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("/fonts/JetBrainsMono-Bold.woff2") format("woff2");
}
body {
font-family: var(--font-mono);
font-size: 13px;