merge: sync upstream/main into fork main (v0.15.2)

This commit is contained in:
leeguooooo
2026-03-03 09:57:17 +09:00
41 changed files with 1615 additions and 1505 deletions
+1 -1
View File
@@ -1005,7 +1005,7 @@ async function handleSnapshot(
});
// Simplify refs for output (just role and name)
const simpleRefs: Record<string, { role: string; name?: string }> = {};
const simpleRefs: Record<string, { role: string; name: string }> = {};
for (const [ref, data] of Object.entries(refs)) {
simpleRefs[ref] = { role: data.role, name: data.name };
}
+24
View File
@@ -361,6 +361,30 @@ describe('BrowserManager', () => {
});
});
describe('unnamed-button ref uniqueness', () => {
it('should click the correct unnamed button among named buttons', async () => {
const page = browser.getPage();
// 1 unnamed button among 2 named buttons
await page.setContent(`
<html><body>
<button>OK</button>
<button onclick="document.title='unnamed'"></button>
<button>Cancel</button>
</body></html>
`);
const snapshot = await browser.getSnapshot();
const refs = snapshot.refs;
const unnamedRefs = Object.entries(refs).filter(([, v]) => v.role === 'button' && !v.name);
expect(unnamedRefs.length).toBe(1);
const [refId] = unnamedRefs[0];
await executeCommand({ id: 'test', action: 'click', selector: `@${refId}` }, browser);
const title = await page.title();
expect(title).toBe('unnamed');
});
});
describe('cursor-ref selector uniqueness', () => {
it('should produce unique selectors for repeated DOM structures', async () => {
const page = browser.getPage();
+4 -6
View File
@@ -553,12 +553,10 @@ export class BrowserManager {
}
// Build locator with exact: true to avoid substring matches
let locator: Locator;
if (refData.name) {
locator = page.getByRole(refData.role as any, { name: refData.name, exact: true });
} else {
locator = page.getByRole(refData.role as any);
}
let locator: Locator = page.getByRole(refData.role as any, {
name: refData.name,
exact: true,
});
// If an nth index is stored (for disambiguation), use it
if (refData.nth !== undefined) {
+6 -1
View File
@@ -262,7 +262,12 @@ export function isDaemonRunning(session?: string): boolean {
// Check if process exists (works on both Unix and Windows)
process.kill(pid, 0);
return true;
} catch {
} catch (err: unknown) {
// EPERM means the process exists but we lack permission to signal it
// (e.g. caller is inside a macOS sandbox). Only ESRCH means it's gone.
if (err instanceof Error && (err as NodeJS.ErrnoException).code === 'EPERM') {
return true;
}
// Process doesn't exist, clean up stale files
cleanupSocket(session);
return false;
+16 -16
View File
@@ -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
};