fix: resolve unnamed element refs matching multiple elements (#573)
* fix: resolve unnamed element refs matching multiple elements (#500) When a page has one unnamed button among several named buttons, clicking its ref fails with "matched N elements" because the locator `getByRole('button')` matches all buttons on the page. Normalize unnamed interactive elements to `name: ""` so the selector becomes `getByRole('button', { name: "", exact: true })` which matches only buttons with empty accessible names. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: remove dead code branch in buildSelector Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: make RefMap.name required string, remove dead code branches Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: hyunjinee <leehj0110@kakao.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
hyunjinee
parent
79d8dfe34c
commit
7238b7da4c
+16
-16
@@ -23,7 +23,7 @@ export interface RefMap {
|
||||
[ref: string]: {
|
||||
selector: string;
|
||||
role: string;
|
||||
name?: string;
|
||||
name: string;
|
||||
/** Index for disambiguation when multiple elements have same role+name */
|
||||
nth?: number;
|
||||
};
|
||||
@@ -130,12 +130,9 @@ const STRUCTURAL_ROLES = new Set([
|
||||
/**
|
||||
* Build a selector string for storing in ref map
|
||||
*/
|
||||
function buildSelector(role: string, name?: string): string {
|
||||
if (name) {
|
||||
const escapedName = JSON.stringify(name);
|
||||
return `getByRole('${role}', { name: ${escapedName}, exact: true })`;
|
||||
}
|
||||
return `getByRole('${role}')`;
|
||||
function buildSelector(role: string, name: string): string {
|
||||
const escapedName = JSON.stringify(name);
|
||||
return `getByRole('${role}', { name: ${escapedName}, exact: true })`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -293,7 +290,7 @@ export async function getEnhancedSnapshot(
|
||||
const cursorElements = await findCursorInteractiveElements(page, options.selector);
|
||||
|
||||
// Filter out elements whose text is already captured in the snapshot
|
||||
const existingTexts = new Set(Object.values(refs).map((r) => r.name?.toLowerCase()));
|
||||
const existingTexts = new Set(Object.values(refs).map((r) => r.name.toLowerCase()));
|
||||
// Also extract quoted strings from the ARIA tree for broader dedup
|
||||
for (const m of enhancedTree.matchAll(/"([^"]+)"/g)) {
|
||||
existingTexts.add(m[1].toLowerCase());
|
||||
@@ -404,12 +401,13 @@ function processAriaTree(ariaTree: string, refs: RefMap, options: SnapshotOption
|
||||
|
||||
if (INTERACTIVE_ROLES.has(roleLower)) {
|
||||
const ref = nextRef();
|
||||
const nth = tracker.getNextIndex(roleLower, name);
|
||||
tracker.trackRef(roleLower, name, ref);
|
||||
const resolvedName = name ?? '';
|
||||
const nth = tracker.getNextIndex(roleLower, resolvedName);
|
||||
tracker.trackRef(roleLower, resolvedName, ref);
|
||||
refs[ref] = {
|
||||
selector: buildSelector(roleLower, name),
|
||||
selector: buildSelector(roleLower, resolvedName),
|
||||
role: roleLower,
|
||||
name,
|
||||
name: resolvedName,
|
||||
nth, // Always store nth, we'll use it for duplicates
|
||||
};
|
||||
|
||||
@@ -531,13 +529,15 @@ function processLine(
|
||||
|
||||
if (shouldHaveRef) {
|
||||
const ref = nextRef();
|
||||
const nth = tracker.getNextIndex(roleLower, name);
|
||||
tracker.trackRef(roleLower, name, ref);
|
||||
// Normalize to "" so unnamed elements get exact-match selectors
|
||||
const resolvedName = isInteractive ? (name ?? '') : name!;
|
||||
const nth = tracker.getNextIndex(roleLower, resolvedName);
|
||||
tracker.trackRef(roleLower, resolvedName, ref);
|
||||
|
||||
refs[ref] = {
|
||||
selector: buildSelector(roleLower, name),
|
||||
selector: buildSelector(roleLower, resolvedName),
|
||||
role: roleLower,
|
||||
name,
|
||||
name: resolvedName,
|
||||
nth, // Always store nth, we'll clean up non-duplicates later
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user