Files
2026-03-03 23:49:13 +01:00

231 lines
8.7 KiB
JavaScript

export async function getStylesForLocator({ locator, cdp: cdpSession, includeUserAgentStyles = false, }) {
const cdp = cdpSession;
await cdp.send('DOM.enable');
await cdp.send('CSS.enable');
const elementHandle = await locator.elementHandle();
if (!elementHandle) {
throw new Error('Could not get element handle from locator');
}
const remoteObject = elementHandle._channel?.objectId
? { objectId: elementHandle._channel.objectId }
: null;
let backendNodeId;
if (remoteObject?.objectId) {
const nodeInfo = await cdp.send('DOM.describeNode', {
objectId: remoteObject.objectId,
});
backendNodeId = nodeInfo.node.backendNodeId;
}
else {
const box = await elementHandle.boundingBox();
if (!box) {
throw new Error('Element has no bounding box');
}
const docResult = await cdp.send('DOM.getDocument', { depth: 0 });
const nodeAtPoint = await cdp.send('DOM.getNodeForLocation', {
x: Math.round(box.x + box.width / 2),
y: Math.round(box.y + box.height / 2),
});
backendNodeId = nodeAtPoint.backendNodeId;
}
const pushResult = await cdp.send('DOM.pushNodesByBackendIdsToFrontend', {
backendNodeIds: [backendNodeId],
});
const nodeId = pushResult.nodeIds[0];
if (!nodeId) {
throw new Error('Could not get nodeId for element');
}
const nodeInfo = await cdp.send('DOM.describeNode', { nodeId });
const elementDescription = formatElementDescription(nodeInfo.node);
const matchedStyles = await cdp.send('CSS.getMatchedStylesForNode', { nodeId });
const stylesheetUrls = new Map();
const processStyleSheetId = async (styleSheetId) => {
if (!styleSheetId) {
return null;
}
if (!stylesheetUrls.has(styleSheetId)) {
try {
const header = await cdp.send('CSS.getStyleSheetText', { styleSheetId });
stylesheetUrls.set(styleSheetId, '');
}
catch {
stylesheetUrls.set(styleSheetId, '');
}
}
return null;
};
const rules = [];
if (matchedStyles.matchedCSSRules) {
for (const ruleMatch of matchedStyles.matchedCSSRules) {
const rule = ruleMatch.rule;
const sourceRange = rule.selectorList?.range;
const styleSheetId = rule.styleSheetId;
let source = null;
if (styleSheetId && sourceRange) {
const styleSheet = matchedStyles.cssStyleSheetHeaders?.find((h) => h.styleSheetId === styleSheetId);
const url = styleSheet?.sourceURL || rule.origin === 'user-agent' ? 'user-agent' : `stylesheet:${styleSheetId}`;
source = {
url: rule.styleSheetId ? await getStylesheetUrl(cdp, styleSheetId) : 'user-agent',
line: sourceRange.startLine + 1,
column: sourceRange.startColumn,
};
}
rules.push({
selector: rule.selectorList.text,
source,
origin: rule.origin,
declarations: extractDeclarations(rule.style),
inheritedFrom: null,
});
}
}
if (matchedStyles.inherited) {
for (let i = 0; i < matchedStyles.inherited.length; i++) {
const inheritedEntry = matchedStyles.inherited[i];
const ancestorDesc = `ancestor[${i + 1}]`;
if (inheritedEntry.inlineStyle) {
const declarations = extractDeclarations(inheritedEntry.inlineStyle);
if (Object.keys(declarations).length > 0) {
rules.push({
selector: 'element.style',
source: null,
origin: 'regular',
declarations,
inheritedFrom: ancestorDesc,
});
}
}
for (const ruleMatch of inheritedEntry.matchedCSSRules) {
const rule = ruleMatch.rule;
const sourceRange = rule.selectorList?.range;
const styleSheetId = rule.styleSheetId;
let source = null;
if (styleSheetId && sourceRange) {
source = {
url: await getStylesheetUrl(cdp, styleSheetId),
line: sourceRange.startLine + 1,
column: sourceRange.startColumn,
};
}
const declarations = extractDeclarations(rule.style);
if (Object.keys(declarations).length > 0) {
rules.push({
selector: rule.selectorList.text,
source,
origin: rule.origin,
declarations,
inheritedFrom: ancestorDesc,
});
}
}
}
}
let inlineStyle = null;
if (matchedStyles.inlineStyle) {
const declarations = extractDeclarations(matchedStyles.inlineStyle);
if (Object.keys(declarations).length > 0) {
inlineStyle = declarations;
}
}
const filteredRules = includeUserAgentStyles ? rules : rules.filter((r) => r.origin !== 'user-agent');
return {
element: elementDescription,
inlineStyle,
rules: filteredRules,
};
}
function extractDeclarations(style) {
if (!style?.cssProperties) {
return {};
}
const result = {};
for (const prop of style.cssProperties) {
if (!prop.value || prop.value === 'initial' || prop.name.startsWith('-webkit-')) {
continue;
}
const value = prop.important ? `${prop.value} !important` : prop.value;
result[prop.name] = value;
}
return result;
}
function formatElementDescription(node) {
let desc = node.localName || node.nodeName?.toLowerCase() || 'element';
if (node.attributes) {
const attrs = {};
for (let i = 0; i < node.attributes.length; i += 2) {
attrs[node.attributes[i]] = node.attributes[i + 1];
}
if (attrs.id) {
desc += `#${attrs.id}`;
}
if (attrs.class) {
desc += `.${attrs.class.split(' ').join('.')}`;
}
}
return desc;
}
async function getStylesheetUrl(cdp, styleSheetId) {
try {
await cdp.send('CSS.getStyleSheetText', { styleSheetId });
return `stylesheet:${styleSheetId}`;
}
catch {
return `stylesheet:${styleSheetId}`;
}
}
export function formatStylesAsText(styles) {
const lines = [];
lines.push(`Element: ${styles.element}`);
lines.push('');
if (styles.inlineStyle) {
lines.push('Inline styles:');
for (const [prop, value] of Object.entries(styles.inlineStyle)) {
lines.push(` ${prop}: ${value}`);
}
lines.push('');
}
const directRules = styles.rules.filter((r) => !r.inheritedFrom);
const inheritedRules = styles.rules.filter((r) => r.inheritedFrom);
if (directRules.length > 0) {
lines.push('Matched rules:');
for (const rule of directRules) {
lines.push(` ${rule.selector} {`);
const sourceInfo = rule.source ? ` /* ${rule.source.url}:${rule.source.line}:${rule.source.column} */` : '';
if (sourceInfo) {
lines.push(` ${sourceInfo}`);
}
for (const [prop, value] of Object.entries(rule.declarations)) {
lines.push(` ${prop}: ${value};`);
}
lines.push(' }');
}
lines.push('');
}
if (inheritedRules.length > 0) {
const byAncestor = new Map();
for (const rule of inheritedRules) {
const key = rule.inheritedFrom;
if (!byAncestor.has(key)) {
byAncestor.set(key, []);
}
byAncestor.get(key).push(rule);
}
for (const [ancestor, rules] of byAncestor) {
lines.push(`Inherited from ${ancestor}:`);
for (const rule of rules) {
lines.push(` ${rule.selector} {`);
const sourceInfo = rule.source ? ` /* ${rule.source.url}:${rule.source.line}:${rule.source.column} */` : '';
if (sourceInfo) {
lines.push(` ${sourceInfo}`);
}
for (const [prop, value] of Object.entries(rule.declarations)) {
lines.push(` ${prop}: ${value};`);
}
lines.push(' }');
}
lines.push('');
}
}
return lines.join('\n');
}
//# sourceMappingURL=styles.js.map