computed-styles-regression-test
Version:
DOM & CSSOM based regression testing utility
168 lines • 6.9 kB
JavaScript
import { createErr, createOk, unwrapOk, isErr, unwrapErr } from 'option-t/plain_result';
import { isNotNull } from 'option-t/nullable/nullable';
export async function getComputedStyles(traverser, nodeId) {
const computedStyleResult = await traverser.getComputedStyleForNode(nodeId);
if (isErr(computedStyleResult)) {
return computedStyleResult;
}
const { computedStyle } = unwrapOk(computedStyleResult);
const styles = computedStyle.reduce((acc, style) => {
acc[style.name] = style.value;
return acc;
}, {});
return createOk(styles);
}
export async function getMatchedStyles(traverser, nodeId) {
const matchedStylesResult = await traverser.getMatchedStylesForNode(nodeId);
if (isErr(matchedStylesResult)) {
return matchedStylesResult;
}
const { matchedCSSRules, inherited } = unwrapOk(matchedStylesResult);
const styles = {};
// Process matched CSS rules
if (matchedCSSRules) {
for (const ruleMatch of matchedCSSRules) {
const rule = ruleMatch.rule;
if (rule.style && rule.style.cssProperties) {
for (const cssProperty of rule.style.cssProperties) {
if (cssProperty.name && cssProperty.value) {
styles[cssProperty.name] = cssProperty.value;
}
}
}
}
}
// Process inherited styles
if (inherited) {
for (const inheritedEntry of inherited) {
if (inheritedEntry.matchedCSSRules) {
for (const ruleMatch of inheritedEntry.matchedCSSRules) {
const rule = ruleMatch.rule;
if (rule.style && rule.style.cssProperties) {
for (const cssProperty of rule.style.cssProperties) {
if (cssProperty.name && cssProperty.value) {
// Only add if not already set by direct rules
if (!(cssProperty.name in styles)) {
styles[cssProperty.name] = cssProperty.value;
}
}
}
}
}
}
}
}
return createOk(styles);
}
export async function getComputedStylesWithPseudoState(traverser, nodeId, pseudoState) {
if (!pseudoState) {
return getMatchedStyles(traverser, nodeId);
}
try {
// Force the pseudo-state using CDP CSS.forcePseudoState
await forcePseudoState(traverser, nodeId, pseudoState);
// Get matched styles with the pseudo-state active
// This will include the pseudo-class rules that are now active
const stylesResult = await getMatchedStyles(traverser, nodeId);
// Clear the forced pseudo-state
await clearForcedPseudoState(traverser, nodeId);
return stylesResult;
}
catch (error) {
// If forcing pseudo-state fails, fall back to normal matched styles
console.warn(`Failed to force pseudo-state ${pseudoState}:`, error);
return getMatchedStyles(traverser, nodeId);
}
}
async function forcePseudoState(traverser, nodeId, pseudoState) {
const pseudoClasses = [pseudoState];
const result = await traverser.forcePseudoState(nodeId, pseudoClasses);
if (isErr(result)) {
throw unwrapErr(result);
}
}
async function clearForcedPseudoState(traverser, nodeId) {
const result = await traverser.forcePseudoState(nodeId, []);
if (isErr(result)) {
console.warn('Failed to clear forced pseudo-state:', unwrapErr(result));
}
}
export async function traverseElement(traverser, nodeId, siblingIndex, options) {
const describeNodeResult = await traverser.describeNode(nodeId);
if (isErr(describeNodeResult)) {
return describeNodeResult;
}
const { node } = unwrapOk(describeNodeResult);
if (node.nodeType !== 1) {
return createErr(new Error('Node is not an element'));
}
const attributes = {};
if (node.attributes) {
for (let i = 0; i < node.attributes.length; i += 2) {
const name = node.attributes[i];
const value = node.attributes[i + 1] || '';
attributes[name] = value;
}
}
const computedStylesResult = await getMatchedStyles(traverser, nodeId);
// Generate unique selector for this element
const uniqueSelector = generateUniqueSelector(node, siblingIndex);
// Check if this element has pseudo-states defined
const pseudoStates = {};
if (options.pseudoStatesMap && options.pseudoStatesMap[uniqueSelector]) {
for (const pseudoState of options.pseudoStatesMap[uniqueSelector]) {
const pseudoStylesResult = await getComputedStylesWithPseudoState(traverser, nodeId, pseudoState);
if (!isErr(pseudoStylesResult)) {
pseudoStates[pseudoState] = unwrapOk(pseudoStylesResult);
}
}
}
const childrenResults = options.includeChildren && node.children
? (await Promise.all(node.children.map(async (child, index) => {
if (child.nodeType !== 1) {
return null;
}
return traverseElement(traverser, child.nodeId, index, {
includeChildren: true,
pseudoStatesMap: options.pseudoStatesMap,
});
}))).filter(isNotNull)
: [];
const errors = childrenResults.filter(isErr);
if (errors.length > 0) {
return createErr(new Error('Failed to traverse children', {
cause: new AggregateError(errors.map(unwrapErr)),
}));
}
const children = childrenResults.map(unwrapOk);
const elementNode = {
nodeName: node.nodeName,
uniqueSelector,
computedStyles: unwrapOk(computedStylesResult),
children,
attributes,
textContent: node.nodeValue || undefined,
};
// Add pseudo-states if any were found
if (Object.keys(pseudoStates).length > 0) {
elementNode.pseudoStates = pseudoStates;
}
return createOk(elementNode);
}
function generateUniqueSelector(element, siblingIndex) {
const selectorParts = [];
if (element.nodeName) {
selectorParts.push(element.nodeName.toLowerCase());
}
const idIndex = element.attributes?.findIndex((attribute) => attribute === 'id');
const classNameIndex = element.attributes?.findIndex((attribute) => attribute === 'class');
if (classNameIndex !== -1 && classNameIndex != null) {
selectorParts.push(`.${element.attributes?.[classNameIndex + 1]}`);
}
if (idIndex !== -1 && idIndex != null) {
selectorParts.push(`#${element.attributes?.[idIndex + 1]}`);
}
selectorParts.push(`@${siblingIndex}`);
return selectorParts.join('');
}
//# sourceMappingURL=node.js.map