feat: descriptive click timeout errors + faster action timeouts for agents
Two improvements for agent experience with Playwright actions:
1. Descriptive timeout errors (via @xmorse/playwright-core 1.59.6):
When locator.click() times out, the error now explains WHY — e.g.
'Element is not visible', '<div id="overlay"> intercepts pointer events'.
The 'not visible' case includes tips about common causes (collapsed
<details>, inactive tab, CSS) and suggests { force: true }.
2. Faster action timeouts in executor:
- Action timeout (click, fill, hover): 2s (was 10s)
- Navigation timeout (goto, reload): 10s (unchanged)
- Code execution timeout: 10s (unchanged)
Agents now get fast failure with descriptive errors instead of waiting
10 seconds for a generic 'Timeout exceeded.'
Also removed the context.setDefaultTimeout(timeout) override in execute()
that was resetting action timeouts back to 10s on every call.
Tests added for three click failure scenarios: hidden element in collapsed
<details>, element covered by overlay, and display:none element.
This commit is contained in:
+1
-1
Submodule playwright updated: 1ab300291c...502df20d75
@@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## 0.0.80
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Descriptive click timeout errors**: When `locator.click()` times out due to actionability failures, the error now includes the reason (e.g. "Element is not visible", "Element is not stable", "<button> intercepts pointer events") instead of just "Timeout exceeded."
|
||||
- **Faster action timeouts for agents**: Default Playwright action timeout reduced from 10s to 2s. Navigation timeout remains at 10s. Agents now get fast failure with descriptive errors instead of waiting 10 seconds for a generic timeout.
|
||||
|
||||
## 0.0.79
|
||||
|
||||
### Improvements
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "playwriter",
|
||||
"description": "",
|
||||
"version": "0.0.79",
|
||||
"version": "0.0.80",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -592,6 +592,11 @@ export class PlaywrightExecutor {
|
||||
const contexts = browser.contexts()
|
||||
const context = contexts.length > 0 ? contexts[0] : await browser.newContext()
|
||||
|
||||
// Action timeout (click, fill, hover, etc.) is short for fast agent failure.
|
||||
// Navigation timeout (goto, reload) is longer since page loads are slower.
|
||||
context.setDefaultTimeout(2000)
|
||||
context.setDefaultNavigationTimeout(10000)
|
||||
|
||||
context.on('page', (page) => {
|
||||
this.setupPageListeners(page)
|
||||
})
|
||||
@@ -669,6 +674,11 @@ export class PlaywrightExecutor {
|
||||
const contexts = browser.contexts()
|
||||
const context = contexts.length > 0 ? contexts[0] : await browser.newContext()
|
||||
|
||||
// Action timeout (click, fill, hover, etc.) is short for fast agent failure.
|
||||
// Navigation timeout (goto, reload) is longer since page loads are slower.
|
||||
context.setDefaultTimeout(2000)
|
||||
context.setDefaultNavigationTimeout(10000)
|
||||
|
||||
context.on('page', (page) => {
|
||||
this.setupPageListeners(page)
|
||||
})
|
||||
@@ -717,7 +727,6 @@ export class PlaywrightExecutor {
|
||||
await this.ensureConnection()
|
||||
const page = await this.getCurrentPage(timeout)
|
||||
const context = this.context || page.context()
|
||||
context.setDefaultTimeout(timeout)
|
||||
|
||||
this.logger.log('Executing code:', code)
|
||||
|
||||
|
||||
@@ -1018,4 +1018,93 @@ describe('Relay Core Tests', () => {
|
||||
// Cleanup
|
||||
await page2.close()
|
||||
}, 60000)
|
||||
|
||||
it('should show descriptive error when clicking a hidden element', async () => {
|
||||
// Create a fresh page and set content with a collapsed details element
|
||||
await client.callTool({
|
||||
name: 'execute',
|
||||
arguments: {
|
||||
code: js`
|
||||
state.errorTestPage = await context.newPage();
|
||||
await state.errorTestPage.setContent(\`
|
||||
<details>
|
||||
<summary>Toggle</summary>
|
||||
<button id="hidden-btn">Hidden Button</button>
|
||||
</details>
|
||||
\`);
|
||||
`,
|
||||
},
|
||||
})
|
||||
const result = await client.callTool({
|
||||
name: 'execute',
|
||||
arguments: {
|
||||
code: js`
|
||||
await state.errorTestPage.click('#hidden-btn');
|
||||
`,
|
||||
},
|
||||
})
|
||||
expect((result as any).isError).toBe(true)
|
||||
const text = (result as any).content[0].text
|
||||
// Strip stack traces and call logs to only match the descriptive error line
|
||||
const errorLine = text.split('\n').find((l: string) => l.includes('Timeout') || l.includes('not visible') || l.includes('not stable'))
|
||||
expect(errorLine).toMatchInlineSnapshot(`"Error executing code: page.click: Timeout 2000ms exceeded. Element is not visible"`)
|
||||
// Cleanup
|
||||
await client.callTool({ name: 'execute', arguments: { code: js`await state.errorTestPage.close(); delete state.errorTestPage;` } })
|
||||
}, 30000)
|
||||
|
||||
it('should show descriptive error when clicking an element covered by another', async () => {
|
||||
await client.callTool({
|
||||
name: 'execute',
|
||||
arguments: {
|
||||
code: js`
|
||||
state.errorTestPage = await context.newPage();
|
||||
await state.errorTestPage.setContent(\`
|
||||
<div style="position:relative">
|
||||
<button id="covered-btn" style="position:absolute;top:0;left:0">Covered</button>
|
||||
<div id="overlay" style="position:absolute;top:0;left:0;width:200px;height:200px;background:red;z-index:10">Overlay</div>
|
||||
</div>
|
||||
\`);
|
||||
`,
|
||||
},
|
||||
})
|
||||
const result = await client.callTool({
|
||||
name: 'execute',
|
||||
arguments: {
|
||||
code: js`
|
||||
await state.errorTestPage.click('#covered-btn');
|
||||
`,
|
||||
},
|
||||
})
|
||||
expect((result as any).isError).toBe(true)
|
||||
const text = (result as any).content[0].text
|
||||
const errorLine = text.split('\n').find((l: string) => l.includes('Timeout') || l.includes('intercepts'))
|
||||
expect(errorLine).toMatchInlineSnapshot(`"Error executing code: page.click: Timeout 2000ms exceeded. <div id="overlay">Overlay</div> intercepts pointer events"`)
|
||||
await client.callTool({ name: 'execute', arguments: { code: js`await state.errorTestPage.close(); delete state.errorTestPage;` } })
|
||||
}, 30000)
|
||||
|
||||
it('should show descriptive error when clicking a display:none element', async () => {
|
||||
await client.callTool({
|
||||
name: 'execute',
|
||||
arguments: {
|
||||
code: js`
|
||||
state.errorTestPage = await context.newPage();
|
||||
await state.errorTestPage.setContent('<button id="invisible" style="display:none">Invisible</button>');
|
||||
`,
|
||||
},
|
||||
})
|
||||
const result = await client.callTool({
|
||||
name: 'execute',
|
||||
arguments: {
|
||||
code: js`
|
||||
await state.errorTestPage.click('#invisible');
|
||||
`,
|
||||
},
|
||||
})
|
||||
expect((result as any).isError).toBe(true)
|
||||
const text = (result as any).content[0].text
|
||||
const errorLine = text.split('\n').find((l: string) => l.includes('Timeout') || l.includes('not visible'))
|
||||
expect(errorLine).toMatchInlineSnapshot(`"Error executing code: page.click: Timeout 2000ms exceeded. Element is not visible"`)
|
||||
await client.callTool({ name: 'execute', arguments: { code: js`await state.errorTestPage.close(); delete state.errorTestPage;` } })
|
||||
}, 30000)
|
||||
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user