feat: add 'get styles' command for computed styles extraction (#142)
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user