Improve PageAction selector robustness by prioritizing test-id attributes#280
Improve PageAction selector robustness by prioritizing test-id attributes#280
Conversation
Co-authored-by: ujiro99 <677231+ujiro99@users.noreply.github.com>
Co-authored-by: ujiro99 <677231+ujiro99@users.noreply.github.com>
Co-authored-by: ujiro99 <677231+ujiro99@users.noreply.github.com>
| temp = temp.concat(this.transfAddId(xPath, element)) | ||
| temp = temp.concat(this.transfAddText(xPath, element)) | ||
| temp = temp.concat(this.transfAddAttribute(xPath, element)) | ||
| temp = temp.concat(this.transfAddText(xPath, element)) |
| if (this.isAttributeValueUsable(ancestor.id)) { | ||
| const newXPath: XPath = new XPath(xPath.getValue()) | ||
| newXPath.addPredicateToHead(`[@id='${ancestor.id}']`) | ||
| newXPath.addPredicateToHead(`[@id='${this.escapeXPathValue(ancestor.id)}']`) |
There was a problem hiding this comment.
Pull request overview
This PR improves the robustness of generated RobulaPlus XPath selectors for PageAction recording by prioritizing language-agnostic data-test* attributes and excluding locale-dependent aria-label.
Changes:
- Added a
data-test*-first transformation (transfAddDataTestId) and reordered the transformation pipeline to prefer stable attributes early. - Blacklisted
aria-labelso it won’t be used in generated selectors. - Introduced XPath value escaping and added Vitest coverage for data-test* prioritization and aria-label exclusion.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| packages/extension/src/lib/robula-plus/index.ts | Adds data-test* prioritization + pipeline reorder, blacklists aria-label, and introduces XPath escaping helper. |
| packages/extension/src/lib/robula-plus/index.test.ts | Adds unit tests validating data-test* prioritization and aria-label exclusion behavior. |
Comments suppressed due to low confidence (1)
packages/extension/src/lib/robula-plus/index.ts:27
- Because
data-test*attributes are now handled explicitly intransfAddDataTestId, also adding them toattributePriorizationListmeanstransfAddAttributecan generate the same[@data-testid='...']predicate again (for values that passisAttributeUsable). Sincetemp = [...new Set(temp)]de-dupes by object identity (not XPath string), this can inflate the search space. Consider either (1) skippingDATA_TESTID_ATTRIBUTESinsidetransfAddAttribute, or (2) de-duping generated XPaths byx.getValue().
private attributePriorizationList: string[] = [
...RobulaPlus.DATA_TESTID_ATTRIBUTES,
"name",
"class",
"title",
"alt",
]
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| it("should prioritize data-testid over other attributes", () => { | ||
| container.innerHTML = ` | ||
| <div> | ||
| <button data-testid="submit-btn" class="btn-primary" name="submit">Submit</button> | ||
| <button data-testid="cancel-btn" class="btn-secondary" name="cancel">Cancel</button> | ||
| </div> | ||
| ` | ||
| const buttons = container.querySelectorAll("button") | ||
| const robula = new RobulaPlus() | ||
| const xpath = robula.getRobustXPath(buttons[0], document) | ||
|
|
||
| // Should use data-testid in the XPath because there are multiple buttons | ||
| expect(xpath).toContain("data-testid") | ||
| expect(xpath).toContain("submit-btn") | ||
| }) |
There was a problem hiding this comment.
The PR adds XPath escaping behavior, but there’s no test covering attribute/text values that contain single quotes (or both quote types), which is the main risk area for this change. Add a unit test that builds an element with e.g. data-testid="o'reilly" (and/or text with quotes) and asserts getRobustXPath both generates a valid XPath and uniquelyLocate returns true.
| // Data-testid type attributes in priority order | ||
| private static readonly DATA_TESTID_ATTRIBUTES = [ | ||
| "data-testid", | ||
| "data-test-id", | ||
| "data-test", | ||
| ] as const |
There was a problem hiding this comment.
DATA_TESTID_ATTRIBUTES is declared private static, but it’s referenced from RobulaPlusOptions (...RobulaPlus.DATA_TESTID_ATTRIBUTES). This will fail TypeScript compilation. Make it public static, or move the constant to a module-level exported const that both classes can consume.
| /** | ||
| * Escapes single quotes in attribute values for safe XPath generation | ||
| * @param value - The attribute value to escape | ||
| * @returns The escaped value safe for use in XPath predicates | ||
| */ | ||
| private escapeXPathValue(value: string | null | undefined): string { | ||
| if (value === null || value === undefined) { | ||
| return "" | ||
| } | ||
| return value.replace(/'/g, "'") | ||
| } |
There was a problem hiding this comment.
escapeXPathValue replaces ' with ', but XPath string literals do not interpret XML/HTML entities. This will generate selectors that don’t match when attribute/text contains a single quote (e.g. it will look for the literal ' in the DOM). Consider generating a proper XPath string literal (use single quotes when possible, double quotes when needed, and concat(...) when both quote types appear), and adjust call sites to avoid hard-coding surrounding quotes.
PageAction selectors using
aria-labelfail across locales, anddata-testidattributes weren't prioritized despite being language-agnostic and developer-specified.Changes
aria-label- locale-dependent values break selector stabilitytransfAddDataTestIdtransformation - prioritizesdata-testid,data-test-id,data-testbefore all other attributesbtn-1,btn-2are intentional identifiers, not randomImplementation
Selectors now prefer stable, developer-specified test attributes over locale-sensitive or positional selectors.
Warning
Firewall rules blocked me from connecting to one or more addresses (expand for details)
I tried to connect to the following addresses, but was blocked by firewall rules:
www.google-analytics.com/opt/hostedtoolcache/node/24.13.0/x64/bin/node /opt/hostedtoolcache/node/24.13.0/x64/bin/node --conditions node --conditions development /home/REDACTED/work/selection-command/selection-command/node_modules/tinypool/dist/entry/process.js(dns block)/opt/hostedtoolcache/node/24.13.0/x64/bin/node /opt/hostedtoolcache/node/24.13.0/x64/bin/node --conditions node --conditions development /home/REDACTED/work/selection-command/selection-command/node_modules/tinypool/dist/entry/process.js origin /usr/bin/sort /entry/process.js(dns block)/opt/hostedtoolcache/node/24.13.0/x64/bin/node /opt/hostedtoolcache/node/24.13.0/x64/bin/node --conditions node --conditions development /home/REDACTED/work/selection-command/selection-command/node_modules/tinypool/dist/entry/process.js 20235335 /bin/sh /entry/process.js(dns block)If you need me to access, download, or install something from one of these locations, you can either:
Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.