feat: add 'get styles' command for computed styles extraction (#142)

This commit is contained in:
Danila Poyarkov
2026-01-17 18:56:52 -06:00
committed by GitHub
parent b19ca760aa
commit e6e832d2bc
5 changed files with 145 additions and 3 deletions
+63
View File
@@ -50,6 +50,7 @@ import type {
IsCheckedCommand,
CountCommand,
BoundingBoxCommand,
StylesCommand,
TraceStartCommand,
TraceStopCommand,
HarStopCommand,
@@ -117,6 +118,7 @@ import type {
RecordingStopData,
RecordingRestartData,
InputEventData,
StylesData,
} from './types.js';
import { successResponse, errorResponse } from './protocol.js';
@@ -318,6 +320,8 @@ export async function executeCommand(command: Command, browser: BrowserManager):
return await handleCount(command, browser);
case 'boundingbox':
return await handleBoundingBox(command, browser);
case 'styles':
return await handleStyles(command, browser);
case 'video_start':
return await handleVideoStart(command, browser);
case 'video_stop':
@@ -1246,6 +1250,65 @@ async function handleBoundingBox(
return successResponse(command.id, { box });
}
async function handleStyles(
command: StylesCommand,
browser: BrowserManager
): Promise<Response<StylesData>> {
const page = browser.getPage();
// Shared extraction logic as a string to be eval'd in browser context
const extractStylesScript = `(function(el) {
const s = getComputedStyle(el);
const r = el.getBoundingClientRect();
return {
tag: el.tagName.toLowerCase(),
text: el.innerText?.trim().slice(0, 80) || null,
box: {
x: Math.round(r.x),
y: Math.round(r.y),
width: Math.round(r.width),
height: Math.round(r.height),
},
styles: {
fontSize: s.fontSize,
fontWeight: s.fontWeight,
fontFamily: s.fontFamily.split(',')[0].trim().replace(/"/g, ''),
color: s.color,
backgroundColor: s.backgroundColor,
borderRadius: s.borderRadius,
border: s.border !== 'none' && s.borderWidth !== '0px' ? s.border : null,
boxShadow: s.boxShadow !== 'none' ? s.boxShadow : null,
padding: s.padding,
},
};
})`;
// Check if it's a ref - single element
if (browser.isRef(command.selector)) {
const locator = browser.getLocator(command.selector);
const element = (await locator.evaluate(
(el, script) => {
const fn = eval(script);
return fn(el);
},
extractStylesScript
)) as StylesData['elements'][0];
return successResponse(command.id, { elements: [element] });
}
// CSS selector - can match multiple elements
const elements = (await page.$$eval(
command.selector,
(els, script) => {
const fn = eval(script);
return els.map((el) => fn(el));
},
extractStylesScript
)) as StylesData['elements'];
return successResponse(command.id, { elements });
}
// Advanced handlers
async function handleVideoStart(
+6
View File
@@ -306,6 +306,11 @@ const boundingBoxSchema = baseCommandSchema.extend({
selector: z.string().min(1),
});
const stylesSchema = baseCommandSchema.extend({
action: z.literal('styles'),
selector: z.string().min(1),
});
const videoStartSchema = baseCommandSchema.extend({
action: z.literal('video_start'),
path: z.string().min(1),
@@ -820,6 +825,7 @@ const commandSchema = z.discriminatedUnion('action', [
isCheckedSchema,
countSchema,
boundingBoxSchema,
stylesSchema,
videoStartSchema,
videoStopSchema,
recordingStartSchema,
+29
View File
@@ -316,6 +316,12 @@ export interface BoundingBoxCommand extends BaseCommand {
selector: string;
}
// Computed styles
export interface StylesCommand extends BaseCommand {
action: 'styles';
selector: string;
}
// More semantic locators
export interface GetByAltTextCommand extends BaseCommand {
action: 'getbyalttext';
@@ -857,6 +863,7 @@ export type Command =
| IsCheckedCommand
| CountCommand
| BoundingBoxCommand
| StylesCommand
| VideoStartCommand
| VideoStopCommand
| RecordingStartCommand
@@ -1017,6 +1024,28 @@ export interface InputEventData {
injected: boolean;
}
// Element styles data
export interface ElementStyleInfo {
tag: string;
text: string | null;
box: { x: number; y: number; width: number; height: number };
styles: {
fontSize: string;
fontWeight: string;
fontFamily: string;
color: string;
backgroundColor: string;
borderRadius: string;
border: string | null;
boxShadow: string | null;
padding: string;
};
}
export interface StylesData {
elements: ElementStyleInfo[];
}
// Browser state
export interface BrowserState {
browser: Browser | null;