UNPKG

computed-styles-regression-test

Version:

DOM & CSSOM based regression testing utility

168 lines 6.9 kB
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