 aacb45156b
			
		
	
	aacb45156b
	
	
	
		
			
			- Install Jest for unit testing with React Testing Library - Install Playwright for end-to-end testing - Configure Jest with proper TypeScript support and module mapping - Create test setup files and utilities for both unit and e2e tests Components: * Jest configuration with coverage thresholds * Playwright configuration with browser automation * Unit tests for LoginForm, AuthContext, and useSocketIO hook * E2E tests for authentication, dashboard, and agents workflows * GitHub Actions workflow for automated testing * Mock data and API utilities for consistent testing * Test documentation with best practices Testing features: - Unit tests with 70% coverage threshold - E2E tests with API mocking and user journey testing - CI/CD integration for automated test runs - Cross-browser testing support with Playwright - Authentication system testing end-to-end 🚀 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
		
			
				
	
	
		
			1946 lines
		
	
	
		
			54 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1946 lines
		
	
	
		
			54 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | ||
| 
 | ||
| var redent = require('redent');
 | ||
| var cssTools = require('@adobe/css-tools');
 | ||
| var domAccessibilityApi = require('dom-accessibility-api');
 | ||
| var ariaQuery = require('aria-query');
 | ||
| var chalk = require('chalk');
 | ||
| var isEqualWith = require('lodash/isEqualWith.js');
 | ||
| var escape = require('css.escape');
 | ||
| 
 | ||
| class GenericTypeError extends Error {
 | ||
|   constructor(expectedString, received, matcherFn, context) {
 | ||
|     super();
 | ||
| 
 | ||
|     /* istanbul ignore next */
 | ||
|     if (Error.captureStackTrace) {
 | ||
|       Error.captureStackTrace(this, matcherFn);
 | ||
|     }
 | ||
|     let withType = '';
 | ||
|     try {
 | ||
|       withType = context.utils.printWithType(
 | ||
|         'Received',
 | ||
|         received,
 | ||
|         context.utils.printReceived,
 | ||
|       );
 | ||
|     } catch (e) {
 | ||
|       // Can throw for Document:
 | ||
|       // https://github.com/jsdom/jsdom/issues/2304
 | ||
|     }
 | ||
|     this.message = [
 | ||
|       context.utils.matcherHint(
 | ||
|         `${context.isNot ? '.not' : ''}.${matcherFn.name}`,
 | ||
|         'received',
 | ||
|         '',
 | ||
|       ),
 | ||
|       '',
 | ||
|       // eslint-disable-next-line new-cap
 | ||
|       `${context.utils.RECEIVED_COLOR(
 | ||
|         'received',
 | ||
|       )} value must ${expectedString}.`,
 | ||
|       withType,
 | ||
|     ].join('\n');
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| class HtmlElementTypeError extends GenericTypeError {
 | ||
|   constructor(...args) {
 | ||
|     super('be an HTMLElement or an SVGElement', ...args);
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| class NodeTypeError extends GenericTypeError {
 | ||
|   constructor(...args) {
 | ||
|     super('be a Node', ...args);
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| function checkHasWindow(htmlElement, ErrorClass, ...args) {
 | ||
|   if (
 | ||
|     !htmlElement ||
 | ||
|     !htmlElement.ownerDocument ||
 | ||
|     !htmlElement.ownerDocument.defaultView
 | ||
|   ) {
 | ||
|     throw new ErrorClass(htmlElement, ...args)
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| function checkNode(node, ...args) {
 | ||
|   checkHasWindow(node, NodeTypeError, ...args);
 | ||
|   const window = node.ownerDocument.defaultView;
 | ||
| 
 | ||
|   if (!(node instanceof window.Node)) {
 | ||
|     throw new NodeTypeError(node, ...args)
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| function checkHtmlElement(htmlElement, ...args) {
 | ||
|   checkHasWindow(htmlElement, HtmlElementTypeError, ...args);
 | ||
|   const window = htmlElement.ownerDocument.defaultView;
 | ||
| 
 | ||
|   if (
 | ||
|     !(htmlElement instanceof window.HTMLElement) &&
 | ||
|     !(htmlElement instanceof window.SVGElement)
 | ||
|   ) {
 | ||
|     throw new HtmlElementTypeError(htmlElement, ...args)
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| class InvalidCSSError extends Error {
 | ||
|   constructor(received, matcherFn, context) {
 | ||
|     super();
 | ||
| 
 | ||
|     /* istanbul ignore next */
 | ||
|     if (Error.captureStackTrace) {
 | ||
|       Error.captureStackTrace(this, matcherFn);
 | ||
|     }
 | ||
|     this.message = [
 | ||
|       received.message,
 | ||
|       '',
 | ||
|       // eslint-disable-next-line new-cap
 | ||
|       context.utils.RECEIVED_COLOR(`Failing css:`),
 | ||
|       // eslint-disable-next-line new-cap
 | ||
|       context.utils.RECEIVED_COLOR(`${received.css}`),
 | ||
|     ].join('\n');
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| function parseCSS(css, ...args) {
 | ||
|   const ast = cssTools.parse(`selector { ${css} }`, {silent: true}).stylesheet;
 | ||
| 
 | ||
|   if (ast.parsingErrors && ast.parsingErrors.length > 0) {
 | ||
|     const {reason, line} = ast.parsingErrors[0];
 | ||
| 
 | ||
|     throw new InvalidCSSError(
 | ||
|       {
 | ||
|         css,
 | ||
|         message: `Syntax error parsing expected css: ${reason} on line: ${line}`,
 | ||
|       },
 | ||
|       ...args,
 | ||
|     )
 | ||
|   }
 | ||
| 
 | ||
|   const parsedRules = ast.rules[0].declarations
 | ||
|     .filter(d => d.type === 'declaration')
 | ||
|     .reduce(
 | ||
|       (obj, {property, value}) => Object.assign(obj, {[property]: value}),
 | ||
|       {},
 | ||
|     );
 | ||
|   return parsedRules
 | ||
| }
 | ||
| 
 | ||
| function display(context, value) {
 | ||
|   return typeof value === 'string' ? value : context.utils.stringify(value)
 | ||
| }
 | ||
| 
 | ||
| function getMessage(
 | ||
|   context,
 | ||
|   matcher,
 | ||
|   expectedLabel,
 | ||
|   expectedValue,
 | ||
|   receivedLabel,
 | ||
|   receivedValue,
 | ||
| ) {
 | ||
|   return [
 | ||
|     `${matcher}\n`,
 | ||
|     // eslint-disable-next-line new-cap
 | ||
|     `${expectedLabel}:\n${context.utils.EXPECTED_COLOR(
 | ||
|       redent(display(context, expectedValue), 2),
 | ||
|     )}`,
 | ||
|     // eslint-disable-next-line new-cap
 | ||
|     `${receivedLabel}:\n${context.utils.RECEIVED_COLOR(
 | ||
|       redent(display(context, receivedValue), 2),
 | ||
|     )}`,
 | ||
|   ].join('\n')
 | ||
| }
 | ||
| 
 | ||
| function matches(textToMatch, matcher) {
 | ||
|   if (matcher instanceof RegExp) {
 | ||
|     return matcher.test(textToMatch)
 | ||
|   } else {
 | ||
|     return textToMatch.includes(String(matcher))
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| function deprecate(name, replacementText) {
 | ||
|   // Notify user that they are using deprecated functionality.
 | ||
|   // eslint-disable-next-line no-console
 | ||
|   console.warn(
 | ||
|     `Warning: ${name} has been deprecated and will be removed in future updates.`,
 | ||
|     replacementText,
 | ||
|   );
 | ||
| }
 | ||
| 
 | ||
| function normalize(text) {
 | ||
|   return text.replace(/\s+/g, ' ').trim()
 | ||
| }
 | ||
| 
 | ||
| function getTag(element) {
 | ||
|   return element.tagName && element.tagName.toLowerCase()
 | ||
| }
 | ||
| 
 | ||
| function getSelectValue({multiple, options}) {
 | ||
|   const selectedOptions = [...options].filter(option => option.selected);
 | ||
| 
 | ||
|   if (multiple) {
 | ||
|     return [...selectedOptions].map(opt => opt.value)
 | ||
|   }
 | ||
|   /* istanbul ignore if */
 | ||
|   if (selectedOptions.length === 0) {
 | ||
|     return undefined // Couldn't make this happen, but just in case
 | ||
|   }
 | ||
|   return selectedOptions[0].value
 | ||
| }
 | ||
| 
 | ||
| function getInputValue(inputElement) {
 | ||
|   switch (inputElement.type) {
 | ||
|     case 'number':
 | ||
|       return inputElement.value === '' ? null : Number(inputElement.value)
 | ||
|     case 'checkbox':
 | ||
|       return inputElement.checked
 | ||
|     default:
 | ||
|       return inputElement.value
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| const rolesSupportingValues = ['meter', 'progressbar', 'slider', 'spinbutton'];
 | ||
| function getAccessibleValue(element) {
 | ||
|   if (!rolesSupportingValues.includes(element.getAttribute('role'))) {
 | ||
|     return undefined
 | ||
|   }
 | ||
|   return Number(element.getAttribute('aria-valuenow'))
 | ||
| }
 | ||
| 
 | ||
| function getSingleElementValue(element) {
 | ||
|   /* istanbul ignore if */
 | ||
|   if (!element) {
 | ||
|     return undefined
 | ||
|   }
 | ||
| 
 | ||
|   switch (element.tagName.toLowerCase()) {
 | ||
|     case 'input':
 | ||
|       return getInputValue(element)
 | ||
|     case 'select':
 | ||
|       return getSelectValue(element)
 | ||
|     default: {
 | ||
|       return element.value ?? getAccessibleValue(element)
 | ||
|     }
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| function toSentence(
 | ||
|   array,
 | ||
|   {wordConnector = ', ', lastWordConnector = ' and '} = {},
 | ||
| ) {
 | ||
|   return [array.slice(0, -1).join(wordConnector), array[array.length - 1]].join(
 | ||
|     array.length > 1 ? lastWordConnector : '',
 | ||
|   )
 | ||
| }
 | ||
| 
 | ||
| function compareArraysAsSet(arr1, arr2) {
 | ||
|   if (Array.isArray(arr1) && Array.isArray(arr2)) {
 | ||
|     return [...new Set(arr1)].every(v => new Set(arr2).has(v))
 | ||
|   }
 | ||
|   return undefined
 | ||
| }
 | ||
| 
 | ||
| function toBeInTheDOM(element, container) {
 | ||
|   deprecate(
 | ||
|     'toBeInTheDOM',
 | ||
|     'Please use toBeInTheDocument for searching the entire document and toContainElement for searching a specific container.',
 | ||
|   );
 | ||
| 
 | ||
|   if (element) {
 | ||
|     checkHtmlElement(element, toBeInTheDOM, this);
 | ||
|   }
 | ||
| 
 | ||
|   if (container) {
 | ||
|     checkHtmlElement(container, toBeInTheDOM, this);
 | ||
|   }
 | ||
| 
 | ||
|   return {
 | ||
|     pass: container ? container.contains(element) : !!element,
 | ||
|     message: () => {
 | ||
|       return [
 | ||
|         this.utils.matcherHint(
 | ||
|           `${this.isNot ? '.not' : ''}.toBeInTheDOM`,
 | ||
|           'element',
 | ||
|           '',
 | ||
|         ),
 | ||
|         '',
 | ||
|         'Received:',
 | ||
|         `  ${this.utils.printReceived(
 | ||
|           element ? element.cloneNode(false) : element,
 | ||
|         )}`,
 | ||
|       ].join('\n')
 | ||
|     },
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| function toBeInTheDocument(element) {
 | ||
|   if (element !== null || !this.isNot) {
 | ||
|     checkHtmlElement(element, toBeInTheDocument, this);
 | ||
|   }
 | ||
| 
 | ||
|   const pass =
 | ||
|     element === null
 | ||
|       ? false
 | ||
|       : element.ownerDocument === element.getRootNode({composed: true});
 | ||
| 
 | ||
|   const errorFound = () => {
 | ||
|     return `expected document not to contain element, found ${this.utils.stringify(
 | ||
|       element.cloneNode(true),
 | ||
|     )} instead`
 | ||
|   };
 | ||
|   const errorNotFound = () => {
 | ||
|     return `element could not be found in the document`
 | ||
|   };
 | ||
| 
 | ||
|   return {
 | ||
|     pass,
 | ||
|     message: () => {
 | ||
|       return [
 | ||
|         this.utils.matcherHint(
 | ||
|           `${this.isNot ? '.not' : ''}.toBeInTheDocument`,
 | ||
|           'element',
 | ||
|           '',
 | ||
|         ),
 | ||
|         '',
 | ||
|         // eslint-disable-next-line new-cap
 | ||
|         this.utils.RECEIVED_COLOR(this.isNot ? errorFound() : errorNotFound()),
 | ||
|       ].join('\n')
 | ||
|     },
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| function toBeEmpty(element) {
 | ||
|   deprecate(
 | ||
|     'toBeEmpty',
 | ||
|     'Please use instead toBeEmptyDOMElement for finding empty nodes in the DOM.',
 | ||
|   );
 | ||
|   checkHtmlElement(element, toBeEmpty, this);
 | ||
| 
 | ||
|   return {
 | ||
|     pass: element.innerHTML === '',
 | ||
|     message: () => {
 | ||
|       return [
 | ||
|         this.utils.matcherHint(
 | ||
|           `${this.isNot ? '.not' : ''}.toBeEmpty`,
 | ||
|           'element',
 | ||
|           '',
 | ||
|         ),
 | ||
|         '',
 | ||
|         'Received:',
 | ||
|         `  ${this.utils.printReceived(element.innerHTML)}`,
 | ||
|       ].join('\n')
 | ||
|     },
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| function toBeEmptyDOMElement(element) {
 | ||
|   checkHtmlElement(element, toBeEmptyDOMElement, this);
 | ||
| 
 | ||
|   return {
 | ||
|     pass: isEmptyElement(element),
 | ||
|     message: () => {
 | ||
|       return [
 | ||
|         this.utils.matcherHint(
 | ||
|           `${this.isNot ? '.not' : ''}.toBeEmptyDOMElement`,
 | ||
|           'element',
 | ||
|           '',
 | ||
|         ),
 | ||
|         '',
 | ||
|         'Received:',
 | ||
|         `  ${this.utils.printReceived(element.innerHTML)}`,
 | ||
|       ].join('\n')
 | ||
|     },
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| /**
 | ||
|  * Identifies if an element doesn't contain child nodes (excluding comments)
 | ||
|  * ℹ Node.COMMENT_NODE can't be used because of the following issue 
 | ||
|  * https://github.com/jsdom/jsdom/issues/2220
 | ||
|  *
 | ||
|  * @param {*} element an HtmlElement or SVGElement
 | ||
|  * @return {*} true if the element only contains comments or none
 | ||
|  */
 | ||
| function isEmptyElement(element){
 | ||
|   const nonCommentChildNodes = [...element.childNodes].filter(node => node.nodeType !== 8);
 | ||
|   return nonCommentChildNodes.length === 0;
 | ||
| }
 | ||
| 
 | ||
| function toContainElement(container, element) {
 | ||
|   checkHtmlElement(container, toContainElement, this);
 | ||
| 
 | ||
|   if (element !== null) {
 | ||
|     checkHtmlElement(element, toContainElement, this);
 | ||
|   }
 | ||
| 
 | ||
|   return {
 | ||
|     pass: container.contains(element),
 | ||
|     message: () => {
 | ||
|       return [
 | ||
|         this.utils.matcherHint(
 | ||
|           `${this.isNot ? '.not' : ''}.toContainElement`,
 | ||
|           'element',
 | ||
|           'element',
 | ||
|         ),
 | ||
|         '',
 | ||
|         // eslint-disable-next-line new-cap
 | ||
|         this.utils.RECEIVED_COLOR(`${this.utils.stringify(
 | ||
|           container.cloneNode(false),
 | ||
|         )} ${
 | ||
|           this.isNot ? 'contains:' : 'does not contain:'
 | ||
|         } ${this.utils.stringify(element ? element.cloneNode(false) : element)}
 | ||
|         `),
 | ||
|       ].join('\n')
 | ||
|     },
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| function getNormalizedHtml(container, htmlText) {
 | ||
|   const div = container.ownerDocument.createElement('div');
 | ||
|   div.innerHTML = htmlText;
 | ||
|   return div.innerHTML
 | ||
| }
 | ||
| 
 | ||
| function toContainHTML(container, htmlText) {
 | ||
|   checkHtmlElement(container, toContainHTML, this);
 | ||
| 
 | ||
|   if (typeof htmlText !== 'string') {
 | ||
|     throw new Error(`.toContainHTML() expects a string value, got ${htmlText}`)
 | ||
|   }
 | ||
| 
 | ||
|   return {
 | ||
|     pass: container.outerHTML.includes(getNormalizedHtml(container, htmlText)),
 | ||
|     message: () => {
 | ||
|       return [
 | ||
|         this.utils.matcherHint(
 | ||
|           `${this.isNot ? '.not' : ''}.toContainHTML`,
 | ||
|           'element',
 | ||
|           '',
 | ||
|         ),
 | ||
|         'Expected:',
 | ||
|         // eslint-disable-next-line new-cap
 | ||
|         `  ${this.utils.EXPECTED_COLOR(htmlText)}`,
 | ||
|         'Received:',
 | ||
|         `  ${this.utils.printReceived(container.cloneNode(true))}`,
 | ||
|       ].join('\n')
 | ||
|     },
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| function toHaveTextContent(
 | ||
|   node,
 | ||
|   checkWith,
 | ||
|   options = {normalizeWhitespace: true},
 | ||
| ) {
 | ||
|   checkNode(node, toHaveTextContent, this);
 | ||
| 
 | ||
|   const textContent = options.normalizeWhitespace
 | ||
|     ? normalize(node.textContent)
 | ||
|     : node.textContent.replace(/\u00a0/g, ' '); // Replace   with normal spaces
 | ||
| 
 | ||
|   const checkingWithEmptyString = textContent !== '' && checkWith === '';
 | ||
| 
 | ||
|   return {
 | ||
|     pass: !checkingWithEmptyString && matches(textContent, checkWith),
 | ||
|     message: () => {
 | ||
|       const to = this.isNot ? 'not to' : 'to';
 | ||
|       return getMessage(
 | ||
|         this,
 | ||
|         this.utils.matcherHint(
 | ||
|           `${this.isNot ? '.not' : ''}.toHaveTextContent`,
 | ||
|           'element',
 | ||
|           '',
 | ||
|         ),
 | ||
|         checkingWithEmptyString
 | ||
|           ? `Checking with empty string will always match, use .toBeEmptyDOMElement() instead`
 | ||
|           : `Expected element ${to} have text content`,
 | ||
|         checkWith,
 | ||
|         'Received',
 | ||
|         textContent,
 | ||
|       )
 | ||
|     },
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| function toHaveAccessibleDescription(
 | ||
|   htmlElement,
 | ||
|   expectedAccessibleDescription,
 | ||
| ) {
 | ||
|   checkHtmlElement(htmlElement, toHaveAccessibleDescription, this);
 | ||
|   const actualAccessibleDescription = domAccessibilityApi.computeAccessibleDescription(htmlElement);
 | ||
|   const missingExpectedValue = arguments.length === 1;
 | ||
| 
 | ||
|   let pass = false;
 | ||
|   if (missingExpectedValue) {
 | ||
|     // When called without an expected value we only want to validate that the element has an
 | ||
|     // accessible description, whatever it may be.
 | ||
|     pass = actualAccessibleDescription !== '';
 | ||
|   } else {
 | ||
|     pass =
 | ||
|       expectedAccessibleDescription instanceof RegExp
 | ||
|         ? expectedAccessibleDescription.test(actualAccessibleDescription)
 | ||
|         : this.equals(
 | ||
|             actualAccessibleDescription,
 | ||
|             expectedAccessibleDescription,
 | ||
|           );
 | ||
|   }
 | ||
| 
 | ||
|   return {
 | ||
|     pass,
 | ||
| 
 | ||
|     message: () => {
 | ||
|       const to = this.isNot ? 'not to' : 'to';
 | ||
|       return getMessage(
 | ||
|         this,
 | ||
|         this.utils.matcherHint(
 | ||
|           `${this.isNot ? '.not' : ''}.${toHaveAccessibleDescription.name}`,
 | ||
|           'element',
 | ||
|           '',
 | ||
|         ),
 | ||
|         `Expected element ${to} have accessible description`,
 | ||
|         expectedAccessibleDescription,
 | ||
|         'Received',
 | ||
|         actualAccessibleDescription,
 | ||
|       )
 | ||
|     },
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| const ariaInvalidName = 'aria-invalid';
 | ||
| const validStates = ['false'];
 | ||
| 
 | ||
| // See `aria-errormessage` spec at https://www.w3.org/TR/wai-aria-1.2/#aria-errormessage
 | ||
| function toHaveAccessibleErrorMessage(
 | ||
|   htmlElement,
 | ||
|   expectedAccessibleErrorMessage,
 | ||
| ) {
 | ||
|   checkHtmlElement(htmlElement, toHaveAccessibleErrorMessage, this);
 | ||
|   const to = this.isNot ? 'not to' : 'to';
 | ||
|   const method = this.isNot
 | ||
|     ? '.not.toHaveAccessibleErrorMessage'
 | ||
|     : '.toHaveAccessibleErrorMessage';
 | ||
| 
 | ||
|   // Enforce Valid Id
 | ||
|   const errormessageId = htmlElement.getAttribute('aria-errormessage');
 | ||
|   const errormessageIdInvalid = !!errormessageId && /\s+/.test(errormessageId);
 | ||
| 
 | ||
|   if (errormessageIdInvalid) {
 | ||
|     return {
 | ||
|       pass: false,
 | ||
|       message: () => {
 | ||
|         return getMessage(
 | ||
|           this,
 | ||
|           this.utils.matcherHint(method, 'element'),
 | ||
|           "Expected element's `aria-errormessage` attribute to be empty or a single, valid ID",
 | ||
|           '',
 | ||
|           'Received',
 | ||
|           `aria-errormessage="${errormessageId}"`,
 | ||
|         )
 | ||
|       },
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   // See `aria-invalid` spec at https://www.w3.org/TR/wai-aria-1.2/#aria-invalid
 | ||
|   const ariaInvalidVal = htmlElement.getAttribute(ariaInvalidName);
 | ||
|   const fieldValid =
 | ||
|     !htmlElement.hasAttribute(ariaInvalidName) ||
 | ||
|     validStates.includes(ariaInvalidVal);
 | ||
| 
 | ||
|   // Enforce Valid `aria-invalid` Attribute
 | ||
|   if (fieldValid) {
 | ||
|     return {
 | ||
|       pass: false,
 | ||
|       message: () => {
 | ||
|         return getMessage(
 | ||
|           this,
 | ||
|           this.utils.matcherHint(method, 'element'),
 | ||
|           'Expected element to be marked as invalid with attribute',
 | ||
|           `${ariaInvalidName}="${String(true)}"`,
 | ||
|           'Received',
 | ||
|           htmlElement.hasAttribute('aria-invalid')
 | ||
|             ? `${ariaInvalidName}="${htmlElement.getAttribute(ariaInvalidName)}`
 | ||
|             : null,
 | ||
|         )
 | ||
|       },
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   const error = normalize(
 | ||
|     htmlElement.ownerDocument.getElementById(errormessageId)?.textContent ?? '',
 | ||
|   );
 | ||
| 
 | ||
|   return {
 | ||
|     pass:
 | ||
|       expectedAccessibleErrorMessage === undefined
 | ||
|         ? Boolean(error)
 | ||
|         : expectedAccessibleErrorMessage instanceof RegExp
 | ||
|         ? expectedAccessibleErrorMessage.test(error)
 | ||
|         : this.equals(error, expectedAccessibleErrorMessage),
 | ||
| 
 | ||
|     message: () => {
 | ||
|       return getMessage(
 | ||
|         this,
 | ||
|         this.utils.matcherHint(method, 'element'),
 | ||
|         `Expected element ${to} have accessible error message`,
 | ||
|         expectedAccessibleErrorMessage ?? '',
 | ||
|         'Received',
 | ||
|         error,
 | ||
|       )
 | ||
|     },
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| const elementRoleList = buildElementRoleList(ariaQuery.elementRoles);
 | ||
| 
 | ||
| function toHaveRole(htmlElement, expectedRole) {
 | ||
|   checkHtmlElement(htmlElement, toHaveRole, this);
 | ||
| 
 | ||
|   const actualRoles = getExplicitOrImplicitRoles(htmlElement);
 | ||
|   const pass = actualRoles.some(el => el === expectedRole);
 | ||
| 
 | ||
|   return {
 | ||
|     pass,
 | ||
| 
 | ||
|     message: () => {
 | ||
|       const to = this.isNot ? 'not to' : 'to';
 | ||
|       return getMessage(
 | ||
|         this,
 | ||
|         this.utils.matcherHint(
 | ||
|           `${this.isNot ? '.not' : ''}.${toHaveRole.name}`,
 | ||
|           'element',
 | ||
|           '',
 | ||
|         ),
 | ||
|         `Expected element ${to} have role`,
 | ||
|         expectedRole,
 | ||
|         'Received',
 | ||
|         actualRoles.join(', '),
 | ||
|       )
 | ||
|     },
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| function getExplicitOrImplicitRoles(htmlElement) {
 | ||
|   const hasExplicitRole = htmlElement.hasAttribute('role');
 | ||
| 
 | ||
|   if (hasExplicitRole) {
 | ||
|     const roleValue = htmlElement.getAttribute('role');
 | ||
| 
 | ||
|     // Handle fallback roles, such as role="switch button"
 | ||
|     // testing-library gates this behind the `queryFallbacks` flag; it is
 | ||
|     // unclear why, but it makes sense to support this pattern out of the box
 | ||
|     // https://testing-library.com/docs/queries/byrole/#queryfallbacks
 | ||
|     return roleValue.split(' ').filter(Boolean)
 | ||
|   }
 | ||
| 
 | ||
|   const implicitRoles = getImplicitAriaRoles(htmlElement);
 | ||
| 
 | ||
|   return implicitRoles
 | ||
| }
 | ||
| 
 | ||
| function getImplicitAriaRoles(currentNode) {
 | ||
|   for (const {match, roles} of elementRoleList) {
 | ||
|     if (match(currentNode)) {
 | ||
|       return [...roles]
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   /* istanbul ignore next */
 | ||
|   return [] // this does not get reached in practice, since elements have at least a 'generic' role
 | ||
| }
 | ||
| 
 | ||
| /**
 | ||
|  * Transform the roles map (with required attributes and constraints) to a list
 | ||
|  * of roles. Each item in the list has functions to match an element against it.
 | ||
|  *
 | ||
|  * Essentially copied over from [dom-testing-library's
 | ||
|  * helpers](https://github.com/testing-library/dom-testing-library/blob/bd04cf95a1ed85a2238f7dfc1a77d5d16b4f59dc/src/role-helpers.js#L80)
 | ||
|  *
 | ||
|  * TODO: If we are truly just copying over stuff, would it make sense to move
 | ||
|  * this to a separate package?
 | ||
|  *
 | ||
|  * TODO: This technique relies on CSS selectors; are those consistently
 | ||
|  * available in all jest-dom environments? Why do other matchers in this package
 | ||
|  * not use them like this?
 | ||
|  */
 | ||
| function buildElementRoleList(elementRolesMap) {
 | ||
|   function makeElementSelector({name, attributes}) {
 | ||
|     return `${name}${attributes
 | ||
|       .map(({name: attributeName, value, constraints = []}) => {
 | ||
|         const shouldNotExist = constraints.indexOf('undefined') !== -1;
 | ||
|         if (shouldNotExist) {
 | ||
|           return `:not([${attributeName}])`
 | ||
|         } else if (value) {
 | ||
|           return `[${attributeName}="${value}"]`
 | ||
|         } else {
 | ||
|           return `[${attributeName}]`
 | ||
|         }
 | ||
|       })
 | ||
|       .join('')}`
 | ||
|   }
 | ||
| 
 | ||
|   function getSelectorSpecificity({attributes = []}) {
 | ||
|     return attributes.length
 | ||
|   }
 | ||
| 
 | ||
|   function bySelectorSpecificity(
 | ||
|     {specificity: leftSpecificity},
 | ||
|     {specificity: rightSpecificity},
 | ||
|   ) {
 | ||
|     return rightSpecificity - leftSpecificity
 | ||
|   }
 | ||
| 
 | ||
|   function match(element) {
 | ||
|     let {attributes = []} = element;
 | ||
| 
 | ||
|     // https://github.com/testing-library/dom-testing-library/issues/814
 | ||
|     const typeTextIndex = attributes.findIndex(
 | ||
|       attribute =>
 | ||
|         attribute.value &&
 | ||
|         attribute.name === 'type' &&
 | ||
|         attribute.value === 'text',
 | ||
|     );
 | ||
| 
 | ||
|     if (typeTextIndex >= 0) {
 | ||
|       // not using splice to not mutate the attributes array
 | ||
|       attributes = [
 | ||
|         ...attributes.slice(0, typeTextIndex),
 | ||
|         ...attributes.slice(typeTextIndex + 1),
 | ||
|       ];
 | ||
|     }
 | ||
| 
 | ||
|     const selector = makeElementSelector({...element, attributes});
 | ||
| 
 | ||
|     return node => {
 | ||
|       if (typeTextIndex >= 0 && node.type !== 'text') {
 | ||
|         return false
 | ||
|       }
 | ||
| 
 | ||
|       return node.matches(selector)
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   let result = [];
 | ||
| 
 | ||
|   for (const [element, roles] of elementRolesMap.entries()) {
 | ||
|     result = [
 | ||
|       ...result,
 | ||
|       {
 | ||
|         match: match(element),
 | ||
|         roles: Array.from(roles),
 | ||
|         specificity: getSelectorSpecificity(element),
 | ||
|       },
 | ||
|     ];
 | ||
|   }
 | ||
| 
 | ||
|   return result.sort(bySelectorSpecificity)
 | ||
| }
 | ||
| 
 | ||
| function toHaveAccessibleName(htmlElement, expectedAccessibleName) {
 | ||
|   checkHtmlElement(htmlElement, toHaveAccessibleName, this);
 | ||
|   const actualAccessibleName = domAccessibilityApi.computeAccessibleName(htmlElement);
 | ||
|   const missingExpectedValue = arguments.length === 1;
 | ||
| 
 | ||
|   let pass = false;
 | ||
|   if (missingExpectedValue) {
 | ||
|     // When called without an expected value we only want to validate that the element has an
 | ||
|     // accessible name, whatever it may be.
 | ||
|     pass = actualAccessibleName !== '';
 | ||
|   } else {
 | ||
|     pass =
 | ||
|       expectedAccessibleName instanceof RegExp
 | ||
|         ? expectedAccessibleName.test(actualAccessibleName)
 | ||
|         : this.equals(actualAccessibleName, expectedAccessibleName);
 | ||
|   }
 | ||
| 
 | ||
|   return {
 | ||
|     pass,
 | ||
| 
 | ||
|     message: () => {
 | ||
|       const to = this.isNot ? 'not to' : 'to';
 | ||
|       return getMessage(
 | ||
|         this,
 | ||
|         this.utils.matcherHint(
 | ||
|           `${this.isNot ? '.not' : ''}.${toHaveAccessibleName.name}`,
 | ||
|           'element',
 | ||
|           '',
 | ||
|         ),
 | ||
|         `Expected element ${to} have accessible name`,
 | ||
|         expectedAccessibleName,
 | ||
|         'Received',
 | ||
|         actualAccessibleName,
 | ||
|       )
 | ||
|     },
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| function printAttribute(stringify, name, value) {
 | ||
|   return value === undefined ? name : `${name}=${stringify(value)}`
 | ||
| }
 | ||
| 
 | ||
| function getAttributeComment(stringify, name, value) {
 | ||
|   return value === undefined
 | ||
|     ? `element.hasAttribute(${stringify(name)})`
 | ||
|     : `element.getAttribute(${stringify(name)}) === ${stringify(value)}`
 | ||
| }
 | ||
| 
 | ||
| function toHaveAttribute(htmlElement, name, expectedValue) {
 | ||
|   checkHtmlElement(htmlElement, toHaveAttribute, this);
 | ||
|   const isExpectedValuePresent = expectedValue !== undefined;
 | ||
|   const hasAttribute = htmlElement.hasAttribute(name);
 | ||
|   const receivedValue = htmlElement.getAttribute(name);
 | ||
|   return {
 | ||
|     pass: isExpectedValuePresent
 | ||
|       ? hasAttribute && this.equals(receivedValue, expectedValue)
 | ||
|       : hasAttribute,
 | ||
|     message: () => {
 | ||
|       const to = this.isNot ? 'not to' : 'to';
 | ||
|       const receivedAttribute = hasAttribute
 | ||
|         ? printAttribute(this.utils.stringify, name, receivedValue)
 | ||
|         : null;
 | ||
|       const matcher = this.utils.matcherHint(
 | ||
|         `${this.isNot ? '.not' : ''}.toHaveAttribute`,
 | ||
|         'element',
 | ||
|         this.utils.printExpected(name),
 | ||
|         {
 | ||
|           secondArgument: isExpectedValuePresent
 | ||
|             ? this.utils.printExpected(expectedValue)
 | ||
|             : undefined,
 | ||
|           comment: getAttributeComment(
 | ||
|             this.utils.stringify,
 | ||
|             name,
 | ||
|             expectedValue,
 | ||
|           ),
 | ||
|         },
 | ||
|       );
 | ||
|       return getMessage(
 | ||
|         this,
 | ||
|         matcher,
 | ||
|         `Expected the element ${to} have attribute`,
 | ||
|         printAttribute(this.utils.stringify, name, expectedValue),
 | ||
|         'Received',
 | ||
|         receivedAttribute,
 | ||
|       )
 | ||
|     },
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| function getExpectedClassNamesAndOptions(params) {
 | ||
|   const lastParam = params.pop();
 | ||
|   let expectedClassNames, options;
 | ||
| 
 | ||
|   if (typeof lastParam === 'object' && !(lastParam instanceof RegExp)) {
 | ||
|     expectedClassNames = params;
 | ||
|     options = lastParam;
 | ||
|   } else {
 | ||
|     expectedClassNames = params.concat(lastParam);
 | ||
|     options = {exact: false};
 | ||
|   }
 | ||
|   return {expectedClassNames, options}
 | ||
| }
 | ||
| 
 | ||
| function splitClassNames(str) {
 | ||
|   if (!str) return []
 | ||
|   return str.split(/\s+/).filter(s => s.length > 0)
 | ||
| }
 | ||
| 
 | ||
| function isSubset$1(subset, superset) {
 | ||
|   return subset.every(strOrRegexp =>
 | ||
|     typeof strOrRegexp === 'string'
 | ||
|       ? superset.includes(strOrRegexp)
 | ||
|       : superset.some(className => strOrRegexp.test(className)),
 | ||
|   )
 | ||
| }
 | ||
| 
 | ||
| function toHaveClass(htmlElement, ...params) {
 | ||
|   checkHtmlElement(htmlElement, toHaveClass, this);
 | ||
|   const {expectedClassNames, options} = getExpectedClassNamesAndOptions(params);
 | ||
| 
 | ||
|   const received = splitClassNames(htmlElement.getAttribute('class'));
 | ||
|   const expected = expectedClassNames.reduce(
 | ||
|     (acc, className) =>
 | ||
|       acc.concat(
 | ||
|         typeof className === 'string' || !className
 | ||
|           ? splitClassNames(className)
 | ||
|           : className,
 | ||
|       ),
 | ||
|     [],
 | ||
|   );
 | ||
| 
 | ||
|   const hasRegExp = expected.some(className => className instanceof RegExp);
 | ||
|   if (options.exact && hasRegExp) {
 | ||
|     throw new Error('Exact option does not support RegExp expected class names')
 | ||
|   }
 | ||
| 
 | ||
|   if (options.exact) {
 | ||
|     return {
 | ||
|       pass: isSubset$1(expected, received) && expected.length === received.length,
 | ||
|       message: () => {
 | ||
|         const to = this.isNot ? 'not to' : 'to';
 | ||
|         return getMessage(
 | ||
|           this,
 | ||
|           this.utils.matcherHint(
 | ||
|             `${this.isNot ? '.not' : ''}.toHaveClass`,
 | ||
|             'element',
 | ||
|             this.utils.printExpected(expected.join(' ')),
 | ||
|           ),
 | ||
|           `Expected the element ${to} have EXACTLY defined classes`,
 | ||
|           expected.join(' '),
 | ||
|           'Received',
 | ||
|           received.join(' '),
 | ||
|         )
 | ||
|       },
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   return expected.length > 0
 | ||
|     ? {
 | ||
|         pass: isSubset$1(expected, received),
 | ||
|         message: () => {
 | ||
|           const to = this.isNot ? 'not to' : 'to';
 | ||
|           return getMessage(
 | ||
|             this,
 | ||
|             this.utils.matcherHint(
 | ||
|               `${this.isNot ? '.not' : ''}.toHaveClass`,
 | ||
|               'element',
 | ||
|               this.utils.printExpected(expected.join(' ')),
 | ||
|             ),
 | ||
|             `Expected the element ${to} have class`,
 | ||
|             expected.join(' '),
 | ||
|             'Received',
 | ||
|             received.join(' '),
 | ||
|           )
 | ||
|         },
 | ||
|       }
 | ||
|     : {
 | ||
|         pass: this.isNot ? received.length > 0 : false,
 | ||
|         message: () =>
 | ||
|           this.isNot
 | ||
|             ? getMessage(
 | ||
|                 this,
 | ||
|                 this.utils.matcherHint('.not.toHaveClass', 'element', ''),
 | ||
|                 'Expected the element to have classes',
 | ||
|                 '(none)',
 | ||
|                 'Received',
 | ||
|                 received.join(' '),
 | ||
|               )
 | ||
|             : [
 | ||
|                 this.utils.matcherHint(`.toHaveClass`, 'element'),
 | ||
|                 'At least one expected class must be provided.',
 | ||
|               ].join('\n'),
 | ||
|       }
 | ||
| }
 | ||
| 
 | ||
| function getStyleDeclaration(document, css) {
 | ||
|   const styles = {};
 | ||
| 
 | ||
|   // The next block is necessary to normalize colors
 | ||
|   const copy = document.createElement('div');
 | ||
|   Object.keys(css).forEach(property => {
 | ||
|     copy.style[property] = css[property];
 | ||
|     styles[property] = copy.style[property];
 | ||
|   });
 | ||
| 
 | ||
|   return styles
 | ||
| }
 | ||
| 
 | ||
| function isSubset(styles, computedStyle) {
 | ||
|   return (
 | ||
|     !!Object.keys(styles).length &&
 | ||
|     Object.entries(styles).every(([prop, value]) => {
 | ||
|       const isCustomProperty = prop.startsWith('--');
 | ||
|       const spellingVariants = [prop];
 | ||
|       if (!isCustomProperty) spellingVariants.push(prop.toLowerCase());
 | ||
| 
 | ||
|       return spellingVariants.some(
 | ||
|         name =>
 | ||
|           computedStyle[name] === value ||
 | ||
|           computedStyle.getPropertyValue(name) === value,
 | ||
|       )
 | ||
|     })
 | ||
|   )
 | ||
| }
 | ||
| 
 | ||
| function printoutStyles(styles) {
 | ||
|   return Object.keys(styles)
 | ||
|     .sort()
 | ||
|     .map(prop => `${prop}: ${styles[prop]};`)
 | ||
|     .join('\n')
 | ||
| }
 | ||
| 
 | ||
| // Highlights only style rules that were expected but were not found in the
 | ||
| // received computed styles
 | ||
| function expectedDiff(diffFn, expected, computedStyles) {
 | ||
|   const received = Array.from(computedStyles)
 | ||
|     .filter(prop => expected[prop] !== undefined)
 | ||
|     .reduce(
 | ||
|       (obj, prop) =>
 | ||
|         Object.assign(obj, {[prop]: computedStyles.getPropertyValue(prop)}),
 | ||
|       {},
 | ||
|     );
 | ||
|   const diffOutput = diffFn(printoutStyles(expected), printoutStyles(received));
 | ||
|   // Remove the "+ Received" annotation because this is a one-way diff
 | ||
|   return diffOutput.replace(`${chalk.red('+ Received')}\n`, '')
 | ||
| }
 | ||
| 
 | ||
| function toHaveStyle(htmlElement, css) {
 | ||
|   checkHtmlElement(htmlElement, toHaveStyle, this);
 | ||
|   const parsedCSS =
 | ||
|     typeof css === 'object' ? css : parseCSS(css, toHaveStyle, this);
 | ||
|   const {getComputedStyle} = htmlElement.ownerDocument.defaultView;
 | ||
| 
 | ||
|   const expected = getStyleDeclaration(htmlElement.ownerDocument, parsedCSS);
 | ||
|   const received = getComputedStyle(htmlElement);
 | ||
| 
 | ||
|   return {
 | ||
|     pass: isSubset(expected, received),
 | ||
|     message: () => {
 | ||
|       const matcher = `${this.isNot ? '.not' : ''}.toHaveStyle`;
 | ||
|       return [
 | ||
|         this.utils.matcherHint(matcher, 'element', ''),
 | ||
|         expectedDiff(this.utils.diff, expected, received),
 | ||
|       ].join('\n\n')
 | ||
|     },
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| function toHaveFocus(element) {
 | ||
|   checkHtmlElement(element, toHaveFocus, this);
 | ||
| 
 | ||
|   return {
 | ||
|     pass: element.ownerDocument.activeElement === element,
 | ||
|     message: () => {
 | ||
|       return [
 | ||
|         this.utils.matcherHint(
 | ||
|           `${this.isNot ? '.not' : ''}.toHaveFocus`,
 | ||
|           'element',
 | ||
|           '',
 | ||
|         ),
 | ||
|         '',
 | ||
|         ...(this.isNot
 | ||
|           ? [
 | ||
|               'Received element is focused:',
 | ||
|               `  ${this.utils.printReceived(element)}`,
 | ||
|             ]
 | ||
|           : [
 | ||
|               'Expected element with focus:',
 | ||
|               `  ${this.utils.printExpected(element)}`,
 | ||
|               'Received element with focus:',
 | ||
|               `  ${this.utils.printReceived(
 | ||
|                 element.ownerDocument.activeElement,
 | ||
|               )}`,
 | ||
|             ]),
 | ||
|       ].join('\n')
 | ||
|     },
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| // Returns the combined value of several elements that have the same name
 | ||
| // e.g. radio buttons or groups of checkboxes
 | ||
| function getMultiElementValue(elements) {
 | ||
|   const types = [...new Set(elements.map(element => element.type))];
 | ||
|   if (types.length !== 1) {
 | ||
|     throw new Error(
 | ||
|       'Multiple form elements with the same name must be of the same type',
 | ||
|     )
 | ||
|   }
 | ||
|   switch (types[0]) {
 | ||
|     case 'radio': {
 | ||
|       const theChosenOne = elements.find(radio => radio.checked);
 | ||
|       return theChosenOne ? theChosenOne.value : undefined
 | ||
|     }
 | ||
|     case 'checkbox':
 | ||
|       return elements
 | ||
|         .filter(checkbox => checkbox.checked)
 | ||
|         .map(checkbox => checkbox.value)
 | ||
|     default:
 | ||
|       // NOTE: Not even sure this is a valid use case, but just in case...
 | ||
|       return elements.map(element => element.value)
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| function getFormValue(container, name) {
 | ||
|   const elements = [...container.querySelectorAll(`[name="${escape(name)}"]`)];
 | ||
|   /* istanbul ignore if */
 | ||
|   if (elements.length === 0) {
 | ||
|     return undefined // shouldn't happen, but just in case
 | ||
|   }
 | ||
|   switch (elements.length) {
 | ||
|     case 1:
 | ||
|       return getSingleElementValue(elements[0])
 | ||
|     default:
 | ||
|       return getMultiElementValue(elements)
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| // Strips the `[]` suffix off a form value name
 | ||
| function getPureName(name) {
 | ||
|   return /\[\]$/.test(name) ? name.slice(0, -2) : name
 | ||
| }
 | ||
| 
 | ||
| function getAllFormValues(container) {
 | ||
|   const names = Array.from(container.elements).map(element => element.name);
 | ||
|   return names.reduce(
 | ||
|     (obj, name) => ({
 | ||
|       ...obj,
 | ||
|       [getPureName(name)]: getFormValue(container, name),
 | ||
|     }),
 | ||
|     {},
 | ||
|   )
 | ||
| }
 | ||
| 
 | ||
| function toHaveFormValues(formElement, expectedValues) {
 | ||
|   checkHtmlElement(formElement, toHaveFormValues, this);
 | ||
|   if (!formElement.elements) {
 | ||
|     // TODO: Change condition to use instanceof against the appropriate element classes instead
 | ||
|     throw new Error('toHaveFormValues must be called on a form or a fieldset')
 | ||
|   }
 | ||
|   const formValues = getAllFormValues(formElement);
 | ||
|   return {
 | ||
|     pass: Object.entries(expectedValues).every(([name, expectedValue]) =>
 | ||
|       isEqualWith(formValues[name], expectedValue, compareArraysAsSet),
 | ||
|     ),
 | ||
|     message: () => {
 | ||
|       const to = this.isNot ? 'not to' : 'to';
 | ||
|       const matcher = `${this.isNot ? '.not' : ''}.toHaveFormValues`;
 | ||
|       const commonKeyValues = Object.keys(formValues)
 | ||
|         .filter(key => expectedValues.hasOwnProperty(key))
 | ||
|         .reduce((obj, key) => ({...obj, [key]: formValues[key]}), {});
 | ||
|       return [
 | ||
|         this.utils.matcherHint(matcher, 'element', ''),
 | ||
|         `Expected the element ${to} have form values`,
 | ||
|         this.utils.diff(expectedValues, commonKeyValues),
 | ||
|       ].join('\n\n')
 | ||
|     },
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| function isStyleVisible(element) {
 | ||
|   const {getComputedStyle} = element.ownerDocument.defaultView;
 | ||
| 
 | ||
|   const {display, visibility, opacity} = getComputedStyle(element);
 | ||
|   return (
 | ||
|     display !== 'none' &&
 | ||
|     visibility !== 'hidden' &&
 | ||
|     visibility !== 'collapse' &&
 | ||
|     opacity !== '0' &&
 | ||
|     opacity !== 0
 | ||
|   )
 | ||
| }
 | ||
| 
 | ||
| function isAttributeVisible(element, previousElement) {
 | ||
|   let detailsVisibility;
 | ||
| 
 | ||
|   if (previousElement) {
 | ||
|     detailsVisibility =
 | ||
|       element.nodeName === 'DETAILS' && previousElement.nodeName !== 'SUMMARY'
 | ||
|         ? element.hasAttribute('open')
 | ||
|         : true;
 | ||
|   } else {
 | ||
|     detailsVisibility =
 | ||
|       element.nodeName === 'DETAILS' ? element.hasAttribute('open') : true;
 | ||
|   }
 | ||
| 
 | ||
|   return !element.hasAttribute('hidden') && detailsVisibility
 | ||
| }
 | ||
| 
 | ||
| function isElementVisible(element, previousElement) {
 | ||
|   return (
 | ||
|     isStyleVisible(element) &&
 | ||
|     isAttributeVisible(element, previousElement) &&
 | ||
|     (!element.parentElement || isElementVisible(element.parentElement, element))
 | ||
|   )
 | ||
| }
 | ||
| 
 | ||
| function toBeVisible(element) {
 | ||
|   checkHtmlElement(element, toBeVisible, this);
 | ||
|   const isInDocument =
 | ||
|     element.ownerDocument === element.getRootNode({composed: true});
 | ||
|   const isVisible = isInDocument && isElementVisible(element);
 | ||
|   return {
 | ||
|     pass: isVisible,
 | ||
|     message: () => {
 | ||
|       const is = isVisible ? 'is' : 'is not';
 | ||
|       return [
 | ||
|         this.utils.matcherHint(
 | ||
|           `${this.isNot ? '.not' : ''}.toBeVisible`,
 | ||
|           'element',
 | ||
|           '',
 | ||
|         ),
 | ||
|         '',
 | ||
|         `Received element ${is} visible${
 | ||
|           isInDocument ? '' : ' (element is not in the document)'
 | ||
|         }:`,
 | ||
|         `  ${this.utils.printReceived(element.cloneNode(false))}`,
 | ||
|       ].join('\n')
 | ||
|     },
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| // form elements that support 'disabled'
 | ||
| const FORM_TAGS$2 = [
 | ||
|   'fieldset',
 | ||
|   'input',
 | ||
|   'select',
 | ||
|   'optgroup',
 | ||
|   'option',
 | ||
|   'button',
 | ||
|   'textarea',
 | ||
| ];
 | ||
| 
 | ||
| /*
 | ||
|  * According to specification:
 | ||
|  * If <fieldset> is disabled, the form controls that are its descendants,
 | ||
|  * except descendants of its first optional <legend> element, are disabled
 | ||
|  *
 | ||
|  * https://html.spec.whatwg.org/multipage/form-elements.html#concept-fieldset-disabled
 | ||
|  *
 | ||
|  * This method tests whether element is first legend child of fieldset parent
 | ||
|  */
 | ||
| function isFirstLegendChildOfFieldset(element, parent) {
 | ||
|   return (
 | ||
|     getTag(element) === 'legend' &&
 | ||
|     getTag(parent) === 'fieldset' &&
 | ||
|     element.isSameNode(
 | ||
|       Array.from(parent.children).find(child => getTag(child) === 'legend'),
 | ||
|     )
 | ||
|   )
 | ||
| }
 | ||
| 
 | ||
| function isElementDisabledByParent(element, parent) {
 | ||
|   return (
 | ||
|     isElementDisabled(parent) && !isFirstLegendChildOfFieldset(element, parent)
 | ||
|   )
 | ||
| }
 | ||
| 
 | ||
| function isCustomElement(tag) {
 | ||
|   return tag.includes('-')
 | ||
| }
 | ||
| 
 | ||
| /*
 | ||
|  * Only certain form elements and custom elements can actually be disabled:
 | ||
|  * https://html.spec.whatwg.org/multipage/semantics-other.html#disabled-elements
 | ||
|  */
 | ||
| function canElementBeDisabled(element) {
 | ||
|   const tag = getTag(element);
 | ||
|   return FORM_TAGS$2.includes(tag) || isCustomElement(tag)
 | ||
| }
 | ||
| 
 | ||
| function isElementDisabled(element) {
 | ||
|   return canElementBeDisabled(element) && element.hasAttribute('disabled')
 | ||
| }
 | ||
| 
 | ||
| function isAncestorDisabled(element) {
 | ||
|   const parent = element.parentElement;
 | ||
|   return (
 | ||
|     Boolean(parent) &&
 | ||
|     (isElementDisabledByParent(element, parent) || isAncestorDisabled(parent))
 | ||
|   )
 | ||
| }
 | ||
| 
 | ||
| function isElementOrAncestorDisabled(element) {
 | ||
|   return (
 | ||
|     canElementBeDisabled(element) &&
 | ||
|     (isElementDisabled(element) || isAncestorDisabled(element))
 | ||
|   )
 | ||
| }
 | ||
| 
 | ||
| function toBeDisabled(element) {
 | ||
|   checkHtmlElement(element, toBeDisabled, this);
 | ||
| 
 | ||
|   const isDisabled = isElementOrAncestorDisabled(element);
 | ||
| 
 | ||
|   return {
 | ||
|     pass: isDisabled,
 | ||
|     message: () => {
 | ||
|       const is = isDisabled ? 'is' : 'is not';
 | ||
|       return [
 | ||
|         this.utils.matcherHint(
 | ||
|           `${this.isNot ? '.not' : ''}.toBeDisabled`,
 | ||
|           'element',
 | ||
|           '',
 | ||
|         ),
 | ||
|         '',
 | ||
|         `Received element ${is} disabled:`,
 | ||
|         `  ${this.utils.printReceived(element.cloneNode(false))}`,
 | ||
|       ].join('\n')
 | ||
|     },
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| function toBeEnabled(element) {
 | ||
|   checkHtmlElement(element, toBeEnabled, this);
 | ||
| 
 | ||
|   const isEnabled = !isElementOrAncestorDisabled(element);
 | ||
| 
 | ||
|   return {
 | ||
|     pass: isEnabled,
 | ||
|     message: () => {
 | ||
|       const is = isEnabled ? 'is' : 'is not';
 | ||
|       return [
 | ||
|         this.utils.matcherHint(
 | ||
|           `${this.isNot ? '.not' : ''}.toBeEnabled`,
 | ||
|           'element',
 | ||
|           '',
 | ||
|         ),
 | ||
|         '',
 | ||
|         `Received element ${is} enabled:`,
 | ||
|         `  ${this.utils.printReceived(element.cloneNode(false))}`,
 | ||
|       ].join('\n')
 | ||
|     },
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| // form elements that support 'required'
 | ||
| const FORM_TAGS$1 = ['select', 'textarea'];
 | ||
| 
 | ||
| const ARIA_FORM_TAGS = ['input', 'select', 'textarea'];
 | ||
| 
 | ||
| const UNSUPPORTED_INPUT_TYPES = [
 | ||
|   'color',
 | ||
|   'hidden',
 | ||
|   'range',
 | ||
|   'submit',
 | ||
|   'image',
 | ||
|   'reset',
 | ||
| ];
 | ||
| 
 | ||
| const SUPPORTED_ARIA_ROLES = [
 | ||
|   'checkbox',
 | ||
|   'combobox',
 | ||
|   'gridcell',
 | ||
|   'listbox',
 | ||
|   'radiogroup',
 | ||
|   'spinbutton',
 | ||
|   'textbox',
 | ||
|   'tree',
 | ||
| ];
 | ||
| 
 | ||
| function isRequiredOnFormTagsExceptInput(element) {
 | ||
|   return FORM_TAGS$1.includes(getTag(element)) && element.hasAttribute('required')
 | ||
| }
 | ||
| 
 | ||
| function isRequiredOnSupportedInput(element) {
 | ||
|   return (
 | ||
|     getTag(element) === 'input' &&
 | ||
|     element.hasAttribute('required') &&
 | ||
|     ((element.hasAttribute('type') &&
 | ||
|       !UNSUPPORTED_INPUT_TYPES.includes(element.getAttribute('type'))) ||
 | ||
|       !element.hasAttribute('type'))
 | ||
|   )
 | ||
| }
 | ||
| 
 | ||
| function isElementRequiredByARIA(element) {
 | ||
|   return (
 | ||
|     element.hasAttribute('aria-required') &&
 | ||
|     element.getAttribute('aria-required') === 'true' &&
 | ||
|     (ARIA_FORM_TAGS.includes(getTag(element)) ||
 | ||
|       (element.hasAttribute('role') &&
 | ||
|         SUPPORTED_ARIA_ROLES.includes(element.getAttribute('role'))))
 | ||
|   )
 | ||
| }
 | ||
| 
 | ||
| function toBeRequired(element) {
 | ||
|   checkHtmlElement(element, toBeRequired, this);
 | ||
| 
 | ||
|   const isRequired =
 | ||
|     isRequiredOnFormTagsExceptInput(element) ||
 | ||
|     isRequiredOnSupportedInput(element) ||
 | ||
|     isElementRequiredByARIA(element);
 | ||
| 
 | ||
|   return {
 | ||
|     pass: isRequired,
 | ||
|     message: () => {
 | ||
|       const is = isRequired ? 'is' : 'is not';
 | ||
|       return [
 | ||
|         this.utils.matcherHint(
 | ||
|           `${this.isNot ? '.not' : ''}.toBeRequired`,
 | ||
|           'element',
 | ||
|           '',
 | ||
|         ),
 | ||
|         '',
 | ||
|         `Received element ${is} required:`,
 | ||
|         `  ${this.utils.printReceived(element.cloneNode(false))}`,
 | ||
|       ].join('\n')
 | ||
|     },
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| const FORM_TAGS = ['form', 'input', 'select', 'textarea'];
 | ||
| 
 | ||
| function isElementHavingAriaInvalid(element) {
 | ||
|   return (
 | ||
|     element.hasAttribute('aria-invalid') &&
 | ||
|     element.getAttribute('aria-invalid') !== 'false'
 | ||
|   )
 | ||
| }
 | ||
| 
 | ||
| function isSupportsValidityMethod(element) {
 | ||
|   return FORM_TAGS.includes(getTag(element))
 | ||
| }
 | ||
| 
 | ||
| function isElementInvalid(element) {
 | ||
|   const isHaveAriaInvalid = isElementHavingAriaInvalid(element);
 | ||
|   if (isSupportsValidityMethod(element)) {
 | ||
|     return isHaveAriaInvalid || !element.checkValidity()
 | ||
|   } else {
 | ||
|     return isHaveAriaInvalid
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| function toBeInvalid(element) {
 | ||
|   checkHtmlElement(element, toBeInvalid, this);
 | ||
| 
 | ||
|   const isInvalid = isElementInvalid(element);
 | ||
| 
 | ||
|   return {
 | ||
|     pass: isInvalid,
 | ||
|     message: () => {
 | ||
|       const is = isInvalid ? 'is' : 'is not';
 | ||
|       return [
 | ||
|         this.utils.matcherHint(
 | ||
|           `${this.isNot ? '.not' : ''}.toBeInvalid`,
 | ||
|           'element',
 | ||
|           '',
 | ||
|         ),
 | ||
|         '',
 | ||
|         `Received element ${is} currently invalid:`,
 | ||
|         `  ${this.utils.printReceived(element.cloneNode(false))}`,
 | ||
|       ].join('\n')
 | ||
|     },
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| function toBeValid(element) {
 | ||
|   checkHtmlElement(element, toBeValid, this);
 | ||
| 
 | ||
|   const isValid = !isElementInvalid(element);
 | ||
| 
 | ||
|   return {
 | ||
|     pass: isValid,
 | ||
|     message: () => {
 | ||
|       const is = isValid ? 'is' : 'is not';
 | ||
|       return [
 | ||
|         this.utils.matcherHint(
 | ||
|           `${this.isNot ? '.not' : ''}.toBeValid`,
 | ||
|           'element',
 | ||
|           '',
 | ||
|         ),
 | ||
|         '',
 | ||
|         `Received element ${is} currently valid:`,
 | ||
|         `  ${this.utils.printReceived(element.cloneNode(false))}`,
 | ||
|       ].join('\n')
 | ||
|     },
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| function toHaveValue(htmlElement, expectedValue) {
 | ||
|   checkHtmlElement(htmlElement, toHaveValue, this);
 | ||
| 
 | ||
|   if (
 | ||
|     htmlElement.tagName.toLowerCase() === 'input' &&
 | ||
|     ['checkbox', 'radio'].includes(htmlElement.type)
 | ||
|   ) {
 | ||
|     throw new Error(
 | ||
|       'input with type=checkbox or type=radio cannot be used with .toHaveValue(). Use .toBeChecked() for type=checkbox or .toHaveFormValues() instead',
 | ||
|     )
 | ||
|   }
 | ||
| 
 | ||
|   const receivedValue = getSingleElementValue(htmlElement);
 | ||
|   const expectsValue = expectedValue !== undefined;
 | ||
| 
 | ||
|   let expectedTypedValue = expectedValue;
 | ||
|   let receivedTypedValue = receivedValue;
 | ||
|   if (expectedValue == receivedValue && expectedValue !== receivedValue) {
 | ||
|     expectedTypedValue = `${expectedValue} (${typeof expectedValue})`;
 | ||
|     receivedTypedValue = `${receivedValue} (${typeof receivedValue})`;
 | ||
|   }
 | ||
| 
 | ||
|   return {
 | ||
|     pass: expectsValue
 | ||
|       ? isEqualWith(receivedValue, expectedValue, compareArraysAsSet)
 | ||
|       : Boolean(receivedValue),
 | ||
|     message: () => {
 | ||
|       const to = this.isNot ? 'not to' : 'to';
 | ||
|       const matcher = this.utils.matcherHint(
 | ||
|         `${this.isNot ? '.not' : ''}.toHaveValue`,
 | ||
|         'element',
 | ||
|         expectedValue,
 | ||
|       );
 | ||
|       return getMessage(
 | ||
|         this,
 | ||
|         matcher,
 | ||
|         `Expected the element ${to} have value`,
 | ||
|         expectsValue ? expectedTypedValue : '(any)',
 | ||
|         'Received',
 | ||
|         receivedTypedValue,
 | ||
|       )
 | ||
|     },
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| function toHaveDisplayValue(htmlElement, expectedValue) {
 | ||
|   checkHtmlElement(htmlElement, toHaveDisplayValue, this);
 | ||
|   const tagName = htmlElement.tagName.toLowerCase();
 | ||
| 
 | ||
|   if (!['select', 'input', 'textarea'].includes(tagName)) {
 | ||
|     throw new Error(
 | ||
|       '.toHaveDisplayValue() currently supports only input, textarea or select elements, try with another matcher instead.',
 | ||
|     )
 | ||
|   }
 | ||
| 
 | ||
|   if (tagName === 'input' && ['radio', 'checkbox'].includes(htmlElement.type)) {
 | ||
|     throw new Error(
 | ||
|       `.toHaveDisplayValue() currently does not support input[type="${htmlElement.type}"], try with another matcher instead.`,
 | ||
|     )
 | ||
|   }
 | ||
| 
 | ||
|   const values = getValues(tagName, htmlElement);
 | ||
|   const expectedValues = getExpectedValues(expectedValue);
 | ||
|   const numberOfMatchesWithValues = expectedValues.filter(expected =>
 | ||
|     values.some(value =>
 | ||
|       expected instanceof RegExp
 | ||
|         ? expected.test(value)
 | ||
|         : this.equals(value, String(expected)),
 | ||
|     ),
 | ||
|   ).length;
 | ||
| 
 | ||
|   const matchedWithAllValues = numberOfMatchesWithValues === values.length;
 | ||
|   const matchedWithAllExpectedValues =
 | ||
|     numberOfMatchesWithValues === expectedValues.length;
 | ||
| 
 | ||
|   return {
 | ||
|     pass: matchedWithAllValues && matchedWithAllExpectedValues,
 | ||
|     message: () =>
 | ||
|       getMessage(
 | ||
|         this,
 | ||
|         this.utils.matcherHint(
 | ||
|           `${this.isNot ? '.not' : ''}.toHaveDisplayValue`,
 | ||
|           'element',
 | ||
|           '',
 | ||
|         ),
 | ||
|         `Expected element ${this.isNot ? 'not ' : ''}to have display value`,
 | ||
|         expectedValue,
 | ||
|         'Received',
 | ||
|         values,
 | ||
|       ),
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| function getValues(tagName, htmlElement) {
 | ||
|   return tagName === 'select'
 | ||
|     ? Array.from(htmlElement)
 | ||
|         .filter(option => option.selected)
 | ||
|         .map(option => option.textContent)
 | ||
|     : [htmlElement.value]
 | ||
| }
 | ||
| 
 | ||
| function getExpectedValues(expectedValue) {
 | ||
|   return expectedValue instanceof Array ? expectedValue : [expectedValue]
 | ||
| }
 | ||
| 
 | ||
| function toBeChecked(element) {
 | ||
|   checkHtmlElement(element, toBeChecked, this);
 | ||
| 
 | ||
|   const isValidInput = () => {
 | ||
|     return (
 | ||
|       element.tagName.toLowerCase() === 'input' &&
 | ||
|       ['checkbox', 'radio'].includes(element.type)
 | ||
|     )
 | ||
|   };
 | ||
| 
 | ||
|   const isValidAriaElement = () => {
 | ||
|     return (
 | ||
|       roleSupportsChecked(element.getAttribute('role')) &&
 | ||
|       ['true', 'false'].includes(element.getAttribute('aria-checked'))
 | ||
|     )
 | ||
|   };
 | ||
| 
 | ||
|   if (!isValidInput() && !isValidAriaElement()) {
 | ||
|     return {
 | ||
|       pass: false,
 | ||
|       message: () =>
 | ||
|         `only inputs with type="checkbox" or type="radio" or elements with ${supportedRolesSentence()} and a valid aria-checked attribute can be used with .toBeChecked(). Use .toHaveValue() instead`,
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   const isChecked = () => {
 | ||
|     if (isValidInput()) return element.checked
 | ||
|     return element.getAttribute('aria-checked') === 'true'
 | ||
|   };
 | ||
| 
 | ||
|   return {
 | ||
|     pass: isChecked(),
 | ||
|     message: () => {
 | ||
|       const is = isChecked() ? 'is' : 'is not';
 | ||
|       return [
 | ||
|         this.utils.matcherHint(
 | ||
|           `${this.isNot ? '.not' : ''}.toBeChecked`,
 | ||
|           'element',
 | ||
|           '',
 | ||
|         ),
 | ||
|         '',
 | ||
|         `Received element ${is} checked:`,
 | ||
|         `  ${this.utils.printReceived(element.cloneNode(false))}`,
 | ||
|       ].join('\n')
 | ||
|     },
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| function supportedRolesSentence() {
 | ||
|   return toSentence(
 | ||
|     supportedRoles().map(role => `role="${role}"`),
 | ||
|     {lastWordConnector: ' or '},
 | ||
|   )
 | ||
| }
 | ||
| 
 | ||
| function supportedRoles() {
 | ||
|   return ariaQuery.roles.keys().filter(roleSupportsChecked)
 | ||
| }
 | ||
| 
 | ||
| function roleSupportsChecked(role) {
 | ||
|   return ariaQuery.roles.get(role)?.props['aria-checked'] !== undefined
 | ||
| }
 | ||
| 
 | ||
| function toBePartiallyChecked(element) {
 | ||
|   checkHtmlElement(element, toBePartiallyChecked, this);
 | ||
| 
 | ||
|   const isValidInput = () => {
 | ||
|     return (
 | ||
|       element.tagName.toLowerCase() === 'input' && element.type === 'checkbox'
 | ||
|     )
 | ||
|   };
 | ||
| 
 | ||
|   const isValidAriaElement = () => {
 | ||
|     return element.getAttribute('role') === 'checkbox'
 | ||
|   };
 | ||
| 
 | ||
|   if (!isValidInput() && !isValidAriaElement()) {
 | ||
|     return {
 | ||
|       pass: false,
 | ||
|       message: () =>
 | ||
|         'only inputs with type="checkbox" or elements with role="checkbox" and a valid aria-checked attribute can be used with .toBePartiallyChecked(). Use .toHaveValue() instead',
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   const isPartiallyChecked = () => {
 | ||
|     const isAriaMixed = element.getAttribute('aria-checked') === 'mixed';
 | ||
| 
 | ||
|     if (isValidInput()) {
 | ||
|       return element.indeterminate || isAriaMixed
 | ||
|     }
 | ||
| 
 | ||
|     return isAriaMixed
 | ||
|   };
 | ||
| 
 | ||
|   return {
 | ||
|     pass: isPartiallyChecked(),
 | ||
|     message: () => {
 | ||
|       const is = isPartiallyChecked() ? 'is' : 'is not';
 | ||
|       return [
 | ||
|         this.utils.matcherHint(
 | ||
|           `${this.isNot ? '.not' : ''}.toBePartiallyChecked`,
 | ||
|           'element',
 | ||
|           '',
 | ||
|         ),
 | ||
|         '',
 | ||
|         `Received element ${is} partially checked:`,
 | ||
|         `  ${this.utils.printReceived(element.cloneNode(false))}`,
 | ||
|       ].join('\n')
 | ||
|     },
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| // See algoritm: https://www.w3.org/TR/accname-1.1/#mapping_additional_nd_description
 | ||
| function toHaveDescription(htmlElement, checkWith) {
 | ||
|   deprecate(
 | ||
|     'toHaveDescription',
 | ||
|     'Please use toHaveAccessibleDescription.',
 | ||
|   );
 | ||
| 
 | ||
|   checkHtmlElement(htmlElement, toHaveDescription, this);
 | ||
| 
 | ||
|   const expectsDescription = checkWith !== undefined;
 | ||
| 
 | ||
|   const descriptionIDRaw = htmlElement.getAttribute('aria-describedby') || '';
 | ||
|   const descriptionIDs = descriptionIDRaw.split(/\s+/).filter(Boolean);
 | ||
|   let description = '';
 | ||
|   if (descriptionIDs.length > 0) {
 | ||
|     const document = htmlElement.ownerDocument;
 | ||
|     const descriptionEls = descriptionIDs
 | ||
|       .map(descriptionID => document.getElementById(descriptionID))
 | ||
|       .filter(Boolean);
 | ||
|     description = normalize(descriptionEls.map(el => el.textContent).join(' '));
 | ||
|   }
 | ||
| 
 | ||
|   return {
 | ||
|     pass: expectsDescription
 | ||
|       ? checkWith instanceof RegExp
 | ||
|         ? checkWith.test(description)
 | ||
|         : this.equals(description, checkWith)
 | ||
|       : Boolean(description),
 | ||
|     message: () => {
 | ||
|       const to = this.isNot ? 'not to' : 'to';
 | ||
|       return getMessage(
 | ||
|         this,
 | ||
|         this.utils.matcherHint(
 | ||
|           `${this.isNot ? '.not' : ''}.toHaveDescription`,
 | ||
|           'element',
 | ||
|           '',
 | ||
|         ),
 | ||
|         `Expected the element ${to} have description`,
 | ||
|         this.utils.printExpected(checkWith),
 | ||
|         'Received',
 | ||
|         this.utils.printReceived(description),
 | ||
|       )
 | ||
|     },
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| // See aria-errormessage spec https://www.w3.org/TR/wai-aria-1.2/#aria-errormessage
 | ||
| function toHaveErrorMessage(htmlElement, checkWith) {
 | ||
|   deprecate('toHaveErrorMessage', 'Please use toHaveAccessibleErrorMessage.');
 | ||
|   checkHtmlElement(htmlElement, toHaveErrorMessage, this);
 | ||
| 
 | ||
|   if (
 | ||
|     !htmlElement.hasAttribute('aria-invalid') ||
 | ||
|     htmlElement.getAttribute('aria-invalid') === 'false'
 | ||
|   ) {
 | ||
|     const not = this.isNot ? '.not' : '';
 | ||
| 
 | ||
|     return {
 | ||
|       pass: false,
 | ||
|       message: () => {
 | ||
|         return getMessage(
 | ||
|           this,
 | ||
|           this.utils.matcherHint(`${not}.toHaveErrorMessage`, 'element', ''),
 | ||
|           `Expected the element to have invalid state indicated by`,
 | ||
|           'aria-invalid="true"',
 | ||
|           'Received',
 | ||
|           htmlElement.hasAttribute('aria-invalid')
 | ||
|             ? `aria-invalid="${htmlElement.getAttribute('aria-invalid')}"`
 | ||
|             : this.utils.printReceived(''),
 | ||
|         )
 | ||
|       },
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   const expectsErrorMessage = checkWith !== undefined;
 | ||
| 
 | ||
|   const errormessageIDRaw = htmlElement.getAttribute('aria-errormessage') || '';
 | ||
|   const errormessageIDs = errormessageIDRaw.split(/\s+/).filter(Boolean);
 | ||
| 
 | ||
|   let errormessage = '';
 | ||
|   if (errormessageIDs.length > 0) {
 | ||
|     const document = htmlElement.ownerDocument;
 | ||
| 
 | ||
|     const errormessageEls = errormessageIDs
 | ||
|       .map(errormessageID => document.getElementById(errormessageID))
 | ||
|       .filter(Boolean);
 | ||
| 
 | ||
|     errormessage = normalize(
 | ||
|       errormessageEls.map(el => el.textContent).join(' '),
 | ||
|     );
 | ||
|   }
 | ||
| 
 | ||
|   return {
 | ||
|     pass: expectsErrorMessage
 | ||
|       ? checkWith instanceof RegExp
 | ||
|         ? checkWith.test(errormessage)
 | ||
|         : this.equals(errormessage, checkWith)
 | ||
|       : Boolean(errormessage),
 | ||
|     message: () => {
 | ||
|       const to = this.isNot ? 'not to' : 'to';
 | ||
|       return getMessage(
 | ||
|         this,
 | ||
|         this.utils.matcherHint(
 | ||
|           `${this.isNot ? '.not' : ''}.toHaveErrorMessage`,
 | ||
|           'element',
 | ||
|           '',
 | ||
|         ),
 | ||
|         `Expected the element ${to} have error message`,
 | ||
|         this.utils.printExpected(checkWith),
 | ||
|         'Received',
 | ||
|         this.utils.printReceived(errormessage),
 | ||
|       )
 | ||
|     },
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| /**
 | ||
|  * Returns the selection from the element.
 | ||
|  * 
 | ||
|  * @param element {HTMLElement} The element to get the selection from.
 | ||
|  * @returns {String} The selection.
 | ||
|  */
 | ||
| function getSelection(element) {
 | ||
|   const selection = element.ownerDocument.getSelection();
 | ||
| 
 | ||
|   if (['input', 'textarea'].includes(element.tagName.toLowerCase())) {
 | ||
|     if (['radio', 'checkbox'].includes(element.type)) return ''
 | ||
|     return element.value
 | ||
|       .toString()
 | ||
|       .substring(element.selectionStart, element.selectionEnd)
 | ||
|   }
 | ||
| 
 | ||
|   if (selection.anchorNode === null || selection.focusNode === null) {
 | ||
|     // No selection
 | ||
|     return ''
 | ||
|   }
 | ||
| 
 | ||
|   const originalRange = selection.getRangeAt(0);
 | ||
|   const temporaryRange = element.ownerDocument.createRange();
 | ||
| 
 | ||
|   if (selection.containsNode(element, false)) {
 | ||
|     // Whole element is inside selection
 | ||
|     temporaryRange.selectNodeContents(element);
 | ||
|     selection.removeAllRanges();
 | ||
|     selection.addRange(temporaryRange);
 | ||
|   } else if (
 | ||
|     element.contains(selection.anchorNode) &&
 | ||
|     element.contains(selection.focusNode)
 | ||
|   ) ; else {
 | ||
|     // Element is partially selected
 | ||
|     const selectionStartsWithinElement =
 | ||
|       element === originalRange.startContainer ||
 | ||
|       element.contains(originalRange.startContainer);
 | ||
|     const selectionEndsWithinElement =
 | ||
|       element === originalRange.endContainer ||
 | ||
|       element.contains(originalRange.endContainer);
 | ||
|     selection.removeAllRanges();
 | ||
| 
 | ||
|     if (selectionStartsWithinElement || selectionEndsWithinElement) {
 | ||
|       temporaryRange.selectNodeContents(element);
 | ||
| 
 | ||
|       if (selectionStartsWithinElement) {
 | ||
|         temporaryRange.setStart(
 | ||
|           originalRange.startContainer,
 | ||
|           originalRange.startOffset,
 | ||
|         );
 | ||
|       }
 | ||
|       if (selectionEndsWithinElement) {
 | ||
|         temporaryRange.setEnd(
 | ||
|           originalRange.endContainer,
 | ||
|           originalRange.endOffset,
 | ||
|         );
 | ||
|       }
 | ||
| 
 | ||
|       selection.addRange(temporaryRange);
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   const result = selection.toString();
 | ||
| 
 | ||
|   selection.removeAllRanges();
 | ||
|   selection.addRange(originalRange);
 | ||
| 
 | ||
|   return result
 | ||
| }
 | ||
| 
 | ||
| /**
 | ||
|  * Checks if the element has the string selected.
 | ||
|  *
 | ||
|  * @param htmlElement {HTMLElement} The html element to check the selection for.
 | ||
|  * @param expectedSelection {String} The selection as a string.
 | ||
|  */
 | ||
| function toHaveSelection(htmlElement, expectedSelection) {
 | ||
|   checkHtmlElement(htmlElement, toHaveSelection, this);
 | ||
| 
 | ||
|   const expectsSelection = expectedSelection !== undefined;
 | ||
| 
 | ||
|   if (expectsSelection && typeof expectedSelection !== 'string') {
 | ||
|     throw new Error(`expected selection must be a string or undefined`)
 | ||
|   }
 | ||
| 
 | ||
|   const receivedSelection = getSelection(htmlElement);
 | ||
| 
 | ||
|   return {
 | ||
|     pass: expectsSelection
 | ||
|       ? isEqualWith(receivedSelection, expectedSelection, compareArraysAsSet)
 | ||
|       : Boolean(receivedSelection),
 | ||
|     message: () => {
 | ||
|       const to = this.isNot ? 'not to' : 'to';
 | ||
|       const matcher = this.utils.matcherHint(
 | ||
|         `${this.isNot ? '.not' : ''}.toHaveSelection`,
 | ||
|         'element',
 | ||
|         expectedSelection,
 | ||
|       );
 | ||
|       return getMessage(
 | ||
|         this,
 | ||
|         matcher,
 | ||
|         `Expected the element ${to} have selection`,
 | ||
|         expectsSelection ? expectedSelection : '(any)',
 | ||
|         'Received',
 | ||
|         receivedSelection,
 | ||
|       )
 | ||
|     },
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| var extensions = /*#__PURE__*/Object.freeze({
 | ||
|   __proto__: null,
 | ||
|   toBeChecked: toBeChecked,
 | ||
|   toBeDisabled: toBeDisabled,
 | ||
|   toBeEmpty: toBeEmpty,
 | ||
|   toBeEmptyDOMElement: toBeEmptyDOMElement,
 | ||
|   toBeEnabled: toBeEnabled,
 | ||
|   toBeInTheDOM: toBeInTheDOM,
 | ||
|   toBeInTheDocument: toBeInTheDocument,
 | ||
|   toBeInvalid: toBeInvalid,
 | ||
|   toBePartiallyChecked: toBePartiallyChecked,
 | ||
|   toBeRequired: toBeRequired,
 | ||
|   toBeValid: toBeValid,
 | ||
|   toBeVisible: toBeVisible,
 | ||
|   toContainElement: toContainElement,
 | ||
|   toContainHTML: toContainHTML,
 | ||
|   toHaveAccessibleDescription: toHaveAccessibleDescription,
 | ||
|   toHaveAccessibleErrorMessage: toHaveAccessibleErrorMessage,
 | ||
|   toHaveAccessibleName: toHaveAccessibleName,
 | ||
|   toHaveAttribute: toHaveAttribute,
 | ||
|   toHaveClass: toHaveClass,
 | ||
|   toHaveDescription: toHaveDescription,
 | ||
|   toHaveDisplayValue: toHaveDisplayValue,
 | ||
|   toHaveErrorMessage: toHaveErrorMessage,
 | ||
|   toHaveFocus: toHaveFocus,
 | ||
|   toHaveFormValues: toHaveFormValues,
 | ||
|   toHaveRole: toHaveRole,
 | ||
|   toHaveSelection: toHaveSelection,
 | ||
|   toHaveStyle: toHaveStyle,
 | ||
|   toHaveTextContent: toHaveTextContent,
 | ||
|   toHaveValue: toHaveValue
 | ||
| });
 | ||
| 
 | ||
| exports.extensions = extensions;
 | ||
| exports.toBeChecked = toBeChecked;
 | ||
| exports.toBeDisabled = toBeDisabled;
 | ||
| exports.toBeEmpty = toBeEmpty;
 | ||
| exports.toBeEmptyDOMElement = toBeEmptyDOMElement;
 | ||
| exports.toBeEnabled = toBeEnabled;
 | ||
| exports.toBeInTheDOM = toBeInTheDOM;
 | ||
| exports.toBeInTheDocument = toBeInTheDocument;
 | ||
| exports.toBeInvalid = toBeInvalid;
 | ||
| exports.toBePartiallyChecked = toBePartiallyChecked;
 | ||
| exports.toBeRequired = toBeRequired;
 | ||
| exports.toBeValid = toBeValid;
 | ||
| exports.toBeVisible = toBeVisible;
 | ||
| exports.toContainElement = toContainElement;
 | ||
| exports.toContainHTML = toContainHTML;
 | ||
| exports.toHaveAccessibleDescription = toHaveAccessibleDescription;
 | ||
| exports.toHaveAccessibleErrorMessage = toHaveAccessibleErrorMessage;
 | ||
| exports.toHaveAccessibleName = toHaveAccessibleName;
 | ||
| exports.toHaveAttribute = toHaveAttribute;
 | ||
| exports.toHaveClass = toHaveClass;
 | ||
| exports.toHaveDescription = toHaveDescription;
 | ||
| exports.toHaveDisplayValue = toHaveDisplayValue;
 | ||
| exports.toHaveErrorMessage = toHaveErrorMessage;
 | ||
| exports.toHaveFocus = toHaveFocus;
 | ||
| exports.toHaveFormValues = toHaveFormValues;
 | ||
| exports.toHaveRole = toHaveRole;
 | ||
| exports.toHaveSelection = toHaveSelection;
 | ||
| exports.toHaveStyle = toHaveStyle;
 | ||
| exports.toHaveTextContent = toHaveTextContent;
 | ||
| exports.toHaveValue = toHaveValue;
 |