mcp-web-inspector 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1017 -0
- package/dist/evals/evals.d.ts +5 -0
- package/dist/evals/evals.js +41 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +62 -0
- package/dist/requestHandler.d.ts +3 -0
- package/dist/requestHandler.js +53 -0
- package/dist/toolHandler.d.ts +91 -0
- package/dist/toolHandler.js +725 -0
- package/dist/tools/api/base.d.ts +33 -0
- package/dist/tools/api/base.js +49 -0
- package/dist/tools/api/index.d.ts +2 -0
- package/dist/tools/api/index.js +3 -0
- package/dist/tools/api/requests.d.ts +47 -0
- package/dist/tools/api/requests.js +168 -0
- package/dist/tools/browser/base.d.ts +51 -0
- package/dist/tools/browser/base.js +111 -0
- package/dist/tools/browser/cleanSession.d.ts +10 -0
- package/dist/tools/browser/cleanSession.js +42 -0
- package/dist/tools/browser/comparePositions.d.ts +11 -0
- package/dist/tools/browser/comparePositions.js +149 -0
- package/dist/tools/browser/computedStyles.d.ts +11 -0
- package/dist/tools/browser/computedStyles.js +128 -0
- package/dist/tools/browser/console.d.ts +37 -0
- package/dist/tools/browser/console.js +106 -0
- package/dist/tools/browser/elementExists.d.ts +9 -0
- package/dist/tools/browser/elementExists.js +57 -0
- package/dist/tools/browser/elementInspection.d.ts +21 -0
- package/dist/tools/browser/elementInspection.js +151 -0
- package/dist/tools/browser/elementPosition.d.ts +11 -0
- package/dist/tools/browser/elementPosition.js +107 -0
- package/dist/tools/browser/elementVisibility.d.ts +12 -0
- package/dist/tools/browser/elementVisibility.js +224 -0
- package/dist/tools/browser/findByText.d.ts +13 -0
- package/dist/tools/browser/findByText.js +207 -0
- package/dist/tools/browser/getRequestDetails.d.ts +9 -0
- package/dist/tools/browser/getRequestDetails.js +137 -0
- package/dist/tools/browser/getTestIds.d.ts +12 -0
- package/dist/tools/browser/getTestIds.js +148 -0
- package/dist/tools/browser/index.d.ts +7 -0
- package/dist/tools/browser/index.js +7 -0
- package/dist/tools/browser/inspectDom.d.ts +12 -0
- package/dist/tools/browser/inspectDom.js +447 -0
- package/dist/tools/browser/interaction.d.ts +104 -0
- package/dist/tools/browser/interaction.js +259 -0
- package/dist/tools/browser/listNetworkRequests.d.ts +10 -0
- package/dist/tools/browser/listNetworkRequests.js +74 -0
- package/dist/tools/browser/measureElement.d.ts +9 -0
- package/dist/tools/browser/measureElement.js +139 -0
- package/dist/tools/browser/navigation.d.ts +38 -0
- package/dist/tools/browser/navigation.js +109 -0
- package/dist/tools/browser/output.d.ts +11 -0
- package/dist/tools/browser/output.js +29 -0
- package/dist/tools/browser/querySelectorAll.d.ts +12 -0
- package/dist/tools/browser/querySelectorAll.js +201 -0
- package/dist/tools/browser/response.d.ts +29 -0
- package/dist/tools/browser/response.js +67 -0
- package/dist/tools/browser/screenshot.d.ts +16 -0
- package/dist/tools/browser/screenshot.js +70 -0
- package/dist/tools/browser/useragent.d.ts +15 -0
- package/dist/tools/browser/useragent.js +32 -0
- package/dist/tools/browser/visiblePage.d.ts +20 -0
- package/dist/tools/browser/visiblePage.js +170 -0
- package/dist/tools/browser/waitForElement.d.ts +10 -0
- package/dist/tools/browser/waitForElement.js +38 -0
- package/dist/tools/browser/waitForNetworkIdle.d.ts +8 -0
- package/dist/tools/browser/waitForNetworkIdle.js +32 -0
- package/dist/tools/codegen/generator.d.ts +21 -0
- package/dist/tools/codegen/generator.js +158 -0
- package/dist/tools/codegen/index.d.ts +11 -0
- package/dist/tools/codegen/index.js +187 -0
- package/dist/tools/codegen/recorder.d.ts +14 -0
- package/dist/tools/codegen/recorder.js +62 -0
- package/dist/tools/codegen/types.d.ts +28 -0
- package/dist/tools/codegen/types.js +1 -0
- package/dist/tools/common/types.d.ts +17 -0
- package/dist/tools/common/types.js +20 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +2 -0
- package/dist/tools.d.ts +557 -0
- package/dist/tools.js +554 -0
- package/dist/types.d.ts +16 -0
- package/dist/types.js +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { BrowserToolBase } from './base.js';
|
|
2
|
+
import { ToolContext, ToolResponse } from '../common/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Tool for getting element position and size
|
|
5
|
+
*/
|
|
6
|
+
export declare class ElementPositionTool extends BrowserToolBase {
|
|
7
|
+
/**
|
|
8
|
+
* Execute the element position tool
|
|
9
|
+
*/
|
|
10
|
+
execute(args: any, context: ToolContext): Promise<ToolResponse>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { BrowserToolBase } from './base.js';
|
|
2
|
+
import { createSuccessResponse, createErrorResponse } from '../common/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Tool for getting element position and size
|
|
5
|
+
*/
|
|
6
|
+
export class ElementPositionTool extends BrowserToolBase {
|
|
7
|
+
/**
|
|
8
|
+
* Execute the element position tool
|
|
9
|
+
*/
|
|
10
|
+
async execute(args, context) {
|
|
11
|
+
return this.safeExecute(context, async (page) => {
|
|
12
|
+
const selector = this.normalizeSelector(args.selector);
|
|
13
|
+
const locator = page.locator(selector);
|
|
14
|
+
try {
|
|
15
|
+
// Check if element exists
|
|
16
|
+
const count = await locator.count();
|
|
17
|
+
if (count === 0) {
|
|
18
|
+
return createErrorResponse(`Element not found: ${args.selector}`);
|
|
19
|
+
}
|
|
20
|
+
// Handle multiple matches by using first() - show warning
|
|
21
|
+
const targetLocator = count > 1 ? locator.first() : locator;
|
|
22
|
+
const multipleMatchWarning = count > 1
|
|
23
|
+
? `⚠ Warning: Selector matched ${count} elements, showing first:\n\n`
|
|
24
|
+
: '';
|
|
25
|
+
// Get bounding box
|
|
26
|
+
const boundingBox = await targetLocator.boundingBox();
|
|
27
|
+
if (!boundingBox) {
|
|
28
|
+
// Get element info for better error message
|
|
29
|
+
const hiddenInfo = await targetLocator.evaluate((element) => {
|
|
30
|
+
const styles = window.getComputedStyle(element);
|
|
31
|
+
const tagName = element.tagName.toLowerCase();
|
|
32
|
+
const testId = element.getAttribute('data-testid') || element.getAttribute('data-test') || element.getAttribute('data-cy');
|
|
33
|
+
let descriptor = `<${tagName}`;
|
|
34
|
+
if (testId)
|
|
35
|
+
descriptor += ` data-testid="${testId}"`;
|
|
36
|
+
descriptor += '>';
|
|
37
|
+
return {
|
|
38
|
+
descriptor,
|
|
39
|
+
display: styles.display,
|
|
40
|
+
opacity: styles.opacity,
|
|
41
|
+
visibility: styles.visibility,
|
|
42
|
+
width: parseFloat(styles.width) || 0,
|
|
43
|
+
height: parseFloat(styles.height) || 0
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
// Return structured response instead of error
|
|
47
|
+
let reason = 'unknown reason';
|
|
48
|
+
if (hiddenInfo.display === 'none') {
|
|
49
|
+
reason = 'display: none';
|
|
50
|
+
}
|
|
51
|
+
else if (hiddenInfo.width === 0 || hiddenInfo.height === 0) {
|
|
52
|
+
reason = 'zero size';
|
|
53
|
+
}
|
|
54
|
+
else if (parseFloat(hiddenInfo.opacity) === 0) {
|
|
55
|
+
reason = 'opacity: 0';
|
|
56
|
+
}
|
|
57
|
+
else if (hiddenInfo.visibility === 'hidden') {
|
|
58
|
+
reason = 'visibility: hidden';
|
|
59
|
+
}
|
|
60
|
+
const output = multipleMatchWarning +
|
|
61
|
+
`Position: ${hiddenInfo.descriptor}\n` +
|
|
62
|
+
`@ null (element hidden: ${reason})\n` +
|
|
63
|
+
`display: ${hiddenInfo.display}, opacity: ${hiddenInfo.opacity}, visibility: ${hiddenInfo.visibility}`;
|
|
64
|
+
return createSuccessResponse(output);
|
|
65
|
+
}
|
|
66
|
+
// Check if in viewport and get element tag info
|
|
67
|
+
const elementData = await targetLocator.evaluate((element) => {
|
|
68
|
+
const rect = element.getBoundingClientRect();
|
|
69
|
+
const viewportHeight = window.innerHeight;
|
|
70
|
+
const viewportWidth = window.innerWidth;
|
|
71
|
+
// Element is in viewport if any part is visible
|
|
72
|
+
const inViewport = (rect.bottom > 0 &&
|
|
73
|
+
rect.right > 0 &&
|
|
74
|
+
rect.top < viewportHeight &&
|
|
75
|
+
rect.left < viewportWidth);
|
|
76
|
+
// Build element descriptor
|
|
77
|
+
const tagName = element.tagName.toLowerCase();
|
|
78
|
+
const testId = element.getAttribute('data-testid') || element.getAttribute('data-test') || element.getAttribute('data-cy');
|
|
79
|
+
const id = element.id ? `#${element.id}` : '';
|
|
80
|
+
const classes = element.className && typeof element.className === 'string' ? `.${element.className.split(' ').filter(c => c).join('.')}` : '';
|
|
81
|
+
let descriptor = `<${tagName}`;
|
|
82
|
+
if (testId)
|
|
83
|
+
descriptor += ` data-testid="${testId}"`;
|
|
84
|
+
else if (id)
|
|
85
|
+
descriptor += id;
|
|
86
|
+
else if (classes)
|
|
87
|
+
descriptor += classes;
|
|
88
|
+
descriptor += '>';
|
|
89
|
+
return { inViewport, descriptor };
|
|
90
|
+
});
|
|
91
|
+
// Build compact text format
|
|
92
|
+
const x = Math.round(boundingBox.x);
|
|
93
|
+
const y = Math.round(boundingBox.y);
|
|
94
|
+
const width = Math.round(boundingBox.width);
|
|
95
|
+
const height = Math.round(boundingBox.height);
|
|
96
|
+
const viewportSymbol = elementData.inViewport ? '✓' : '✗';
|
|
97
|
+
const viewportStatus = elementData.inViewport ? 'in viewport' : 'outside viewport';
|
|
98
|
+
const output = multipleMatchWarning +
|
|
99
|
+
`Position: ${elementData.descriptor}\n@ (${x},${y}) ${width}x${height}px, ${viewportSymbol} ${viewportStatus}`;
|
|
100
|
+
return createSuccessResponse(output);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
return createErrorResponse(`Failed to get position: ${error.message}`);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BrowserToolBase } from './base.js';
|
|
2
|
+
import { ToolContext, ToolResponse } from '../common/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Tool for checking element visibility with detailed diagnostics
|
|
5
|
+
* Addresses the #1 debugging pain point: "Why won't it click?"
|
|
6
|
+
*/
|
|
7
|
+
export declare class ElementVisibilityTool extends BrowserToolBase {
|
|
8
|
+
/**
|
|
9
|
+
* Execute the element visibility tool
|
|
10
|
+
*/
|
|
11
|
+
execute(args: any, context: ToolContext): Promise<ToolResponse>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { BrowserToolBase } from './base.js';
|
|
2
|
+
import { createSuccessResponse, createErrorResponse } from '../common/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Tool for checking element visibility with detailed diagnostics
|
|
5
|
+
* Addresses the #1 debugging pain point: "Why won't it click?"
|
|
6
|
+
*/
|
|
7
|
+
export class ElementVisibilityTool extends BrowserToolBase {
|
|
8
|
+
/**
|
|
9
|
+
* Execute the element visibility tool
|
|
10
|
+
*/
|
|
11
|
+
async execute(args, context) {
|
|
12
|
+
return this.safeExecute(context, async (page) => {
|
|
13
|
+
const selector = this.normalizeSelector(args.selector);
|
|
14
|
+
const locator = page.locator(selector);
|
|
15
|
+
try {
|
|
16
|
+
// Check if element exists
|
|
17
|
+
const count = await locator.count();
|
|
18
|
+
if (count === 0) {
|
|
19
|
+
return createErrorResponse(`Element not found: ${args.selector}`);
|
|
20
|
+
}
|
|
21
|
+
// Handle multiple matches by using first() - show warning
|
|
22
|
+
const targetLocator = count > 1 ? locator.first() : locator;
|
|
23
|
+
const multipleMatchWarning = count > 1
|
|
24
|
+
? `⚠ Warning: Selector matched ${count} elements, showing first:\n\n`
|
|
25
|
+
: '';
|
|
26
|
+
// Get basic visibility (Playwright's isVisible)
|
|
27
|
+
const isVisible = await targetLocator.isVisible();
|
|
28
|
+
// Evaluate detailed visibility information in browser context
|
|
29
|
+
const visibilityData = await targetLocator.evaluate((element) => {
|
|
30
|
+
const rect = element.getBoundingClientRect();
|
|
31
|
+
const viewportHeight = window.innerHeight;
|
|
32
|
+
const viewportWidth = window.innerWidth;
|
|
33
|
+
// Calculate viewport intersection ratio
|
|
34
|
+
const visibleTop = Math.max(0, rect.top);
|
|
35
|
+
const visibleBottom = Math.min(viewportHeight, rect.bottom);
|
|
36
|
+
const visibleLeft = Math.max(0, rect.left);
|
|
37
|
+
const visibleRight = Math.min(viewportWidth, rect.right);
|
|
38
|
+
const visibleHeight = Math.max(0, visibleBottom - visibleTop);
|
|
39
|
+
const visibleWidth = Math.max(0, visibleRight - visibleLeft);
|
|
40
|
+
const visibleArea = visibleHeight * visibleWidth;
|
|
41
|
+
const totalArea = rect.height * rect.width;
|
|
42
|
+
const viewportRatio = totalArea > 0 ? visibleArea / totalArea : 0;
|
|
43
|
+
// Check if element is in viewport
|
|
44
|
+
const isInViewport = viewportRatio > 0;
|
|
45
|
+
// Get computed styles
|
|
46
|
+
const styles = window.getComputedStyle(element);
|
|
47
|
+
const opacity = parseFloat(styles.opacity);
|
|
48
|
+
const display = styles.display;
|
|
49
|
+
const visibility = styles.visibility;
|
|
50
|
+
// Check if clipped by overflow:hidden
|
|
51
|
+
let isClipped = false;
|
|
52
|
+
let parent = element.parentElement;
|
|
53
|
+
while (parent) {
|
|
54
|
+
const parentStyle = window.getComputedStyle(parent);
|
|
55
|
+
if (parentStyle.overflow === 'hidden' ||
|
|
56
|
+
parentStyle.overflowX === 'hidden' ||
|
|
57
|
+
parentStyle.overflowY === 'hidden') {
|
|
58
|
+
const parentRect = parent.getBoundingClientRect();
|
|
59
|
+
// Check if element is outside parent bounds
|
|
60
|
+
if (rect.right < parentRect.left ||
|
|
61
|
+
rect.left > parentRect.right ||
|
|
62
|
+
rect.bottom < parentRect.top ||
|
|
63
|
+
rect.top > parentRect.bottom) {
|
|
64
|
+
isClipped = true;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
parent = parent.parentElement;
|
|
69
|
+
}
|
|
70
|
+
// Check if covered by another element (check center point and corners)
|
|
71
|
+
const centerX = rect.left + rect.width / 2;
|
|
72
|
+
const centerY = rect.top + rect.height / 2;
|
|
73
|
+
const topElement = document.elementFromPoint(centerX, centerY);
|
|
74
|
+
const isCovered = topElement !== element && !element.contains(topElement);
|
|
75
|
+
// Get covering element info if covered
|
|
76
|
+
let coveringElementInfo = '';
|
|
77
|
+
let coveragePercent = 0;
|
|
78
|
+
if (isCovered && topElement) {
|
|
79
|
+
const coveringTagName = topElement.tagName.toLowerCase();
|
|
80
|
+
const coveringTestId = topElement.getAttribute('data-testid');
|
|
81
|
+
const coveringId = topElement.id ? `#${topElement.id}` : '';
|
|
82
|
+
const coveringClasses = topElement.className && typeof topElement.className === 'string'
|
|
83
|
+
? `.${topElement.className.split(' ').filter((c) => c).slice(0, 2).join('.')}`
|
|
84
|
+
: '';
|
|
85
|
+
const coveringStyles = window.getComputedStyle(topElement);
|
|
86
|
+
const zIndex = coveringStyles.zIndex;
|
|
87
|
+
let descriptor = `<${coveringTagName}`;
|
|
88
|
+
if (coveringTestId)
|
|
89
|
+
descriptor += ` data-testid="${coveringTestId}"`;
|
|
90
|
+
else if (coveringId)
|
|
91
|
+
descriptor += coveringId;
|
|
92
|
+
else if (coveringClasses)
|
|
93
|
+
descriptor += coveringClasses;
|
|
94
|
+
descriptor += `> (z-index: ${zIndex})`;
|
|
95
|
+
coveringElementInfo = descriptor;
|
|
96
|
+
// Calculate approximate coverage by checking multiple points
|
|
97
|
+
const samplePoints = [
|
|
98
|
+
[centerX, centerY],
|
|
99
|
+
[rect.left + rect.width * 0.25, rect.top + rect.height * 0.25],
|
|
100
|
+
[rect.left + rect.width * 0.75, rect.top + rect.height * 0.25],
|
|
101
|
+
[rect.left + rect.width * 0.25, rect.top + rect.height * 0.75],
|
|
102
|
+
[rect.left + rect.width * 0.75, rect.top + rect.height * 0.75],
|
|
103
|
+
];
|
|
104
|
+
let coveredPoints = 0;
|
|
105
|
+
samplePoints.forEach(([x, y]) => {
|
|
106
|
+
const pointElement = document.elementFromPoint(x, y);
|
|
107
|
+
if (pointElement !== element && !element.contains(pointElement)) {
|
|
108
|
+
coveredPoints++;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
coveragePercent = Math.round((coveredPoints / samplePoints.length) * 100);
|
|
112
|
+
}
|
|
113
|
+
// Check interactability
|
|
114
|
+
const computedStyles = window.getComputedStyle(element);
|
|
115
|
+
const pointerEvents = computedStyles.pointerEvents;
|
|
116
|
+
const isDisabled = element.disabled || false;
|
|
117
|
+
const isReadonly = element.readOnly || false;
|
|
118
|
+
const ariaDisabled = element.getAttribute('aria-disabled') === 'true';
|
|
119
|
+
return {
|
|
120
|
+
viewportRatio,
|
|
121
|
+
isInViewport,
|
|
122
|
+
opacity,
|
|
123
|
+
display,
|
|
124
|
+
visibility,
|
|
125
|
+
isClipped,
|
|
126
|
+
isCovered,
|
|
127
|
+
coveringElementInfo,
|
|
128
|
+
coveragePercent,
|
|
129
|
+
pointerEvents,
|
|
130
|
+
isDisabled,
|
|
131
|
+
isReadonly,
|
|
132
|
+
ariaDisabled,
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
// Determine if scroll is needed
|
|
136
|
+
const needsScroll = isVisible && !visibilityData.isInViewport;
|
|
137
|
+
// Get element tag name for output
|
|
138
|
+
const tagInfo = await targetLocator.evaluate((el) => {
|
|
139
|
+
const tagName = el.tagName.toLowerCase();
|
|
140
|
+
const testId = el.getAttribute('data-testid') || el.getAttribute('data-test') || el.getAttribute('data-cy');
|
|
141
|
+
const id = el.id ? `#${el.id}` : '';
|
|
142
|
+
const classes = el.className && typeof el.className === 'string' ? `.${el.className.split(' ').filter(c => c).join('.')}` : '';
|
|
143
|
+
let descriptor = `<${tagName}`;
|
|
144
|
+
if (testId)
|
|
145
|
+
descriptor += ` data-testid="${testId}"`;
|
|
146
|
+
else if (id)
|
|
147
|
+
descriptor += id;
|
|
148
|
+
else if (classes)
|
|
149
|
+
descriptor += classes;
|
|
150
|
+
descriptor += '>';
|
|
151
|
+
return descriptor;
|
|
152
|
+
});
|
|
153
|
+
// Build compact text format
|
|
154
|
+
const viewportPercent = Math.round(visibilityData.viewportRatio * 100);
|
|
155
|
+
let output = multipleMatchWarning + `Visibility: ${tagInfo}\n\n`;
|
|
156
|
+
// Status line
|
|
157
|
+
const visibleSymbol = isVisible ? '✓' : '✗';
|
|
158
|
+
const viewportSymbol = visibilityData.isInViewport ? '✓' : '✗';
|
|
159
|
+
const viewportText = visibilityData.isInViewport
|
|
160
|
+
? `in viewport${viewportPercent < 100 ? ` (${viewportPercent}% visible)` : ''}`
|
|
161
|
+
: `not in viewport${viewportPercent > 0 ? ` (${viewportPercent}% visible)` : ''}`;
|
|
162
|
+
output += `${visibleSymbol} ${isVisible ? 'visible' : 'hidden'}, ${viewportSymbol} ${viewportText}\n`;
|
|
163
|
+
// CSS properties
|
|
164
|
+
output += `opacity: ${visibilityData.opacity}, display: ${visibilityData.display}, visibility: ${visibilityData.visibility}\n`;
|
|
165
|
+
// Interactability section
|
|
166
|
+
const interactabilityIssues = [];
|
|
167
|
+
if (visibilityData.isDisabled) {
|
|
168
|
+
interactabilityIssues.push('disabled');
|
|
169
|
+
}
|
|
170
|
+
if (visibilityData.isReadonly) {
|
|
171
|
+
interactabilityIssues.push('readonly');
|
|
172
|
+
}
|
|
173
|
+
if (visibilityData.ariaDisabled) {
|
|
174
|
+
interactabilityIssues.push('aria-disabled');
|
|
175
|
+
}
|
|
176
|
+
if (visibilityData.pointerEvents === 'none') {
|
|
177
|
+
interactabilityIssues.push('pointer-events: none');
|
|
178
|
+
}
|
|
179
|
+
if (interactabilityIssues.length > 0) {
|
|
180
|
+
output += `⚠ interactability: ${interactabilityIssues.join(', ')}\n`;
|
|
181
|
+
}
|
|
182
|
+
// Issues section
|
|
183
|
+
const issues = [];
|
|
184
|
+
if (visibilityData.isClipped) {
|
|
185
|
+
issues.push(' ✗ clipped by parent overflow:hidden');
|
|
186
|
+
}
|
|
187
|
+
if (visibilityData.isCovered) {
|
|
188
|
+
const coverageInfo = visibilityData.coveragePercent > 0
|
|
189
|
+
? ` (~${visibilityData.coveragePercent}% covered)`
|
|
190
|
+
: '';
|
|
191
|
+
issues.push(` ✗ covered by another element${coverageInfo}`);
|
|
192
|
+
if (visibilityData.coveringElementInfo) {
|
|
193
|
+
issues.push(` Covering: ${visibilityData.coveringElementInfo}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (needsScroll) {
|
|
197
|
+
issues.push(' ⚠ needs scroll to bring into view');
|
|
198
|
+
}
|
|
199
|
+
if (issues.length > 0) {
|
|
200
|
+
output += '\nIssues:\n';
|
|
201
|
+
output += issues.join('\n') + '\n';
|
|
202
|
+
}
|
|
203
|
+
// Suggestions
|
|
204
|
+
const suggestions = [];
|
|
205
|
+
if (needsScroll) {
|
|
206
|
+
suggestions.push('→ Call scroll_to_element before clicking');
|
|
207
|
+
}
|
|
208
|
+
if (visibilityData.isCovered) {
|
|
209
|
+
suggestions.push('→ Element may be behind modal, overlay, or fixed header');
|
|
210
|
+
}
|
|
211
|
+
if (interactabilityIssues.length > 0) {
|
|
212
|
+
suggestions.push('→ Element cannot be interacted with in current state');
|
|
213
|
+
}
|
|
214
|
+
if (suggestions.length > 0) {
|
|
215
|
+
output += '\n' + suggestions.join('\n');
|
|
216
|
+
}
|
|
217
|
+
return createSuccessResponse(output.trim());
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
return createErrorResponse(`Failed to check visibility: ${error.message}`);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ToolHandler } from '../common/types.js';
|
|
2
|
+
import { BrowserToolBase } from './base.js';
|
|
3
|
+
import type { ToolContext, ToolResponse } from '../common/types.js';
|
|
4
|
+
export interface FindByTextArgs {
|
|
5
|
+
text: string;
|
|
6
|
+
exact?: boolean;
|
|
7
|
+
caseSensitive?: boolean;
|
|
8
|
+
regex?: boolean;
|
|
9
|
+
limit?: number;
|
|
10
|
+
}
|
|
11
|
+
export declare class FindByTextTool extends BrowserToolBase implements ToolHandler {
|
|
12
|
+
execute(args: FindByTextArgs, context: ToolContext): Promise<ToolResponse>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { BrowserToolBase } from './base.js';
|
|
2
|
+
export class FindByTextTool extends BrowserToolBase {
|
|
3
|
+
async execute(args, context) {
|
|
4
|
+
return this.safeExecute(context, async (page) => {
|
|
5
|
+
const { text, exact = false, caseSensitive = false, regex = false, limit = 10 } = args;
|
|
6
|
+
// Build the text selector based on exact, caseSensitive, and regex options
|
|
7
|
+
let selector;
|
|
8
|
+
if (regex) {
|
|
9
|
+
// Validate and use user-provided regex pattern
|
|
10
|
+
try {
|
|
11
|
+
// Extract pattern and flags from /pattern/flags format
|
|
12
|
+
const regexMatch = text.match(/^\/(.+?)\/([gimuy]*)$/);
|
|
13
|
+
if (regexMatch) {
|
|
14
|
+
const [, pattern, flags] = regexMatch;
|
|
15
|
+
// Validate the regex pattern
|
|
16
|
+
new RegExp(pattern, flags);
|
|
17
|
+
selector = `text=/${pattern}/${flags}`;
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
// If not in /pattern/flags format, treat as raw pattern
|
|
21
|
+
new RegExp(text);
|
|
22
|
+
selector = `text=/${text}/`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return {
|
|
27
|
+
content: [
|
|
28
|
+
{
|
|
29
|
+
type: 'text',
|
|
30
|
+
text: `✗ Invalid regex pattern: ${error.message}`
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
isError: true
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else if (exact) {
|
|
38
|
+
selector = `text="${text}"`;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// Use regex for partial match with case sensitivity
|
|
42
|
+
const flags = caseSensitive ? '' : 'i';
|
|
43
|
+
const escapedText = text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
44
|
+
selector = `text=/${escapedText}/${flags}`;
|
|
45
|
+
}
|
|
46
|
+
// Find all matching elements
|
|
47
|
+
const locator = page.locator(selector);
|
|
48
|
+
const count = await locator.count();
|
|
49
|
+
if (count === 0) {
|
|
50
|
+
// Build "not found" message based on search type
|
|
51
|
+
let notFoundMsg;
|
|
52
|
+
if (regex) {
|
|
53
|
+
notFoundMsg = `✗ No elements found matching regex ${text}`;
|
|
54
|
+
}
|
|
55
|
+
else if (exact) {
|
|
56
|
+
notFoundMsg = `✗ No elements found with exact text "${text}"`;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
notFoundMsg = `✗ No elements found containing "${text}"`;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
content: [
|
|
63
|
+
{
|
|
64
|
+
type: 'text',
|
|
65
|
+
text: notFoundMsg
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
isError: false
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// Limit results
|
|
72
|
+
const elementsToShow = Math.min(count, limit);
|
|
73
|
+
const elements = [];
|
|
74
|
+
for (let i = 0; i < elementsToShow; i++) {
|
|
75
|
+
const element = locator.nth(i);
|
|
76
|
+
// Get element info
|
|
77
|
+
const [tagName, boundingBox, textContent, isVisible, isEnabled] = await Promise.all([
|
|
78
|
+
element.evaluate((el) => el.tagName.toLowerCase()),
|
|
79
|
+
element.boundingBox().catch(() => null),
|
|
80
|
+
element.textContent().catch(() => ''),
|
|
81
|
+
element.isVisible().catch(() => false),
|
|
82
|
+
element.isEnabled().catch(() => true)
|
|
83
|
+
]);
|
|
84
|
+
// Get selector attributes for better identification
|
|
85
|
+
const selectorInfo = await element.evaluate((el) => {
|
|
86
|
+
const attrs = {};
|
|
87
|
+
if (el.id)
|
|
88
|
+
attrs.id = el.id;
|
|
89
|
+
if (el.className && typeof el.className === 'string') {
|
|
90
|
+
attrs.class = el.className.split(' ').slice(0, 2).join(' ');
|
|
91
|
+
}
|
|
92
|
+
const testId = el.getAttribute('data-testid') || el.getAttribute('data-test') || el.getAttribute('data-cy');
|
|
93
|
+
if (testId)
|
|
94
|
+
attrs.testid = testId;
|
|
95
|
+
if (el.getAttribute('href'))
|
|
96
|
+
attrs.href = el.getAttribute('href') || '';
|
|
97
|
+
if (el.getAttribute('name'))
|
|
98
|
+
attrs.name = el.getAttribute('name') || '';
|
|
99
|
+
if (el.getAttribute('type'))
|
|
100
|
+
attrs.type = el.getAttribute('type') || '';
|
|
101
|
+
if (el.getAttribute('aria-label'))
|
|
102
|
+
attrs['aria-label'] = el.getAttribute('aria-label') || '';
|
|
103
|
+
return attrs;
|
|
104
|
+
});
|
|
105
|
+
// Build selector string
|
|
106
|
+
let selectorStr = `<${tagName}`;
|
|
107
|
+
if (selectorInfo.id)
|
|
108
|
+
selectorStr += `#${selectorInfo.id}`;
|
|
109
|
+
if (selectorInfo.class)
|
|
110
|
+
selectorStr += ` class="${selectorInfo.class}"`;
|
|
111
|
+
if (selectorInfo.testid)
|
|
112
|
+
selectorStr += ` data-testid="${selectorInfo.testid}"`;
|
|
113
|
+
if (selectorInfo.name)
|
|
114
|
+
selectorStr += ` name="${selectorInfo.name}"`;
|
|
115
|
+
if (selectorInfo.type)
|
|
116
|
+
selectorStr += ` type="${selectorInfo.type}"`;
|
|
117
|
+
if (selectorInfo.href)
|
|
118
|
+
selectorStr += ` href="${selectorInfo.href.slice(0, 30)}${selectorInfo.href.length > 30 ? '...' : ''}"`;
|
|
119
|
+
if (selectorInfo['aria-label'])
|
|
120
|
+
selectorStr += ` aria-label="${selectorInfo['aria-label']}"`;
|
|
121
|
+
selectorStr += '>';
|
|
122
|
+
// Format position
|
|
123
|
+
let positionStr = '';
|
|
124
|
+
if (boundingBox) {
|
|
125
|
+
const x = Math.round(boundingBox.x);
|
|
126
|
+
const y = Math.round(boundingBox.y);
|
|
127
|
+
const w = Math.round(boundingBox.width);
|
|
128
|
+
const h = Math.round(boundingBox.height);
|
|
129
|
+
positionStr = ` @ (${x},${y}) ${w}x${h}px`;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
positionStr = ` @ (no bounding box)`;
|
|
133
|
+
}
|
|
134
|
+
// Format text content (truncate if too long)
|
|
135
|
+
const truncatedText = textContent && textContent.length > 100
|
|
136
|
+
? textContent.slice(0, 100) + '...'
|
|
137
|
+
: textContent;
|
|
138
|
+
const textStr = ` "${truncatedText}"`;
|
|
139
|
+
// Format state
|
|
140
|
+
let stateStr = ' ';
|
|
141
|
+
if (isVisible) {
|
|
142
|
+
stateStr += '✓ visible';
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
stateStr += '✗ hidden';
|
|
146
|
+
// Try to detect why it's hidden
|
|
147
|
+
const hiddenReason = await element.evaluate((el) => {
|
|
148
|
+
const style = window.getComputedStyle(el);
|
|
149
|
+
if (style.display === 'none')
|
|
150
|
+
return '(display: none)';
|
|
151
|
+
if (style.visibility === 'hidden')
|
|
152
|
+
return '(visibility: hidden)';
|
|
153
|
+
if (style.opacity === '0')
|
|
154
|
+
return '(opacity: 0)';
|
|
155
|
+
const rect = el.getBoundingClientRect();
|
|
156
|
+
if (rect.width === 0 || rect.height === 0)
|
|
157
|
+
return '(zero size)';
|
|
158
|
+
return '';
|
|
159
|
+
}).catch(() => '');
|
|
160
|
+
if (hiddenReason)
|
|
161
|
+
stateStr += ` ${hiddenReason}`;
|
|
162
|
+
}
|
|
163
|
+
// Check if interactive
|
|
164
|
+
const isInteractive = await element.evaluate((el) => {
|
|
165
|
+
const tag = el.tagName.toLowerCase();
|
|
166
|
+
if (['a', 'button', 'input', 'select', 'textarea'].includes(tag))
|
|
167
|
+
return true;
|
|
168
|
+
if (el.getAttribute('onclick'))
|
|
169
|
+
return true;
|
|
170
|
+
if (el.getAttribute('role') === 'button')
|
|
171
|
+
return true;
|
|
172
|
+
return false;
|
|
173
|
+
}).catch(() => false);
|
|
174
|
+
if (isVisible && isInteractive && isEnabled) {
|
|
175
|
+
stateStr += ', ⚡ interactive';
|
|
176
|
+
}
|
|
177
|
+
else if (isVisible && isInteractive && !isEnabled) {
|
|
178
|
+
stateStr += ', ✗ disabled';
|
|
179
|
+
}
|
|
180
|
+
elements.push(`[${i}] ${selectorStr}\n${positionStr}\n${textStr}\n${stateStr}`);
|
|
181
|
+
}
|
|
182
|
+
// Build header message based on search type
|
|
183
|
+
let searchDesc;
|
|
184
|
+
if (regex) {
|
|
185
|
+
searchDesc = `matching regex ${text}`;
|
|
186
|
+
}
|
|
187
|
+
else if (exact) {
|
|
188
|
+
searchDesc = `with exact text "${text}"`;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
searchDesc = `containing "${text}"`;
|
|
192
|
+
}
|
|
193
|
+
const header = count > limit
|
|
194
|
+
? `Found ${count} elements ${searchDesc} (showing first ${limit}):\n`
|
|
195
|
+
: `Found ${count} element${count > 1 ? 's' : ''} ${searchDesc}:\n`;
|
|
196
|
+
return {
|
|
197
|
+
content: [
|
|
198
|
+
{
|
|
199
|
+
type: 'text',
|
|
200
|
+
text: header + '\n' + elements.join('\n\n')
|
|
201
|
+
}
|
|
202
|
+
],
|
|
203
|
+
isError: false
|
|
204
|
+
};
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ToolContext, ToolResponse } from '../common/types.js';
|
|
2
|
+
import { BrowserToolBase } from './base.js';
|
|
3
|
+
interface GetRequestDetailsArgs {
|
|
4
|
+
index: number;
|
|
5
|
+
}
|
|
6
|
+
export declare class GetRequestDetailsTool extends BrowserToolBase {
|
|
7
|
+
execute(args: GetRequestDetailsArgs, context: ToolContext): Promise<ToolResponse>;
|
|
8
|
+
}
|
|
9
|
+
export {};
|