matchers-57b266e9.js 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663
  1. 'use strict';
  2. var redent = require('redent');
  3. var isEqual = require('lodash/isEqual.js');
  4. var cssTools = require('@adobe/css-tools');
  5. var domAccessibilityApi = require('dom-accessibility-api');
  6. var chalk = require('chalk');
  7. var isEqualWith = require('lodash/isEqualWith.js');
  8. var uniq = require('lodash/uniq.js');
  9. var escape = require('css.escape');
  10. var ariaQuery = require('aria-query');
  11. class GenericTypeError extends Error {
  12. constructor(expectedString, received, matcherFn, context) {
  13. super();
  14. /* istanbul ignore next */
  15. if (Error.captureStackTrace) {
  16. Error.captureStackTrace(this, matcherFn);
  17. }
  18. let withType = '';
  19. try {
  20. withType = context.utils.printWithType(
  21. 'Received',
  22. received,
  23. context.utils.printReceived,
  24. );
  25. } catch (e) {
  26. // Can throw for Document:
  27. // https://github.com/jsdom/jsdom/issues/2304
  28. }
  29. this.message = [
  30. context.utils.matcherHint(
  31. `${context.isNot ? '.not' : ''}.${matcherFn.name}`,
  32. 'received',
  33. '',
  34. ),
  35. '',
  36. // eslint-disable-next-line new-cap
  37. `${context.utils.RECEIVED_COLOR(
  38. 'received',
  39. )} value must ${expectedString}.`,
  40. withType,
  41. ].join('\n');
  42. }
  43. }
  44. class HtmlElementTypeError extends GenericTypeError {
  45. constructor(...args) {
  46. super('be an HTMLElement or an SVGElement', ...args);
  47. }
  48. }
  49. class NodeTypeError extends GenericTypeError {
  50. constructor(...args) {
  51. super('be a Node', ...args);
  52. }
  53. }
  54. function checkHasWindow(htmlElement, ErrorClass, ...args) {
  55. if (
  56. !htmlElement ||
  57. !htmlElement.ownerDocument ||
  58. !htmlElement.ownerDocument.defaultView
  59. ) {
  60. throw new ErrorClass(htmlElement, ...args)
  61. }
  62. }
  63. function checkNode(node, ...args) {
  64. checkHasWindow(node, NodeTypeError, ...args);
  65. const window = node.ownerDocument.defaultView;
  66. if (!(node instanceof window.Node)) {
  67. throw new NodeTypeError(node, ...args)
  68. }
  69. }
  70. function checkHtmlElement(htmlElement, ...args) {
  71. checkHasWindow(htmlElement, HtmlElementTypeError, ...args);
  72. const window = htmlElement.ownerDocument.defaultView;
  73. if (
  74. !(htmlElement instanceof window.HTMLElement) &&
  75. !(htmlElement instanceof window.SVGElement)
  76. ) {
  77. throw new HtmlElementTypeError(htmlElement, ...args)
  78. }
  79. }
  80. class InvalidCSSError extends Error {
  81. constructor(received, matcherFn, context) {
  82. super();
  83. /* istanbul ignore next */
  84. if (Error.captureStackTrace) {
  85. Error.captureStackTrace(this, matcherFn);
  86. }
  87. this.message = [
  88. received.message,
  89. '',
  90. // eslint-disable-next-line new-cap
  91. context.utils.RECEIVED_COLOR(`Failing css:`),
  92. // eslint-disable-next-line new-cap
  93. context.utils.RECEIVED_COLOR(`${received.css}`),
  94. ].join('\n');
  95. }
  96. }
  97. function parseCSS(css, ...args) {
  98. const ast = cssTools.parse(`selector { ${css} }`, {silent: true}).stylesheet;
  99. if (ast.parsingErrors && ast.parsingErrors.length > 0) {
  100. const {reason, line} = ast.parsingErrors[0];
  101. throw new InvalidCSSError(
  102. {
  103. css,
  104. message: `Syntax error parsing expected css: ${reason} on line: ${line}`,
  105. },
  106. ...args,
  107. )
  108. }
  109. const parsedRules = ast.rules[0].declarations
  110. .filter(d => d.type === 'declaration')
  111. .reduce(
  112. (obj, {property, value}) => Object.assign(obj, {[property]: value}),
  113. {},
  114. );
  115. return parsedRules
  116. }
  117. function display(context, value) {
  118. return typeof value === 'string' ? value : context.utils.stringify(value)
  119. }
  120. function getMessage(
  121. context,
  122. matcher,
  123. expectedLabel,
  124. expectedValue,
  125. receivedLabel,
  126. receivedValue,
  127. ) {
  128. return [
  129. `${matcher}\n`,
  130. // eslint-disable-next-line new-cap
  131. `${expectedLabel}:\n${context.utils.EXPECTED_COLOR(
  132. redent(display(context, expectedValue), 2),
  133. )}`,
  134. // eslint-disable-next-line new-cap
  135. `${receivedLabel}:\n${context.utils.RECEIVED_COLOR(
  136. redent(display(context, receivedValue), 2),
  137. )}`,
  138. ].join('\n')
  139. }
  140. function matches(textToMatch, matcher) {
  141. if (matcher instanceof RegExp) {
  142. return matcher.test(textToMatch)
  143. } else {
  144. return textToMatch.includes(String(matcher))
  145. }
  146. }
  147. function deprecate(name, replacementText) {
  148. // Notify user that they are using deprecated functionality.
  149. // eslint-disable-next-line no-console
  150. console.warn(
  151. `Warning: ${name} has been deprecated and will be removed in future updates.`,
  152. replacementText,
  153. );
  154. }
  155. function normalize(text) {
  156. return text.replace(/\s+/g, ' ').trim()
  157. }
  158. function getTag(element) {
  159. return element.tagName && element.tagName.toLowerCase()
  160. }
  161. function getSelectValue({multiple, options}) {
  162. const selectedOptions = [...options].filter(option => option.selected);
  163. if (multiple) {
  164. return [...selectedOptions].map(opt => opt.value)
  165. }
  166. /* istanbul ignore if */
  167. if (selectedOptions.length === 0) {
  168. return undefined // Couldn't make this happen, but just in case
  169. }
  170. return selectedOptions[0].value
  171. }
  172. function getInputValue(inputElement) {
  173. switch (inputElement.type) {
  174. case 'number':
  175. return inputElement.value === '' ? null : Number(inputElement.value)
  176. case 'checkbox':
  177. return inputElement.checked
  178. default:
  179. return inputElement.value
  180. }
  181. }
  182. function getSingleElementValue(element) {
  183. /* istanbul ignore if */
  184. if (!element) {
  185. return undefined
  186. }
  187. switch (element.tagName.toLowerCase()) {
  188. case 'input':
  189. return getInputValue(element)
  190. case 'select':
  191. return getSelectValue(element)
  192. default:
  193. return element.value
  194. }
  195. }
  196. function compareArraysAsSet(a, b) {
  197. if (Array.isArray(a) && Array.isArray(b)) {
  198. return isEqual(new Set(a), new Set(b))
  199. }
  200. return undefined
  201. }
  202. function toSentence(
  203. array,
  204. {wordConnector = ', ', lastWordConnector = ' and '} = {},
  205. ) {
  206. return [array.slice(0, -1).join(wordConnector), array[array.length - 1]].join(
  207. array.length > 1 ? lastWordConnector : '',
  208. )
  209. }
  210. function toBeInTheDOM(element, container) {
  211. deprecate(
  212. 'toBeInTheDOM',
  213. 'Please use toBeInTheDocument for searching the entire document and toContainElement for searching a specific container.',
  214. );
  215. if (element) {
  216. checkHtmlElement(element, toBeInTheDOM, this);
  217. }
  218. if (container) {
  219. checkHtmlElement(container, toBeInTheDOM, this);
  220. }
  221. return {
  222. pass: container ? container.contains(element) : !!element,
  223. message: () => {
  224. return [
  225. this.utils.matcherHint(
  226. `${this.isNot ? '.not' : ''}.toBeInTheDOM`,
  227. 'element',
  228. '',
  229. ),
  230. '',
  231. 'Received:',
  232. ` ${this.utils.printReceived(
  233. element ? element.cloneNode(false) : element,
  234. )}`,
  235. ].join('\n')
  236. },
  237. }
  238. }
  239. function toBeInTheDocument(element) {
  240. if (element !== null || !this.isNot) {
  241. checkHtmlElement(element, toBeInTheDocument, this);
  242. }
  243. const pass =
  244. element === null
  245. ? false
  246. : element.ownerDocument === element.getRootNode({composed: true});
  247. const errorFound = () => {
  248. return `expected document not to contain element, found ${this.utils.stringify(
  249. element.cloneNode(true),
  250. )} instead`
  251. };
  252. const errorNotFound = () => {
  253. return `element could not be found in the document`
  254. };
  255. return {
  256. pass,
  257. message: () => {
  258. return [
  259. this.utils.matcherHint(
  260. `${this.isNot ? '.not' : ''}.toBeInTheDocument`,
  261. 'element',
  262. '',
  263. ),
  264. '',
  265. // eslint-disable-next-line new-cap
  266. this.utils.RECEIVED_COLOR(this.isNot ? errorFound() : errorNotFound()),
  267. ].join('\n')
  268. },
  269. }
  270. }
  271. function toBeEmpty(element) {
  272. deprecate(
  273. 'toBeEmpty',
  274. 'Please use instead toBeEmptyDOMElement for finding empty nodes in the DOM.',
  275. );
  276. checkHtmlElement(element, toBeEmpty, this);
  277. return {
  278. pass: element.innerHTML === '',
  279. message: () => {
  280. return [
  281. this.utils.matcherHint(
  282. `${this.isNot ? '.not' : ''}.toBeEmpty`,
  283. 'element',
  284. '',
  285. ),
  286. '',
  287. 'Received:',
  288. ` ${this.utils.printReceived(element.innerHTML)}`,
  289. ].join('\n')
  290. },
  291. }
  292. }
  293. function toBeEmptyDOMElement(element) {
  294. checkHtmlElement(element, toBeEmptyDOMElement, this);
  295. return {
  296. pass: isEmptyElement(element),
  297. message: () => {
  298. return [
  299. this.utils.matcherHint(
  300. `${this.isNot ? '.not' : ''}.toBeEmptyDOMElement`,
  301. 'element',
  302. '',
  303. ),
  304. '',
  305. 'Received:',
  306. ` ${this.utils.printReceived(element.innerHTML)}`,
  307. ].join('\n')
  308. },
  309. }
  310. }
  311. /**
  312. * Identifies if an element doesn't contain child nodes (excluding comments)
  313. * ℹ Node.COMMENT_NODE can't be used because of the following issue
  314. * https://github.com/jsdom/jsdom/issues/2220
  315. *
  316. * @param {*} element an HtmlElement or SVGElement
  317. * @return {*} true if the element only contains comments or none
  318. */
  319. function isEmptyElement(element){
  320. const nonCommentChildNodes = [...element.childNodes].filter(node => node.nodeType !== 8);
  321. return nonCommentChildNodes.length === 0;
  322. }
  323. function toContainElement(container, element) {
  324. checkHtmlElement(container, toContainElement, this);
  325. if (element !== null) {
  326. checkHtmlElement(element, toContainElement, this);
  327. }
  328. return {
  329. pass: container.contains(element),
  330. message: () => {
  331. return [
  332. this.utils.matcherHint(
  333. `${this.isNot ? '.not' : ''}.toContainElement`,
  334. 'element',
  335. 'element',
  336. ),
  337. '',
  338. // eslint-disable-next-line new-cap
  339. this.utils.RECEIVED_COLOR(`${this.utils.stringify(
  340. container.cloneNode(false),
  341. )} ${
  342. this.isNot ? 'contains:' : 'does not contain:'
  343. } ${this.utils.stringify(element ? element.cloneNode(false) : element)}
  344. `),
  345. ].join('\n')
  346. },
  347. }
  348. }
  349. function getNormalizedHtml(container, htmlText) {
  350. const div = container.ownerDocument.createElement('div');
  351. div.innerHTML = htmlText;
  352. return div.innerHTML
  353. }
  354. function toContainHTML(container, htmlText) {
  355. checkHtmlElement(container, toContainHTML, this);
  356. if (typeof htmlText !== 'string') {
  357. throw new Error(`.toContainHTML() expects a string value, got ${htmlText}`)
  358. }
  359. return {
  360. pass: container.outerHTML.includes(getNormalizedHtml(container, htmlText)),
  361. message: () => {
  362. return [
  363. this.utils.matcherHint(
  364. `${this.isNot ? '.not' : ''}.toContainHTML`,
  365. 'element',
  366. '',
  367. ),
  368. 'Expected:',
  369. // eslint-disable-next-line new-cap
  370. ` ${this.utils.EXPECTED_COLOR(htmlText)}`,
  371. 'Received:',
  372. ` ${this.utils.printReceived(container.cloneNode(true))}`,
  373. ].join('\n')
  374. },
  375. }
  376. }
  377. function toHaveTextContent(
  378. node,
  379. checkWith,
  380. options = {normalizeWhitespace: true},
  381. ) {
  382. checkNode(node, toHaveTextContent, this);
  383. const textContent = options.normalizeWhitespace
  384. ? normalize(node.textContent)
  385. : node.textContent.replace(/\u00a0/g, ' '); // Replace   with normal spaces
  386. const checkingWithEmptyString = textContent !== '' && checkWith === '';
  387. return {
  388. pass: !checkingWithEmptyString && matches(textContent, checkWith),
  389. message: () => {
  390. const to = this.isNot ? 'not to' : 'to';
  391. return getMessage(
  392. this,
  393. this.utils.matcherHint(
  394. `${this.isNot ? '.not' : ''}.toHaveTextContent`,
  395. 'element',
  396. '',
  397. ),
  398. checkingWithEmptyString
  399. ? `Checking with empty string will always match, use .toBeEmptyDOMElement() instead`
  400. : `Expected element ${to} have text content`,
  401. checkWith,
  402. 'Received',
  403. textContent,
  404. )
  405. },
  406. }
  407. }
  408. function toHaveAccessibleDescription(
  409. htmlElement,
  410. expectedAccessibleDescription,
  411. ) {
  412. checkHtmlElement(htmlElement, toHaveAccessibleDescription, this);
  413. const actualAccessibleDescription = domAccessibilityApi.computeAccessibleDescription(htmlElement);
  414. const missingExpectedValue = arguments.length === 1;
  415. let pass = false;
  416. if (missingExpectedValue) {
  417. // When called without an expected value we only want to validate that the element has an
  418. // accessible description, whatever it may be.
  419. pass = actualAccessibleDescription !== '';
  420. } else {
  421. pass =
  422. expectedAccessibleDescription instanceof RegExp
  423. ? expectedAccessibleDescription.test(actualAccessibleDescription)
  424. : this.equals(
  425. actualAccessibleDescription,
  426. expectedAccessibleDescription,
  427. );
  428. }
  429. return {
  430. pass,
  431. message: () => {
  432. const to = this.isNot ? 'not to' : 'to';
  433. return getMessage(
  434. this,
  435. this.utils.matcherHint(
  436. `${this.isNot ? '.not' : ''}.${toHaveAccessibleDescription.name}`,
  437. 'element',
  438. '',
  439. ),
  440. `Expected element ${to} have accessible description`,
  441. expectedAccessibleDescription,
  442. 'Received',
  443. actualAccessibleDescription,
  444. )
  445. },
  446. }
  447. }
  448. const ariaInvalidName = 'aria-invalid';
  449. const validStates = ['false'];
  450. // See `aria-errormessage` spec at https://www.w3.org/TR/wai-aria-1.2/#aria-errormessage
  451. function toHaveAccessibleErrorMessage(
  452. htmlElement,
  453. expectedAccessibleErrorMessage,
  454. ) {
  455. checkHtmlElement(htmlElement, toHaveAccessibleErrorMessage, this);
  456. const to = this.isNot ? 'not to' : 'to';
  457. const method = this.isNot
  458. ? '.not.toHaveAccessibleErrorMessage'
  459. : '.toHaveAccessibleErrorMessage';
  460. // Enforce Valid Id
  461. const errormessageId = htmlElement.getAttribute('aria-errormessage');
  462. const errormessageIdInvalid = !!errormessageId && /\s+/.test(errormessageId);
  463. if (errormessageIdInvalid) {
  464. return {
  465. pass: false,
  466. message: () => {
  467. return getMessage(
  468. this,
  469. this.utils.matcherHint(method, 'element'),
  470. "Expected element's `aria-errormessage` attribute to be empty or a single, valid ID",
  471. '',
  472. 'Received',
  473. `aria-errormessage="${errormessageId}"`,
  474. )
  475. },
  476. }
  477. }
  478. // See `aria-invalid` spec at https://www.w3.org/TR/wai-aria-1.2/#aria-invalid
  479. const ariaInvalidVal = htmlElement.getAttribute(ariaInvalidName);
  480. const fieldValid =
  481. !htmlElement.hasAttribute(ariaInvalidName) ||
  482. validStates.includes(ariaInvalidVal);
  483. // Enforce Valid `aria-invalid` Attribute
  484. if (fieldValid) {
  485. return {
  486. pass: false,
  487. message: () => {
  488. return getMessage(
  489. this,
  490. this.utils.matcherHint(method, 'element'),
  491. 'Expected element to be marked as invalid with attribute',
  492. `${ariaInvalidName}="${String(true)}"`,
  493. 'Received',
  494. htmlElement.hasAttribute('aria-invalid')
  495. ? `${ariaInvalidName}="${htmlElement.getAttribute(ariaInvalidName)}`
  496. : null,
  497. )
  498. },
  499. }
  500. }
  501. const error = normalize(
  502. htmlElement.ownerDocument.getElementById(errormessageId)?.textContent ?? '',
  503. );
  504. return {
  505. pass:
  506. expectedAccessibleErrorMessage === undefined
  507. ? Boolean(error)
  508. : expectedAccessibleErrorMessage instanceof RegExp
  509. ? expectedAccessibleErrorMessage.test(error)
  510. : this.equals(error, expectedAccessibleErrorMessage),
  511. message: () => {
  512. return getMessage(
  513. this,
  514. this.utils.matcherHint(method, 'element'),
  515. `Expected element ${to} have accessible error message`,
  516. expectedAccessibleErrorMessage ?? '',
  517. 'Received',
  518. error,
  519. )
  520. },
  521. }
  522. }
  523. function toHaveAccessibleName(htmlElement, expectedAccessibleName) {
  524. checkHtmlElement(htmlElement, toHaveAccessibleName, this);
  525. const actualAccessibleName = domAccessibilityApi.computeAccessibleName(htmlElement);
  526. const missingExpectedValue = arguments.length === 1;
  527. let pass = false;
  528. if (missingExpectedValue) {
  529. // When called without an expected value we only want to validate that the element has an
  530. // accessible name, whatever it may be.
  531. pass = actualAccessibleName !== '';
  532. } else {
  533. pass =
  534. expectedAccessibleName instanceof RegExp
  535. ? expectedAccessibleName.test(actualAccessibleName)
  536. : this.equals(actualAccessibleName, expectedAccessibleName);
  537. }
  538. return {
  539. pass,
  540. message: () => {
  541. const to = this.isNot ? 'not to' : 'to';
  542. return getMessage(
  543. this,
  544. this.utils.matcherHint(
  545. `${this.isNot ? '.not' : ''}.${toHaveAccessibleName.name}`,
  546. 'element',
  547. '',
  548. ),
  549. `Expected element ${to} have accessible name`,
  550. expectedAccessibleName,
  551. 'Received',
  552. actualAccessibleName,
  553. )
  554. },
  555. }
  556. }
  557. function printAttribute(stringify, name, value) {
  558. return value === undefined ? name : `${name}=${stringify(value)}`
  559. }
  560. function getAttributeComment(stringify, name, value) {
  561. return value === undefined
  562. ? `element.hasAttribute(${stringify(name)})`
  563. : `element.getAttribute(${stringify(name)}) === ${stringify(value)}`
  564. }
  565. function toHaveAttribute(htmlElement, name, expectedValue) {
  566. checkHtmlElement(htmlElement, toHaveAttribute, this);
  567. const isExpectedValuePresent = expectedValue !== undefined;
  568. const hasAttribute = htmlElement.hasAttribute(name);
  569. const receivedValue = htmlElement.getAttribute(name);
  570. return {
  571. pass: isExpectedValuePresent
  572. ? hasAttribute && this.equals(receivedValue, expectedValue)
  573. : hasAttribute,
  574. message: () => {
  575. const to = this.isNot ? 'not to' : 'to';
  576. const receivedAttribute = hasAttribute
  577. ? printAttribute(this.utils.stringify, name, receivedValue)
  578. : null;
  579. const matcher = this.utils.matcherHint(
  580. `${this.isNot ? '.not' : ''}.toHaveAttribute`,
  581. 'element',
  582. this.utils.printExpected(name),
  583. {
  584. secondArgument: isExpectedValuePresent
  585. ? this.utils.printExpected(expectedValue)
  586. : undefined,
  587. comment: getAttributeComment(
  588. this.utils.stringify,
  589. name,
  590. expectedValue,
  591. ),
  592. },
  593. );
  594. return getMessage(
  595. this,
  596. matcher,
  597. `Expected the element ${to} have attribute`,
  598. printAttribute(this.utils.stringify, name, expectedValue),
  599. 'Received',
  600. receivedAttribute,
  601. )
  602. },
  603. }
  604. }
  605. function getExpectedClassNamesAndOptions(params) {
  606. const lastParam = params.pop();
  607. let expectedClassNames, options;
  608. if (typeof lastParam === 'object') {
  609. expectedClassNames = params;
  610. options = lastParam;
  611. } else {
  612. expectedClassNames = params.concat(lastParam);
  613. options = {exact: false};
  614. }
  615. return {expectedClassNames, options}
  616. }
  617. function splitClassNames(str) {
  618. if (!str) {
  619. return []
  620. }
  621. return str.split(/\s+/).filter(s => s.length > 0)
  622. }
  623. function isSubset$1(subset, superset) {
  624. return subset.every(item => superset.includes(item))
  625. }
  626. function toHaveClass(htmlElement, ...params) {
  627. checkHtmlElement(htmlElement, toHaveClass, this);
  628. const {expectedClassNames, options} = getExpectedClassNamesAndOptions(params);
  629. const received = splitClassNames(htmlElement.getAttribute('class'));
  630. const expected = expectedClassNames.reduce(
  631. (acc, className) => acc.concat(splitClassNames(className)),
  632. [],
  633. );
  634. if (options.exact) {
  635. return {
  636. pass: isSubset$1(expected, received) && expected.length === received.length,
  637. message: () => {
  638. const to = this.isNot ? 'not to' : 'to';
  639. return getMessage(
  640. this,
  641. this.utils.matcherHint(
  642. `${this.isNot ? '.not' : ''}.toHaveClass`,
  643. 'element',
  644. this.utils.printExpected(expected.join(' ')),
  645. ),
  646. `Expected the element ${to} have EXACTLY defined classes`,
  647. expected.join(' '),
  648. 'Received',
  649. received.join(' '),
  650. )
  651. },
  652. }
  653. }
  654. return expected.length > 0
  655. ? {
  656. pass: isSubset$1(expected, received),
  657. message: () => {
  658. const to = this.isNot ? 'not to' : 'to';
  659. return getMessage(
  660. this,
  661. this.utils.matcherHint(
  662. `${this.isNot ? '.not' : ''}.toHaveClass`,
  663. 'element',
  664. this.utils.printExpected(expected.join(' ')),
  665. ),
  666. `Expected the element ${to} have class`,
  667. expected.join(' '),
  668. 'Received',
  669. received.join(' '),
  670. )
  671. },
  672. }
  673. : {
  674. pass: this.isNot ? received.length > 0 : false,
  675. message: () =>
  676. this.isNot
  677. ? getMessage(
  678. this,
  679. this.utils.matcherHint('.not.toHaveClass', 'element', ''),
  680. 'Expected the element to have classes',
  681. '(none)',
  682. 'Received',
  683. received.join(' '),
  684. )
  685. : [
  686. this.utils.matcherHint(`.toHaveClass`, 'element'),
  687. 'At least one expected class must be provided.',
  688. ].join('\n'),
  689. }
  690. }
  691. function getStyleDeclaration(document, css) {
  692. const styles = {};
  693. // The next block is necessary to normalize colors
  694. const copy = document.createElement('div');
  695. Object.keys(css).forEach(property => {
  696. copy.style[property] = css[property];
  697. styles[property] = copy.style[property];
  698. });
  699. return styles
  700. }
  701. function isSubset(styles, computedStyle) {
  702. return (
  703. !!Object.keys(styles).length &&
  704. Object.entries(styles).every(([prop, value]) => {
  705. const isCustomProperty = prop.startsWith('--');
  706. const spellingVariants = [prop];
  707. if (!isCustomProperty) spellingVariants.push(prop.toLowerCase());
  708. return spellingVariants.some(
  709. name =>
  710. computedStyle[name] === value ||
  711. computedStyle.getPropertyValue(name) === value,
  712. )
  713. })
  714. )
  715. }
  716. function printoutStyles(styles) {
  717. return Object.keys(styles)
  718. .sort()
  719. .map(prop => `${prop}: ${styles[prop]};`)
  720. .join('\n')
  721. }
  722. // Highlights only style rules that were expected but were not found in the
  723. // received computed styles
  724. function expectedDiff(diffFn, expected, computedStyles) {
  725. const received = Array.from(computedStyles)
  726. .filter(prop => expected[prop] !== undefined)
  727. .reduce(
  728. (obj, prop) =>
  729. Object.assign(obj, {[prop]: computedStyles.getPropertyValue(prop)}),
  730. {},
  731. );
  732. const diffOutput = diffFn(printoutStyles(expected), printoutStyles(received));
  733. // Remove the "+ Received" annotation because this is a one-way diff
  734. return diffOutput.replace(`${chalk.red('+ Received')}\n`, '')
  735. }
  736. function toHaveStyle(htmlElement, css) {
  737. checkHtmlElement(htmlElement, toHaveStyle, this);
  738. const parsedCSS =
  739. typeof css === 'object' ? css : parseCSS(css, toHaveStyle, this);
  740. const {getComputedStyle} = htmlElement.ownerDocument.defaultView;
  741. const expected = getStyleDeclaration(htmlElement.ownerDocument, parsedCSS);
  742. const received = getComputedStyle(htmlElement);
  743. return {
  744. pass: isSubset(expected, received),
  745. message: () => {
  746. const matcher = `${this.isNot ? '.not' : ''}.toHaveStyle`;
  747. return [
  748. this.utils.matcherHint(matcher, 'element', ''),
  749. expectedDiff(this.utils.diff, expected, received),
  750. ].join('\n\n')
  751. },
  752. }
  753. }
  754. function toHaveFocus(element) {
  755. checkHtmlElement(element, toHaveFocus, this);
  756. return {
  757. pass: element.ownerDocument.activeElement === element,
  758. message: () => {
  759. return [
  760. this.utils.matcherHint(
  761. `${this.isNot ? '.not' : ''}.toHaveFocus`,
  762. 'element',
  763. '',
  764. ),
  765. '',
  766. ...(this.isNot
  767. ? [
  768. 'Received element is focused:',
  769. ` ${this.utils.printReceived(element)}`,
  770. ]
  771. : [
  772. 'Expected element with focus:',
  773. ` ${this.utils.printExpected(element)}`,
  774. 'Received element with focus:',
  775. ` ${this.utils.printReceived(
  776. element.ownerDocument.activeElement,
  777. )}`,
  778. ]),
  779. ].join('\n')
  780. },
  781. }
  782. }
  783. // Returns the combined value of several elements that have the same name
  784. // e.g. radio buttons or groups of checkboxes
  785. function getMultiElementValue(elements) {
  786. const types = uniq(elements.map(element => element.type));
  787. if (types.length !== 1) {
  788. throw new Error(
  789. 'Multiple form elements with the same name must be of the same type',
  790. )
  791. }
  792. switch (types[0]) {
  793. case 'radio': {
  794. const theChosenOne = elements.find(radio => radio.checked);
  795. return theChosenOne ? theChosenOne.value : undefined
  796. }
  797. case 'checkbox':
  798. return elements
  799. .filter(checkbox => checkbox.checked)
  800. .map(checkbox => checkbox.value)
  801. default:
  802. // NOTE: Not even sure this is a valid use case, but just in case...
  803. return elements.map(element => element.value)
  804. }
  805. }
  806. function getFormValue(container, name) {
  807. const elements = [...container.querySelectorAll(`[name="${escape(name)}"]`)];
  808. /* istanbul ignore if */
  809. if (elements.length === 0) {
  810. return undefined // shouldn't happen, but just in case
  811. }
  812. switch (elements.length) {
  813. case 1:
  814. return getSingleElementValue(elements[0])
  815. default:
  816. return getMultiElementValue(elements)
  817. }
  818. }
  819. // Strips the `[]` suffix off a form value name
  820. function getPureName(name) {
  821. return /\[\]$/.test(name) ? name.slice(0, -2) : name
  822. }
  823. function getAllFormValues(container) {
  824. const names = Array.from(container.elements).map(element => element.name);
  825. return names.reduce(
  826. (obj, name) => ({
  827. ...obj,
  828. [getPureName(name)]: getFormValue(container, name),
  829. }),
  830. {},
  831. )
  832. }
  833. function toHaveFormValues(formElement, expectedValues) {
  834. checkHtmlElement(formElement, toHaveFormValues, this);
  835. if (!formElement.elements) {
  836. // TODO: Change condition to use instanceof against the appropriate element classes instead
  837. throw new Error('toHaveFormValues must be called on a form or a fieldset')
  838. }
  839. const formValues = getAllFormValues(formElement);
  840. return {
  841. pass: Object.entries(expectedValues).every(([name, expectedValue]) =>
  842. isEqualWith(formValues[name], expectedValue, compareArraysAsSet),
  843. ),
  844. message: () => {
  845. const to = this.isNot ? 'not to' : 'to';
  846. const matcher = `${this.isNot ? '.not' : ''}.toHaveFormValues`;
  847. const commonKeyValues = Object.keys(formValues)
  848. .filter(key => expectedValues.hasOwnProperty(key))
  849. .reduce((obj, key) => ({...obj, [key]: formValues[key]}), {});
  850. return [
  851. this.utils.matcherHint(matcher, 'element', ''),
  852. `Expected the element ${to} have form values`,
  853. this.utils.diff(expectedValues, commonKeyValues),
  854. ].join('\n\n')
  855. },
  856. }
  857. }
  858. function isStyleVisible(element) {
  859. const {getComputedStyle} = element.ownerDocument.defaultView;
  860. const {display, visibility, opacity} = getComputedStyle(element);
  861. return (
  862. display !== 'none' &&
  863. visibility !== 'hidden' &&
  864. visibility !== 'collapse' &&
  865. opacity !== '0' &&
  866. opacity !== 0
  867. )
  868. }
  869. function isAttributeVisible(element, previousElement) {
  870. let detailsVisibility;
  871. if (previousElement) {
  872. detailsVisibility =
  873. element.nodeName === 'DETAILS' && previousElement.nodeName !== 'SUMMARY'
  874. ? element.hasAttribute('open')
  875. : true;
  876. } else {
  877. detailsVisibility =
  878. element.nodeName === 'DETAILS' ? element.hasAttribute('open') : true;
  879. }
  880. return !element.hasAttribute('hidden') && detailsVisibility
  881. }
  882. function isElementVisible(element, previousElement) {
  883. return (
  884. isStyleVisible(element) &&
  885. isAttributeVisible(element, previousElement) &&
  886. (!element.parentElement || isElementVisible(element.parentElement, element))
  887. )
  888. }
  889. function toBeVisible(element) {
  890. checkHtmlElement(element, toBeVisible, this);
  891. const isInDocument =
  892. element.ownerDocument === element.getRootNode({composed: true});
  893. const isVisible = isInDocument && isElementVisible(element);
  894. return {
  895. pass: isVisible,
  896. message: () => {
  897. const is = isVisible ? 'is' : 'is not';
  898. return [
  899. this.utils.matcherHint(
  900. `${this.isNot ? '.not' : ''}.toBeVisible`,
  901. 'element',
  902. '',
  903. ),
  904. '',
  905. `Received element ${is} visible${
  906. isInDocument ? '' : ' (element is not in the document)'
  907. }:`,
  908. ` ${this.utils.printReceived(element.cloneNode(false))}`,
  909. ].join('\n')
  910. },
  911. }
  912. }
  913. // form elements that support 'disabled'
  914. const FORM_TAGS$2 = [
  915. 'fieldset',
  916. 'input',
  917. 'select',
  918. 'optgroup',
  919. 'option',
  920. 'button',
  921. 'textarea',
  922. ];
  923. /*
  924. * According to specification:
  925. * If <fieldset> is disabled, the form controls that are its descendants,
  926. * except descendants of its first optional <legend> element, are disabled
  927. *
  928. * https://html.spec.whatwg.org/multipage/form-elements.html#concept-fieldset-disabled
  929. *
  930. * This method tests whether element is first legend child of fieldset parent
  931. */
  932. function isFirstLegendChildOfFieldset(element, parent) {
  933. return (
  934. getTag(element) === 'legend' &&
  935. getTag(parent) === 'fieldset' &&
  936. element.isSameNode(
  937. Array.from(parent.children).find(child => getTag(child) === 'legend'),
  938. )
  939. )
  940. }
  941. function isElementDisabledByParent(element, parent) {
  942. return (
  943. isElementDisabled(parent) && !isFirstLegendChildOfFieldset(element, parent)
  944. )
  945. }
  946. function isCustomElement(tag) {
  947. return tag.includes('-')
  948. }
  949. /*
  950. * Only certain form elements and custom elements can actually be disabled:
  951. * https://html.spec.whatwg.org/multipage/semantics-other.html#disabled-elements
  952. */
  953. function canElementBeDisabled(element) {
  954. const tag = getTag(element);
  955. return FORM_TAGS$2.includes(tag) || isCustomElement(tag)
  956. }
  957. function isElementDisabled(element) {
  958. return canElementBeDisabled(element) && element.hasAttribute('disabled')
  959. }
  960. function isAncestorDisabled(element) {
  961. const parent = element.parentElement;
  962. return (
  963. Boolean(parent) &&
  964. (isElementDisabledByParent(element, parent) || isAncestorDisabled(parent))
  965. )
  966. }
  967. function isElementOrAncestorDisabled(element) {
  968. return (
  969. canElementBeDisabled(element) &&
  970. (isElementDisabled(element) || isAncestorDisabled(element))
  971. )
  972. }
  973. function toBeDisabled(element) {
  974. checkHtmlElement(element, toBeDisabled, this);
  975. const isDisabled = isElementOrAncestorDisabled(element);
  976. return {
  977. pass: isDisabled,
  978. message: () => {
  979. const is = isDisabled ? 'is' : 'is not';
  980. return [
  981. this.utils.matcherHint(
  982. `${this.isNot ? '.not' : ''}.toBeDisabled`,
  983. 'element',
  984. '',
  985. ),
  986. '',
  987. `Received element ${is} disabled:`,
  988. ` ${this.utils.printReceived(element.cloneNode(false))}`,
  989. ].join('\n')
  990. },
  991. }
  992. }
  993. function toBeEnabled(element) {
  994. checkHtmlElement(element, toBeEnabled, this);
  995. const isEnabled = !isElementOrAncestorDisabled(element);
  996. return {
  997. pass: isEnabled,
  998. message: () => {
  999. const is = isEnabled ? 'is' : 'is not';
  1000. return [
  1001. this.utils.matcherHint(
  1002. `${this.isNot ? '.not' : ''}.toBeEnabled`,
  1003. 'element',
  1004. '',
  1005. ),
  1006. '',
  1007. `Received element ${is} enabled:`,
  1008. ` ${this.utils.printReceived(element.cloneNode(false))}`,
  1009. ].join('\n')
  1010. },
  1011. }
  1012. }
  1013. // form elements that support 'required'
  1014. const FORM_TAGS$1 = ['select', 'textarea'];
  1015. const ARIA_FORM_TAGS = ['input', 'select', 'textarea'];
  1016. const UNSUPPORTED_INPUT_TYPES = [
  1017. 'color',
  1018. 'hidden',
  1019. 'range',
  1020. 'submit',
  1021. 'image',
  1022. 'reset',
  1023. ];
  1024. const SUPPORTED_ARIA_ROLES = [
  1025. 'combobox',
  1026. 'gridcell',
  1027. 'radiogroup',
  1028. 'spinbutton',
  1029. 'tree',
  1030. ];
  1031. function isRequiredOnFormTagsExceptInput(element) {
  1032. return FORM_TAGS$1.includes(getTag(element)) && element.hasAttribute('required')
  1033. }
  1034. function isRequiredOnSupportedInput(element) {
  1035. return (
  1036. getTag(element) === 'input' &&
  1037. element.hasAttribute('required') &&
  1038. ((element.hasAttribute('type') &&
  1039. !UNSUPPORTED_INPUT_TYPES.includes(element.getAttribute('type'))) ||
  1040. !element.hasAttribute('type'))
  1041. )
  1042. }
  1043. function isElementRequiredByARIA(element) {
  1044. return (
  1045. element.hasAttribute('aria-required') &&
  1046. element.getAttribute('aria-required') === 'true' &&
  1047. (ARIA_FORM_TAGS.includes(getTag(element)) ||
  1048. (element.hasAttribute('role') &&
  1049. SUPPORTED_ARIA_ROLES.includes(element.getAttribute('role'))))
  1050. )
  1051. }
  1052. function toBeRequired(element) {
  1053. checkHtmlElement(element, toBeRequired, this);
  1054. const isRequired =
  1055. isRequiredOnFormTagsExceptInput(element) ||
  1056. isRequiredOnSupportedInput(element) ||
  1057. isElementRequiredByARIA(element);
  1058. return {
  1059. pass: isRequired,
  1060. message: () => {
  1061. const is = isRequired ? 'is' : 'is not';
  1062. return [
  1063. this.utils.matcherHint(
  1064. `${this.isNot ? '.not' : ''}.toBeRequired`,
  1065. 'element',
  1066. '',
  1067. ),
  1068. '',
  1069. `Received element ${is} required:`,
  1070. ` ${this.utils.printReceived(element.cloneNode(false))}`,
  1071. ].join('\n')
  1072. },
  1073. }
  1074. }
  1075. const FORM_TAGS = ['form', 'input', 'select', 'textarea'];
  1076. function isElementHavingAriaInvalid(element) {
  1077. return (
  1078. element.hasAttribute('aria-invalid') &&
  1079. element.getAttribute('aria-invalid') !== 'false'
  1080. )
  1081. }
  1082. function isSupportsValidityMethod(element) {
  1083. return FORM_TAGS.includes(getTag(element))
  1084. }
  1085. function isElementInvalid(element) {
  1086. const isHaveAriaInvalid = isElementHavingAriaInvalid(element);
  1087. if (isSupportsValidityMethod(element)) {
  1088. return isHaveAriaInvalid || !element.checkValidity()
  1089. } else {
  1090. return isHaveAriaInvalid
  1091. }
  1092. }
  1093. function toBeInvalid(element) {
  1094. checkHtmlElement(element, toBeInvalid, this);
  1095. const isInvalid = isElementInvalid(element);
  1096. return {
  1097. pass: isInvalid,
  1098. message: () => {
  1099. const is = isInvalid ? 'is' : 'is not';
  1100. return [
  1101. this.utils.matcherHint(
  1102. `${this.isNot ? '.not' : ''}.toBeInvalid`,
  1103. 'element',
  1104. '',
  1105. ),
  1106. '',
  1107. `Received element ${is} currently invalid:`,
  1108. ` ${this.utils.printReceived(element.cloneNode(false))}`,
  1109. ].join('\n')
  1110. },
  1111. }
  1112. }
  1113. function toBeValid(element) {
  1114. checkHtmlElement(element, toBeValid, this);
  1115. const isValid = !isElementInvalid(element);
  1116. return {
  1117. pass: isValid,
  1118. message: () => {
  1119. const is = isValid ? 'is' : 'is not';
  1120. return [
  1121. this.utils.matcherHint(
  1122. `${this.isNot ? '.not' : ''}.toBeValid`,
  1123. 'element',
  1124. '',
  1125. ),
  1126. '',
  1127. `Received element ${is} currently valid:`,
  1128. ` ${this.utils.printReceived(element.cloneNode(false))}`,
  1129. ].join('\n')
  1130. },
  1131. }
  1132. }
  1133. function toHaveValue(htmlElement, expectedValue) {
  1134. checkHtmlElement(htmlElement, toHaveValue, this);
  1135. if (
  1136. htmlElement.tagName.toLowerCase() === 'input' &&
  1137. ['checkbox', 'radio'].includes(htmlElement.type)
  1138. ) {
  1139. throw new Error(
  1140. 'input with type=checkbox or type=radio cannot be used with .toHaveValue(). Use .toBeChecked() for type=checkbox or .toHaveFormValues() instead',
  1141. )
  1142. }
  1143. const receivedValue = getSingleElementValue(htmlElement);
  1144. const expectsValue = expectedValue !== undefined;
  1145. let expectedTypedValue = expectedValue;
  1146. let receivedTypedValue = receivedValue;
  1147. if (expectedValue == receivedValue && expectedValue !== receivedValue) {
  1148. expectedTypedValue = `${expectedValue} (${typeof expectedValue})`;
  1149. receivedTypedValue = `${receivedValue} (${typeof receivedValue})`;
  1150. }
  1151. return {
  1152. pass: expectsValue
  1153. ? isEqualWith(receivedValue, expectedValue, compareArraysAsSet)
  1154. : Boolean(receivedValue),
  1155. message: () => {
  1156. const to = this.isNot ? 'not to' : 'to';
  1157. const matcher = this.utils.matcherHint(
  1158. `${this.isNot ? '.not' : ''}.toHaveValue`,
  1159. 'element',
  1160. expectedValue,
  1161. );
  1162. return getMessage(
  1163. this,
  1164. matcher,
  1165. `Expected the element ${to} have value`,
  1166. expectsValue ? expectedTypedValue : '(any)',
  1167. 'Received',
  1168. receivedTypedValue,
  1169. )
  1170. },
  1171. }
  1172. }
  1173. function toHaveDisplayValue(htmlElement, expectedValue) {
  1174. checkHtmlElement(htmlElement, toHaveDisplayValue, this);
  1175. const tagName = htmlElement.tagName.toLowerCase();
  1176. if (!['select', 'input', 'textarea'].includes(tagName)) {
  1177. throw new Error(
  1178. '.toHaveDisplayValue() currently supports only input, textarea or select elements, try with another matcher instead.',
  1179. )
  1180. }
  1181. if (tagName === 'input' && ['radio', 'checkbox'].includes(htmlElement.type)) {
  1182. throw new Error(
  1183. `.toHaveDisplayValue() currently does not support input[type="${htmlElement.type}"], try with another matcher instead.`,
  1184. )
  1185. }
  1186. const values = getValues(tagName, htmlElement);
  1187. const expectedValues = getExpectedValues(expectedValue);
  1188. const numberOfMatchesWithValues = expectedValues.filter(expected =>
  1189. values.some(value =>
  1190. expected instanceof RegExp
  1191. ? expected.test(value)
  1192. : this.equals(value, String(expected)),
  1193. ),
  1194. ).length;
  1195. const matchedWithAllValues = numberOfMatchesWithValues === values.length;
  1196. const matchedWithAllExpectedValues =
  1197. numberOfMatchesWithValues === expectedValues.length;
  1198. return {
  1199. pass: matchedWithAllValues && matchedWithAllExpectedValues,
  1200. message: () =>
  1201. getMessage(
  1202. this,
  1203. this.utils.matcherHint(
  1204. `${this.isNot ? '.not' : ''}.toHaveDisplayValue`,
  1205. 'element',
  1206. '',
  1207. ),
  1208. `Expected element ${this.isNot ? 'not ' : ''}to have display value`,
  1209. expectedValue,
  1210. 'Received',
  1211. values,
  1212. ),
  1213. }
  1214. }
  1215. function getValues(tagName, htmlElement) {
  1216. return tagName === 'select'
  1217. ? Array.from(htmlElement)
  1218. .filter(option => option.selected)
  1219. .map(option => option.textContent)
  1220. : [htmlElement.value]
  1221. }
  1222. function getExpectedValues(expectedValue) {
  1223. return expectedValue instanceof Array ? expectedValue : [expectedValue]
  1224. }
  1225. function toBeChecked(element) {
  1226. checkHtmlElement(element, toBeChecked, this);
  1227. const isValidInput = () => {
  1228. return (
  1229. element.tagName.toLowerCase() === 'input' &&
  1230. ['checkbox', 'radio'].includes(element.type)
  1231. )
  1232. };
  1233. const isValidAriaElement = () => {
  1234. return (
  1235. roleSupportsChecked(element.getAttribute('role')) &&
  1236. ['true', 'false'].includes(element.getAttribute('aria-checked'))
  1237. )
  1238. };
  1239. if (!isValidInput() && !isValidAriaElement()) {
  1240. return {
  1241. pass: false,
  1242. message: () =>
  1243. `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`,
  1244. }
  1245. }
  1246. const isChecked = () => {
  1247. if (isValidInput()) return element.checked
  1248. return element.getAttribute('aria-checked') === 'true'
  1249. };
  1250. return {
  1251. pass: isChecked(),
  1252. message: () => {
  1253. const is = isChecked() ? 'is' : 'is not';
  1254. return [
  1255. this.utils.matcherHint(
  1256. `${this.isNot ? '.not' : ''}.toBeChecked`,
  1257. 'element',
  1258. '',
  1259. ),
  1260. '',
  1261. `Received element ${is} checked:`,
  1262. ` ${this.utils.printReceived(element.cloneNode(false))}`,
  1263. ].join('\n')
  1264. },
  1265. }
  1266. }
  1267. function supportedRolesSentence() {
  1268. return toSentence(
  1269. supportedRoles().map(role => `role="${role}"`),
  1270. {lastWordConnector: ' or '},
  1271. )
  1272. }
  1273. function supportedRoles() {
  1274. return ariaQuery.roles.keys().filter(roleSupportsChecked)
  1275. }
  1276. function roleSupportsChecked(role) {
  1277. return ariaQuery.roles.get(role)?.props['aria-checked'] !== undefined
  1278. }
  1279. function toBePartiallyChecked(element) {
  1280. checkHtmlElement(element, toBePartiallyChecked, this);
  1281. const isValidInput = () => {
  1282. return (
  1283. element.tagName.toLowerCase() === 'input' && element.type === 'checkbox'
  1284. )
  1285. };
  1286. const isValidAriaElement = () => {
  1287. return element.getAttribute('role') === 'checkbox'
  1288. };
  1289. if (!isValidInput() && !isValidAriaElement()) {
  1290. return {
  1291. pass: false,
  1292. message: () =>
  1293. 'only inputs with type="checkbox" or elements with role="checkbox" and a valid aria-checked attribute can be used with .toBePartiallyChecked(). Use .toHaveValue() instead',
  1294. }
  1295. }
  1296. const isPartiallyChecked = () => {
  1297. const isAriaMixed = element.getAttribute('aria-checked') === 'mixed';
  1298. if (isValidInput()) {
  1299. return element.indeterminate || isAriaMixed
  1300. }
  1301. return isAriaMixed
  1302. };
  1303. return {
  1304. pass: isPartiallyChecked(),
  1305. message: () => {
  1306. const is = isPartiallyChecked() ? 'is' : 'is not';
  1307. return [
  1308. this.utils.matcherHint(
  1309. `${this.isNot ? '.not' : ''}.toBePartiallyChecked`,
  1310. 'element',
  1311. '',
  1312. ),
  1313. '',
  1314. `Received element ${is} partially checked:`,
  1315. ` ${this.utils.printReceived(element.cloneNode(false))}`,
  1316. ].join('\n')
  1317. },
  1318. }
  1319. }
  1320. // See algoritm: https://www.w3.org/TR/accname-1.1/#mapping_additional_nd_description
  1321. function toHaveDescription(htmlElement, checkWith) {
  1322. deprecate(
  1323. 'toHaveDescription',
  1324. 'Please use toHaveAccessibleDescription.',
  1325. );
  1326. checkHtmlElement(htmlElement, toHaveDescription, this);
  1327. const expectsDescription = checkWith !== undefined;
  1328. const descriptionIDRaw = htmlElement.getAttribute('aria-describedby') || '';
  1329. const descriptionIDs = descriptionIDRaw.split(/\s+/).filter(Boolean);
  1330. let description = '';
  1331. if (descriptionIDs.length > 0) {
  1332. const document = htmlElement.ownerDocument;
  1333. const descriptionEls = descriptionIDs
  1334. .map(descriptionID => document.getElementById(descriptionID))
  1335. .filter(Boolean);
  1336. description = normalize(descriptionEls.map(el => el.textContent).join(' '));
  1337. }
  1338. return {
  1339. pass: expectsDescription
  1340. ? checkWith instanceof RegExp
  1341. ? checkWith.test(description)
  1342. : this.equals(description, checkWith)
  1343. : Boolean(description),
  1344. message: () => {
  1345. const to = this.isNot ? 'not to' : 'to';
  1346. return getMessage(
  1347. this,
  1348. this.utils.matcherHint(
  1349. `${this.isNot ? '.not' : ''}.toHaveDescription`,
  1350. 'element',
  1351. '',
  1352. ),
  1353. `Expected the element ${to} have description`,
  1354. this.utils.printExpected(checkWith),
  1355. 'Received',
  1356. this.utils.printReceived(description),
  1357. )
  1358. },
  1359. }
  1360. }
  1361. // See aria-errormessage spec https://www.w3.org/TR/wai-aria-1.2/#aria-errormessage
  1362. function toHaveErrorMessage(htmlElement, checkWith) {
  1363. deprecate('toHaveErrorMessage', 'Please use toHaveAccessibleErrorMessage.');
  1364. checkHtmlElement(htmlElement, toHaveErrorMessage, this);
  1365. if (
  1366. !htmlElement.hasAttribute('aria-invalid') ||
  1367. htmlElement.getAttribute('aria-invalid') === 'false'
  1368. ) {
  1369. const not = this.isNot ? '.not' : '';
  1370. return {
  1371. pass: false,
  1372. message: () => {
  1373. return getMessage(
  1374. this,
  1375. this.utils.matcherHint(`${not}.toHaveErrorMessage`, 'element', ''),
  1376. `Expected the element to have invalid state indicated by`,
  1377. 'aria-invalid="true"',
  1378. 'Received',
  1379. htmlElement.hasAttribute('aria-invalid')
  1380. ? `aria-invalid="${htmlElement.getAttribute('aria-invalid')}"`
  1381. : this.utils.printReceived(''),
  1382. )
  1383. },
  1384. }
  1385. }
  1386. const expectsErrorMessage = checkWith !== undefined;
  1387. const errormessageIDRaw = htmlElement.getAttribute('aria-errormessage') || '';
  1388. const errormessageIDs = errormessageIDRaw.split(/\s+/).filter(Boolean);
  1389. let errormessage = '';
  1390. if (errormessageIDs.length > 0) {
  1391. const document = htmlElement.ownerDocument;
  1392. const errormessageEls = errormessageIDs
  1393. .map(errormessageID => document.getElementById(errormessageID))
  1394. .filter(Boolean);
  1395. errormessage = normalize(
  1396. errormessageEls.map(el => el.textContent).join(' '),
  1397. );
  1398. }
  1399. return {
  1400. pass: expectsErrorMessage
  1401. ? checkWith instanceof RegExp
  1402. ? checkWith.test(errormessage)
  1403. : this.equals(errormessage, checkWith)
  1404. : Boolean(errormessage),
  1405. message: () => {
  1406. const to = this.isNot ? 'not to' : 'to';
  1407. return getMessage(
  1408. this,
  1409. this.utils.matcherHint(
  1410. `${this.isNot ? '.not' : ''}.toHaveErrorMessage`,
  1411. 'element',
  1412. '',
  1413. ),
  1414. `Expected the element ${to} have error message`,
  1415. this.utils.printExpected(checkWith),
  1416. 'Received',
  1417. this.utils.printReceived(errormessage),
  1418. )
  1419. },
  1420. }
  1421. }
  1422. var extensions = /*#__PURE__*/Object.freeze({
  1423. __proto__: null,
  1424. toBeChecked: toBeChecked,
  1425. toBeDisabled: toBeDisabled,
  1426. toBeEmpty: toBeEmpty,
  1427. toBeEmptyDOMElement: toBeEmptyDOMElement,
  1428. toBeEnabled: toBeEnabled,
  1429. toBeInTheDOM: toBeInTheDOM,
  1430. toBeInTheDocument: toBeInTheDocument,
  1431. toBeInvalid: toBeInvalid,
  1432. toBePartiallyChecked: toBePartiallyChecked,
  1433. toBeRequired: toBeRequired,
  1434. toBeValid: toBeValid,
  1435. toBeVisible: toBeVisible,
  1436. toContainElement: toContainElement,
  1437. toContainHTML: toContainHTML,
  1438. toHaveAccessibleDescription: toHaveAccessibleDescription,
  1439. toHaveAccessibleErrorMessage: toHaveAccessibleErrorMessage,
  1440. toHaveAccessibleName: toHaveAccessibleName,
  1441. toHaveAttribute: toHaveAttribute,
  1442. toHaveClass: toHaveClass,
  1443. toHaveDescription: toHaveDescription,
  1444. toHaveDisplayValue: toHaveDisplayValue,
  1445. toHaveErrorMessage: toHaveErrorMessage,
  1446. toHaveFocus: toHaveFocus,
  1447. toHaveFormValues: toHaveFormValues,
  1448. toHaveStyle: toHaveStyle,
  1449. toHaveTextContent: toHaveTextContent,
  1450. toHaveValue: toHaveValue
  1451. });
  1452. exports.extensions = extensions;
  1453. exports.toBeChecked = toBeChecked;
  1454. exports.toBeDisabled = toBeDisabled;
  1455. exports.toBeEmpty = toBeEmpty;
  1456. exports.toBeEmptyDOMElement = toBeEmptyDOMElement;
  1457. exports.toBeEnabled = toBeEnabled;
  1458. exports.toBeInTheDOM = toBeInTheDOM;
  1459. exports.toBeInTheDocument = toBeInTheDocument;
  1460. exports.toBeInvalid = toBeInvalid;
  1461. exports.toBePartiallyChecked = toBePartiallyChecked;
  1462. exports.toBeRequired = toBeRequired;
  1463. exports.toBeValid = toBeValid;
  1464. exports.toBeVisible = toBeVisible;
  1465. exports.toContainElement = toContainElement;
  1466. exports.toContainHTML = toContainHTML;
  1467. exports.toHaveAccessibleDescription = toHaveAccessibleDescription;
  1468. exports.toHaveAccessibleErrorMessage = toHaveAccessibleErrorMessage;
  1469. exports.toHaveAccessibleName = toHaveAccessibleName;
  1470. exports.toHaveAttribute = toHaveAttribute;
  1471. exports.toHaveClass = toHaveClass;
  1472. exports.toHaveDescription = toHaveDescription;
  1473. exports.toHaveDisplayValue = toHaveDisplayValue;
  1474. exports.toHaveErrorMessage = toHaveErrorMessage;
  1475. exports.toHaveFocus = toHaveFocus;
  1476. exports.toHaveFormValues = toHaveFormValues;
  1477. exports.toHaveStyle = toHaveStyle;
  1478. exports.toHaveTextContent = toHaveTextContent;
  1479. exports.toHaveValue = toHaveValue;