Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ body:
attributes:
label: react-devtool-cli version
description: Output of `rdt --version`
placeholder: 0.1.34
placeholder: 0.2.0
validations:
required: true
- type: textarea
Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,13 +159,16 @@ rdt profiler export --session demo --compress
- Use built-in `interact` commands before reaching for external Playwright helper scripts.
- Current supported actions:
- `rdt interact click --session <name> (--selector <css> | --text <value> | --role <role>) [--nth <index>] [--strict] [--delivery auto|playwright|dom]`
- `rdt interact type --session <name> --selector <css> --text <value>`
- `rdt interact press --session <name> --key <name> [--selector <css>]`
- `rdt interact type --session <name> (--selector <css> | --target-text <label> | --role <role>) [--nth <index>] [--strict] --text <value>`
- `rdt interact press --session <name> --key <name> [--selector <css> | --target-text <label> | --role <role>] [--nth <index>] [--strict]`
- `rdt interact wait --session <name> --ms <n>`
- These commands execute through the same Playwright session that owns the current `rdt` browser page.
- `interact click` can resolve targets by CSS selector, visible text, or ARIA role.
- `interact type` and targeted `interact press` can resolve controls by CSS selector, label text, or ARIA role.
- `interact press` without a target remains a page-level keyboard action for compatibility; use a target when focus ambiguity would make the result nondeterministic.
- `--nth` selects one match from a broader result set, and `--strict` requires exactly one match.
- Responses now include `targetingStrategy`, `matchCount`, and `resolvedNth` alongside the delivery metadata.
- For `interact type` and targeted `interact press`, `--strict` and `--nth` only make sense when one of `--selector`, `--target-text`, or `--role` is present.
- Responses now include `targetingStrategy`, `targetingResolution`, `matchCount`, and `resolvedNth` alongside the delivery metadata.
- `interact click` defaults to `--delivery auto`.
- In `auto`, profiler-active clicks fall back to DOM dispatch and report `requestedDelivery`, `effectiveDelivery`, `profilerActive`, and `fallbackApplied`.
- Use `--delivery playwright` to force Playwright pointer input, or `--delivery dom` to force DOM dispatch.
Expand Down Expand Up @@ -281,7 +284,8 @@ Use `node pick` when the agent knows the visible element but not the component n
- `source reveal --structured` returns `status`, `available`, `mode`, `reason`, and `source` so automation can distinguish unavailable source data from a successful source payload.
- `source reveal` without `--structured` preserves the raw legacy behavior and may return literal `null`.
- `dom` is the first host element summary used for CLI highlight and DOM-oriented inspection.
- `node search --structured` wraps search results in `{ items, query, snapshotId, matchCount, runtimeWarnings }`.
- `node search --structured` wraps search results in `{ items, query, snapshotId, matchCount, returnedCount, truncated, runtimeWarnings }`.
- `node search --limit <n>` trims the returned items to the requested count while preserving the full `matchCount`.
- 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.
- Profiler summary fields are commit-oriented CLI metrics, not the full DevTools profiler session schema.
- `profiler summary` and exported summaries explicitly report:
Expand Down
9 changes: 6 additions & 3 deletions docs/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ rdt source reveal <nodeId> --session demo --snapshot <snapshotId> --structured
```

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

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

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

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

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

## Profile a real update

```bash
rdt profiler start --session demo
rdt interact type --session demo --selector 'input[name="query"]' --text hello
rdt interact type --session demo --target-text 'Filter inventory' --text hello
rdt profiler stop --session demo
rdt profiler summary --session demo
rdt profiler ranked <commitId> --session demo --limit 10
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-devtool-cli",
"version": "0.1.35-rc.3",
"version": "0.2.0",
"description": "Agent-first CLI for React component tree inspection, snapshot-aware node debugging, and profiler analysis through a Playwright-managed browser session.",
"license": "MIT",
"type": "module",
Expand Down
193 changes: 193 additions & 0 deletions scripts/run-integration-harness.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,33 @@ async function main() {
);
ensure(Array.isArray(searchPayload.items) && searchPayload.matchCount >= 1, "node-search-structured did not return any App matches");

const limitedSearchPayload = parseJsonResult(
"node-search-limit",
await runRdt([
"node",
"search",
"ResultRow",
"--session",
sessionName,
"--snapshot",
statsPayload.snapshotId,
"--structured",
"--limit",
"5",
"--format",
"json",
]),
);
ensure(Array.isArray(limitedSearchPayload.items) && limitedSearchPayload.items.length === 5, "node-search-limit did not truncate items to 5");
ensure(limitedSearchPayload.returnedCount === 5, "node-search-limit did not report returnedCount 5");
ensure(limitedSearchPayload.matchCount > limitedSearchPayload.returnedCount, "node-search-limit did not report a truncated result set");
ensure(limitedSearchPayload.truncated === true, "node-search-limit did not report truncated=true");
ensure(
Array.isArray(limitedSearchPayload.runtimeWarnings)
&& limitedSearchPayload.runtimeWarnings.some((warning) => warning.includes("--limit 5")),
"node-search-limit did not report the limit warning",
);

const zeroMatchPayload = parseJsonResult(
"node-search-zero-match",
await runRdt([
Expand All @@ -362,6 +389,21 @@ async function main() {
);
ensure(zeroMatchPayload.matchCount === 0, "node-search-zero-match did not return matchCount 0");
ensure(Array.isArray(zeroMatchPayload.runtimeWarnings) && zeroMatchPayload.runtimeWarnings.length > 0, "node-search-zero-match did not return runtimeWarnings");

const invalidSearchLimitResult = await runRdt([
"node",
"search",
"ResultRow",
"--session",
sessionName,
"--structured",
"--limit",
"0",
"--format",
"json",
]);
ensure(invalidSearchLimitResult.code !== 0, "node-search-invalid-limit unexpectedly succeeded");
ensure(invalidSearchLimitResult.stderr.includes("--limit"), "node-search-invalid-limit did not explain the limit failure");
logScenarioOk("tree-stats-and-structured-search");

const appNodeId = searchPayload.items[0]?.id;
Expand Down Expand Up @@ -488,6 +530,157 @@ async function main() {
"profiler-stop",
await runRdt(["profiler", "stop", "--session", sessionName, "--format", "json"]),
);

const targetTextType = parseJsonResult(
"type-target-text",
await runRdt([
"interact",
"type",
"--session",
sessionName,
"--target-text",
"Filter inventory",
"--text",
"billing",
"--format",
"json",
]),
);
ensure(targetTextType.targetingStrategy === "target-text", "type-target-text did not use target-text targeting");
ensure(targetTextType.targetingResolution === "label-control", "type-target-text did not resolve through label-control");
ensure(targetTextType.target?.tagName === "input", "type-target-text did not resolve to an input");
ensure(targetTextType.textLength === 7, "type-target-text did not report the expected text length");

const targetTextPress = parseJsonResult(
"press-target-text",
await runRdt([
"interact",
"press",
"--session",
sessionName,
"--key",
"Enter",
"--target-text",
"Filter inventory",
"--format",
"json",
]),
);
ensure(targetTextPress.targetingStrategy === "target-text", "press-target-text did not use target-text targeting");
ensure(targetTextPress.targetingResolution === "label-control", "press-target-text did not resolve through label-control");
ensure(targetTextPress.target?.tagName === "input", "press-target-text did not resolve to an input");

const roleType = parseJsonResult(
"type-role-targeting",
await runRdt([
"interact",
"type",
"--session",
sessionName,
"--role",
"textbox",
"--strict",
"--text",
"analytics",
"--format",
"json",
]),
);
ensure(roleType.action === "type", "type-role-targeting did not report type action");
ensure(roleType.targetingStrategy === "role", "type-role-targeting did not use role targeting");
ensure(roleType.strict === true, "type-role-targeting did not report strict=true");

const rolePress = parseJsonResult(
"press-role-targeting",
await runRdt([
"interact",
"press",
"--session",
sessionName,
"--key",
"Enter",
"--role",
"textbox",
"--strict",
"--format",
"json",
]),
);
ensure(rolePress.action === "press", "press-role-targeting did not report press action");
ensure(rolePress.targetingStrategy === "role", "press-role-targeting did not use role targeting");
ensure(rolePress.strict === true, "press-role-targeting did not report strict=true");
ensure(rolePress.effectiveDelivery === "keyboard", "press-role-targeting did not use keyboard delivery");

const untargetedPress = parseJsonResult(
"press-page-keyboard",
await runRdt([
"interact",
"press",
"--session",
sessionName,
"--key",
"Escape",
"--format",
"json",
]),
);
ensure(untargetedPress.action === "press", "press-page-keyboard did not report press action");
ensure(untargetedPress.targetingStrategy === null, "press-page-keyboard unexpectedly reported a targetingStrategy");
ensure(untargetedPress.target === null, "press-page-keyboard unexpectedly resolved a target");
ensure(
Array.isArray(untargetedPress.runtimeWarnings)
&& untargetedPress.runtimeWarnings.some((warning) => warning.includes("active page keyboard focus")),
"press-page-keyboard did not warn about page-level keyboard focus",
);

const invalidClickResult = await runRdt([
"interact",
"click",
"--session",
sessionName,
"--selector",
"button.counter",
"--text",
"Count is",
]);
ensure(invalidClickResult.code !== 0, "invalid-click-targeting unexpectedly succeeded");
ensure(invalidClickResult.stderr.includes("Use exactly one"), "invalid-click-targeting did not explain the conflicting target failure");

const invalidTypeResult = await runRdt([
"interact",
"type",
"--session",
sessionName,
"--text",
"hello",
]);
ensure(invalidTypeResult.code !== 0, "invalid-type-targeting unexpectedly succeeded");
ensure(invalidTypeResult.stderr.includes("Missing type target"), "invalid-type-targeting did not explain the missing target failure");

const invalidPressResult = await runRdt([
"interact",
"press",
"--session",
sessionName,
"--key",
"Enter",
"--strict",
]);
ensure(invalidPressResult.code !== 0, "invalid-press-targeting unexpectedly succeeded");
ensure(invalidPressResult.stderr.includes("Missing press target"), "invalid-press-targeting did not explain the missing target failure");

const invalidPressNthResult = await runRdt([
"interact",
"press",
"--session",
sessionName,
"--key",
"Enter",
"--nth",
"0",
]);
ensure(invalidPressNthResult.code !== 0, "invalid-press-nth-targeting unexpectedly succeeded");
ensure(invalidPressNthResult.stderr.includes("Missing press target"), "invalid-press-nth-targeting did not explain the missing target failure");
logScenarioOk("click-targeting-and-delivery");
} finally {
await closeSession(sessionName);
Expand Down
Loading
Loading