Skip to content

Commit 6e77a15

Browse files
authored
Stabilize CLI contracts and cut 0.2.0 (#13)
* Restore post-rc3 CLI contracts for search and interaction flows This commit lands the contract fixes that were validated against the browser-backed harness and the manual 4310 scenario set. It wires `node search --limit` through the CLI/runtime boundary, restores structured truncation metadata, and makes `interact type` / `interact press` honor `--target-text` and `--role` instead of silently falling back or failing on selector-only assumptions. Constraint: Must preserve the published click/source contracts while extending type/press targeting Constraint: `.omx/` and PERF_VALIDATION.md are local-only and intentionally excluded Rejected: Patch only the runtime search response | CLI still ignored --limit and invalid limit 0 still succeeded Rejected: Add null-target guards only | would mask the missing targeting support instead of fixing it Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep the browser-backed harness aligned with the manual PR-check scenario set whenever interaction contracts change Tested: npm run build; npm test; npm run test:integration; manual 4310 session open/search/source/click/type/press validation Not-tested: Cross-platform manual interaction behavior outside the local Chromium-backed open-session path * Clarify untargeted press compatibility without widening the API surface The remaining risk after the contract-restoration pass was not a broken code path but an ambiguous one: untargeted `interact press` was still supported for compatibility, yet its response copy implied selector-based targeting and it was not locked by explicit harness coverage. This commit makes that compatibility behavior explicit in responses, help text, and docs while adding regression checks for page-level press and invalid strict/nth usage without a target. Constraint: Keep untargeted press backward compatible instead of making press target-only Constraint: Follow the existing public response shape rather than inventing new target sentinel values Rejected: Require a target for every press action | higher safety, but a breaking change to an existing compatibility path Rejected: Leave untargeted press undocumented | keeps the risk of future accidental regression and misleading output Confidence: high Scope-risk: narrow Reversibility: clean Directive: If untargeted press is ever removed, update help/docs/harness in the same change and treat it as a breaking CLI contract change Tested: npm run build; npm test; npm run test:integration Not-tested: Manual untargeted press behavior on non-Chromium browsers or non-open transports * Promote the CLI surface from prerelease to 0.2.0 The search and interaction contracts are now validated through unit, integration, docs-build, pack, and publish dry-run lanes, so this version bump marks the first stable cut after the rc series. The bug report template is updated alongside the package metadata so the reported version examples match the released channel. Constraint: Publish workflow requires the GitHub release tag to match package.json exactly Constraint: Stable versions publish to the npm latest tag instead of next Rejected: Keep shipping rc builds | the CLI contracts are now stable enough to make the next stable line the source of truth Rejected: Bump only package.json | would leave package-lock and issue template inconsistent with the release line Confidence: high Scope-risk: narrow Reversibility: clean Directive: Future stable releases should update package metadata, release tag, and user-facing version examples in the same change Tested: npm run build; npm test; npm run test:integration; npm run docs:build; npm pack --dry-run; npm publish --dry-run Not-tested: End-to-end npm publish from GitHub release until the release is actually created from main
1 parent f40dec6 commit 6e77a15

10 files changed

Lines changed: 460 additions & 67 deletions

File tree

.github/ISSUE_TEMPLATE/bug_report.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ body:
1313
attributes:
1414
label: react-devtool-cli version
1515
description: Output of `rdt --version`
16-
placeholder: 0.1.34
16+
placeholder: 0.2.0
1717
validations:
1818
required: true
1919
- type: textarea

README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,13 +159,16 @@ rdt profiler export --session demo --compress
159159
- Use built-in `interact` commands before reaching for external Playwright helper scripts.
160160
- Current supported actions:
161161
- `rdt interact click --session <name> (--selector <css> | --text <value> | --role <role>) [--nth <index>] [--strict] [--delivery auto|playwright|dom]`
162-
- `rdt interact type --session <name> --selector <css> --text <value>`
163-
- `rdt interact press --session <name> --key <name> [--selector <css>]`
162+
- `rdt interact type --session <name> (--selector <css> | --target-text <label> | --role <role>) [--nth <index>] [--strict] --text <value>`
163+
- `rdt interact press --session <name> --key <name> [--selector <css> | --target-text <label> | --role <role>] [--nth <index>] [--strict]`
164164
- `rdt interact wait --session <name> --ms <n>`
165165
- These commands execute through the same Playwright session that owns the current `rdt` browser page.
166166
- `interact click` can resolve targets by CSS selector, visible text, or ARIA role.
167+
- `interact type` and targeted `interact press` can resolve controls by CSS selector, label text, or ARIA role.
168+
- `interact press` without a target remains a page-level keyboard action for compatibility; use a target when focus ambiguity would make the result nondeterministic.
167169
- `--nth` selects one match from a broader result set, and `--strict` requires exactly one match.
168-
- Responses now include `targetingStrategy`, `matchCount`, and `resolvedNth` alongside the delivery metadata.
170+
- For `interact type` and targeted `interact press`, `--strict` and `--nth` only make sense when one of `--selector`, `--target-text`, or `--role` is present.
171+
- Responses now include `targetingStrategy`, `targetingResolution`, `matchCount`, and `resolvedNth` alongside the delivery metadata.
169172
- `interact click` defaults to `--delivery auto`.
170173
- In `auto`, profiler-active clicks fall back to DOM dispatch and report `requestedDelivery`, `effectiveDelivery`, `profilerActive`, and `fallbackApplied`.
171174
- Use `--delivery playwright` to force Playwright pointer input, or `--delivery dom` to force DOM dispatch.
@@ -281,7 +284,8 @@ Use `node pick` when the agent knows the visible element but not the component n
281284
- `source reveal --structured` returns `status`, `available`, `mode`, `reason`, and `source` so automation can distinguish unavailable source data from a successful source payload.
282285
- `source reveal` without `--structured` preserves the raw legacy behavior and may return literal `null`.
283286
- `dom` is the first host element summary used for CLI highlight and DOM-oriented inspection.
284-
- `node search --structured` wraps search results in `{ items, query, snapshotId, matchCount, runtimeWarnings }`.
287+
- `node search --structured` wraps search results in `{ items, query, snapshotId, matchCount, returnedCount, truncated, runtimeWarnings }`.
288+
- `node search --limit <n>` trims the returned items to the requested count while preserving the full `matchCount`.
285289
- When `node search --structured` returns `matchCount: 0`, `runtimeWarnings` explains that the component may be absent from the current snapshot rather than absent from the codebase.
286290
- Profiler summary fields are commit-oriented CLI metrics, not the full DevTools profiler session schema.
287291
- `profiler summary` and exported summaries explicitly report:

docs/workflows.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ rdt source reveal <nodeId> --session demo --snapshot <snapshotId> --structured
4848
```
4949

5050
- `tree stats` is the lightweight summary path when `tree get --format json` is too heavy.
51-
- `node search --structured` keeps the default array-returning behavior opt-in while adding `matchCount` and `runtimeWarnings`.
51+
- `node search --structured` keeps the default array-returning behavior opt-in while adding `matchCount`, `returnedCount`, `truncated`, and `runtimeWarnings`.
5252
- `source reveal --structured` returns availability metadata instead of only raw `null`.
5353

5454
Recovery flow:
@@ -79,22 +79,25 @@ Built-in interactions keep the investigation inside the same session instead of
7979

8080
```bash
8181
rdt interact click --session demo --role button --nth 0 --delivery auto
82-
rdt interact type --session demo --selector 'input[name="query"]' --text hello
82+
rdt interact type --session demo --target-text 'Filter inventory' --text hello
8383
rdt interact wait --session demo --ms 500
8484
```
8585

8686
- `interact click --delivery auto` uses Playwright pointer input by default.
8787
- When the profiler is active, `auto` may fall back to DOM dispatch and reports the applied delivery in the response payload.
8888
- Use one targeting mode per click: `--selector`, `--text`, or `--role`.
89+
- `interact type` and targeted `interact press` accept `--selector`, `--target-text`, or `--role`.
90+
- `interact press --key <name>` without a target remains a page-level keyboard action and depends on the browser's current focus state.
8991
- Add `--nth` to choose one match from a broader result set, or `--strict` to require exactly one match.
92+
- For `interact type` and targeted `interact press`, `--strict` and `--nth` require an explicit target.
9093

9194
After interaction, verify the app settled by collecting a fresh tree or reading profiler output instead of assuming the UI state changed correctly.
9295

9396
## Profile a real update
9497

9598
```bash
9699
rdt profiler start --session demo
97-
rdt interact type --session demo --selector 'input[name="query"]' --text hello
100+
rdt interact type --session demo --target-text 'Filter inventory' --text hello
98101
rdt profiler stop --session demo
99102
rdt profiler summary --session demo
100103
rdt profiler ranked <commitId> --session demo --limit 10

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-devtool-cli",
3-
"version": "0.1.35-rc.3",
3+
"version": "0.2.0",
44
"description": "Agent-first CLI for React component tree inspection, snapshot-aware node debugging, and profiler analysis through a Playwright-managed browser session.",
55
"license": "MIT",
66
"type": "module",

scripts/run-integration-harness.mjs

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,33 @@ async function main() {
345345
);
346346
ensure(Array.isArray(searchPayload.items) && searchPayload.matchCount >= 1, "node-search-structured did not return any App matches");
347347

348+
const limitedSearchPayload = parseJsonResult(
349+
"node-search-limit",
350+
await runRdt([
351+
"node",
352+
"search",
353+
"ResultRow",
354+
"--session",
355+
sessionName,
356+
"--snapshot",
357+
statsPayload.snapshotId,
358+
"--structured",
359+
"--limit",
360+
"5",
361+
"--format",
362+
"json",
363+
]),
364+
);
365+
ensure(Array.isArray(limitedSearchPayload.items) && limitedSearchPayload.items.length === 5, "node-search-limit did not truncate items to 5");
366+
ensure(limitedSearchPayload.returnedCount === 5, "node-search-limit did not report returnedCount 5");
367+
ensure(limitedSearchPayload.matchCount > limitedSearchPayload.returnedCount, "node-search-limit did not report a truncated result set");
368+
ensure(limitedSearchPayload.truncated === true, "node-search-limit did not report truncated=true");
369+
ensure(
370+
Array.isArray(limitedSearchPayload.runtimeWarnings)
371+
&& limitedSearchPayload.runtimeWarnings.some((warning) => warning.includes("--limit 5")),
372+
"node-search-limit did not report the limit warning",
373+
);
374+
348375
const zeroMatchPayload = parseJsonResult(
349376
"node-search-zero-match",
350377
await runRdt([
@@ -362,6 +389,21 @@ async function main() {
362389
);
363390
ensure(zeroMatchPayload.matchCount === 0, "node-search-zero-match did not return matchCount 0");
364391
ensure(Array.isArray(zeroMatchPayload.runtimeWarnings) && zeroMatchPayload.runtimeWarnings.length > 0, "node-search-zero-match did not return runtimeWarnings");
392+
393+
const invalidSearchLimitResult = await runRdt([
394+
"node",
395+
"search",
396+
"ResultRow",
397+
"--session",
398+
sessionName,
399+
"--structured",
400+
"--limit",
401+
"0",
402+
"--format",
403+
"json",
404+
]);
405+
ensure(invalidSearchLimitResult.code !== 0, "node-search-invalid-limit unexpectedly succeeded");
406+
ensure(invalidSearchLimitResult.stderr.includes("--limit"), "node-search-invalid-limit did not explain the limit failure");
365407
logScenarioOk("tree-stats-and-structured-search");
366408

367409
const appNodeId = searchPayload.items[0]?.id;
@@ -488,6 +530,157 @@ async function main() {
488530
"profiler-stop",
489531
await runRdt(["profiler", "stop", "--session", sessionName, "--format", "json"]),
490532
);
533+
534+
const targetTextType = parseJsonResult(
535+
"type-target-text",
536+
await runRdt([
537+
"interact",
538+
"type",
539+
"--session",
540+
sessionName,
541+
"--target-text",
542+
"Filter inventory",
543+
"--text",
544+
"billing",
545+
"--format",
546+
"json",
547+
]),
548+
);
549+
ensure(targetTextType.targetingStrategy === "target-text", "type-target-text did not use target-text targeting");
550+
ensure(targetTextType.targetingResolution === "label-control", "type-target-text did not resolve through label-control");
551+
ensure(targetTextType.target?.tagName === "input", "type-target-text did not resolve to an input");
552+
ensure(targetTextType.textLength === 7, "type-target-text did not report the expected text length");
553+
554+
const targetTextPress = parseJsonResult(
555+
"press-target-text",
556+
await runRdt([
557+
"interact",
558+
"press",
559+
"--session",
560+
sessionName,
561+
"--key",
562+
"Enter",
563+
"--target-text",
564+
"Filter inventory",
565+
"--format",
566+
"json",
567+
]),
568+
);
569+
ensure(targetTextPress.targetingStrategy === "target-text", "press-target-text did not use target-text targeting");
570+
ensure(targetTextPress.targetingResolution === "label-control", "press-target-text did not resolve through label-control");
571+
ensure(targetTextPress.target?.tagName === "input", "press-target-text did not resolve to an input");
572+
573+
const roleType = parseJsonResult(
574+
"type-role-targeting",
575+
await runRdt([
576+
"interact",
577+
"type",
578+
"--session",
579+
sessionName,
580+
"--role",
581+
"textbox",
582+
"--strict",
583+
"--text",
584+
"analytics",
585+
"--format",
586+
"json",
587+
]),
588+
);
589+
ensure(roleType.action === "type", "type-role-targeting did not report type action");
590+
ensure(roleType.targetingStrategy === "role", "type-role-targeting did not use role targeting");
591+
ensure(roleType.strict === true, "type-role-targeting did not report strict=true");
592+
593+
const rolePress = parseJsonResult(
594+
"press-role-targeting",
595+
await runRdt([
596+
"interact",
597+
"press",
598+
"--session",
599+
sessionName,
600+
"--key",
601+
"Enter",
602+
"--role",
603+
"textbox",
604+
"--strict",
605+
"--format",
606+
"json",
607+
]),
608+
);
609+
ensure(rolePress.action === "press", "press-role-targeting did not report press action");
610+
ensure(rolePress.targetingStrategy === "role", "press-role-targeting did not use role targeting");
611+
ensure(rolePress.strict === true, "press-role-targeting did not report strict=true");
612+
ensure(rolePress.effectiveDelivery === "keyboard", "press-role-targeting did not use keyboard delivery");
613+
614+
const untargetedPress = parseJsonResult(
615+
"press-page-keyboard",
616+
await runRdt([
617+
"interact",
618+
"press",
619+
"--session",
620+
sessionName,
621+
"--key",
622+
"Escape",
623+
"--format",
624+
"json",
625+
]),
626+
);
627+
ensure(untargetedPress.action === "press", "press-page-keyboard did not report press action");
628+
ensure(untargetedPress.targetingStrategy === null, "press-page-keyboard unexpectedly reported a targetingStrategy");
629+
ensure(untargetedPress.target === null, "press-page-keyboard unexpectedly resolved a target");
630+
ensure(
631+
Array.isArray(untargetedPress.runtimeWarnings)
632+
&& untargetedPress.runtimeWarnings.some((warning) => warning.includes("active page keyboard focus")),
633+
"press-page-keyboard did not warn about page-level keyboard focus",
634+
);
635+
636+
const invalidClickResult = await runRdt([
637+
"interact",
638+
"click",
639+
"--session",
640+
sessionName,
641+
"--selector",
642+
"button.counter",
643+
"--text",
644+
"Count is",
645+
]);
646+
ensure(invalidClickResult.code !== 0, "invalid-click-targeting unexpectedly succeeded");
647+
ensure(invalidClickResult.stderr.includes("Use exactly one"), "invalid-click-targeting did not explain the conflicting target failure");
648+
649+
const invalidTypeResult = await runRdt([
650+
"interact",
651+
"type",
652+
"--session",
653+
sessionName,
654+
"--text",
655+
"hello",
656+
]);
657+
ensure(invalidTypeResult.code !== 0, "invalid-type-targeting unexpectedly succeeded");
658+
ensure(invalidTypeResult.stderr.includes("Missing type target"), "invalid-type-targeting did not explain the missing target failure");
659+
660+
const invalidPressResult = await runRdt([
661+
"interact",
662+
"press",
663+
"--session",
664+
sessionName,
665+
"--key",
666+
"Enter",
667+
"--strict",
668+
]);
669+
ensure(invalidPressResult.code !== 0, "invalid-press-targeting unexpectedly succeeded");
670+
ensure(invalidPressResult.stderr.includes("Missing press target"), "invalid-press-targeting did not explain the missing target failure");
671+
672+
const invalidPressNthResult = await runRdt([
673+
"interact",
674+
"press",
675+
"--session",
676+
sessionName,
677+
"--key",
678+
"Enter",
679+
"--nth",
680+
"0",
681+
]);
682+
ensure(invalidPressNthResult.code !== 0, "invalid-press-nth-targeting unexpectedly succeeded");
683+
ensure(invalidPressNthResult.stderr.includes("Missing press target"), "invalid-press-nth-targeting did not explain the missing target failure");
491684
logScenarioOk("click-targeting-and-delivery");
492685
} finally {
493686
await closeSession(sessionName);

0 commit comments

Comments
 (0)