diff --git a/src/actions.test.ts b/src/actions.test.ts
new file mode 100644
index 0000000..91d7afa
--- /dev/null
+++ b/src/actions.test.ts
@@ -0,0 +1,39 @@
+import { describe, it, expect } from 'vitest';
+import { toAIFriendlyError } from './actions.js';
+
+describe('toAIFriendlyError', () => {
+ describe('element blocked by overlay', () => {
+ it('should detect intercepts pointer events even when Timeout is in message', () => {
+ // This is the exact error from Playwright when a cookie banner blocks an element
+ // Bug: Previously this was incorrectly reported as "not found or not visible"
+ const error = new Error(
+ 'TimeoutError: locator.click: Timeout 10000ms exceeded.\n' +
+ 'Call log:\n' +
+ " - waiting for getByRole('link', { name: 'Anmelden', exact: true }).first()\n" +
+ ' - locator resolved to Anmelden\n' +
+ ' - attempting click action\n' +
+ ' 2 x waiting for element to be visible, enabled and stable\n' +
+ ' - element is visible, enabled and stable\n' +
+ ' - scrolling into view if needed\n' +
+ ' - done scrolling\n' +
+ ' -
... intercepts pointer events\n' +
+ ' - retrying click action'
+ );
+
+ const result = toAIFriendlyError(error, '@e4');
+
+ // Must NOT say "not found" - the element WAS found
+ expect(result.message).not.toContain('not found');
+ // Must indicate the element is blocked
+ expect(result.message).toContain('blocked by another element');
+ expect(result.message).toContain('modal or overlay');
+ });
+
+ it('should suggest dismissing cookie banners', () => {
+ const error = new Error(' intercepts pointer events');
+ const result = toAIFriendlyError(error, '@e1');
+
+ expect(result.message).toContain('cookie banners');
+ });
+ });
+});
diff --git a/src/actions.ts b/src/actions.ts
index e9af532..50f540d 100644
--- a/src/actions.ts
+++ b/src/actions.ts
@@ -134,8 +134,9 @@ interface SnapshotData {
/**
* Convert Playwright errors to AI-friendly messages
+ * @internal Exported for testing
*/
-function toAIFriendlyError(error: unknown, selector: string): Error {
+export function toAIFriendlyError(error: unknown, selector: string): Error {
const message = error instanceof Error ? error.message : String(error);
// Handle strict mode violation (multiple elements match)
@@ -150,7 +151,24 @@ function toAIFriendlyError(error: unknown, selector: string): Error {
);
}
- // Handle element not found
+ // Handle element not interactable (must be checked BEFORE timeout case)
+ // This includes cases where an overlay/modal blocks the element
+ if (message.includes('intercepts pointer events')) {
+ return new Error(
+ `Element "${selector}" is blocked by another element (likely a modal or overlay). ` +
+ `Try dismissing any modals/cookie banners first.`
+ );
+ }
+
+ // Handle element not visible
+ if (message.includes('not visible') && !message.includes('Timeout')) {
+ return new Error(
+ `Element "${selector}" is not visible. ` +
+ `Try scrolling it into view or check if it's hidden.`
+ );
+ }
+
+ // Handle element not found (timeout waiting for element)
if (
message.includes('waiting for') &&
(message.includes('to be visible') || message.includes('Timeout'))
@@ -161,14 +179,6 @@ function toAIFriendlyError(error: unknown, selector: string): Error {
);
}
- // Handle element not interactable
- if (message.includes('intercepts pointer events') || message.includes('not visible')) {
- return new Error(
- `Element "${selector}" is not interactable (may be hidden or covered). ` +
- `Try scrolling it into view or check if a modal/overlay is blocking it.`
- );
- }
-
// Return original error for unknown cases
return error instanceof Error ? error : new Error(message);
}