Skip to content

pyyush/useid

Repository files navigation

uSEID — Universal Semantic Element ID

npm version CI License Node >= 20

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/useid

Zero 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

About

uSEID — Universal Semantic Element ID for stable cross-run element identity

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors