@pyyush/useid
v0.1.0
Published
uSEID - Universal Semantic Element ID for stable cross-run element identity
Maintainers
Readme
uSEID
Your selectors break. uSEID doesn't.
Browser agents and E2E tests fail when the UI changes. A developer renames a CSS class, wraps a button in a new <div>, or ships an A/B variant — and suddenly your carefully crafted selectors target the wrong element. Or nothing at all.
uSEID solves this by identifying elements the way a human would: by what they are (a "Submit" button), where they sit (inside the checkout form), and where they appear (bottom-right of the main content). When the DOM reshuffles but the element is still there, uSEID finds it. When it's genuinely gone or ambiguous, uSEID tells you — it never silently acts on the wrong thing.
How It Works
Build Resolve
───── ───────
Snapshots ──→ Canonicalize ──→ Extract ──→ Signature Signature + New Snapshots
(DOM + A11y) (normalize) (features) (portable) ↓
Candidates → Score → Safety Gate
↓
Match (with confidence)
or Abstain (with reason)uSEID builds a portable signature from three signals:
| Signal | What it captures | Why it's stable | |--------|-----------------|-----------------| | Semantic | ARIA role + accessible name | Standardized by W3C, rarely changes | | Structural | Ancestor roles, sibling labels, form associations | Survives wrapper changes | | Spatial | Bounding box position | Catches layout-only changes |
Install
npm install @pyyush/useidZero config. One dependency (zod). Works with any Node.js 20+ project.
Quick Start
import { buildUSEID, resolveUSEID } from "@pyyush/useid";
// Capture snapshots from your browser automation tool
const domSnapshot = {
snapshot: await cdpSession.send("DOMSnapshot.captureSnapshot", {
computedStyles: ["display", "visibility", "opacity", "position"],
includeDOMRects: true,
}),
};
const a11ySnapshot = {
tree: await page.accessibility.snapshot({ interestingOnly: false }),
};
// Build a signature for the "Add to Cart" button
const signature = buildUSEID({
domSnapshot,
accessibilitySnapshot: a11ySnapshot,
elementIndex: 0, // Index in the extracted element list
pageUrl: "https://shop.example.com/product/42",
});
// Store the signature. Ship it. Come back next week.
// Resolve it against fresh snapshots — even after a redesign
const result = resolveUSEID({
signature,
domSnapshot: freshDomSnapshot,
accessibilitySnapshot: freshA11ySnapshot,
pageUrl: "https://shop.example.com/product/42",
});
if (result.resolved) {
console.log(result.selectorHint); // role=button[name="add to cart"]
console.log(result.confidence); // 0.94
} else {
console.log(result.abstentionReason); // "below_threshold"
console.log(result.explanation); // human-readable why
}Safety: Wrong Element Is Worse Than No Element
Most selector strategies fail silently — they click something, just not the right thing. uSEID's safety gate ensures that doesn't happen:
| When this happens... | uSEID does this | Why |
|---------------------|----------------|-----|
| Page URL doesn't match signature | Abstains (binding_mismatch) | Prevents cross-page false matches |
| No elements match the expected role | Abstains (no_candidates) | Element was removed |
| Best match scores below 0.85 | Abstains (below_threshold) | Not confident enough |
| Two candidates score too close | Abstains (ambiguous_match) | Can't tell which is right |
Every abstention comes with an explanation string and a ranked candidates list so you can debug or escalate to a human.
Configurable Scoring
The defaults work well for most cases. When they don't, everything is tunable:
resolveUSEID({
signature,
domSnapshot,
accessibilitySnapshot: a11ySnapshot,
pageUrl: "https://example.com/page",
config: {
threshold: 0.9, // Stricter (default: 0.85)
marginConstraint: 0.15, // Wider gap required (default: 0.1)
weights: {
semantic: 0.7, // Trust names more (default: 0.5)
structural: 0.2, // Trust DOM context less (default: 0.3)
spatial: 0.1, // Trust position less (default: 0.2)
},
},
});Privacy Built In
Element signatures can contain accessible names from form labels. For logging or storage:
import { redactUSEID } from "@pyyush/useid";
const safe = redactUSEID(signature);
// accessible names → hashed, sibling tokens → stripped, form labels → removed
// Safe to log. NOT resolvable after redaction (by design).Bring Your Own Automation
uSEID is framework-agnostic. It accepts two minimal interfaces:
interface DOMSnapshotResult {
snapshot: unknown; // CDP DOMSnapshot.captureSnapshot response
}
interface AccessibilitySnapshotResult {
tree: unknown; // Playwright, Puppeteer, or any a11y tree
}No Playwright dependency. No CDP dependency. If your tool can produce a DOM tree and an accessibility tree, uSEID works with it.
What Works Today (v0.1.0)
| | Supported | Behavior |
|-|-----------|----------|
| Chromium | Yes | Full CDP snapshot support |
| Main frame | Yes | Default |
| Same-origin iframes | Yes | Via framePath binding |
| Cross-origin iframes | No | Abstains with explanation |
| Open shadow DOM | Yes | Flattened by CDP DOMSnapshot |
| Closed shadow DOM | No | Abstains |
Full API
| Function | Purpose |
|----------|---------|
| buildUSEID(opts) | Build a portable signature from snapshots |
| resolveUSEID(opts) | Resolve a signature against current snapshots |
| compareUSEID(a, b) | Compare two signatures (0, 0.5, or 1) |
| explainResolution(result) | Human-readable explanation |
| redactUSEID(signature) | Strip PII for safe logging |
Lower-level functions are also exported for custom pipelines: extractElements, generateCandidates, scoreCandidates, applySafetyGate, checkBinding.
License
Apache-2.0
