///
/* eslint-disable @typescript-eslint/no-explicit-any */
import { fromPairs, toArray } from 'lodash';
import { buildException, mapSelectorTypeToDisplaySelectorName } from './utils';
import type { SelectorType } from './SelectorBuilder';
type XPathQueryResult = boolean | string | number | Element;
const xpath = (
subject: any,
selector: string,
options: Map,
type: SelectorType,
) => {
if (Cypress.dom.isElement(subject) && subject.length > 1) {
throw buildException(
`Failed to find an element by XPath("${selector}") - the parent is not an element but a collection of ${subject.length} elements.`,
);
}
const resolveResult = () =>
Cypress.Promise.try(() => evaluateXPath(selector, subject)).then((rawValue) => {
const isElements = Array.isArray(rawValue);
const value = Array.isArray(rawValue)
? Cypress.$(rawValue.values.length === 1 ? rawValue[0] : rawValue)
: rawValue;
if (isElements)
value.selector = selector; /* This is to log query in error message in case of failure */
// @ts-ignore
return cy.verifyUpcomingAssertions(value, options, { onRetry: resolveResult });
});
return resolveResult().then((result: XPathQueryResult) => {
Cypress.log(generateLogEntryForXPathResult(result, selector, type));
return result;
});
};
type LogEntry = {
name: string;
consoleProps: () => {
'Node Type': string;
'XPath Selector': string;
'XPath Result': XPathQueryResult;
};
};
const generateLogEntryForXPathResult = (
result: XPathQueryResult,
selector: string,
type: SelectorType,
): LogEntry => {
const BASE = { 'XPath Selector': selector, 'XPath Result': result };
const name = type === 'xpath' ? `XPath` : `XPath(${mapSelectorTypeToDisplaySelectorName(type)})`;
if (typeof result !== 'object')
return {
name,
consoleProps: () => ({ ...BASE, 'Node Type': typeof result }),
};
else {
if ('length' in result && 'nodeType' in result === false) {
const nodeTypes = toArray(result)
.map((r, id) => `${id}: ${NOTE_TYPE_TO_LABEL_MAPPING[r.nodeType]}`)
.join(', ');
if (result['length'] === 0) BASE['XPath Result'] = 'None';
return { name, consoleProps: () => ({ ...BASE, 'Node Type': nodeTypes }) };
}
const { nodeType } = result;
return {
name,
consoleProps: () => ({
...BASE,
'Node Type': NOTE_TYPE_TO_LABEL_MAPPING[nodeType]
? `${nodeType} - ${NOTE_TYPE_TO_LABEL_MAPPING[nodeType]}`
: `${nodeType} - UNKNOWN_NODE_TYPE`,
}),
};
}
};
const evaluateXPath = (xpath: string, subject = getDocument()) => {
let contextNode;
// @ts-ignore
const [withinSubject, { document }] = [cy.state('withinSubject'), cy.state('window')];
if (Cypress.dom.isElement(subject)) contextNode = (subject as any)[0];
else if (Cypress.dom.isDocument(subject)) contextNode = subject;
else if (withinSubject) contextNode = withinSubject[0];
else contextNode = document;
const result = (contextNode.ownerDocument || contextNode).evaluate(xpath, contextNode);
if (result.resultType === XPathResult.NUMBER_TYPE) return result.numberValue;
else if (result.resultType === XPathResult.STRING_TYPE) return result.stringValue;
else if (result.resultType === XPathResult.BOOLEAN_TYPE) return result.booleanValue;
return collectNodes(result);
};
const collectNodes = (nodeIterator: XPathResult): Array | Node => {
const nodes = [];
let node = undefined;
while ((node = nodeIterator.iterateNext())) nodes.push(node);
return nodes.length === 1 ? nodes[0] : nodes;
};
// @ts-ignore
const getDocument = (): Document => cy.state('window').document;
/* Source of types: https://developer.mozilla.org/ru/docs/Web/API/Node/nodeType */
const NOTE_TYPE_TO_LABEL_MAPPING = fromPairs([
[1, 'ELEMENT_NODE'],
[2, 'ATTRIBUTE_NODE'],
[3, 'TEXT_NODE'],
[4, 'CDATA_SECTION_NODE'],
[5, 'ENTITY_REFERENCE_NODE'],
[6, 'ENTITY_NODE'],
[7, 'PROCESSING_INSTRUCTION_NODE'],
[8, 'COMMENT_NODE'],
[9, 'DOCUMENT_NODE'],
[10, 'DOCUMENT_TYPE_NODE'],
[11, 'DOCUMENT_FRAGMENT_NODE'],
[12, 'NOTATION_NODE'],
]);
const registerInternalXPathCommand = (): void =>
Cypress.Commands.add(
'__cypress_selectors_xpath',
{ prevSubject: ['optional', 'element', 'document'] },
xpath,
);
export { registerInternalXPathCommand, generateLogEntryForXPathResult };