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,149 @@
|
|
|
1
|
+
import { BrowserToolBase } from './base.js';
|
|
2
|
+
import { createSuccessResponse, createErrorResponse } from '../common/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Tool for comparing positions and sizes of two elements
|
|
5
|
+
*/
|
|
6
|
+
export class ComparePositionsTool extends BrowserToolBase {
|
|
7
|
+
/**
|
|
8
|
+
* Execute the compare positions tool
|
|
9
|
+
*/
|
|
10
|
+
async execute(args, context) {
|
|
11
|
+
return this.safeExecute(context, async (page) => {
|
|
12
|
+
const selector1 = this.normalizeSelector(args.selector1);
|
|
13
|
+
const selector2 = this.normalizeSelector(args.selector2);
|
|
14
|
+
const checkAlignment = args.checkAlignment;
|
|
15
|
+
// Validate checkAlignment parameter
|
|
16
|
+
const validAlignments = ['top', 'left', 'right', 'bottom', 'width', 'height'];
|
|
17
|
+
if (!validAlignments.includes(checkAlignment)) {
|
|
18
|
+
return createErrorResponse(`Invalid checkAlignment value: "${checkAlignment}". Must be one of: ${validAlignments.join(', ')}`);
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
// Get locators for both elements
|
|
22
|
+
const locator1 = page.locator(selector1);
|
|
23
|
+
const locator2 = page.locator(selector2);
|
|
24
|
+
// Check if both elements exist
|
|
25
|
+
const count1 = await locator1.count();
|
|
26
|
+
const count2 = await locator2.count();
|
|
27
|
+
if (count1 === 0) {
|
|
28
|
+
return createErrorResponse(`First element not found: ${args.selector1}`);
|
|
29
|
+
}
|
|
30
|
+
if (count2 === 0) {
|
|
31
|
+
return createErrorResponse(`Second element not found: ${args.selector2}`);
|
|
32
|
+
}
|
|
33
|
+
// Handle multiple matches by using first() - show warning
|
|
34
|
+
const targetLocator1 = count1 > 1 ? locator1.first() : locator1;
|
|
35
|
+
const targetLocator2 = count2 > 1 ? locator2.first() : locator2;
|
|
36
|
+
let warnings = '';
|
|
37
|
+
if (count1 > 1) {
|
|
38
|
+
warnings += `⚠ Warning: First selector matched ${count1} elements, using first\n`;
|
|
39
|
+
}
|
|
40
|
+
if (count2 > 1) {
|
|
41
|
+
warnings += `⚠ Warning: Second selector matched ${count2} elements, using first\n`;
|
|
42
|
+
}
|
|
43
|
+
if (warnings) {
|
|
44
|
+
warnings += '\n';
|
|
45
|
+
}
|
|
46
|
+
// Get bounding boxes
|
|
47
|
+
const box1 = await targetLocator1.boundingBox();
|
|
48
|
+
const box2 = await targetLocator2.boundingBox();
|
|
49
|
+
// Get element descriptors
|
|
50
|
+
const getDescriptor = async (locator) => {
|
|
51
|
+
return await locator.evaluate((element) => {
|
|
52
|
+
const tagName = element.tagName.toLowerCase();
|
|
53
|
+
const testId = element.getAttribute('data-testid') || element.getAttribute('data-test') || element.getAttribute('data-cy');
|
|
54
|
+
const id = element.id ? `#${element.id}` : '';
|
|
55
|
+
const classes = element.className && typeof element.className === 'string'
|
|
56
|
+
? `.${element.className.split(' ').filter(c => c).join('.')}`
|
|
57
|
+
: '';
|
|
58
|
+
let descriptor = `<${tagName}`;
|
|
59
|
+
if (testId)
|
|
60
|
+
descriptor += ` data-testid="${testId}"`;
|
|
61
|
+
else if (id)
|
|
62
|
+
descriptor += id;
|
|
63
|
+
else if (classes)
|
|
64
|
+
descriptor += classes;
|
|
65
|
+
descriptor += '>';
|
|
66
|
+
return descriptor;
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
const descriptor1 = await getDescriptor(targetLocator1);
|
|
70
|
+
const descriptor2 = await getDescriptor(targetLocator2);
|
|
71
|
+
// Handle hidden elements
|
|
72
|
+
if (!box1) {
|
|
73
|
+
return createErrorResponse(`First element is hidden or has no dimensions: ${descriptor1}`);
|
|
74
|
+
}
|
|
75
|
+
if (!box2) {
|
|
76
|
+
return createErrorResponse(`Second element is hidden or has no dimensions: ${descriptor2}`);
|
|
77
|
+
}
|
|
78
|
+
// Calculate values based on alignment type
|
|
79
|
+
let value1;
|
|
80
|
+
let value2;
|
|
81
|
+
let label;
|
|
82
|
+
let unit = 'px';
|
|
83
|
+
switch (checkAlignment) {
|
|
84
|
+
case 'top':
|
|
85
|
+
value1 = Math.round(box1.y);
|
|
86
|
+
value2 = Math.round(box2.y);
|
|
87
|
+
label = 'Top';
|
|
88
|
+
break;
|
|
89
|
+
case 'left':
|
|
90
|
+
value1 = Math.round(box1.x);
|
|
91
|
+
value2 = Math.round(box2.x);
|
|
92
|
+
label = 'Left';
|
|
93
|
+
break;
|
|
94
|
+
case 'right':
|
|
95
|
+
value1 = Math.round(box1.x + box1.width);
|
|
96
|
+
value2 = Math.round(box2.x + box2.width);
|
|
97
|
+
label = 'Right';
|
|
98
|
+
break;
|
|
99
|
+
case 'bottom':
|
|
100
|
+
value1 = Math.round(box1.y + box1.height);
|
|
101
|
+
value2 = Math.round(box2.y + box2.height);
|
|
102
|
+
label = 'Bottom';
|
|
103
|
+
break;
|
|
104
|
+
case 'width':
|
|
105
|
+
value1 = Math.round(box1.width);
|
|
106
|
+
value2 = Math.round(box2.width);
|
|
107
|
+
label = 'Width';
|
|
108
|
+
break;
|
|
109
|
+
case 'height':
|
|
110
|
+
value1 = Math.round(box1.height);
|
|
111
|
+
value2 = Math.round(box2.height);
|
|
112
|
+
label = 'Height';
|
|
113
|
+
break;
|
|
114
|
+
default:
|
|
115
|
+
return createErrorResponse(`Unexpected alignment type: ${checkAlignment}`);
|
|
116
|
+
}
|
|
117
|
+
const difference = Math.abs(value1 - value2);
|
|
118
|
+
const aligned = difference === 0;
|
|
119
|
+
const alignmentSymbol = aligned ? '✓' : '✗';
|
|
120
|
+
const alignmentStatus = aligned ? 'aligned' : 'not aligned';
|
|
121
|
+
// Extract short name from descriptor for compact output
|
|
122
|
+
const getShortName = (descriptor, selector) => {
|
|
123
|
+
const testIdMatch = descriptor.match(/data-testid="([^"]+)"/);
|
|
124
|
+
if (testIdMatch)
|
|
125
|
+
return testIdMatch[1];
|
|
126
|
+
const idMatch = descriptor.match(/#([^>]+)/);
|
|
127
|
+
if (idMatch)
|
|
128
|
+
return idMatch[1];
|
|
129
|
+
// Use original selector if available
|
|
130
|
+
return selector;
|
|
131
|
+
};
|
|
132
|
+
const name1 = getShortName(descriptor1, args.selector1);
|
|
133
|
+
const name2 = getShortName(descriptor2, args.selector2);
|
|
134
|
+
// Build compact text format
|
|
135
|
+
const output = warnings +
|
|
136
|
+
`Alignment Check:\n` +
|
|
137
|
+
`${descriptor1} vs ${descriptor2}\n\n` +
|
|
138
|
+
`${label}: ${alignmentSymbol} ${alignmentStatus}\n` +
|
|
139
|
+
` ${name1}: ${value1}${unit}\n` +
|
|
140
|
+
` ${name2}: ${value2}${unit}\n` +
|
|
141
|
+
` Difference: ${difference}${unit}`;
|
|
142
|
+
return createSuccessResponse(output);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
return createErrorResponse(`Failed to compare positions: ${error.message}`);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
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 GetComputedStylesArgs {
|
|
5
|
+
selector: string;
|
|
6
|
+
properties?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class GetComputedStylesTool extends BrowserToolBase implements ToolHandler {
|
|
9
|
+
private readonly DEFAULT_PROPERTIES;
|
|
10
|
+
execute(args: GetComputedStylesArgs, context: ToolContext): Promise<ToolResponse>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { BrowserToolBase } from './base.js';
|
|
2
|
+
export class GetComputedStylesTool extends BrowserToolBase {
|
|
3
|
+
constructor() {
|
|
4
|
+
super(...arguments);
|
|
5
|
+
this.DEFAULT_PROPERTIES = [
|
|
6
|
+
'display', 'position', 'width', 'height',
|
|
7
|
+
'opacity', 'visibility', 'z-index', 'overflow',
|
|
8
|
+
'margin', 'padding',
|
|
9
|
+
'font-size', 'font-weight', 'color', 'background-color'
|
|
10
|
+
];
|
|
11
|
+
}
|
|
12
|
+
async execute(args, context) {
|
|
13
|
+
return this.safeExecute(context, async (page) => {
|
|
14
|
+
const normalizedSelector = this.normalizeSelector(args.selector);
|
|
15
|
+
// Parse properties parameter
|
|
16
|
+
const properties = args.properties
|
|
17
|
+
? args.properties.split(',').map(p => p.trim())
|
|
18
|
+
: this.DEFAULT_PROPERTIES;
|
|
19
|
+
// Find the element
|
|
20
|
+
const locator = page.locator(normalizedSelector);
|
|
21
|
+
const count = await locator.count();
|
|
22
|
+
if (count === 0) {
|
|
23
|
+
return {
|
|
24
|
+
content: [
|
|
25
|
+
{
|
|
26
|
+
type: 'text',
|
|
27
|
+
text: `✗ Element not found: ${args.selector}`
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
isError: true
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// Handle multiple matches by using first() - show warning (consistent with compare_positions)
|
|
34
|
+
const targetLocator = count > 1 ? locator.first() : locator;
|
|
35
|
+
let warning = '';
|
|
36
|
+
if (count > 1) {
|
|
37
|
+
warning = `⚠ Warning: Selector matched ${count} elements, using first\n\n`;
|
|
38
|
+
}
|
|
39
|
+
// Get element tag and selector info
|
|
40
|
+
const elementInfo = await targetLocator.evaluate((el) => {
|
|
41
|
+
const attrs = [];
|
|
42
|
+
const tag = el.tagName.toLowerCase();
|
|
43
|
+
if (el.id)
|
|
44
|
+
attrs.push(`#${el.id}`);
|
|
45
|
+
if (el.className && typeof el.className === 'string') {
|
|
46
|
+
const classes = el.className.split(' ').filter(c => c).slice(0, 2);
|
|
47
|
+
if (classes.length)
|
|
48
|
+
attrs.push(`.${classes.join('.')}`);
|
|
49
|
+
}
|
|
50
|
+
const testId = el.getAttribute('data-testid') || el.getAttribute('data-test') || el.getAttribute('data-cy');
|
|
51
|
+
if (testId)
|
|
52
|
+
attrs.push(`data-testid="${testId}"`);
|
|
53
|
+
return {
|
|
54
|
+
tag,
|
|
55
|
+
display: attrs.length ? `<${tag} ${attrs.join(' ')}>` : `<${tag}>`
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
// Get computed styles
|
|
59
|
+
const styles = await targetLocator.evaluate((el, props) => {
|
|
60
|
+
const computed = window.getComputedStyle(el);
|
|
61
|
+
const result = {};
|
|
62
|
+
props.forEach((prop) => {
|
|
63
|
+
result[prop] = computed.getPropertyValue(prop);
|
|
64
|
+
});
|
|
65
|
+
return result;
|
|
66
|
+
}, properties);
|
|
67
|
+
// Group styles by category
|
|
68
|
+
const layout = [];
|
|
69
|
+
const visibility = [];
|
|
70
|
+
const spacing = [];
|
|
71
|
+
const typography = [];
|
|
72
|
+
const other = [];
|
|
73
|
+
const layoutProps = ['display', 'position', 'width', 'height', 'top', 'left', 'right', 'bottom'];
|
|
74
|
+
const visibilityProps = ['opacity', 'visibility', 'z-index', 'overflow', 'overflow-x', 'overflow-y'];
|
|
75
|
+
const spacingProps = ['margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
|
|
76
|
+
'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left'];
|
|
77
|
+
const typographyProps = ['font-size', 'font-weight', 'font-family', 'color', 'line-height', 'text-align'];
|
|
78
|
+
Object.entries(styles).forEach(([prop, value]) => {
|
|
79
|
+
const line = ` ${prop}: ${value}`;
|
|
80
|
+
if (layoutProps.includes(prop)) {
|
|
81
|
+
layout.push(line);
|
|
82
|
+
}
|
|
83
|
+
else if (visibilityProps.includes(prop)) {
|
|
84
|
+
visibility.push(line);
|
|
85
|
+
}
|
|
86
|
+
else if (spacingProps.includes(prop)) {
|
|
87
|
+
spacing.push(line);
|
|
88
|
+
}
|
|
89
|
+
else if (typographyProps.includes(prop)) {
|
|
90
|
+
typography.push(line);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
other.push(line);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
// Build output
|
|
97
|
+
const sections = [];
|
|
98
|
+
if (warning) {
|
|
99
|
+
sections.push(warning.trim());
|
|
100
|
+
}
|
|
101
|
+
sections.push(`Computed Styles: ${elementInfo.display}\n`);
|
|
102
|
+
if (layout.length) {
|
|
103
|
+
sections.push('Layout:\n' + layout.join('\n'));
|
|
104
|
+
}
|
|
105
|
+
if (visibility.length) {
|
|
106
|
+
sections.push('Visibility:\n' + visibility.join('\n'));
|
|
107
|
+
}
|
|
108
|
+
if (spacing.length) {
|
|
109
|
+
sections.push('Spacing:\n' + spacing.join('\n'));
|
|
110
|
+
}
|
|
111
|
+
if (typography.length) {
|
|
112
|
+
sections.push('Typography:\n' + typography.join('\n'));
|
|
113
|
+
}
|
|
114
|
+
if (other.length) {
|
|
115
|
+
sections.push('Other:\n' + other.join('\n'));
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
content: [
|
|
119
|
+
{
|
|
120
|
+
type: 'text',
|
|
121
|
+
text: sections.join('\n\n')
|
|
122
|
+
}
|
|
123
|
+
],
|
|
124
|
+
isError: false
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { BrowserToolBase } from './base.js';
|
|
2
|
+
import { ToolContext, ToolResponse } from '../common/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Tool for retrieving and filtering console logs from the browser
|
|
5
|
+
*/
|
|
6
|
+
export declare class ConsoleLogsTool extends BrowserToolBase {
|
|
7
|
+
private consoleLogs;
|
|
8
|
+
private lastCallTimestamp;
|
|
9
|
+
private lastNavigationTimestamp;
|
|
10
|
+
private lastInteractionTimestamp;
|
|
11
|
+
/**
|
|
12
|
+
* Register a console message
|
|
13
|
+
* @param type The type of console message
|
|
14
|
+
* @param text The text content of the message
|
|
15
|
+
*/
|
|
16
|
+
registerConsoleMessage(type: string, text: string): void;
|
|
17
|
+
/**
|
|
18
|
+
* Update the last navigation timestamp
|
|
19
|
+
*/
|
|
20
|
+
updateLastNavigationTimestamp(): void;
|
|
21
|
+
/**
|
|
22
|
+
* Update the last interaction timestamp
|
|
23
|
+
*/
|
|
24
|
+
updateLastInteractionTimestamp(): void;
|
|
25
|
+
/**
|
|
26
|
+
* Execute the console logs tool
|
|
27
|
+
*/
|
|
28
|
+
execute(args: any, context: ToolContext): Promise<ToolResponse>;
|
|
29
|
+
/**
|
|
30
|
+
* Get all console logs
|
|
31
|
+
*/
|
|
32
|
+
getConsoleLogs(): string[];
|
|
33
|
+
/**
|
|
34
|
+
* Clear all console logs
|
|
35
|
+
*/
|
|
36
|
+
clearConsoleLogs(): void;
|
|
37
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { BrowserToolBase } from './base.js';
|
|
2
|
+
import { createSuccessResponse } from '../common/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Tool for retrieving and filtering console logs from the browser
|
|
5
|
+
*/
|
|
6
|
+
export class ConsoleLogsTool extends BrowserToolBase {
|
|
7
|
+
constructor() {
|
|
8
|
+
super(...arguments);
|
|
9
|
+
this.consoleLogs = [];
|
|
10
|
+
this.lastCallTimestamp = 0;
|
|
11
|
+
this.lastNavigationTimestamp = 0;
|
|
12
|
+
this.lastInteractionTimestamp = 0;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Register a console message
|
|
16
|
+
* @param type The type of console message
|
|
17
|
+
* @param text The text content of the message
|
|
18
|
+
*/
|
|
19
|
+
registerConsoleMessage(type, text) {
|
|
20
|
+
const logEntry = {
|
|
21
|
+
timestamp: Date.now(),
|
|
22
|
+
message: `[${type}] ${text}`
|
|
23
|
+
};
|
|
24
|
+
this.consoleLogs.push(logEntry);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Update the last navigation timestamp
|
|
28
|
+
*/
|
|
29
|
+
updateLastNavigationTimestamp() {
|
|
30
|
+
this.lastNavigationTimestamp = Date.now();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Update the last interaction timestamp
|
|
34
|
+
*/
|
|
35
|
+
updateLastInteractionTimestamp() {
|
|
36
|
+
this.lastInteractionTimestamp = Date.now();
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Execute the console logs tool
|
|
40
|
+
*/
|
|
41
|
+
async execute(args, context) {
|
|
42
|
+
// No need to use safeExecute here as we don't need to interact with the page
|
|
43
|
+
// We're just filtering and returning logs that are already stored
|
|
44
|
+
let logs = [...this.consoleLogs];
|
|
45
|
+
// Filter by timestamp if 'since' parameter is specified
|
|
46
|
+
if (args.since) {
|
|
47
|
+
let sinceTimestamp;
|
|
48
|
+
switch (args.since) {
|
|
49
|
+
case 'last-call':
|
|
50
|
+
sinceTimestamp = this.lastCallTimestamp;
|
|
51
|
+
break;
|
|
52
|
+
case 'last-navigation':
|
|
53
|
+
sinceTimestamp = this.lastNavigationTimestamp;
|
|
54
|
+
break;
|
|
55
|
+
case 'last-interaction':
|
|
56
|
+
sinceTimestamp = this.lastInteractionTimestamp;
|
|
57
|
+
break;
|
|
58
|
+
default:
|
|
59
|
+
return createSuccessResponse(`Invalid 'since' value: ${args.since}. Must be one of: last-call, last-navigation, last-interaction`);
|
|
60
|
+
}
|
|
61
|
+
logs = logs.filter(log => log.timestamp > sinceTimestamp);
|
|
62
|
+
}
|
|
63
|
+
// Update last call timestamp
|
|
64
|
+
this.lastCallTimestamp = Date.now();
|
|
65
|
+
// Filter by type if specified
|
|
66
|
+
if (args.type && args.type !== 'all') {
|
|
67
|
+
logs = logs.filter(log => log.message.startsWith(`[${args.type}]`));
|
|
68
|
+
}
|
|
69
|
+
// Filter by search text if specified
|
|
70
|
+
if (args.search) {
|
|
71
|
+
logs = logs.filter(log => log.message.includes(args.search));
|
|
72
|
+
}
|
|
73
|
+
// Limit the number of logs if specified
|
|
74
|
+
if (args.limit && args.limit > 0) {
|
|
75
|
+
logs = logs.slice(-args.limit);
|
|
76
|
+
}
|
|
77
|
+
// Extract messages from log entries
|
|
78
|
+
const messages = logs.map(log => log.message);
|
|
79
|
+
// Clear logs if requested
|
|
80
|
+
if (args.clear) {
|
|
81
|
+
this.consoleLogs = [];
|
|
82
|
+
}
|
|
83
|
+
// Format the response
|
|
84
|
+
if (messages.length === 0) {
|
|
85
|
+
return createSuccessResponse("No console logs matching the criteria");
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
return createSuccessResponse([
|
|
89
|
+
`Retrieved ${messages.length} console log(s):`,
|
|
90
|
+
...messages
|
|
91
|
+
]);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get all console logs
|
|
96
|
+
*/
|
|
97
|
+
getConsoleLogs() {
|
|
98
|
+
return this.consoleLogs.map(log => log.message);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Clear all console logs
|
|
102
|
+
*/
|
|
103
|
+
clearConsoleLogs() {
|
|
104
|
+
this.consoleLogs = [];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
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 ElementExistsArgs {
|
|
5
|
+
selector: string;
|
|
6
|
+
}
|
|
7
|
+
export declare class ElementExistsTool extends BrowserToolBase implements ToolHandler {
|
|
8
|
+
execute(args: ElementExistsArgs, context: ToolContext): Promise<ToolResponse>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { BrowserToolBase } from './base.js';
|
|
2
|
+
export class ElementExistsTool extends BrowserToolBase {
|
|
3
|
+
async execute(args, context) {
|
|
4
|
+
return this.safeExecute(context, async (page) => {
|
|
5
|
+
const normalizedSelector = this.normalizeSelector(args.selector);
|
|
6
|
+
const locator = page.locator(normalizedSelector);
|
|
7
|
+
const count = await locator.count();
|
|
8
|
+
if (count === 0) {
|
|
9
|
+
return {
|
|
10
|
+
content: [
|
|
11
|
+
{
|
|
12
|
+
type: 'text',
|
|
13
|
+
text: `✗ not found: ${args.selector}`
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
isError: false
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
// Get element info for better output
|
|
20
|
+
const element = locator.first();
|
|
21
|
+
const elementInfo = await element.evaluate((el) => {
|
|
22
|
+
const tag = el.tagName.toLowerCase();
|
|
23
|
+
const parts = [tag];
|
|
24
|
+
if (el.id)
|
|
25
|
+
parts.push(`#${el.id}`);
|
|
26
|
+
if (el.className && typeof el.className === 'string') {
|
|
27
|
+
const classes = el.className.split(' ').filter(c => c).slice(0, 2);
|
|
28
|
+
if (classes.length) {
|
|
29
|
+
classes.forEach(c => parts.push(`.${c}`));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return parts.join('');
|
|
33
|
+
}).catch(() => args.selector);
|
|
34
|
+
if (count === 1) {
|
|
35
|
+
return {
|
|
36
|
+
content: [
|
|
37
|
+
{
|
|
38
|
+
type: 'text',
|
|
39
|
+
text: `✓ exists: <${elementInfo}>`
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
isError: false
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// Multiple matches
|
|
46
|
+
return {
|
|
47
|
+
content: [
|
|
48
|
+
{
|
|
49
|
+
type: 'text',
|
|
50
|
+
text: `✓ exists: <${elementInfo}> (${count} matches)`
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
isError: false
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Tool for getting element position and size
|
|
15
|
+
*/
|
|
16
|
+
export declare class ElementPositionTool extends BrowserToolBase {
|
|
17
|
+
/**
|
|
18
|
+
* Execute the element position tool
|
|
19
|
+
*/
|
|
20
|
+
execute(args: any, context: ToolContext): Promise<ToolResponse>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
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
|
+
// Get basic visibility (Playwright's isVisible)
|
|
22
|
+
const isVisible = await locator.isVisible();
|
|
23
|
+
// Evaluate detailed visibility information in browser context
|
|
24
|
+
const visibilityData = await locator.evaluate((element) => {
|
|
25
|
+
const rect = element.getBoundingClientRect();
|
|
26
|
+
const viewportHeight = window.innerHeight;
|
|
27
|
+
const viewportWidth = window.innerWidth;
|
|
28
|
+
// Calculate viewport intersection ratio
|
|
29
|
+
const visibleTop = Math.max(0, rect.top);
|
|
30
|
+
const visibleBottom = Math.min(viewportHeight, rect.bottom);
|
|
31
|
+
const visibleLeft = Math.max(0, rect.left);
|
|
32
|
+
const visibleRight = Math.min(viewportWidth, rect.right);
|
|
33
|
+
const visibleHeight = Math.max(0, visibleBottom - visibleTop);
|
|
34
|
+
const visibleWidth = Math.max(0, visibleRight - visibleLeft);
|
|
35
|
+
const visibleArea = visibleHeight * visibleWidth;
|
|
36
|
+
const totalArea = rect.height * rect.width;
|
|
37
|
+
const viewportRatio = totalArea > 0 ? visibleArea / totalArea : 0;
|
|
38
|
+
// Check if element is in viewport
|
|
39
|
+
const isInViewport = viewportRatio > 0;
|
|
40
|
+
// Get computed styles
|
|
41
|
+
const styles = window.getComputedStyle(element);
|
|
42
|
+
const opacity = parseFloat(styles.opacity);
|
|
43
|
+
const display = styles.display;
|
|
44
|
+
const visibility = styles.visibility;
|
|
45
|
+
// Check if clipped by overflow:hidden
|
|
46
|
+
let isClipped = false;
|
|
47
|
+
let parent = element.parentElement;
|
|
48
|
+
while (parent) {
|
|
49
|
+
const parentStyle = window.getComputedStyle(parent);
|
|
50
|
+
if (parentStyle.overflow === 'hidden' ||
|
|
51
|
+
parentStyle.overflowX === 'hidden' ||
|
|
52
|
+
parentStyle.overflowY === 'hidden') {
|
|
53
|
+
const parentRect = parent.getBoundingClientRect();
|
|
54
|
+
// Check if element is outside parent bounds
|
|
55
|
+
if (rect.right < parentRect.left ||
|
|
56
|
+
rect.left > parentRect.right ||
|
|
57
|
+
rect.bottom < parentRect.top ||
|
|
58
|
+
rect.top > parentRect.bottom) {
|
|
59
|
+
isClipped = true;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
parent = parent.parentElement;
|
|
64
|
+
}
|
|
65
|
+
// Check if covered by another element (check center point)
|
|
66
|
+
const centerX = rect.left + rect.width / 2;
|
|
67
|
+
const centerY = rect.top + rect.height / 2;
|
|
68
|
+
const topElement = document.elementFromPoint(centerX, centerY);
|
|
69
|
+
const isCovered = topElement !== element && !element.contains(topElement);
|
|
70
|
+
return {
|
|
71
|
+
viewportRatio,
|
|
72
|
+
isInViewport,
|
|
73
|
+
opacity,
|
|
74
|
+
display,
|
|
75
|
+
visibility,
|
|
76
|
+
isClipped,
|
|
77
|
+
isCovered,
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
// Determine if scroll is needed
|
|
81
|
+
const needsScroll = isVisible && !visibilityData.isInViewport;
|
|
82
|
+
const result = {
|
|
83
|
+
// Playwright checks
|
|
84
|
+
isVisible,
|
|
85
|
+
isInViewport: visibilityData.isInViewport,
|
|
86
|
+
viewportRatio: Math.round(visibilityData.viewportRatio * 100) / 100, // Round to 2 decimals
|
|
87
|
+
// CSS properties
|
|
88
|
+
opacity: visibilityData.opacity,
|
|
89
|
+
display: visibilityData.display,
|
|
90
|
+
visibility: visibilityData.visibility,
|
|
91
|
+
// Failure diagnostics
|
|
92
|
+
isClipped: visibilityData.isClipped,
|
|
93
|
+
isCovered: visibilityData.isCovered,
|
|
94
|
+
needsScroll,
|
|
95
|
+
};
|
|
96
|
+
return createSuccessResponse(JSON.stringify(result, null, 2));
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
return createErrorResponse(`Failed to check visibility: ${error.message}`);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Tool for getting element position and size
|
|
106
|
+
*/
|
|
107
|
+
export class ElementPositionTool extends BrowserToolBase {
|
|
108
|
+
/**
|
|
109
|
+
* Execute the element position tool
|
|
110
|
+
*/
|
|
111
|
+
async execute(args, context) {
|
|
112
|
+
return this.safeExecute(context, async (page) => {
|
|
113
|
+
const selector = this.normalizeSelector(args.selector);
|
|
114
|
+
const locator = page.locator(selector);
|
|
115
|
+
try {
|
|
116
|
+
// Check if element exists
|
|
117
|
+
const count = await locator.count();
|
|
118
|
+
if (count === 0) {
|
|
119
|
+
return createErrorResponse(`Element not found: ${args.selector}`);
|
|
120
|
+
}
|
|
121
|
+
// Get bounding box
|
|
122
|
+
const boundingBox = await locator.boundingBox();
|
|
123
|
+
if (!boundingBox) {
|
|
124
|
+
return createErrorResponse(`Element has no bounding box (might be hidden or have display:none): ${args.selector}`);
|
|
125
|
+
}
|
|
126
|
+
// Check if in viewport
|
|
127
|
+
const inViewport = await locator.evaluate((element) => {
|
|
128
|
+
const rect = element.getBoundingClientRect();
|
|
129
|
+
const viewportHeight = window.innerHeight;
|
|
130
|
+
const viewportWidth = window.innerWidth;
|
|
131
|
+
// Element is in viewport if any part is visible
|
|
132
|
+
return (rect.bottom > 0 &&
|
|
133
|
+
rect.right > 0 &&
|
|
134
|
+
rect.top < viewportHeight &&
|
|
135
|
+
rect.left < viewportWidth);
|
|
136
|
+
});
|
|
137
|
+
const result = {
|
|
138
|
+
x: Math.round(boundingBox.x),
|
|
139
|
+
y: Math.round(boundingBox.y),
|
|
140
|
+
width: Math.round(boundingBox.width),
|
|
141
|
+
height: Math.round(boundingBox.height),
|
|
142
|
+
inViewport,
|
|
143
|
+
};
|
|
144
|
+
return createSuccessResponse(JSON.stringify(result, null, 2));
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
return createErrorResponse(`Failed to get position: ${error.message}`);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|