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.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1017 -0
  3. package/dist/evals/evals.d.ts +5 -0
  4. package/dist/evals/evals.js +41 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +62 -0
  7. package/dist/requestHandler.d.ts +3 -0
  8. package/dist/requestHandler.js +53 -0
  9. package/dist/toolHandler.d.ts +91 -0
  10. package/dist/toolHandler.js +725 -0
  11. package/dist/tools/api/base.d.ts +33 -0
  12. package/dist/tools/api/base.js +49 -0
  13. package/dist/tools/api/index.d.ts +2 -0
  14. package/dist/tools/api/index.js +3 -0
  15. package/dist/tools/api/requests.d.ts +47 -0
  16. package/dist/tools/api/requests.js +168 -0
  17. package/dist/tools/browser/base.d.ts +51 -0
  18. package/dist/tools/browser/base.js +111 -0
  19. package/dist/tools/browser/cleanSession.d.ts +10 -0
  20. package/dist/tools/browser/cleanSession.js +42 -0
  21. package/dist/tools/browser/comparePositions.d.ts +11 -0
  22. package/dist/tools/browser/comparePositions.js +149 -0
  23. package/dist/tools/browser/computedStyles.d.ts +11 -0
  24. package/dist/tools/browser/computedStyles.js +128 -0
  25. package/dist/tools/browser/console.d.ts +37 -0
  26. package/dist/tools/browser/console.js +106 -0
  27. package/dist/tools/browser/elementExists.d.ts +9 -0
  28. package/dist/tools/browser/elementExists.js +57 -0
  29. package/dist/tools/browser/elementInspection.d.ts +21 -0
  30. package/dist/tools/browser/elementInspection.js +151 -0
  31. package/dist/tools/browser/elementPosition.d.ts +11 -0
  32. package/dist/tools/browser/elementPosition.js +107 -0
  33. package/dist/tools/browser/elementVisibility.d.ts +12 -0
  34. package/dist/tools/browser/elementVisibility.js +224 -0
  35. package/dist/tools/browser/findByText.d.ts +13 -0
  36. package/dist/tools/browser/findByText.js +207 -0
  37. package/dist/tools/browser/getRequestDetails.d.ts +9 -0
  38. package/dist/tools/browser/getRequestDetails.js +137 -0
  39. package/dist/tools/browser/getTestIds.d.ts +12 -0
  40. package/dist/tools/browser/getTestIds.js +148 -0
  41. package/dist/tools/browser/index.d.ts +7 -0
  42. package/dist/tools/browser/index.js +7 -0
  43. package/dist/tools/browser/inspectDom.d.ts +12 -0
  44. package/dist/tools/browser/inspectDom.js +447 -0
  45. package/dist/tools/browser/interaction.d.ts +104 -0
  46. package/dist/tools/browser/interaction.js +259 -0
  47. package/dist/tools/browser/listNetworkRequests.d.ts +10 -0
  48. package/dist/tools/browser/listNetworkRequests.js +74 -0
  49. package/dist/tools/browser/measureElement.d.ts +9 -0
  50. package/dist/tools/browser/measureElement.js +139 -0
  51. package/dist/tools/browser/navigation.d.ts +38 -0
  52. package/dist/tools/browser/navigation.js +109 -0
  53. package/dist/tools/browser/output.d.ts +11 -0
  54. package/dist/tools/browser/output.js +29 -0
  55. package/dist/tools/browser/querySelectorAll.d.ts +12 -0
  56. package/dist/tools/browser/querySelectorAll.js +201 -0
  57. package/dist/tools/browser/response.d.ts +29 -0
  58. package/dist/tools/browser/response.js +67 -0
  59. package/dist/tools/browser/screenshot.d.ts +16 -0
  60. package/dist/tools/browser/screenshot.js +70 -0
  61. package/dist/tools/browser/useragent.d.ts +15 -0
  62. package/dist/tools/browser/useragent.js +32 -0
  63. package/dist/tools/browser/visiblePage.d.ts +20 -0
  64. package/dist/tools/browser/visiblePage.js +170 -0
  65. package/dist/tools/browser/waitForElement.d.ts +10 -0
  66. package/dist/tools/browser/waitForElement.js +38 -0
  67. package/dist/tools/browser/waitForNetworkIdle.d.ts +8 -0
  68. package/dist/tools/browser/waitForNetworkIdle.js +32 -0
  69. package/dist/tools/codegen/generator.d.ts +21 -0
  70. package/dist/tools/codegen/generator.js +158 -0
  71. package/dist/tools/codegen/index.d.ts +11 -0
  72. package/dist/tools/codegen/index.js +187 -0
  73. package/dist/tools/codegen/recorder.d.ts +14 -0
  74. package/dist/tools/codegen/recorder.js +62 -0
  75. package/dist/tools/codegen/types.d.ts +28 -0
  76. package/dist/tools/codegen/types.js +1 -0
  77. package/dist/tools/common/types.d.ts +17 -0
  78. package/dist/tools/common/types.js +20 -0
  79. package/dist/tools/index.d.ts +2 -0
  80. package/dist/tools/index.js +2 -0
  81. package/dist/tools.d.ts +557 -0
  82. package/dist/tools.js +554 -0
  83. package/dist/types.d.ts +16 -0
  84. package/dist/types.js +1 -0
  85. 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
+ }