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
|
# 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
|
## 0.0.79
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "playwriter",
|
"name": "playwriter",
|
||||||
"description": "",
|
"description": "",
|
||||||
"version": "0.0.79",
|
"version": "0.0.80",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|||||||
@@ -592,6 +592,11 @@ export class PlaywrightExecutor {
|
|||||||
const contexts = browser.contexts()
|
const contexts = browser.contexts()
|
||||||
const context = contexts.length > 0 ? contexts[0] : await browser.newContext()
|
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) => {
|
context.on('page', (page) => {
|
||||||
this.setupPageListeners(page)
|
this.setupPageListeners(page)
|
||||||
})
|
})
|
||||||
@@ -669,6 +674,11 @@ export class PlaywrightExecutor {
|
|||||||
const contexts = browser.contexts()
|
const contexts = browser.contexts()
|
||||||
const context = contexts.length > 0 ? contexts[0] : await browser.newContext()
|
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) => {
|
context.on('page', (page) => {
|
||||||
this.setupPageListeners(page)
|
this.setupPageListeners(page)
|
||||||
})
|
})
|
||||||
@@ -717,7 +727,6 @@ export class PlaywrightExecutor {
|
|||||||
await this.ensureConnection()
|
await this.ensureConnection()
|
||||||
const page = await this.getCurrentPage(timeout)
|
const page = await this.getCurrentPage(timeout)
|
||||||
const context = this.context || page.context()
|
const context = this.context || page.context()
|
||||||
context.setDefaultTimeout(timeout)
|
|
||||||
|
|
||||||
this.logger.log('Executing code:', code)
|
this.logger.log('Executing code:', code)
|
||||||
|
|
||||||
|
|||||||
@@ -1018,4 +1018,93 @@ describe('Relay Core Tests', () => {
|
|||||||
// Cleanup
|
// Cleanup
|
||||||
await page2.close()
|
await page2.close()
|
||||||
}, 60000)
|
}, 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