import { htmlToElement } from './htmlToElement'; import { rankCandidateElements } from './rank'; export interface TargetElements { target: HTMLElement; ancestors: HTMLElement[]; } export const isVisible = (element: HTMLElement): boolean => { return element.offsetWidth > 0 && element.offsetHeight > 0; }; export const buildCandidateSelector = (element: HTMLElement): string => { const tagName = element.tagName.toLowerCase(); if (tagName === 'select') return 'select'; const isClickableInput = tagName === 'input' && ['checkbox', 'radio', 'submit'].includes( (element as HTMLInputElement).type, ); if (tagName === 'button' || isClickableInput) { return 'button,input[type="checkbox"],input[type="radio"],input[type="submit"]'; } if ( ['input', 'textarea'].includes(tagName) || element.contentEditable === 'true' ) { return 'input,textarea,[contenteditable="true"]'; } return '*'; }; export const findCandidateElements = ( root: Document | Element, target: HTMLElement, ): HTMLElement[] => { const selector = buildCandidateSelector(target); const candidates = Array.from( root.querySelectorAll(selector), ) as HTMLElement[]; return candidates.filter(candidate => isVisible(candidate)); }; export const flattenTargetElements = (element: HTMLElement): TargetElements => { let target: HTMLElement = element; const ancestors: HTMLElement[] = []; while (target.children.length) { if (target.children.length > 1) { throw new Error( 'playwright-html-selector: cannot target multiple elements on same level', ); } ancestors.push(target); target = target.children[0] as HTMLElement; } // order the ancestors by ascending distance to the target ancestors.reverse(); return { ancestors, target }; }; export const queryHtmlSelectorAll = ( root: Document | Element, selectorHtml: string, ): HTMLElement[] => { const selectorElement = htmlToElement(selectorHtml); const targetElements = flattenTargetElements(selectorElement); const candidates = findCandidateElements(root, targetElements.target); const rankings = rankCandidateElements(targetElements, candidates); return rankings.map(ranking => ranking.node); };