mcp-web-inspector 0.1.2 โ 0.1.3
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/README.md +129 -0
- package/dist/toolHandler.js +110 -9
- package/dist/tools/browser/ancestorInspection.d.ts +16 -0
- package/dist/tools/browser/ancestorInspection.js +242 -0
- package/dist/tools/browser/compareElementAlignment.js +9 -0
- package/dist/tools/browser/elementVisibility.js +5 -0
- package/dist/tools/browser/inspectDom.js +6 -0
- package/dist/tools/browser/interaction.d.ts +4 -0
- package/dist/tools/browser/interaction.js +60 -3
- package/dist/tools/browser/measureElement.js +8 -0
- package/dist/tools/browser/screenshot.js +13 -1
- package/dist/tools.d.ts +19 -2
- package/dist/tools.js +21 -2
- package/dist/utils/browserCheck.d.ts +12 -0
- package/dist/utils/browserCheck.js +67 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ Modern web applications are complex. Elements are hidden, layouts break, selecto
|
|
|
10
10
|
|
|
11
11
|
- ๐ **Understand any page structure** - Progressive DOM inspection that drills through wrapper divs to find semantic elements
|
|
12
12
|
- ๐ฏ **Debug visibility issues** - Detailed diagnostics showing exactly why clicks fail (clipped, covered, scrolled out of view)
|
|
13
|
+
- ๐ผ **Trace layout constraints** - Walk up the DOM tree to find where unexpected margins, width limits, and overflow clipping come from
|
|
13
14
|
- ๐ **Validate layouts** - Compare element positions to ensure consistent alignment and spacing
|
|
14
15
|
- ๐งช **Test selector reliability** - See all matching elements with their visibility status before writing tests
|
|
15
16
|
- ๐จ **Inspect styles** - Get computed CSS to understand why elements behave unexpectedly
|
|
@@ -139,6 +140,17 @@ code-insiders --add-mcp '{"name":"web-inspector","command":"npx","args":["mcp-we
|
|
|
139
140
|
4. MCP only works in **Agent mode** - switch to agent mode in the chat interface
|
|
140
141
|
5. Open the `mcp.json` file and click the **"Start"** button next to the server
|
|
141
142
|
|
|
143
|
+
### First-Time Browser Setup
|
|
144
|
+
|
|
145
|
+
When you first use the server with `npx`, Playwright browsers will be **automatically installed** on first tool use if not already present. The installation happens once and browsers are stored in your home directory, shared across all projects.
|
|
146
|
+
|
|
147
|
+
If automatic installation doesn't work (firewall, permissions, etc.), you'll see clear instructions to run:
|
|
148
|
+
```bash
|
|
149
|
+
npx playwright install chromium firefox webkit
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Then restart VS Code to use the server.
|
|
153
|
+
|
|
142
154
|
### Note about Embedded Browser
|
|
143
155
|
|
|
144
156
|
GitHub Copilot and VS Code may have an embedded browser feature. If you experience conflicts or prefer using Web Inspector MCP for all web inspection tasks, you may want to disable the built-in browser:
|
|
@@ -458,6 +470,32 @@ Compare positions and alignment of two elements. Validates if elements are align
|
|
|
458
470
|
- Validating grid layouts
|
|
459
471
|
- Checking responsive design consistency
|
|
460
472
|
|
|
473
|
+
#### `inspect_ancestors` โญ **DEBUG LAYOUT CONSTRAINTS**
|
|
474
|
+
Walk up the DOM tree to find where width constraints, margins, borders, and overflow clipping come from. Shows position, size, and layout-critical CSS for each ancestor up to `<body>`.
|
|
475
|
+
|
|
476
|
+
**Key Features:**
|
|
477
|
+
- Default depth: 10 levels (reaches `<body>` in most React apps)
|
|
478
|
+
- Only shows non-default values (omits `border:none`, `overflow:visible`)
|
|
479
|
+
- Auto-detects overflow:hidden clipping, width constraints, auto-margin centering
|
|
480
|
+
- Token-efficient compact text format with diagnostic annotations (๐ฏโ ๏ธ)
|
|
481
|
+
|
|
482
|
+
**Use Cases:**
|
|
483
|
+
- Finding where unexpected margins come from (auto-centering)
|
|
484
|
+
- Discovering parent max-width constraints
|
|
485
|
+
- Locating overflow:hidden containers that clip elements
|
|
486
|
+
- Understanding why elements have constrained widths
|
|
487
|
+
- Debugging deeply nested component library layouts (Material-UI, Chakra, Ant Design)
|
|
488
|
+
|
|
489
|
+
**Example:**
|
|
490
|
+
```
|
|
491
|
+
inspect_ancestors({ selector: "testid:header" })
|
|
492
|
+
โ Shows: [0] header (896px, margins: 160px)
|
|
493
|
+
[1] div (1216px, max-w constraint)
|
|
494
|
+
[2] body (1920px, overflow-x: hidden)
|
|
495
|
+
๐ฏ WIDTH CONSTRAINT found at parent
|
|
496
|
+
โ Auto margins centering (160px each side)
|
|
497
|
+
```
|
|
498
|
+
|
|
461
499
|
### ๐จ Style & Content Inspection
|
|
462
500
|
|
|
463
501
|
#### `get_computed_styles`
|
|
@@ -785,6 +823,32 @@ These step-by-step recipes show how to chain tools together for common testing a
|
|
|
785
823
|
|
|
786
824
|
**Why this works**: Progressive inspection + precise measurements reveal layout problems.
|
|
787
825
|
|
|
826
|
+
### Recipe 2a: Finding Where Unexpected Margins Come From
|
|
827
|
+
|
|
828
|
+
```
|
|
829
|
+
1. navigate({ url: "https://app.example.com" })
|
|
830
|
+
2. inspect_dom({ selector: "main" })
|
|
831
|
+
โ Shows header element with test ID
|
|
832
|
+
3. measure_element({ selector: "testid:event-mode-header" })
|
|
833
|
+
โ @ (160,0) 896x56px
|
|
834
|
+
โ Margin: โ160px โ160px (unexpected!)
|
|
835
|
+
๐ก Unexpected spacing detected. Check parent constraints
|
|
836
|
+
4. inspect_ancestors({ selector: "testid:event-mode-header" })
|
|
837
|
+
โ [0] <header testid:event-mode-header>
|
|
838
|
+
@ (160,0) 896x56px | w:896px max-w:896px m:0 160px
|
|
839
|
+
border-bottom: 1px solid #e5e7eb
|
|
840
|
+
โ Auto margins centering (160px each side)
|
|
841
|
+
โ [1] <div>
|
|
842
|
+
@ (0,0) 1216x56px | w:1216px
|
|
843
|
+
โ [2] <div> flex max-w-[1600px]
|
|
844
|
+
@ (352,60) 1216x900px
|
|
845
|
+
max-width: 1600px
|
|
846
|
+
๐ฏ WIDTH CONSTRAINT
|
|
847
|
+
5. Solution: Remove mx-auto from header (centering already handled by parent)
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
**Why this works**: `inspect_ancestors` traces the layout constraint chain to find the root cause of unexpected spacing in deeply nested React components.
|
|
851
|
+
|
|
788
852
|
### Recipe 3: API Response Testing
|
|
789
853
|
|
|
790
854
|
```
|
|
@@ -953,6 +1017,71 @@ These step-by-step recipes show how to chain tools together for common testing a
|
|
|
953
1017
|
|
|
954
1018
|
**Why this works**: DOM inspection + attribute queries reveal accessibility issues.
|
|
955
1019
|
|
|
1020
|
+
## Troubleshooting
|
|
1021
|
+
|
|
1022
|
+
### Browser Installation Issues
|
|
1023
|
+
|
|
1024
|
+
**Symptom**: Error message about Playwright browsers not being installed, or browser fails to launch.
|
|
1025
|
+
|
|
1026
|
+
**How it works**: Browsers are **automatically installed on first use** when you run any navigation tool. The installation happens once (~1GB download) and browsers are stored in your home directory, shared across all projects.
|
|
1027
|
+
|
|
1028
|
+
**What you'll see on first use**:
|
|
1029
|
+
```
|
|
1030
|
+
๐ญ Playwright browsers not found. Installing automatically...
|
|
1031
|
+
โณ This will download ~1GB of browser binaries. Please wait...
|
|
1032
|
+
[Installation progress...]
|
|
1033
|
+
โ
Browsers installed successfully! Starting browser...
|
|
1034
|
+
```
|
|
1035
|
+
|
|
1036
|
+
**If automatic installation fails** (firewall, permissions, etc.):
|
|
1037
|
+
|
|
1038
|
+
```bash
|
|
1039
|
+
# Manual installation - run this command:
|
|
1040
|
+
npx playwright install chromium firefox webkit
|
|
1041
|
+
|
|
1042
|
+
# With system dependencies (requires admin/sudo):
|
|
1043
|
+
npx playwright install --with-deps chromium firefox webkit
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
**For GitHub Copilot / VS Code users**:
|
|
1047
|
+
- First tool use will auto-install browsers (1-2 minute wait)
|
|
1048
|
+
- Subsequent uses are instant
|
|
1049
|
+
- Installation happens in the background with progress messages
|
|
1050
|
+
- If manual installation is needed, restart your IDE after running the command
|
|
1051
|
+
|
|
1052
|
+
### Server Not Loading
|
|
1053
|
+
|
|
1054
|
+
**Symptom**: Tools from web-inspector are not available in your AI assistant.
|
|
1055
|
+
|
|
1056
|
+
**Solutions**:
|
|
1057
|
+
1. Verify the configuration file is correct (see AI Tool Setup section above)
|
|
1058
|
+
2. Restart your AI tool completely (not just reload window)
|
|
1059
|
+
3. Check server logs (location depends on your AI tool)
|
|
1060
|
+
4. Try removing and re-adding the server configuration
|
|
1061
|
+
|
|
1062
|
+
### Permission Issues
|
|
1063
|
+
|
|
1064
|
+
**Symptom**: Permission denied errors when installing browsers.
|
|
1065
|
+
|
|
1066
|
+
**Solutions**:
|
|
1067
|
+
```bash
|
|
1068
|
+
# If using global installation, you may need sudo (Linux/macOS)
|
|
1069
|
+
sudo npm install -g mcp-web-inspector
|
|
1070
|
+
|
|
1071
|
+
# Or use npx without global installation (recommended)
|
|
1072
|
+
# Just configure with "npx -y mcp-web-inspector" as shown in setup
|
|
1073
|
+
```
|
|
1074
|
+
|
|
1075
|
+
### Browser Crashes or Disconnects
|
|
1076
|
+
|
|
1077
|
+
**Symptom**: Browser becomes unresponsive or disconnects during use.
|
|
1078
|
+
|
|
1079
|
+
**The MCP server automatically handles this**:
|
|
1080
|
+
- Detects disconnected browsers
|
|
1081
|
+
- Resets state and provides clear error messages
|
|
1082
|
+
- Instructs you to retry the navigation/action
|
|
1083
|
+
- No manual intervention needed - just retry your command
|
|
1084
|
+
|
|
956
1085
|
## Development
|
|
957
1086
|
|
|
958
1087
|
### Testing
|
package/dist/toolHandler.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { chromium, firefox, webkit, devices } from 'playwright';
|
|
2
2
|
import { BROWSER_TOOLS } from './tools.js';
|
|
3
|
+
import { checkBrowsersInstalled, getInstallationInstructions } from './utils/browserCheck.js';
|
|
3
4
|
import { ScreenshotTool, NavigationTool, CloseBrowserTool, ConsoleLogsTool } from './tools/browser/index.js';
|
|
4
5
|
import { ClickTool, FillTool, SelectTool, HoverTool, EvaluateTool, UploadFileTool } from './tools/browser/interaction.js';
|
|
5
6
|
import { VisibleTextTool, VisibleHtmlTool } from './tools/browser/visiblePage.js';
|
|
@@ -12,6 +13,7 @@ import { GetComputedStylesTool } from './tools/browser/computedStyles.js';
|
|
|
12
13
|
import { MeasureElementTool } from './tools/browser/measureElement.js';
|
|
13
14
|
import { ElementExistsTool } from './tools/browser/elementExists.js';
|
|
14
15
|
import { CompareElementAlignmentTool } from './tools/browser/compareElementAlignment.js';
|
|
16
|
+
import { InspectAncestorsTool } from './tools/browser/ancestorInspection.js';
|
|
15
17
|
import { GoBackTool, GoForwardTool } from './tools/browser/navigation.js';
|
|
16
18
|
import { DragTool, PressKeyTool } from './tools/browser/interaction.js';
|
|
17
19
|
import { WaitForElementTool } from './tools/browser/waitForElement.js';
|
|
@@ -97,6 +99,7 @@ let getComputedStylesTool;
|
|
|
97
99
|
let measureElementTool;
|
|
98
100
|
let elementExistsTool;
|
|
99
101
|
let compareElementAlignmentTool;
|
|
102
|
+
let inspectAncestorsTool;
|
|
100
103
|
let waitForElementTool;
|
|
101
104
|
let waitForNetworkIdleTool;
|
|
102
105
|
let listNetworkRequestsTool;
|
|
@@ -162,13 +165,21 @@ async function registerConsoleMessage(page) {
|
|
|
162
165
|
page.on("console", (msg) => {
|
|
163
166
|
if (consoleLogsTool) {
|
|
164
167
|
const type = msg.type();
|
|
165
|
-
|
|
168
|
+
let text = msg.text();
|
|
166
169
|
// "Unhandled Rejection In Promise" we injected
|
|
167
170
|
if (text.startsWith("[Playwright]")) {
|
|
168
171
|
const payload = text.replace("[Playwright]", "");
|
|
169
172
|
consoleLogsTool.registerConsoleMessage("exception", payload);
|
|
170
173
|
}
|
|
171
174
|
else {
|
|
175
|
+
// Truncate stack traces for error messages to keep output compact
|
|
176
|
+
if (type === 'error' && text.includes('\n')) {
|
|
177
|
+
const lines = text.split('\n');
|
|
178
|
+
// Keep first line (error message) and up to 3 stack trace lines
|
|
179
|
+
if (lines.length > 4) {
|
|
180
|
+
text = lines.slice(0, 4).join('\n') + '\n ...[stack trace truncated]';
|
|
181
|
+
}
|
|
182
|
+
}
|
|
172
183
|
consoleLogsTool.registerConsoleMessage(type, text);
|
|
173
184
|
}
|
|
174
185
|
}
|
|
@@ -200,11 +211,66 @@ async function registerConsoleMessage(page) {
|
|
|
200
211
|
});
|
|
201
212
|
});
|
|
202
213
|
}
|
|
214
|
+
// Track if we've checked browser installation
|
|
215
|
+
let browserInstallationChecked = false;
|
|
216
|
+
/**
|
|
217
|
+
* Gets the screen size using Playwright's API
|
|
218
|
+
*/
|
|
219
|
+
async function getScreenSize() {
|
|
220
|
+
try {
|
|
221
|
+
// Launch a temporary browser to get screen size
|
|
222
|
+
const tempBrowser = await chromium.launch({ headless: true });
|
|
223
|
+
const tempContext = await tempBrowser.newContext();
|
|
224
|
+
const tempPage = await tempContext.newPage();
|
|
225
|
+
const screenSize = await tempPage.evaluate(() => {
|
|
226
|
+
return {
|
|
227
|
+
width: window.screen.width,
|
|
228
|
+
height: window.screen.height
|
|
229
|
+
};
|
|
230
|
+
});
|
|
231
|
+
await tempBrowser.close();
|
|
232
|
+
// Validate the screen size values
|
|
233
|
+
if (!screenSize || typeof screenSize.width !== 'number' || typeof screenSize.height !== 'number') {
|
|
234
|
+
console.error('Invalid screen size detected, using defaults');
|
|
235
|
+
return { width: 1280, height: 720 };
|
|
236
|
+
}
|
|
237
|
+
return screenSize;
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
console.error('Failed to detect screen size, using defaults:', error);
|
|
241
|
+
return { width: 1280, height: 720 };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
203
244
|
/**
|
|
204
245
|
* Ensures a browser is launched and returns the page
|
|
205
246
|
*/
|
|
206
247
|
export async function ensureBrowser(browserSettings) {
|
|
207
248
|
try {
|
|
249
|
+
// Check if browsers are installed on first launch (only once)
|
|
250
|
+
if (!browser && !browserInstallationChecked) {
|
|
251
|
+
browserInstallationChecked = true;
|
|
252
|
+
const browserCheck = checkBrowsersInstalled();
|
|
253
|
+
if (!browserCheck.installed) {
|
|
254
|
+
// Try to install browsers automatically
|
|
255
|
+
console.error('๐ญ Playwright browsers not found. Installing automatically...');
|
|
256
|
+
console.error('โณ This will download ~1GB of browser binaries. Please wait...');
|
|
257
|
+
try {
|
|
258
|
+
const { execSync } = await import('child_process');
|
|
259
|
+
execSync('npx playwright install chromium firefox webkit', {
|
|
260
|
+
stdio: 'inherit',
|
|
261
|
+
encoding: 'utf8'
|
|
262
|
+
});
|
|
263
|
+
console.error('โ
Browsers installed successfully! Starting browser...');
|
|
264
|
+
// Note: browser variable is still undefined here, which is correct.
|
|
265
|
+
// The code below (line 342) will launch the browser after installation.
|
|
266
|
+
}
|
|
267
|
+
catch (installError) {
|
|
268
|
+
// If auto-install fails, show instructions
|
|
269
|
+
const instructions = getInstallationInstructions();
|
|
270
|
+
throw new Error(`Playwright browsers not installed.\n\n${instructions}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
208
274
|
// Check if browser exists but is disconnected
|
|
209
275
|
if (browser && !browser.isConnected()) {
|
|
210
276
|
console.error("Browser exists but is disconnected. Cleaning up...");
|
|
@@ -273,6 +339,23 @@ export async function ensureBrowser(browserSettings) {
|
|
|
273
339
|
break;
|
|
274
340
|
}
|
|
275
341
|
const executablePath = process.env.CHROME_EXECUTABLE_PATH;
|
|
342
|
+
// Determine viewport size
|
|
343
|
+
let viewportWidth;
|
|
344
|
+
let viewportHeight;
|
|
345
|
+
if (viewport?.width !== undefined || viewport?.height !== undefined) {
|
|
346
|
+
// If any viewport dimension is specified, use specified values or defaults
|
|
347
|
+
viewportWidth = viewport?.width ?? 1280;
|
|
348
|
+
viewportHeight = viewport?.height ?? 720;
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
// If no viewport specified, detect screen size
|
|
352
|
+
const screenSize = await getScreenSize();
|
|
353
|
+
viewportWidth = screenSize?.width ?? 1280;
|
|
354
|
+
viewportHeight = screenSize?.height ?? 720;
|
|
355
|
+
if (screenSize && screenSize.width > 0 && screenSize.height > 0) {
|
|
356
|
+
console.error(`No viewport specified, using screen size: ${viewportWidth}x${viewportHeight}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
276
359
|
// Prepare context options
|
|
277
360
|
const contextOptions = {
|
|
278
361
|
headless,
|
|
@@ -287,8 +370,8 @@ export async function ensureBrowser(browserSettings) {
|
|
|
287
370
|
contextOptions.userAgent = userAgent;
|
|
288
371
|
}
|
|
289
372
|
contextOptions.viewport = {
|
|
290
|
-
width:
|
|
291
|
-
height:
|
|
373
|
+
width: viewportWidth,
|
|
374
|
+
height: viewportHeight,
|
|
292
375
|
};
|
|
293
376
|
contextOptions.deviceScaleFactor = 1;
|
|
294
377
|
}
|
|
@@ -331,8 +414,8 @@ export async function ensureBrowser(browserSettings) {
|
|
|
331
414
|
newContextOptions.userAgent = userAgent;
|
|
332
415
|
}
|
|
333
416
|
newContextOptions.viewport = {
|
|
334
|
-
width:
|
|
335
|
-
height:
|
|
417
|
+
width: viewportWidth,
|
|
418
|
+
height: viewportHeight,
|
|
336
419
|
};
|
|
337
420
|
newContextOptions.deviceScaleFactor = 1;
|
|
338
421
|
}
|
|
@@ -390,6 +473,20 @@ export async function ensureBrowser(browserSettings) {
|
|
|
390
473
|
break;
|
|
391
474
|
}
|
|
392
475
|
const executablePath = process.env.CHROME_EXECUTABLE_PATH;
|
|
476
|
+
// Determine viewport size for retry
|
|
477
|
+
let retryViewportWidth;
|
|
478
|
+
let retryViewportHeight;
|
|
479
|
+
if (viewport?.width !== undefined || viewport?.height !== undefined) {
|
|
480
|
+
// If any viewport dimension is specified, use specified values or defaults
|
|
481
|
+
retryViewportWidth = viewport?.width ?? 1280;
|
|
482
|
+
retryViewportHeight = viewport?.height ?? 720;
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
// If no viewport specified, detect screen size
|
|
486
|
+
const screenSize = await getScreenSize();
|
|
487
|
+
retryViewportWidth = screenSize?.width ?? 1280;
|
|
488
|
+
retryViewportHeight = screenSize?.height ?? 720;
|
|
489
|
+
}
|
|
393
490
|
// Prepare context options
|
|
394
491
|
const retryContextOptions = {
|
|
395
492
|
headless,
|
|
@@ -404,8 +501,8 @@ export async function ensureBrowser(browserSettings) {
|
|
|
404
501
|
retryContextOptions.userAgent = userAgent;
|
|
405
502
|
}
|
|
406
503
|
retryContextOptions.viewport = {
|
|
407
|
-
width:
|
|
408
|
-
height:
|
|
504
|
+
width: retryViewportWidth,
|
|
505
|
+
height: retryViewportHeight,
|
|
409
506
|
};
|
|
410
507
|
retryContextOptions.deviceScaleFactor = 1;
|
|
411
508
|
}
|
|
@@ -444,8 +541,8 @@ export async function ensureBrowser(browserSettings) {
|
|
|
444
541
|
retryNewContextOptions.userAgent = userAgent;
|
|
445
542
|
}
|
|
446
543
|
retryNewContextOptions.viewport = {
|
|
447
|
-
width:
|
|
448
|
-
height:
|
|
544
|
+
width: retryViewportWidth,
|
|
545
|
+
height: retryViewportHeight,
|
|
449
546
|
};
|
|
450
547
|
retryNewContextOptions.deviceScaleFactor = 1;
|
|
451
548
|
}
|
|
@@ -512,6 +609,8 @@ function initializeTools(server) {
|
|
|
512
609
|
elementExistsTool = new ElementExistsTool(server);
|
|
513
610
|
if (!compareElementAlignmentTool)
|
|
514
611
|
compareElementAlignmentTool = new CompareElementAlignmentTool(server);
|
|
612
|
+
if (!inspectAncestorsTool)
|
|
613
|
+
inspectAncestorsTool = new InspectAncestorsTool(server);
|
|
515
614
|
if (!waitForElementTool)
|
|
516
615
|
waitForElementTool = new WaitForElementTool(server);
|
|
517
616
|
if (!waitForNetworkIdleTool)
|
|
@@ -653,6 +752,8 @@ export async function handleToolCall(name, args, server) {
|
|
|
653
752
|
return await elementExistsTool.execute(args, context);
|
|
654
753
|
case "compare_element_alignment":
|
|
655
754
|
return await compareElementAlignmentTool.execute(args, context);
|
|
755
|
+
case "inspect_ancestors":
|
|
756
|
+
return await inspectAncestorsTool.execute(args, context);
|
|
656
757
|
case "wait_for_element":
|
|
657
758
|
return await waitForElementTool.execute(args, context);
|
|
658
759
|
case "wait_for_network_idle":
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BrowserToolBase } from "./base.js";
|
|
2
|
+
import type { ToolResponse, ToolContext } from "../common/types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Tool to inspect ancestor chain of an element
|
|
5
|
+
* Shows parent elements up the DOM tree with layout-critical CSS properties
|
|
6
|
+
*/
|
|
7
|
+
export declare class InspectAncestorsTool extends BrowserToolBase {
|
|
8
|
+
execute(args: {
|
|
9
|
+
selector: string;
|
|
10
|
+
limit?: number;
|
|
11
|
+
}, context: ToolContext): Promise<ToolResponse>;
|
|
12
|
+
private formatAncestorChain;
|
|
13
|
+
private formatBorder;
|
|
14
|
+
private formatOverflow;
|
|
15
|
+
private generateDiagnostics;
|
|
16
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { BrowserToolBase } from "./base.js";
|
|
2
|
+
/**
|
|
3
|
+
* Tool to inspect ancestor chain of an element
|
|
4
|
+
* Shows parent elements up the DOM tree with layout-critical CSS properties
|
|
5
|
+
*/
|
|
6
|
+
export class InspectAncestorsTool extends BrowserToolBase {
|
|
7
|
+
async execute(args, context) {
|
|
8
|
+
return this.safeExecute(context, async (page) => {
|
|
9
|
+
const limit = Math.min(args.limit ?? 10, 15); // Default 10, max 15
|
|
10
|
+
const normalizedSelector = this.normalizeSelector(args.selector);
|
|
11
|
+
const ancestors = await page.evaluate(({ sel, lim }) => {
|
|
12
|
+
const element = document.querySelector(sel);
|
|
13
|
+
if (!element) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const chain = [];
|
|
17
|
+
let current = element;
|
|
18
|
+
for (let i = 0; i < lim && current; i++) {
|
|
19
|
+
const rect = current.getBoundingClientRect();
|
|
20
|
+
const computed = window.getComputedStyle(current);
|
|
21
|
+
chain.push({
|
|
22
|
+
tagName: current.tagName.toLowerCase(),
|
|
23
|
+
testId: current.getAttribute("data-testid"),
|
|
24
|
+
classes: current.className,
|
|
25
|
+
rect: {
|
|
26
|
+
x: Math.round(rect.x),
|
|
27
|
+
y: Math.round(rect.y),
|
|
28
|
+
width: Math.round(rect.width),
|
|
29
|
+
height: Math.round(rect.height),
|
|
30
|
+
},
|
|
31
|
+
// Layout-critical properties
|
|
32
|
+
width: computed.width,
|
|
33
|
+
maxWidth: computed.maxWidth,
|
|
34
|
+
minWidth: computed.minWidth,
|
|
35
|
+
margin: computed.margin,
|
|
36
|
+
padding: computed.padding,
|
|
37
|
+
display: computed.display,
|
|
38
|
+
overflow: computed.overflow,
|
|
39
|
+
overflowX: computed.overflowX,
|
|
40
|
+
overflowY: computed.overflowY,
|
|
41
|
+
border: computed.border,
|
|
42
|
+
borderTop: computed.borderTop,
|
|
43
|
+
borderRight: computed.borderRight,
|
|
44
|
+
borderBottom: computed.borderBottom,
|
|
45
|
+
borderLeft: computed.borderLeft,
|
|
46
|
+
// Flexbox
|
|
47
|
+
flexDirection: computed.flexDirection,
|
|
48
|
+
justifyContent: computed.justifyContent,
|
|
49
|
+
alignItems: computed.alignItems,
|
|
50
|
+
// Conditional
|
|
51
|
+
position: computed.position !== "static" ? computed.position : undefined,
|
|
52
|
+
zIndex: computed.zIndex !== "auto" ? computed.zIndex : undefined,
|
|
53
|
+
transform: computed.transform !== "none" ? computed.transform : undefined,
|
|
54
|
+
});
|
|
55
|
+
current = current.parentElement;
|
|
56
|
+
}
|
|
57
|
+
return chain;
|
|
58
|
+
}, { sel: normalizedSelector, lim: limit });
|
|
59
|
+
if (!ancestors) {
|
|
60
|
+
return {
|
|
61
|
+
content: [
|
|
62
|
+
{
|
|
63
|
+
type: "text",
|
|
64
|
+
text: `Error: Element not found with selector "${args.selector}"`,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
isError: true,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
content: [
|
|
72
|
+
{
|
|
73
|
+
type: "text",
|
|
74
|
+
text: this.formatAncestorChain(ancestors, args.selector),
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
isError: false,
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
formatAncestorChain(ancestors, originalSelector) {
|
|
82
|
+
const lines = [`Ancestor Chain: ${originalSelector}\n`];
|
|
83
|
+
ancestors.forEach((ancestor, index) => {
|
|
84
|
+
const parts = [];
|
|
85
|
+
// Tag and identifier
|
|
86
|
+
let identifier = `[${index}] <${ancestor.tagName}>`;
|
|
87
|
+
if (ancestor.testId) {
|
|
88
|
+
identifier += ` | testid:${ancestor.testId}`;
|
|
89
|
+
}
|
|
90
|
+
else if (ancestor.classes) {
|
|
91
|
+
// Show first few classes for context
|
|
92
|
+
const classes = ancestor.classes.trim().split(/\s+/).slice(0, 3);
|
|
93
|
+
if (classes.length > 0 && classes[0]) {
|
|
94
|
+
identifier += ` | ${classes.join(" ")}`;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
parts.push(identifier);
|
|
98
|
+
// Position and size (always show)
|
|
99
|
+
const layoutInfo = [];
|
|
100
|
+
parts.push(`\n @ (${ancestor.rect.x},${ancestor.rect.y}) ${ancestor.rect.width}x${ancestor.rect.height}px`);
|
|
101
|
+
// Width info (always show)
|
|
102
|
+
layoutInfo.push(`w:${ancestor.width}`);
|
|
103
|
+
// Display (only if not block)
|
|
104
|
+
if (ancestor.display !== "block") {
|
|
105
|
+
layoutInfo.push(`display:${ancestor.display}`);
|
|
106
|
+
if (ancestor.flexDirection && ancestor.flexDirection !== "row") {
|
|
107
|
+
layoutInfo.push(ancestor.flexDirection);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Only show non-default values
|
|
111
|
+
if (ancestor.margin !== "0px") {
|
|
112
|
+
layoutInfo.push(`m:${ancestor.margin}`);
|
|
113
|
+
}
|
|
114
|
+
if (ancestor.padding !== "0px") {
|
|
115
|
+
layoutInfo.push(`p:${ancestor.padding}`);
|
|
116
|
+
}
|
|
117
|
+
if (ancestor.maxWidth !== "none") {
|
|
118
|
+
layoutInfo.push(`max-w:${ancestor.maxWidth}`);
|
|
119
|
+
}
|
|
120
|
+
if (ancestor.minWidth !== "0px") {
|
|
121
|
+
layoutInfo.push(`min-w:${ancestor.minWidth}`);
|
|
122
|
+
}
|
|
123
|
+
if (layoutInfo.length > 0) {
|
|
124
|
+
parts.push(` | ${layoutInfo.join(" ")}`);
|
|
125
|
+
}
|
|
126
|
+
// Border - only if set
|
|
127
|
+
const borderInfo = this.formatBorder(ancestor);
|
|
128
|
+
if (borderInfo) {
|
|
129
|
+
parts.push(`\n ${borderInfo}`);
|
|
130
|
+
}
|
|
131
|
+
// Overflow - only if not visible
|
|
132
|
+
const overflowInfo = this.formatOverflow(ancestor);
|
|
133
|
+
if (overflowInfo) {
|
|
134
|
+
parts.push(`\n ${overflowInfo}`);
|
|
135
|
+
}
|
|
136
|
+
// Position, z-index, transform (only if set)
|
|
137
|
+
const extraInfo = [];
|
|
138
|
+
if (ancestor.position) {
|
|
139
|
+
extraInfo.push(`position:${ancestor.position}`);
|
|
140
|
+
}
|
|
141
|
+
if (ancestor.zIndex) {
|
|
142
|
+
extraInfo.push(`z-index:${ancestor.zIndex}`);
|
|
143
|
+
}
|
|
144
|
+
if (ancestor.transform) {
|
|
145
|
+
extraInfo.push(`transform:${ancestor.transform}`);
|
|
146
|
+
}
|
|
147
|
+
if (extraInfo.length > 0) {
|
|
148
|
+
parts.push(`\n ${extraInfo.join(", ")}`);
|
|
149
|
+
}
|
|
150
|
+
// Add diagnostics
|
|
151
|
+
const diagnostics = this.generateDiagnostics(ancestor, index);
|
|
152
|
+
if (diagnostics) {
|
|
153
|
+
parts.push(`\n ${diagnostics}`);
|
|
154
|
+
}
|
|
155
|
+
lines.push(parts.join(""));
|
|
156
|
+
});
|
|
157
|
+
return lines.join("\n\n");
|
|
158
|
+
}
|
|
159
|
+
formatBorder(ancestor) {
|
|
160
|
+
// Check if main border is set
|
|
161
|
+
if (ancestor.border &&
|
|
162
|
+
ancestor.border !== "none" &&
|
|
163
|
+
ancestor.border !== "0px none" &&
|
|
164
|
+
!ancestor.border.startsWith("0px")) {
|
|
165
|
+
return `border: ${ancestor.border}`;
|
|
166
|
+
}
|
|
167
|
+
// Check directional borders
|
|
168
|
+
const borders = [];
|
|
169
|
+
if (ancestor.borderTop &&
|
|
170
|
+
ancestor.borderTop !== "none" &&
|
|
171
|
+
!ancestor.borderTop.startsWith("0px")) {
|
|
172
|
+
borders.push(`top:${ancestor.borderTop}`);
|
|
173
|
+
}
|
|
174
|
+
if (ancestor.borderRight &&
|
|
175
|
+
ancestor.borderRight !== "none" &&
|
|
176
|
+
!ancestor.borderRight.startsWith("0px")) {
|
|
177
|
+
borders.push(`right:${ancestor.borderRight}`);
|
|
178
|
+
}
|
|
179
|
+
if (ancestor.borderBottom &&
|
|
180
|
+
ancestor.borderBottom !== "none" &&
|
|
181
|
+
!ancestor.borderBottom.startsWith("0px")) {
|
|
182
|
+
borders.push(`bottom:${ancestor.borderBottom}`);
|
|
183
|
+
}
|
|
184
|
+
if (ancestor.borderLeft &&
|
|
185
|
+
ancestor.borderLeft !== "none" &&
|
|
186
|
+
!ancestor.borderLeft.startsWith("0px")) {
|
|
187
|
+
borders.push(`left:${ancestor.borderLeft}`);
|
|
188
|
+
}
|
|
189
|
+
if (borders.length > 0) {
|
|
190
|
+
return `border: ${borders.join(", ")}`;
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
formatOverflow(ancestor) {
|
|
195
|
+
const parts = [];
|
|
196
|
+
// Handle uniform overflow
|
|
197
|
+
if (ancestor.overflow !== "visible" &&
|
|
198
|
+
ancestor.overflowX === ancestor.overflow &&
|
|
199
|
+
ancestor.overflowY === ancestor.overflow) {
|
|
200
|
+
const icon = ancestor.overflow === "hidden"
|
|
201
|
+
? "๐"
|
|
202
|
+
: ancestor.overflow === "auto" || ancestor.overflow === "scroll"
|
|
203
|
+
? "โ๏ธ"
|
|
204
|
+
: "";
|
|
205
|
+
return `overflow: ${icon} ${ancestor.overflow}`;
|
|
206
|
+
}
|
|
207
|
+
// Handle different overflow-x/y
|
|
208
|
+
if (ancestor.overflowX !== "visible" ||
|
|
209
|
+
ancestor.overflowY !== "visible") {
|
|
210
|
+
const xIcon = ancestor.overflowX === "hidden"
|
|
211
|
+
? "๐"
|
|
212
|
+
: ancestor.overflowX === "auto" || ancestor.overflowX === "scroll"
|
|
213
|
+
? "โ๏ธ"
|
|
214
|
+
: "";
|
|
215
|
+
const yIcon = ancestor.overflowY === "hidden"
|
|
216
|
+
? "๐"
|
|
217
|
+
: ancestor.overflowY === "auto" || ancestor.overflowY === "scroll"
|
|
218
|
+
? "โ๏ธ"
|
|
219
|
+
: "";
|
|
220
|
+
parts.push(`overflow-x: ${xIcon} ${ancestor.overflowX}, overflow-y: ${yIcon} ${ancestor.overflowY}`);
|
|
221
|
+
return parts.join(", ");
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
generateDiagnostics(ancestor, index) {
|
|
226
|
+
const diagnostics = [];
|
|
227
|
+
// Overflow hidden warning
|
|
228
|
+
if (ancestor.overflow === "hidden" || ancestor.overflowY === "hidden") {
|
|
229
|
+
diagnostics.push("๐ฏ CLIPPING POINT - May clip overflowing children");
|
|
230
|
+
}
|
|
231
|
+
// Width constraint detection
|
|
232
|
+
if (ancestor.maxWidth !== "none" && index > 0) {
|
|
233
|
+
diagnostics.push("๐ฏ WIDTH CONSTRAINT");
|
|
234
|
+
}
|
|
235
|
+
// Large margins (potential centering)
|
|
236
|
+
const marginMatch = ancestor.margin.match(/0px (\d+)px/);
|
|
237
|
+
if (marginMatch && parseInt(marginMatch[1]) > 100) {
|
|
238
|
+
diagnostics.push(`โ Auto margins centering (${marginMatch[1]}px each side)`);
|
|
239
|
+
}
|
|
240
|
+
return diagnostics.length > 0 ? diagnostics.join("\n ") : null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -161,6 +161,15 @@ export class CompareElementAlignmentTool extends BrowserToolBase {
|
|
|
161
161
|
` Width: ${formatDimension(widthSame, width1, width2, widthDiff)}`,
|
|
162
162
|
` Height: ${formatDimension(heightSame, height1, height2, heightDiff)}`
|
|
163
163
|
];
|
|
164
|
+
// Suggest inspect_ancestors if alignment is off and differences are significant
|
|
165
|
+
const hasSignificantDifference = Math.abs(topDiff) > 5 || Math.abs(leftDiff) > 5 || Math.abs(rightDiff) > 5;
|
|
166
|
+
const isNotAligned = !topAligned && !leftAligned && !rightAligned && !bottomAligned;
|
|
167
|
+
if (isNotAligned && hasSignificantDifference) {
|
|
168
|
+
lines.push('');
|
|
169
|
+
lines.push('๐ก Alignment issue detected. Check if parent layout affects positioning:');
|
|
170
|
+
lines.push(` inspect_ancestors({ selector: "${args.selector1}" })`);
|
|
171
|
+
lines.push(` inspect_ancestors({ selector: "${args.selector2}" })`);
|
|
172
|
+
}
|
|
164
173
|
return createSuccessResponse(lines.filter(l => l !== undefined).join('\n'));
|
|
165
174
|
}
|
|
166
175
|
catch (error) {
|
|
@@ -214,6 +214,11 @@ export class ElementVisibilityTool extends BrowserToolBase {
|
|
|
214
214
|
if (suggestions.length > 0) {
|
|
215
215
|
output += '\n' + suggestions.join('\n');
|
|
216
216
|
}
|
|
217
|
+
// Suggest inspect_ancestors if element is clipped
|
|
218
|
+
if (visibilityData.isClipped) {
|
|
219
|
+
output += '\n\n๐ก Element clipped by parent. Find the clipping container:';
|
|
220
|
+
output += `\n inspect_ancestors({ selector: "${args.selector}" })`;
|
|
221
|
+
}
|
|
217
222
|
return createSuccessResponse(output.trim());
|
|
218
223
|
}
|
|
219
224
|
catch (error) {
|
|
@@ -443,6 +443,12 @@ export class InspectDomTool extends BrowserToolBase {
|
|
|
443
443
|
lines.push(`๐ก Tip: Some elements found, but ${stats.skippedWrappers} wrapper divs were skipped.`);
|
|
444
444
|
lines.push(' Consider adding test IDs to key elements for easier selection.');
|
|
445
445
|
}
|
|
446
|
+
// Suggest inspect_ancestors when drilling through many wrappers
|
|
447
|
+
if (stats.skippedWrappers >= 3) {
|
|
448
|
+
lines.push('');
|
|
449
|
+
lines.push(`๐ก Drilled through ${stats.skippedWrappers} wrapper levels. To see parent constraints:`);
|
|
450
|
+
lines.push(` inspect_ancestors({ selector: "${args.selector || 'body'}" })`);
|
|
451
|
+
}
|
|
446
452
|
}
|
|
447
453
|
return createSuccessResponse(lines.join('\n'));
|
|
448
454
|
}
|
|
@@ -76,6 +76,10 @@ export declare class UploadFileTool extends BrowserToolBase {
|
|
|
76
76
|
* Tool for executing JavaScript in the browser
|
|
77
77
|
*/
|
|
78
78
|
export declare class EvaluateTool extends BrowserToolBase {
|
|
79
|
+
/**
|
|
80
|
+
* Detect common patterns and suggest better tools
|
|
81
|
+
*/
|
|
82
|
+
private detectBetterToolSuggestions;
|
|
79
83
|
/**
|
|
80
84
|
* Execute the evaluate tool
|
|
81
85
|
*/
|
|
@@ -160,6 +160,52 @@ export class UploadFileTool extends BrowserToolBase {
|
|
|
160
160
|
* Tool for executing JavaScript in the browser
|
|
161
161
|
*/
|
|
162
162
|
export class EvaluateTool extends BrowserToolBase {
|
|
163
|
+
/**
|
|
164
|
+
* Detect common patterns and suggest better tools
|
|
165
|
+
*/
|
|
166
|
+
detectBetterToolSuggestions(script) {
|
|
167
|
+
const suggestions = [];
|
|
168
|
+
const scriptLower = script.toLowerCase();
|
|
169
|
+
// Pattern: DOM inspection/querying
|
|
170
|
+
if (scriptLower.match(/queryselector|getelementby|getelement|innerhtml|outerhtml|children|childnodes/)) {
|
|
171
|
+
suggestions.push('๐ DOM Inspection - Use inspect_dom({ selector: "..." }) for page structure');
|
|
172
|
+
}
|
|
173
|
+
// Pattern: Getting text content
|
|
174
|
+
if (scriptLower.match(/textcontent|innertext/)) {
|
|
175
|
+
suggestions.push('๐ Text Content - Use get_visible_text() or find_by_text({ text: "..." })');
|
|
176
|
+
}
|
|
177
|
+
// Pattern: Getting element position/size/layout
|
|
178
|
+
if (scriptLower.match(/getboundingclientrect|offsetwidth|offsetheight|offsetleft|offsettop|clientwidth|clientheight/)) {
|
|
179
|
+
suggestions.push('๐ Element Measurements - Use measure_element({ selector: "..." })');
|
|
180
|
+
}
|
|
181
|
+
// Pattern: Walking up DOM tree / checking parents
|
|
182
|
+
if (scriptLower.match(/parentelement|parentnode|offsetparent|closest/) ||
|
|
183
|
+
(scriptLower.match(/while.*parent/) && scriptLower.match(/getcomputedstyle/))) {
|
|
184
|
+
suggestions.push('๐ผ Parent Chain - Use inspect_ancestors({ selector: "..." }) to see parent constraints');
|
|
185
|
+
}
|
|
186
|
+
// Pattern: Checking visibility
|
|
187
|
+
if (scriptLower.match(/offsetparent|visibility|display.*none|opacity/)) {
|
|
188
|
+
suggestions.push('๐๏ธ Visibility Check - Use element_visibility({ selector: "..." })');
|
|
189
|
+
}
|
|
190
|
+
// Pattern: Getting computed styles
|
|
191
|
+
if (scriptLower.match(/getcomputedstyle|style\.|currentstyle/)) {
|
|
192
|
+
suggestions.push('๐จ CSS Styles - Use get_computed_styles({ selector: "..." })');
|
|
193
|
+
}
|
|
194
|
+
// Pattern: Checking element existence
|
|
195
|
+
if (scriptLower.match(/\!=\s*null|\!==\s*null/) && scriptLower.match(/queryselector/)) {
|
|
196
|
+
suggestions.push('โ Element Existence - Use element_exists({ selector: "..." })');
|
|
197
|
+
}
|
|
198
|
+
// Pattern: Finding test IDs
|
|
199
|
+
if (scriptLower.match(/data-testid|data-test|data-cy/)) {
|
|
200
|
+
suggestions.push('๐ Test IDs - Use get_test_ids() to discover all test identifiers');
|
|
201
|
+
}
|
|
202
|
+
// Pattern: Comparing positions/alignment
|
|
203
|
+
if (scriptLower.match(/getboundingclientrect.*getboundingclientrect/) ||
|
|
204
|
+
(scriptLower.match(/\.left|\.top|\.right|\.bottom/) && scriptLower.match(/===|==|!==|!=/))) {
|
|
205
|
+
suggestions.push('โ๏ธ Position Comparison - Use compare_positions({ selector1: "...", selector2: "..." })');
|
|
206
|
+
}
|
|
207
|
+
return suggestions;
|
|
208
|
+
}
|
|
163
209
|
/**
|
|
164
210
|
* Execute the evaluate tool
|
|
165
211
|
*/
|
|
@@ -175,12 +221,23 @@ export class EvaluateTool extends BrowserToolBase {
|
|
|
175
221
|
catch (error) {
|
|
176
222
|
resultStr = String(result);
|
|
177
223
|
}
|
|
178
|
-
|
|
179
|
-
|
|
224
|
+
const messages = [
|
|
225
|
+
`โ Executed JavaScript:`,
|
|
180
226
|
`${args.script}`,
|
|
227
|
+
``,
|
|
181
228
|
`Result:`,
|
|
182
229
|
`${resultStr}`
|
|
183
|
-
]
|
|
230
|
+
];
|
|
231
|
+
// Detect if specialized tools would be better
|
|
232
|
+
const suggestions = this.detectBetterToolSuggestions(args.script);
|
|
233
|
+
if (suggestions.length > 0) {
|
|
234
|
+
messages.push('');
|
|
235
|
+
messages.push('๐ก Consider using specialized tools instead:');
|
|
236
|
+
suggestions.forEach(suggestion => messages.push(` ${suggestion}`));
|
|
237
|
+
messages.push('');
|
|
238
|
+
messages.push('โน๏ธ Specialized tools are more reliable and token-efficient than evaluate()');
|
|
239
|
+
}
|
|
240
|
+
return createSuccessResponse(messages);
|
|
184
241
|
});
|
|
185
242
|
}
|
|
186
243
|
}
|
|
@@ -125,6 +125,14 @@ export class MeasureElementTool extends BrowserToolBase {
|
|
|
125
125
|
sections.push(` Margin: ${formatSpacing(measurements.marginTop, measurements.marginRight, measurements.marginBottom, measurements.marginLeft)}`);
|
|
126
126
|
sections.push('');
|
|
127
127
|
sections.push(`Total Space: ${totalWidth}x${totalHeight}px (with margin)`);
|
|
128
|
+
// Detect unusual spacing and suggest inspect_ancestors
|
|
129
|
+
const hasUnusualMargins = measurements.marginLeft > 100 || measurements.marginRight > 100;
|
|
130
|
+
const isWidthConstrained = boxWidth < 800 && (measurements.marginLeft + measurements.marginRight) > 200;
|
|
131
|
+
if (hasUnusualMargins || isWidthConstrained) {
|
|
132
|
+
sections.push('');
|
|
133
|
+
sections.push('๐ก Unexpected spacing/width detected. Check parent constraints:');
|
|
134
|
+
sections.push(` inspect_ancestors({ selector: "${args.selector}" })`);
|
|
135
|
+
}
|
|
128
136
|
return {
|
|
129
137
|
content: [
|
|
130
138
|
{
|
|
@@ -49,7 +49,7 @@ export class ScreenshotTool extends BrowserToolBase {
|
|
|
49
49
|
screenshotOptions.path = outputPath;
|
|
50
50
|
const screenshot = await page.screenshot(screenshotOptions);
|
|
51
51
|
const base64Screenshot = screenshot.toString('base64');
|
|
52
|
-
const messages = [
|
|
52
|
+
const messages = [`โ Screenshot saved to: ${path.relative(process.cwd(), outputPath)}`];
|
|
53
53
|
// Handle base64 storage
|
|
54
54
|
if (args.storeBase64 !== false) {
|
|
55
55
|
this.screenshots.set(args.name || 'screenshot', base64Screenshot);
|
|
@@ -58,6 +58,18 @@ export class ScreenshotTool extends BrowserToolBase {
|
|
|
58
58
|
});
|
|
59
59
|
messages.push(`Screenshot also stored in memory with name: '${args.name || 'screenshot'}'`);
|
|
60
60
|
}
|
|
61
|
+
// Add actionable guidance based on screenshot context
|
|
62
|
+
messages.push('');
|
|
63
|
+
messages.push('๐ก To debug layout issues in this screenshot:');
|
|
64
|
+
if (args.selector) {
|
|
65
|
+
messages.push(` inspect_ancestors({ selector: "${args.selector}" })`);
|
|
66
|
+
messages.push(' โ See parent constraints (width, margins, overflow, borders)');
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
messages.push(' 1. Find the element: inspect_dom({}) or get_test_ids()');
|
|
70
|
+
messages.push(' 2. Check parent constraints: inspect_ancestors({ selector: "..." })');
|
|
71
|
+
messages.push(' 3. Compare alignment: compare_element_alignment({ selector1: "...", selector2: "..." })');
|
|
72
|
+
}
|
|
61
73
|
return createSuccessResponse(messages);
|
|
62
74
|
});
|
|
63
75
|
}
|
package/dist/tools.d.ts
CHANGED
|
@@ -25,11 +25,11 @@ export declare function createToolDefinitions(sessionConfig?: SessionConfig): [{
|
|
|
25
25
|
};
|
|
26
26
|
readonly width: {
|
|
27
27
|
readonly type: "number";
|
|
28
|
-
readonly description: "Viewport width in pixels
|
|
28
|
+
readonly description: "Viewport width in pixels. If not specified, automatically matches screen width. Ignored if device is specified.";
|
|
29
29
|
};
|
|
30
30
|
readonly height: {
|
|
31
31
|
readonly type: "number";
|
|
32
|
-
readonly description: "Viewport height in pixels
|
|
32
|
+
readonly description: "Viewport height in pixels. If not specified, automatically matches screen height. Ignored if device is specified.";
|
|
33
33
|
};
|
|
34
34
|
readonly timeout: {
|
|
35
35
|
readonly type: "number";
|
|
@@ -468,6 +468,23 @@ export declare function createToolDefinitions(sessionConfig?: SessionConfig): [{
|
|
|
468
468
|
};
|
|
469
469
|
readonly required: readonly ["selector1", "selector2"];
|
|
470
470
|
};
|
|
471
|
+
}, {
|
|
472
|
+
readonly name: "inspect_ancestors";
|
|
473
|
+
readonly description: "DEBUG LAYOUT CONSTRAINTS: Walk up the DOM tree to find where width constraints, margins, borders, and overflow clipping come from. Essential when elements have unexpected centering (large auto margins), constrained width (max-width from parent), or are clipped (overflow:hidden ancestor). Shows position, size, and layout-critical CSS for each ancestor. Default depth: 10 levels (reaches <body> in most React apps). Use after inspect_dom() when you need to understand parent layout flow.";
|
|
474
|
+
readonly inputSchema: {
|
|
475
|
+
readonly type: "object";
|
|
476
|
+
readonly properties: {
|
|
477
|
+
readonly selector: {
|
|
478
|
+
readonly type: "string";
|
|
479
|
+
readonly description: "CSS selector or testid shorthand for the element to start from (e.g., 'testid:header', '#main')";
|
|
480
|
+
};
|
|
481
|
+
readonly limit: {
|
|
482
|
+
readonly type: "number";
|
|
483
|
+
readonly description: "Maximum number of ancestors to traverse (default: 10, max: 15). Increase for deeply nested component frameworks.";
|
|
484
|
+
};
|
|
485
|
+
};
|
|
486
|
+
readonly required: readonly ["selector"];
|
|
487
|
+
};
|
|
471
488
|
}, {
|
|
472
489
|
readonly name: "wait_for_element";
|
|
473
490
|
readonly description: "Wait for an element to reach a specific state (visible, hidden, attached, detached). Better than sleep() for waiting on dynamic content. Returns duration and current element status. Supports testid shortcuts (e.g., 'testid:submit-button').";
|
package/dist/tools.js
CHANGED
|
@@ -20,8 +20,8 @@ export function createToolDefinitions(sessionConfig) {
|
|
|
20
20
|
description: "Mobile device preset to emulate. Uses Playwright's built-in device configurations for viewport, user agent, and device scale factor. When specified, overrides width/height parameters.",
|
|
21
21
|
enum: ["iphone-se", "iphone-14", "iphone-14-pro", "pixel-5", "ipad", "samsung-s21"]
|
|
22
22
|
},
|
|
23
|
-
width: { type: "number", description: "Viewport width in pixels
|
|
24
|
-
height: { type: "number", description: "Viewport height in pixels
|
|
23
|
+
width: { type: "number", description: "Viewport width in pixels. If not specified, automatically matches screen width. Ignored if device is specified." },
|
|
24
|
+
height: { type: "number", description: "Viewport height in pixels. If not specified, automatically matches screen height. Ignored if device is specified." },
|
|
25
25
|
timeout: { type: "number", description: "Navigation timeout in milliseconds" },
|
|
26
26
|
waitUntil: { type: "string", description: "Navigation wait condition" },
|
|
27
27
|
headless: { type: "boolean", description: "Run browser in headless mode (default: false)" }
|
|
@@ -437,6 +437,24 @@ More efficient than get_html() or evaluate(). Supports testid shortcuts.`,
|
|
|
437
437
|
required: ["selector1", "selector2"],
|
|
438
438
|
},
|
|
439
439
|
},
|
|
440
|
+
{
|
|
441
|
+
name: "inspect_ancestors",
|
|
442
|
+
description: "DEBUG LAYOUT CONSTRAINTS: Walk up the DOM tree to find where width constraints, margins, borders, and overflow clipping come from. Essential when elements have unexpected centering (large auto margins), constrained width (max-width from parent), or are clipped (overflow:hidden ancestor). Shows position, size, and layout-critical CSS for each ancestor. Default depth: 10 levels (reaches <body> in most React apps). Use after inspect_dom() when you need to understand parent layout flow.",
|
|
443
|
+
inputSchema: {
|
|
444
|
+
type: "object",
|
|
445
|
+
properties: {
|
|
446
|
+
selector: {
|
|
447
|
+
type: "string",
|
|
448
|
+
description: "CSS selector or testid shorthand for the element to start from (e.g., 'testid:header', '#main')"
|
|
449
|
+
},
|
|
450
|
+
limit: {
|
|
451
|
+
type: "number",
|
|
452
|
+
description: "Maximum number of ancestors to traverse (default: 10, max: 15). Increase for deeply nested component frameworks."
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
required: ["selector"],
|
|
456
|
+
},
|
|
457
|
+
},
|
|
440
458
|
{
|
|
441
459
|
name: "wait_for_element",
|
|
442
460
|
description: "Wait for an element to reach a specific state (visible, hidden, attached, detached). Better than sleep() for waiting on dynamic content. Returns duration and current element status. Supports testid shortcuts (e.g., 'testid:submit-button').",
|
|
@@ -523,6 +541,7 @@ export const BROWSER_TOOLS = [
|
|
|
523
541
|
// Visibility & Position
|
|
524
542
|
"check_visibility",
|
|
525
543
|
"compare_element_alignment",
|
|
544
|
+
"inspect_ancestors",
|
|
526
545
|
"element_exists",
|
|
527
546
|
"wait_for_element",
|
|
528
547
|
"wait_for_network_idle",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if Playwright browsers are installed
|
|
3
|
+
* Returns true if browsers are available, false otherwise
|
|
4
|
+
*/
|
|
5
|
+
export declare function checkBrowsersInstalled(): {
|
|
6
|
+
installed: boolean;
|
|
7
|
+
message?: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Get installation instructions for the current context
|
|
11
|
+
*/
|
|
12
|
+
export declare function getInstallationInstructions(): string;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
/**
|
|
5
|
+
* Check if Playwright browsers are installed
|
|
6
|
+
* Returns true if browsers are available, false otherwise
|
|
7
|
+
*/
|
|
8
|
+
export function checkBrowsersInstalled() {
|
|
9
|
+
try {
|
|
10
|
+
// Check if playwright is available
|
|
11
|
+
const result = execSync('npx playwright --version', {
|
|
12
|
+
encoding: 'utf8',
|
|
13
|
+
stdio: 'pipe'
|
|
14
|
+
});
|
|
15
|
+
// If we got here, playwright CLI is available
|
|
16
|
+
// Now check if browsers are actually installed
|
|
17
|
+
// Playwright stores browsers in a cache directory
|
|
18
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
19
|
+
if (!homeDir) {
|
|
20
|
+
return {
|
|
21
|
+
installed: false,
|
|
22
|
+
message: 'Could not determine home directory to check for browsers'
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
// Common browser cache locations
|
|
26
|
+
const possibleCacheDirs = [
|
|
27
|
+
join(homeDir, '.cache', 'ms-playwright'), // Linux
|
|
28
|
+
join(homeDir, 'Library', 'Caches', 'ms-playwright'), // macOS
|
|
29
|
+
join(homeDir, 'AppData', 'Local', 'ms-playwright'), // Windows
|
|
30
|
+
];
|
|
31
|
+
const cacheExists = possibleCacheDirs.some(dir => existsSync(dir));
|
|
32
|
+
if (!cacheExists) {
|
|
33
|
+
return {
|
|
34
|
+
installed: false,
|
|
35
|
+
message: 'Playwright browsers not found in cache directories'
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return { installed: true };
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
return {
|
|
42
|
+
installed: false,
|
|
43
|
+
message: `Playwright check failed: ${error.message}`
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get installation instructions for the current context
|
|
49
|
+
*/
|
|
50
|
+
export function getInstallationInstructions() {
|
|
51
|
+
return `
|
|
52
|
+
Playwright browsers are not installed. To fix this, run one of the following commands:
|
|
53
|
+
|
|
54
|
+
1. In your project directory:
|
|
55
|
+
npx playwright install chromium firefox webkit
|
|
56
|
+
|
|
57
|
+
2. If you installed mcp-web-inspector globally:
|
|
58
|
+
cd $(npm root -g)/mcp-web-inspector && npx playwright install chromium firefox webkit
|
|
59
|
+
|
|
60
|
+
3. Or install system-wide with dependencies (requires admin/sudo):
|
|
61
|
+
npx playwright install --with-deps chromium firefox webkit
|
|
62
|
+
|
|
63
|
+
For GitHub Copilot or VS Code users, you may need to:
|
|
64
|
+
- Close and reopen your IDE after installation
|
|
65
|
+
- Or run: npx playwright install chromium
|
|
66
|
+
`.trim();
|
|
67
|
+
}
|