Skip to content

feat(browser): let a project name its test-id attribute - #1395

Merged
divshekhar merged 7 commits into
reticlehq:mainfrom
Asymtode712:feat/project-test-attribute
Oct 5, 2026
Merged

divshekhar merged 7 commits into
reticlehq:mainfrom
Asymtode712:feat/project-test-attribute

Conversation

@Asymtode712

Copy link
Copy Markdown
Contributor

What & why

The testid locator only read data-testid, so apps that mark controls with data-test-subj (Elastic UI) or data-cy (Cypress) got "matched no element" on a button that was on screen.

  • New testIdAttribute option: .reticle.json -> read by @reticlehq/vite-plugin (or set on reticle() / connect() directly) -> setTestIdAttr in the SDK. An invalid attribute name falls back to data-testid.
  • The query, advertised testids, recorded anchors, field/region names and the presenter's flow filter all read the configured attribute through dom/testid-attr.ts, replacing the hard-coded data-testid copies.
  • A missed testid query now returns hint.testidFoundUnder naming the alternative attribute (data-test-subj, data-cy, data-test, data-qa, data-e2e) that holds the value. It never widens matching.
  • Default behaviour is unchanged.

Not addressed: the separate role: "button", name: "Next page" miss on an aria-label button; it needs a minimal EuiDataGrid repro.

Closes #1376

How it was verified

  • New query.testid-attribute.test.ts: configured attribute resolves, default unchanged, near-miss names the attribute, advertised testids follow the setting, invalid name ignored.
  • readConfiguredTestIdAttribute tests in vite-plugin.
  • Browser tests for the touched directories (1141) pass.

Gates run

  • pnpm lint && pnpm typecheck && pnpm test:unit (~2 min — always)
  • pnpm test:e2e (~8 min) — touched the tool surface, core, an observer, or telemetry
  • pnpm gate:install (~15 min) — touched reticle init, vite-plugin, next, or babel-plugin
  • pnpm test:e2e:desktop (~3 min) — touched adapters/realm/electron, adapters/realm/tauri, or desktop capture
  • None of the above tiers apply to this change

Checklist

  • Every commit is signed off (git commit -s) — CI's DCO check fails the PR without it. Already pushed? git rebase --signoff origin/main && git push --force-with-lease
  • Tests added/updated (RED → GREEN); the change is covered by a test that would fail without it
  • No any, no free strings (wire strings live in @reticlehq/core), no non-null !
  • No console.log or internal tracking codes left in the diff
  • Each changed file is under the 1000-line cap
  • Docs updated, and a user-facing change adds a new file under .changes/ (never edit CHANGELOG.md — that file is assembled at release time, and editing it is what makes PRs conflict; format in .changes/README.md)
  • Security-affecting? Auth/redaction/trust-boundary changes keep the localhost-only, no-app-data-leaves-the-machine, no-arbitrary-JS posture (usage telemetry stays anonymous + opt-out per docs/telemetry.md) and are covered by a test

@github-actions github-actions Bot added area/core The wire contract and schemas: core/ area/browser The browser SDK: adapters/realm/browser area/server The daemon, MCP server and CLI: server/ area/vite-plugin The Vite plugin: adapters/build/vite area/docs Documentation and docs site labels Oct 5, 2026
@Asymtode712 Asymtode712 added area/core The wire contract and schemas: core/ area/browser The browser SDK: adapters/realm/browser area/server The daemon, MCP server and CLI: server/ area/vite-plugin The Vite plugin: adapters/build/vite area/docs Documentation and docs site labels Oct 5, 2026
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

Thanks for your first pull request to Reticle! A maintainer aims to review within two days. Before then, pnpm verify locally catches most of what CI will, and CONTRIBUTING.md explains the rest. If CI does not start, a maintainer needs to approve it for first-time contributors; that is normal.

@greptile-apps

greptile-apps Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 1/5

[Medium risk] Adds configurable test-id attribute naming across the SDK.

The PR is not ready to merge because configuring a different attribute can expose sensitive field values, valid-looking settings can break queries, and the project setting does not reach supported hand-written connections.

Findings

  1. P1 Security Sensitive field value can escape ▶
  2. P1 Accepted names break selectors ▶
  3. P1 Manual connects miss project setting ▶
  4. P2 Default attribute missing from hint ▶
  5. P2 Hint misses shadow-root values ▶

Summary

The PR adds a configurable test-id attribute, carries it through generated Vite connections, and uses it for browser queries, recorded anchors, and UI diagnostics. It also adds a hint when a missed value appears under a known alternative attribute.

  • Redaction must continue to recognize sensitive identifiers on data-testid when another attribute is configured.
  • Attribute validation and connection propagation need attention before the configured behavior is reliable across supported paths.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  C[".reticle.json or plugin option"] --> V["Vite generated connect call"]
  V --> S["setTestIdAttr"]
  D["Direct connect option"] --> S
  S --> Q["Queries and hints"]
  S --> A["Anchors and advertised testids"]
  S --> R["Field and snapshot sensitivity checks"]
Loading

Reviews (1) · Last reviewed commit: "Merge branch 'main' into feat/project-te..."

Comment thread core/src/identity/testid-attribute.ts Resolved
Comment thread adapters/build/vite/src/index.ts Resolved
Comment thread adapters/realm/browser/src/dom/query.ts Resolved
Comment thread adapters/realm/browser/src/dom/query.ts Resolved

@divshekhar divshekhar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, the shape is right. One module owns the attribute, every site from #1376 goes through it, and with the default attribute nothing changes, so saved flows keep replaying. I also checked the wire side: testidFoundUnder is optional and the contract fingerprint doesn't cover hint fields, so older daemons and SDKs are fine. And init keeps the new .reticle.json key on a re-run.

There are two real bugs, two red checks, and a few smaller things.

Bugs

  1. Secret redaction gets narrower. With data-cy configured, <input data-testid="api-key"> is no longer treated as sensitive, so its raw value goes out. I reproduced it: isSensitiveField returns true on main and false on this branch. In dom/a11y.ts:361 and observers/field.ts:69, check the configured attribute and DEFAULT_TESTID_ATTR. Changing this setting should only ever widen redaction. Please add that exact case as a test.

  2. Some valid-looking names crash every query. TESTID_ATTR_PATTERN (core/src/identity/testid-attribute.ts:24) accepts . and :. With data.test or xlink:foo, runQuery throws "Invalid selector", and so does closest() in the DOM observer on every mutation. Tightening it to /^[a-zA-Z_][\w-]*$/ fixes it. Add a test for both names. Injection itself is already handled: x],[onclick falls back to the default.

Red checks

  1. verify: server/src/surface/tools/tools.ts goes from 997 to 1003 lines, over the 1000-line cap. Shorten the testidFoundUnder description, or move the hint schema out of that file.

  2. unit-tests: the new testid-attr.ts makes 13 loose files in adapters/realm/browser/src/dom, and server/src/guards/harness/flat-directories-are-recorded.test.ts:347 records 12. Either bump that count with a one-line reason next to it, or put the file in a subfolder.

Smaller

  1. Next.js and Remix ignore the setting. The .reticle.json setting only reaches the SDK through the Vite plugin's generated connect. The Next adapter (adapters/build/next/index.cjs) already reads .reticle.json, so passing testIdAttribute through there is a small change. If you'd rather not, say "Vite only" in the changes file, in connect-options.ts and in the hint text at tools.ts:511.

  2. The miss hint skips data-testid. In query.ts:714, when another attribute is configured, also check DEFAULT_TESTID_ATTR. Otherwise a project that switches attributes gets no hint when its old flows miss.

  3. The wiring has no tests. Deleting setTestIdAttr(options.testIdAttribute) (reticle.ts:428) or the forwarding in connect-args.ts:74 wouldn't fail anything. A connectArgs test, plus one where connect({ testIdAttribute }) makes a query resolve, would cover both.

The red install-gate is a cancelled duplicate run, not your change. #1385 and #1171 also touch query.ts and a11y.ts, so expect a rebase depending on which lands first.

@Asymtode712
Asymtode712 requested a review from divshekhar October 5, 2026 16:24

@divshekhar divshekhar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work. I went through all seven points, deleting each fix to check the tests go red. Six are done properly: the pattern, the line cap, the dom/addressing/ move, the hint fallback, both wiring tests, and the a11y.ts side of redaction. CI is green too.

Two small things left:

  1. field.ts checks both attributes now, but nothing tests it. I deleted the DEFAULT_TESTID_ATTR check at field.ts:72 and all 1816 browser tests still passed. This test in observers/field.test.ts, inside the redaction block, covers it. It passes on your branch and fails with that check removed:

    it('still redacts a data-testid secret after data-cy is configured', () => {
      setTestIdAttr('data-cy'); // reset with setTestIdAttr(undefined) in afterEach
      const { events } = page('<input data-testid="api-key" value="s3cret" />');
      const input = document.querySelector('input');
      if (null === input) throw new Error('no input');
      fire(input, 'change');
      expect(events[0]?.data['value']).toBeUndefined();
      expect(events[0]?.data['redacted']).toBe(true);
    });
  2. connect-options.ts:38 still says "Normally supplied by the build plugin". Make it say it comes from the Vite plugin only, and that Next.js, Remix and manual setups pass it here, to match the changes file.

Also, the comment at review/mark-anchor.ts:12 ("Attribute names — defined locally…") doesn't describe anything anymore. Please delete it.

After those, I'll merge it.

@Asymtode712
Asymtode712 requested a review from divshekhar October 5, 2026 17:36

@divshekhar divshekhar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I checked the new field test by deleting the DEFAULT_TESTID_ATTR check in field.ts, and it fails, so the redaction fix is covered now. The comment is fixed and the stale one is gone. Merging.

@divshekhar
divshekhar added this pull request to the merge queue Oct 5, 2026
Merged via the queue into reticlehq:main with commit 7d312ce Oct 5, 2026
32 checks passed
divshekhar added a commit that referenced this pull request Oct 6, 2026
Brings in the project-chosen test-id attribute (#1395) and init's
no-package.json refactor (#1396). Two budgets recorded with their
measurements: first load 248,736 B (ceiling 249,700) and the
all-tools output-schema surface 146,174 B (budget 146,400).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Signed-off-by: Divyanshu Shekhar <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/browser The browser SDK: adapters/realm/browser area/core The wire contract and schemas: core/ area/docs Documentation and docs site area/server The daemon, MCP server and CLI: server/ area/vite-plugin The Vite plugin: adapters/build/vite

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Let a project name its test-id attribute (data-test-subj, data-cy, data-test)

2 participants