import { ToolHandler, ToolMetadata, SessionConfig } from '../../common/types.js';
import { BrowserToolBase } from '../base.js';
import type { ToolContext, ToolResponse } from '../../common/types.js';
export class MeasureElementTool extends BrowserToolBase implements ToolHandler {
static getMetadata(sessionConfig?: SessionConfig): ToolMetadata {
return {
name: "measure_element",
description: "π MEASUREMENT TOOL - DEBUG SPACING ISSUES: See padding, margin, border, and dimension measurements in visual box model format. Use when elements have unexpected spacing or size. Returns compact visual representation showing content β padding β border β margin with directional arrows (β24px for top margin, etc.). Also provides raw dimensions useful for scroll detection (clientHeight vs content height). For parent-child centering issues, use inspect_dom() first (shows if child is centered in parent). For comparing alignment between two elements, use compare_element_alignment(). For quick scroll detection, use inspect_dom() instead (shows 'scrollable βοΈ'). More readable than get_computed_styles() or evaluate() for box model debugging.",
priority: 7,
outputs: [
"Header: Element: <tag id/class/testid>",
"Position/size line: @ (x,y) widthxheight px",
"Box Model section: Content size, Padding (with directional arrows), Border (with arrows or shorthand), Margin (with arrows)",
"Total Space line: totalWidthxtotalHeight px (with margin)",
"Optional suggestion to run inspect_ancestors when unusual spacing detected",
],
examples: [
"measure_element({ selector: 'testid:card' })",
"measure_element({ selector: '#hero' })",
],
exampleOutputs: [
{
call: "measure_element({ selector: 'testid:card' })",
output: `Element: <div data-testid=\"card\">\n@ (240,320) 360x240px\n\nBox Model:\n Content: 328x208px\n Padding: β16px β16px β8px β8px\n Border: none\n Margin: β0px β24px β0px β0px\n\nTotal Space: 360x264px (with margin)`
}
],
inputSchema: {
type: "object",
properties: {
selector: {
type: "string",
description: "CSS selector or testid shorthand (e.g., 'testid:submit', '#login-button')"
}
},
required: ["selector"],
},
};
}
async execute(args: { selector: string }, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (page) => {
const normalizedSelector = this.normalizeSelector(args.selector);
// Use standard element selection with visibility preference
const locator = page.locator(normalizedSelector);
const { element, elementIndex, totalCount } = await this.selectPreferredLocator(locator, {
originalSelector: args.selector,
});
// Format selection warning if multiple elements matched
const warning = this.formatElementSelectionInfo(
args.selector,
elementIndex,
totalCount
);
// Get element descriptor
const elementInfo = await element.evaluate((el) => {
const tag = el.tagName.toLowerCase();
const testId = el.getAttribute('data-testid') || el.getAttribute('data-test') || el.getAttribute('data-cy');
const id = el.id ? `#${el.id}` : '';
const classes = el.className && typeof el.className === 'string'
? `.${el.className.split(' ').filter(c => c).slice(0, 2).join('.')}`
: '';
let descriptor = `<${tag}`;
if (testId) descriptor += ` data-testid="${testId}"`;
else if (id) descriptor += id;
else if (classes) descriptor += classes;
descriptor += '>';
return { descriptor };
});
// Get box model measurements
const measurements = await element.evaluate((el) => {
const computed = window.getComputedStyle(el);
const rect = el.getBoundingClientRect();
const parseValue = (val: string): number => parseFloat(val) || 0;
return {
x: Math.round(rect.x),
y: Math.round(rect.y),
width: parseValue(computed.width),
height: parseValue(computed.height),
marginTop: parseValue(computed.marginTop),
marginRight: parseValue(computed.marginRight),
marginBottom: parseValue(computed.marginBottom),
marginLeft: parseValue(computed.marginLeft),
paddingTop: parseValue(computed.paddingTop),
paddingRight: parseValue(computed.paddingRight),
paddingBottom: parseValue(computed.paddingBottom),
paddingLeft: parseValue(computed.paddingLeft),
borderTopWidth: parseValue(computed.borderTopWidth),
borderRightWidth: parseValue(computed.borderRightWidth),
borderBottomWidth: parseValue(computed.borderBottomWidth),
borderLeftWidth: parseValue(computed.borderLeftWidth),
borderStyle: computed.borderStyle,
borderColor: computed.borderColor
};
});
// Calculate dimensions
const contentWidth = Math.round(measurements.width - measurements.paddingLeft - measurements.paddingRight);
const contentHeight = Math.round(measurements.height - measurements.paddingTop - measurements.paddingBottom);
const boxWidth = Math.round(measurements.width);
const boxHeight = Math.round(measurements.height);
const totalWidth = Math.round(boxWidth + measurements.marginLeft + measurements.marginRight);
const totalHeight = Math.round(boxHeight + measurements.marginTop + measurements.marginBottom);
// Format border info
const formatBorder = (): string => {
const { borderTopWidth: top, borderRightWidth: right, borderBottomWidth: bottom, borderLeftWidth: left, borderStyle: style } = measurements;
// Check if all sides are the same
if (top === right && right === bottom && bottom === left) {
if (top === 0) return ' Border: none';
return ` Border: ${top}px ${style}`;
}
// Different sides
const lines: string[] = [];
if (top > 0) lines.push(`β${top}px`);
if (right > 0) lines.push(`β${right}px`);
if (bottom > 0) lines.push(`β${bottom}px`);
if (left > 0) lines.push(`β${left}px`);
return lines.length > 0
? ` Border: ${lines.join(' ')} ${style}`
: ' Border: none';
};
// Format spacing (margin/padding) with directional arrows
const formatSpacing = (top: number, right: number, bottom: number, left: number): string => {
const parts: string[] = [];
if (top > 0) parts.push(`β${Math.round(top)}px`);
if (bottom > 0) parts.push(`β${Math.round(bottom)}px`);
if (left > 0) parts.push(`β${Math.round(left)}px`);
if (right > 0) parts.push(`β${Math.round(right)}px`);
return parts.length > 0 ? parts.join(' ') : '0px';
};
// Build output in compact text format
const sections: string[] = [];
if (warning) {
sections.push(warning.trim());
}
sections.push(`Element: ${elementInfo.descriptor}`);
sections.push(`@ (${measurements.x},${measurements.y}) ${boxWidth}x${boxHeight}px`);
sections.push('');
sections.push('Box Model:');
sections.push(` Content: ${contentWidth}x${contentHeight}px`);
sections.push(` Padding: ${formatSpacing(measurements.paddingTop, measurements.paddingRight, measurements.paddingBottom, measurements.paddingLeft)}`);
sections.push(formatBorder());
sections.push(` Margin: ${formatSpacing(measurements.marginTop, measurements.marginRight, measurements.marginBottom, measurements.marginLeft)}`);
sections.push('');
sections.push(`Total Space: ${totalWidth}x${totalHeight}px (with margin)`);
// Detect unusual spacing and suggest inspect_ancestors
const hasUnusualMargins = measurements.marginLeft > 100 || measurements.marginRight > 100;
const isWidthConstrained = boxWidth < 800 && (measurements.marginLeft + measurements.marginRight) > 200;
if (hasUnusualMargins || isWidthConstrained) {
sections.push('');
sections.push('π‘ Unexpected spacing/width detected. Check parent constraints:');
sections.push(` inspect_ancestors({ selector: "${args.selector}" })`);
}
return {
content: [
{
type: 'text',
text: sections.join('\n')
}
],
isError: false
};
});
}
}