Skip to content

Commit ca45146

Browse files
quanruclaude
andauthored
feat(core,web): add default read-write strategy and fix xpath cache default (#1270)
This commit includes two important changes: 1. Add 'read-write' as default cache strategy: - Updated CacheConfig type to support 'read-write' strategy - Changed default strategy from undefined to 'read-write' - Added unit test for explicit 'read-write' configuration 2. Fix xpath cache default behavior (orderSensitive): - Changed default orderSensitive from true to false in cacheFeatureForRect - Ensures generated xpaths use normalize-space() for text-based matching - This makes cached xpaths more stable across DOM changes Before: /html/body/div[1]/h1[1] After: /html/body/div[1]/h1[normalize-space()="Example Domain"] - Added comprehensive unit test to prevent regression 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <[email protected]>
1 parent c78ca37 commit ca45146

File tree

6 files changed

+99
-6
lines changed

6 files changed

+99
-6
lines changed

packages/core/src/agent/agent.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1079,7 +1079,9 @@ export class Agent<
10791079
);
10801080
}
10811081
const id = config.id;
1082-
const isReadOnly = config.strategy === 'read-only';
1082+
// Default strategy is 'read-write'
1083+
const strategy = config.strategy ?? 'read-write';
1084+
const isReadOnly = strategy === 'read-only';
10831085

10841086
return {
10851087
id,

packages/core/src/types.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -580,9 +580,7 @@ export type WebUIContext = UIContext<WebElementInfo>;
580580
* Agent
581581
*/
582582

583-
export type CacheConfig =
584-
| { strategy: 'read-only'; id: string }
585-
| { strategy?: undefined; id: string };
583+
export type CacheConfig = { strategy?: 'read-only' | 'read-write'; id: string };
586584

587585
export type Cache =
588586
| false // No read, no write

packages/web-integration/src/puppeteer/base-page.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ export class Page<
207207
};
208208

209209
try {
210-
const orderSensitive = opt?._orderSensitive ?? true;
210+
const orderSensitive = opt?._orderSensitive ?? false;
211211
const xpaths = await this.getXpathsByPoint(center, orderSensitive);
212212
const sanitized = sanitizeXpaths(xpaths);
213213
if (!sanitized.length) {

packages/web-integration/tests/ai/web/puppeteer/cache-functionality.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ describe('Cache Configuration Tests', () => {
2323
const agent = new PuppeteerAgent(originPage, {
2424
cache: {
2525
id: 'cache-functionality-test',
26+
strategy: 'read-write',
2627
},
2728
testId: 'explicit-cache-test-001',
2829
});

packages/web-integration/tests/unit-test/agent.test.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ describe('PageAgent cache configuration', () => {
435435
}).toThrow('cache configuration requires an explicit id');
436436
});
437437

438-
it('should handle cache: { id: "custom-id" }', () => {
438+
it('should handle cache: { id: "custom-id" } with default read-write strategy', () => {
439439
const agent = new PageAgent(mockPage, {
440440
cache: { id: 'custom-cache-id' },
441441
modelConfig: () => mockedModelConfigFnResult,
@@ -447,6 +447,21 @@ describe('PageAgent cache configuration', () => {
447447
expect(agent.taskCache?.cacheId).toBe('custom-cache-id');
448448
});
449449

450+
it('should handle cache: { strategy: "read-write", id: "custom-id" }', () => {
451+
const agent = new PageAgent(mockPage, {
452+
cache: {
453+
strategy: 'read-write',
454+
id: 'custom-readwrite-cache',
455+
},
456+
modelConfig: () => mockedModelConfigFnResult,
457+
});
458+
459+
expect(agent.taskCache).toBeDefined();
460+
expect(agent.taskCache?.isCacheResultUsed).toBe(true);
461+
expect(agent.taskCache?.readOnlyMode).toBe(false);
462+
expect(agent.taskCache?.cacheId).toBe('custom-readwrite-cache');
463+
});
464+
450465
it('should handle cache: { strategy: "read-only", id: "custom-id" }', () => {
451466
const agent = new PageAgent(mockPage, {
452467
cache: {

packages/web-integration/tests/unit-test/web-extractor.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,83 @@ describe(
386386

387387
await reset();
388388
});
389+
390+
it('cacheFeatureForRect should default to order-insensitive (normalize-space) mode', async () => {
391+
const { page, reset } = await launchPage(`http://127.0.0.1:${port}`, {
392+
viewport: {
393+
width: 1080,
394+
height: 3000,
395+
deviceScaleFactor: 1,
396+
},
397+
});
398+
399+
// Target a button element with text content
400+
// Use coordinates that will hit an actual element with text
401+
const rect = {
402+
left: 100,
403+
top: 400,
404+
width: 100,
405+
height: 40,
406+
};
407+
408+
// Call cacheFeatureForRect without opt parameter (should default to orderSensitive=false)
409+
const cacheFeature = await page.cacheFeatureForRect?.(rect);
410+
411+
expect(cacheFeature).toBeDefined();
412+
const xpaths = (cacheFeature as any)?.xpaths as string[] | undefined;
413+
expect(xpaths).toBeDefined();
414+
expect(xpaths?.length).toBeGreaterThan(0);
415+
416+
const xpath = xpaths?.[0];
417+
expect(xpath).toMatch(/^\/html/);
418+
419+
// Verify with explicit orderSensitive=false
420+
const cacheFeatureInsensitive = await page.cacheFeatureForRect?.(rect, {
421+
_orderSensitive: false,
422+
});
423+
424+
const xpathsInsensitive = (cacheFeatureInsensitive as any)
425+
?.xpaths as string[];
426+
const xpathInsensitive = xpathsInsensitive?.[0];
427+
428+
// Default behavior should match explicit orderSensitive=false
429+
expect(xpath).toBe(xpathInsensitive);
430+
431+
// Verify that orderSensitive=true produces different (index-based) xpath
432+
const cacheFeatureSensitive = await page.cacheFeatureForRect?.(rect, {
433+
_orderSensitive: true,
434+
});
435+
436+
const xpathsSensitive = (cacheFeatureSensitive as any)
437+
?.xpaths as string[];
438+
const xpathSensitive = xpathsSensitive?.[0];
439+
440+
// Skip special elements like body
441+
if (
442+
xpathSensitive &&
443+
xpathSensitive !== '/html/body' &&
444+
xpathSensitive !== '/html'
445+
) {
446+
// Order-sensitive xpath should:
447+
// 1. Either use index format like [1], [2] at the end
448+
// 2. Or NOT use normalize-space (for better distinction)
449+
const isIndexBased = /\[\d+\]/.test(xpathSensitive);
450+
const hasNormalizeSpace =
451+
xpathSensitive.includes('normalize-space()');
452+
453+
// For order-insensitive (default), leaf elements with text should prefer normalize-space
454+
// For order-sensitive, it should use index-based format
455+
if (xpath !== xpathSensitive) {
456+
// They should be different
457+
expect(xpath).not.toBe(xpathSensitive);
458+
459+
// Order-sensitive should either use index or not use normalize-space
460+
expect(isIndexBased || !hasNormalizeSpace).toBe(true);
461+
}
462+
}
463+
464+
await reset();
465+
});
389466
});
390467
},
391468
);

0 commit comments

Comments
 (0)