[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

perf(browser): fork jest-dom instead of bundling it (#7605)

authored by

Vladimir and committed by
GitHub
(Mar 26, 2025, 10:07 AM +0100) 12762ea6 2a8520a8

+7705 -846
+29 -40
pnpm-lock.yaml
··· 502 502 specifier: 'catalog:' 503 503 version: 8.18.1 504 504 devDependencies: 505 - '@testing-library/jest-dom': 506 - specifier: ^6.6.3 507 - version: 6.6.3 508 505 '@types/ws': 509 506 specifier: 'catalog:' 510 507 version: 8.18.0 ··· 530 527 specifier: 'catalog:' 531 528 version: 3.3.3 532 529 ivya: 533 - specifier: ^1.5.1 534 - version: 1.5.1 530 + specifier: ^1.6.0 531 + version: 1.6.0 535 532 mime: 536 533 specifier: ^4.0.6 537 534 version: 4.0.6 ··· 5516 5513 es-module-lexer@1.6.0: 5517 5514 resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} 5518 5515 5519 - es-object-atoms@1.0.0: 5520 - resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} 5521 - engines: {node: '>= 0.4'} 5522 - 5523 5516 es-object-atoms@1.1.1: 5524 5517 resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} 5525 5518 engines: {node: '>= 0.4'} ··· 6596 6589 resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} 6597 6590 engines: {node: '>=8'} 6598 6591 6599 - ivya@1.5.1: 6600 - resolution: {integrity: sha512-WUDzzuxLS01qb4BujtMmqmoSxrG/EsNZjLI7+23xHf28NjlCD0KWHmeoHtaq21mUNrWxqxmgxwxxNDbuE7WOwA==} 6592 + ivya@1.6.0: 6593 + resolution: {integrity: sha512-MVindQ7lUYqEmW6K5o/ZTrLfRmOqhl0GEo/MekwiUm+WfDv8wOB7FRy1bGprz2hxvvrOHrDmgF8KIPw8N+/ONw==} 6601 6594 6602 6595 jackspeak@3.4.0: 6603 6596 resolution: {integrity: sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==} ··· 12601 12594 define-properties: 1.2.1 12602 12595 es-abstract: 1.23.3 12603 12596 es-errors: 1.3.0 12604 - get-intrinsic: 1.2.4 12597 + get-intrinsic: 1.3.0 12605 12598 is-array-buffer: 3.0.4 12606 12599 is-shared-array-buffer: 1.0.3 12607 12600 ··· 13525 13518 data-view-buffer: 1.0.1 13526 13519 data-view-byte-length: 1.0.1 13527 13520 data-view-byte-offset: 1.0.0 13528 - es-define-property: 1.0.0 13521 + es-define-property: 1.0.1 13529 13522 es-errors: 1.3.0 13530 - es-object-atoms: 1.0.0 13523 + es-object-atoms: 1.1.1 13531 13524 es-set-tostringtag: 2.0.3 13532 13525 es-to-primitive: 1.2.1 13533 13526 function.prototype.name: 1.1.6 13534 - get-intrinsic: 1.2.4 13527 + get-intrinsic: 1.3.0 13535 13528 get-symbol-description: 1.0.2 13536 13529 globalthis: 1.0.4 13537 - gopd: 1.0.1 13530 + gopd: 1.2.0 13538 13531 has-property-descriptors: 1.0.2 13539 13532 has-proto: 1.0.3 13540 - has-symbols: 1.0.3 13533 + has-symbols: 1.1.0 13541 13534 hasown: 2.0.2 13542 13535 internal-slot: 1.0.7 13543 13536 is-array-buffer: 3.0.4 ··· 13549 13542 is-string: 1.0.7 13550 13543 is-typed-array: 1.1.13 13551 13544 is-weakref: 1.0.2 13552 - object-inspect: 1.13.2 13545 + object-inspect: 1.13.4 13553 13546 object-keys: 1.1.1 13554 13547 object.assign: 4.1.5 13555 13548 regexp.prototype.flags: 1.5.3 ··· 13587 13580 13588 13581 es-module-lexer@1.6.0: {} 13589 13582 13590 - es-object-atoms@1.0.0: 13591 - dependencies: 13592 - es-errors: 1.3.0 13593 - 13594 13583 es-object-atoms@1.1.1: 13595 13584 dependencies: 13596 13585 es-errors: 1.3.0 13597 13586 13598 13587 es-set-tostringtag@2.0.3: 13599 13588 dependencies: 13600 - get-intrinsic: 1.2.4 13589 + get-intrinsic: 1.3.0 13601 13590 has-tostringtag: 1.0.2 13602 13591 hasown: 2.0.2 13603 13592 ··· 14406 14395 dependencies: 14407 14396 call-bind: 1.0.7 14408 14397 es-errors: 1.3.0 14409 - get-intrinsic: 1.2.4 14398 + get-intrinsic: 1.3.0 14410 14399 14411 14400 get-tsconfig@4.10.0: 14412 14401 dependencies: ··· 14490 14479 globalthis@1.0.4: 14491 14480 dependencies: 14492 14481 define-properties: 1.2.1 14493 - gopd: 1.0.1 14482 + gopd: 1.2.0 14494 14483 14495 14484 globalyzer@0.1.0: {} 14496 14485 ··· 14926 14915 html-escaper: 2.0.2 14927 14916 istanbul-lib-report: 3.0.1 14928 14917 14929 - ivya@1.5.1: {} 14918 + ivya@1.6.0: {} 14930 14919 14931 14920 jackspeak@3.4.0: 14932 14921 dependencies: ··· 16537 16526 safe-array-concat@1.1.2: 16538 16527 dependencies: 16539 16528 call-bind: 1.0.7 16540 - get-intrinsic: 1.2.4 16541 - has-symbols: 1.0.3 16529 + get-intrinsic: 1.3.0 16530 + has-symbols: 1.1.0 16542 16531 isarray: 2.0.5 16543 16532 16544 16533 safe-buffer@5.1.2: {} ··· 16930 16919 define-properties: 1.2.1 16931 16920 es-abstract: 1.23.3 16932 16921 es-errors: 1.3.0 16933 - es-object-atoms: 1.0.0 16934 - get-intrinsic: 1.2.4 16935 - gopd: 1.0.1 16936 - has-symbols: 1.0.3 16922 + es-object-atoms: 1.1.1 16923 + get-intrinsic: 1.3.0 16924 + gopd: 1.2.0 16925 + has-symbols: 1.1.0 16937 16926 internal-slot: 1.0.7 16938 16927 regexp.prototype.flags: 1.5.3 16939 16928 set-function-name: 2.0.2 16940 - side-channel: 1.0.6 16929 + side-channel: 1.1.0 16941 16930 16942 16931 string.prototype.trim@1.2.9: 16943 16932 dependencies: 16944 16933 call-bind: 1.0.7 16945 16934 define-properties: 1.2.1 16946 16935 es-abstract: 1.23.3 16947 - es-object-atoms: 1.0.0 16936 + es-object-atoms: 1.1.1 16948 16937 16949 16938 string.prototype.trimend@1.0.8: 16950 16939 dependencies: 16951 16940 call-bind: 1.0.7 16952 16941 define-properties: 1.2.1 16953 - es-object-atoms: 1.0.0 16942 + es-object-atoms: 1.1.1 16954 16943 16955 16944 string.prototype.trimstart@1.0.8: 16956 16945 dependencies: 16957 16946 call-bind: 1.0.7 16958 16947 define-properties: 1.2.1 16959 - es-object-atoms: 1.0.0 16948 + es-object-atoms: 1.1.1 16960 16949 16961 16950 string_decoder@1.1.1: 16962 16951 dependencies: ··· 17335 17324 dependencies: 17336 17325 call-bind: 1.0.7 17337 17326 for-each: 0.3.3 17338 - gopd: 1.0.1 17327 + gopd: 1.2.0 17339 17328 has-proto: 1.0.3 17340 17329 is-typed-array: 1.1.13 17341 17330 ··· 17344 17333 available-typed-arrays: 1.0.7 17345 17334 call-bind: 1.0.7 17346 17335 for-each: 0.3.3 17347 - gopd: 1.0.1 17336 + gopd: 1.2.0 17348 17337 has-proto: 1.0.3 17349 17338 is-typed-array: 1.1.13 17350 17339 ··· 17352 17341 dependencies: 17353 17342 call-bind: 1.0.7 17354 17343 for-each: 0.3.3 17355 - gopd: 1.0.1 17344 + gopd: 1.2.0 17356 17345 has-proto: 1.0.3 17357 17346 is-typed-array: 1.1.13 17358 17347 possible-typed-array-names: 1.0.0 ··· 17367 17356 dependencies: 17368 17357 call-bind: 1.0.7 17369 17358 has-bigints: 1.0.2 17370 - has-symbols: 1.0.3 17359 + has-symbols: 1.1.0 17371 17360 which-boxed-primitive: 1.0.2 17372 17361 17373 17362 unconfig@0.3.11:
+2 -1
packages/browser/context.d.ts
··· 1 - import type { SerializedConfig } from 'vitest' 1 + import { SerializedConfig } from 'vitest' 2 2 import { ARIARole } from './aria-role.js' 3 + import {} from './matchers.js' 3 4 4 5 export type BufferEncoding = 5 6 | 'ascii'
+623 -712
packages/browser/jest-dom.d.ts
··· 2 2 3 3 import { ARIARole } from './aria-role.ts' 4 4 5 - declare namespace matchers { 6 - interface TestingLibraryMatchers<E, R> { 7 - /** 8 - * @deprecated 9 - * since v1.9.0 10 - * @description 11 - * Assert whether a value is a DOM element, or not. Contrary to what its name implies, this matcher only checks 12 - * that you passed to it a valid DOM element. 13 - * 14 - * It does not have a clear definition of what "the DOM" is. Therefore, it does not check whether that element 15 - * is contained anywhere. 16 - * @see 17 - * [testing-library/jest-dom#toBeInTheDom](https://github.com/testing-library/jest-dom#toBeInTheDom) 18 - */ 19 - toBeInTheDOM(container?: HTMLElement | SVGElement): R 20 - /** 21 - * @description 22 - * Assert whether an element is present in the document or not. 23 - * @example 24 - * <svg data-testid="svg-element"></svg> 25 - * 26 - * expect(queryByTestId('svg-element')).toBeInTheDocument() 27 - * expect(queryByTestId('does-not-exist')).not.toBeInTheDocument() 28 - * @see 29 - * [testing-library/jest-dom#tobeinthedocument](https://github.com/testing-library/jest-dom#tobeinthedocument) 30 - */ 31 - toBeInTheDocument(): R 32 - /** 33 - * @description 34 - * This allows you to check if an element is currently visible to the user. 35 - * 36 - * An element is visible if **all** the following conditions are met: 37 - * * it does not have its css property display set to none 38 - * * it does not have its css property visibility set to either hidden or collapse 39 - * * it does not have its css property opacity set to 0 40 - * * its parent element is also visible (and so on up to the top of the DOM tree) 41 - * * it does not have the hidden attribute 42 - * * if `<details />` it has the open attribute 43 - * @example 44 - * <div 45 - * data-testid="zero-opacity" 46 - * style="opacity: 0" 47 - * > 48 - * Zero Opacity 49 - * </div> 50 - * 51 - * <div data-testid="visible">Visible Example</div> 52 - * 53 - * expect(getByTestId('zero-opacity')).not.toBeVisible() 54 - * expect(getByTestId('visible')).toBeVisible() 55 - * @see 56 - * [testing-library/jest-dom#tobevisible](https://github.com/testing-library/jest-dom#tobevisible) 57 - */ 58 - toBeVisible(): R 59 - /** 60 - * @deprecated 61 - * since v5.9.0 62 - * @description 63 - * Assert whether an element has content or not. 64 - * @example 65 - * <span data-testid="not-empty"> 66 - * <span data-testid="empty"></span> 67 - * </span> 68 - * 69 - * expect(getByTestId('empty')).toBeEmpty() 70 - * expect(getByTestId('not-empty')).not.toBeEmpty() 71 - * @see 72 - * [testing-library/jest-dom#tobeempty](https://github.com/testing-library/jest-dom#tobeempty) 73 - */ 74 - toBeEmpty(): R 75 - /** 76 - * @description 77 - * Assert whether an element has content or not. 78 - * @example 79 - * <span data-testid="not-empty"> 80 - * <span data-testid="empty"></span> 81 - * </span> 82 - * 83 - * expect(getByTestId('empty')).toBeEmptyDOMElement() 84 - * expect(getByTestId('not-empty')).not.toBeEmptyDOMElement() 85 - * @see 86 - * [testing-library/jest-dom#tobeemptydomelement](https://github.com/testing-library/jest-dom#tobeemptydomelement) 87 - */ 88 - toBeEmptyDOMElement(): R 89 - /** 90 - * @description 91 - * Allows you to check whether an element is disabled from the user's perspective. 92 - * 93 - * Matches if the element is a form control and the `disabled` attribute is specified on this element or the 94 - * element is a descendant of a form element with a `disabled` attribute. 95 - * @example 96 - * <button 97 - * data-testid="button" 98 - * type="submit" 99 - * disabled 100 - * > 101 - * submit 102 - * </button> 103 - * 104 - * expect(getByTestId('button')).toBeDisabled() 105 - * @see 106 - * [testing-library/jest-dom#tobedisabled](https://github.com/testing-library/jest-dom#tobedisabled) 107 - */ 108 - toBeDisabled(): R 109 - /** 110 - * @description 111 - * Allows you to check whether an element is not disabled from the user's perspective. 112 - * 113 - * Works like `not.toBeDisabled()`. 114 - * 115 - * Use this matcher to avoid double negation in your tests. 116 - * @example 117 - * <button 118 - * data-testid="button" 119 - * type="submit" 120 - * > 121 - * submit 122 - * </button> 123 - * 124 - * expect(getByTestId('button')).toBeEnabled() 125 - * @see 126 - * [testing-library/jest-dom#tobeenabled](https://github.com/testing-library/jest-dom#tobeenabled) 127 - */ 128 - toBeEnabled(): R 129 - /** 130 - * @description 131 - * Check if a form element, or the entire `form`, is currently invalid. 132 - * 133 - * An `input`, `select`, `textarea`, or `form` element is invalid if it has an `aria-invalid` attribute with no 134 - * value or a value of "true", or if the result of `checkValidity()` is false. 135 - * @example 136 - * <input data-testid="no-aria-invalid" /> 137 - * 138 - * <form data-testid="invalid-form"> 139 - * <input required /> 140 - * </form> 141 - * 142 - * expect(getByTestId('no-aria-invalid')).not.toBeInvalid() 143 - * expect(getByTestId('invalid-form')).toBeInvalid() 144 - * @see 145 - * [testing-library/jest-dom#tobeinvalid](https://github.com/testing-library/jest-dom#tobeinvalid) 146 - */ 147 - toBeInvalid(): R 148 - /** 149 - * @description 150 - * This allows you to check if a form element is currently required. 151 - * 152 - * An element is required if it is having a `required` or `aria-required="true"` attribute. 153 - * @example 154 - * <input data-testid="required-input" required /> 155 - * <div 156 - * data-testid="supported-role" 157 - * role="tree" 158 - * required /> 159 - * 160 - * expect(getByTestId('required-input')).toBeRequired() 161 - * expect(getByTestId('supported-role')).not.toBeRequired() 162 - * @see 163 - * [testing-library/jest-dom#toberequired](https://github.com/testing-library/jest-dom#toberequired) 164 - */ 165 - toBeRequired(): R 166 - /** 167 - * @description 168 - * Allows you to check if a form element is currently required. 169 - * 170 - * An `input`, `select`, `textarea`, or `form` element is invalid if it has an `aria-invalid` attribute with no 171 - * value or a value of "false", or if the result of `checkValidity()` is true. 172 - * @example 173 - * <input data-testid="aria-invalid" aria-invalid /> 174 - * 175 - * <form data-testid="valid-form"> 176 - * <input /> 177 - * </form> 178 - * 179 - * expect(getByTestId('no-aria-invalid')).not.toBeValid() 180 - * expect(getByTestId('invalid-form')).toBeInvalid() 181 - * @see 182 - * [testing-library/jest-dom#tobevalid](https://github.com/testing-library/jest-dom#tobevalid) 183 - */ 184 - toBeValid(): R 185 - /** 186 - * @description 187 - * Allows you to assert whether an element contains another element as a descendant or not. 188 - * @example 189 - * <span data-testid="ancestor"> 190 - * <span data-testid="descendant"></span> 191 - * </span> 192 - * 193 - * const ancestor = getByTestId('ancestor') 194 - * const descendant = getByTestId('descendant') 195 - * const nonExistentElement = getByTestId('does-not-exist') 196 - * expect(ancestor).toContainElement(descendant) 197 - * expect(descendant).not.toContainElement(ancestor) 198 - * expect(ancestor).not.toContainElement(nonExistentElement) 199 - * @see 200 - * [testing-library/jest-dom#tocontainelement](https://github.com/testing-library/jest-dom#tocontainelement) 201 - */ 202 - toContainElement(element: HTMLElement | SVGElement | null): R 203 - /** 204 - * @description 205 - * Assert whether a string representing a HTML element is contained in another element. 206 - * @example 207 - * <span data-testid="parent"><span data-testid="child"></span></span> 208 - * 209 - * expect(getByTestId('parent')).toContainHTML('<span data-testid="child"></span>') 210 - * @see 211 - * [testing-library/jest-dom#tocontainhtml](https://github.com/testing-library/jest-dom#tocontainhtml) 212 - */ 213 - toContainHTML(htmlText: string): R 214 - /** 215 - * @description 216 - * Allows you to check if a given element has an attribute or not. 217 - * 218 - * You can also optionally check that the attribute has a specific expected value or partial match using 219 - * [expect.stringContaining](https://jestjs.io/docs/en/expect.html#expectnotstringcontainingstring) or 220 - * [expect.stringMatching](https://jestjs.io/docs/en/expect.html#expectstringmatchingstring-regexp). 221 - * @example 222 - * <button 223 - * data-testid="ok-button" 224 - * type="submit" 225 - * disabled 226 - * > 227 - * ok 228 - * </button> 229 - * 230 - * expect(button).toHaveAttribute('disabled') 231 - * expect(button).toHaveAttribute('type', 'submit') 232 - * expect(button).not.toHaveAttribute('type', 'button') 233 - * @see 234 - * [testing-library/jest-dom#tohaveattribute](https://github.com/testing-library/jest-dom#tohaveattribute) 235 - */ 236 - toHaveAttribute(attr: string, value?: unknown): R 237 - /** 238 - * @description 239 - * Check whether the given element has certain classes within its `class` attribute. 240 - * 241 - * You must provide at least one class, unless you are asserting that an element does not have any classes. 242 - * @example 243 - * <button 244 - * data-testid="delete-button" 245 - * class="btn xs btn-danger" 246 - * > 247 - * delete item 248 - * </button> 249 - * 250 - * <div data-testid="no-classes">no classes</div> 251 - * 252 - * const deleteButton = getByTestId('delete-button') 253 - * const noClasses = getByTestId('no-classes') 254 - * expect(deleteButton).toHaveClass('btn') 255 - * expect(deleteButton).toHaveClass('btn-danger xs') 256 - * expect(deleteButton).toHaveClass(/danger/, 'xs') 257 - * expect(deleteButton).toHaveClass('btn xs btn-danger', {exact: true}) 258 - * expect(deleteButton).not.toHaveClass('btn xs btn-danger', {exact: true}) 259 - * expect(noClasses).not.toHaveClass() 260 - * @see 261 - * [testing-library/jest-dom#tohaveclass](https://github.com/testing-library/jest-dom#tohaveclass) 262 - */ 263 - toHaveClass(...classNames: (string | RegExp)[] | [string, options?: {exact: boolean}]): R 264 - /** 265 - * @description 266 - * This allows you to check whether the given form element has the specified displayed value (the one the 267 - * end user will see). It accepts <input>, <select> and <textarea> elements with the exception of <input type="checkbox"> 268 - * and <input type="radio">, which can be meaningfully matched only using toBeChecked or toHaveFormValues. 269 - * @example 270 - * <label for="input-example">First name</label> 271 - * <input type="text" id="input-example" value="Luca" /> 272 - * 273 - * <label for="textarea-example">Description</label> 274 - * <textarea id="textarea-example">An example description here.</textarea> 275 - * 276 - * <label for="single-select-example">Fruit</label> 277 - * <select id="single-select-example"> 278 - * <option value="">Select a fruit...</option> 279 - * <option value="banana">Banana</option> 280 - * <option value="ananas">Ananas</option> 281 - * <option value="avocado">Avocado</option> 282 - * </select> 283 - * 284 - * <label for="multiple-select-example">Fruits</label> 285 - * <select id="multiple-select-example" multiple> 286 - * <option value="">Select a fruit...</option> 287 - * <option value="banana" selected>Banana</option> 288 - * <option value="ananas">Ananas</option> 289 - * <option value="avocado" selected>Avocado</option> 290 - * </select> 291 - * 292 - * const input = screen.getByLabelText('First name') 293 - * const textarea = screen.getByLabelText('Description') 294 - * const selectSingle = screen.getByLabelText('Fruit') 295 - * const selectMultiple = screen.getByLabelText('Fruits') 296 - * 297 - * expect(input).toHaveDisplayValue('Luca') 298 - * expect(textarea).toHaveDisplayValue('An example description here.') 299 - * expect(selectSingle).toHaveDisplayValue('Select a fruit...') 300 - * expect(selectMultiple).toHaveDisplayValue(['Banana', 'Avocado']) 301 - * 302 - * @see 303 - * [testing-library/jest-dom#tohavedisplayvalue](https://github.com/testing-library/jest-dom#tohavedisplayvalue) 304 - */ 305 - toHaveDisplayValue(value: string | RegExp | Array<string | RegExp>): R 306 - /** 307 - * @description 308 - * Assert whether an element has focus or not. 309 - * @example 310 - * <div> 311 - * <input type="text" data-testid="element-to-focus" /> 312 - * </div> 313 - * 314 - * const input = getByTestId('element-to-focus') 315 - * input.focus() 316 - * expect(input).toHaveFocus() 317 - * input.blur() 318 - * expect(input).not.toHaveFocus() 319 - * @see 320 - * [testing-library/jest-dom#tohavefocus](https://github.com/testing-library/jest-dom#tohavefocus) 321 - */ 322 - toHaveFocus(): R 323 - /** 324 - * @description 325 - * Check if a form or fieldset contains form controls for each given name, and having the specified value. 326 - * 327 - * Can only be invoked on a form or fieldset element. 328 - * @example 329 - * <form data-testid="login-form"> 330 - * <input type="text" name="username" value="jane.doe" /> 331 - * <input type="password" name="password" value="123" /> 332 - * <input type="checkbox" name="rememberMe" checked /> 333 - * <button type="submit">Sign in</button> 334 - * </form> 335 - * 336 - * expect(getByTestId('login-form')).toHaveFormValues({ 337 - * username: 'jane.doe', 338 - * rememberMe: true, 339 - * }) 340 - * @see 341 - * [testing-library/jest-dom#tohaveformvalues](https://github.com/testing-library/jest-dom#tohaveformvalues) 342 - */ 343 - toHaveFormValues(expectedValues: Record<string, unknown>): R 344 - /** 345 - * @description 346 - * Check if an element has specific css properties with specific values applied. 347 - * 348 - * Only matches if the element has *all* the expected properties applied, not just some of them. 349 - * @example 350 - * <button 351 - * data-testid="submit-button" 352 - * style="background-color: green; display: none" 353 - * > 354 - * submit 355 - * </button> 356 - * 357 - * const button = getByTestId('submit-button') 358 - * expect(button).toHaveStyle('background-color: green') 359 - * expect(button).toHaveStyle({ 360 - * 'background-color': 'green', 361 - * display: 'none' 362 - * }) 363 - * @see 364 - * [testing-library/jest-dom#tohavestyle](https://github.com/testing-library/jest-dom#tohavestyle) 365 - */ 366 - toHaveStyle(css: string | Record<string, unknown>): R 367 - /** 368 - * @description 369 - * Check whether the given element has a text content or not. 370 - * 371 - * When a string argument is passed through, it will perform a partial case-sensitive match to the element 372 - * content. 373 - * 374 - * To perform a case-insensitive match, you can use a RegExp with the `/i` modifier. 375 - * 376 - * If you want to match the whole content, you can use a RegExp to do it. 377 - * @example 378 - * <span data-testid="text-content">Text Content</span> 379 - * 380 - * const element = getByTestId('text-content') 381 - * expect(element).toHaveTextContent('Content') 382 - * // to match the whole content 383 - * expect(element).toHaveTextContent(/^Text Content$/) 384 - * // to use case-insensitive match 385 - * expect(element).toHaveTextContent(/content$/i) 386 - * expect(element).not.toHaveTextContent('content') 387 - * @see 388 - * [testing-library/jest-dom#tohavetextcontent](https://github.com/testing-library/jest-dom#tohavetextcontent) 389 - */ 390 - toHaveTextContent( 391 - text: string | RegExp, 392 - options?: {normalizeWhitespace: boolean}, 393 - ): R 394 - /** 395 - * @description 396 - * Check whether the given form element has the specified value. 397 - * 398 - * Accepts `<input>`, `<select>`, and `<textarea>` elements with the exception of `<input type="checkbox">` and 399 - * `<input type="radiobox">`, which can be matched only using 400 - * [toBeChecked](https://github.com/testing-library/jest-dom#tobechecked) or 401 - * [toHaveFormValues](https://github.com/testing-library/jest-dom#tohaveformvalues). 402 - * @example 403 - * <input 404 - * type="number" 405 - * value="5" 406 - * data-testid="input-number" /> 407 - * 408 - * const numberInput = getByTestId('input-number') 409 - * expect(numberInput).toHaveValue(5) 410 - * @see 411 - * [testing-library/jest-dom#tohavevalue](https://github.com/testing-library/jest-dom#tohavevalue) 412 - */ 413 - toHaveValue(value?: string | string[] | number | null): R 414 - /** 415 - * @description 416 - * Assert whether the given element is checked. 417 - * 418 - * It accepts an `input` of type `checkbox` or `radio` and elements with a `role` of `radio` with a valid 419 - * `aria-checked` attribute of "true" or "false". 420 - * @example 421 - * <input 422 - * type="checkbox" 423 - * checked 424 - * data-testid="input-checkbox" /> 425 - * <input 426 - * type="radio" 427 - * value="foo" 428 - * data-testid="input-radio" /> 429 - * 430 - * const inputCheckbox = getByTestId('input-checkbox') 431 - * const inputRadio = getByTestId('input-radio') 432 - * expect(inputCheckbox).toBeChecked() 433 - * expect(inputRadio).not.toBeChecked() 434 - * @see 435 - * [testing-library/jest-dom#tobechecked](https://github.com/testing-library/jest-dom#tobechecked) 436 - */ 437 - toBeChecked(): R 438 - /** 439 - * @deprecated 440 - * since v5.14.1 441 - * @description 442 - * Check the accessible description for an element. 443 - * This allows you to check whether the given element has a description or not. 444 - * 445 - * An element gets its description via the 446 - * [`aria-describedby` attribute](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-describedby_attribute). 447 - * Set this to the `id` of one or more other elements. These elements may be nested 448 - * inside, be outside, or a sibling of the passed in element. 449 - * 450 - * Whitespace is normalized. Using multiple ids will 451 - * [join the referenced elements’ text content separated by a space](https://www.w3.org/TR/accname-1.1/#mapping_additional_nd_description). 452 - * 453 - * When a `string` argument is passed through, it will perform a whole 454 - * case-sensitive match to the description text. 455 - * 456 - * To perform a case-insensitive match, you can use a `RegExp` with the `/i` 457 - * modifier. 458 - * 459 - * To perform a partial match, you can pass a `RegExp` or use 460 - * `expect.stringContaining("partial string")`. 461 - * 462 - * @example 463 - * <button aria-label="Close" aria-describedby="description-close"> 464 - * X 465 - * </button> 466 - * <div id="description-close"> 467 - * Closing will discard any changes 468 - * </div> 469 - * 470 - * <button>Delete</button> 471 - * 472 - * const closeButton = getByRole('button', {name: 'Close'}) 473 - * 474 - * expect(closeButton).toHaveDescription('Closing will discard any changes') 475 - * expect(closeButton).toHaveDescription(/will discard/) // to partially match 476 - * expect(closeButton).toHaveDescription(expect.stringContaining('will discard')) // to partially match 477 - * expect(closeButton).toHaveDescription(/^closing/i) // to use case-insensitive match 478 - * expect(closeButton).not.toHaveDescription('Other description') 479 - * 480 - * const deleteButton = getByRole('button', {name: 'Delete'}) 481 - * expect(deleteButton).not.toHaveDescription() 482 - * expect(deleteButton).toHaveDescription('') // Missing or empty description always becomes a blank string 483 - * @see 484 - * [testing-library/jest-dom#tohavedescription](https://github.com/testing-library/jest-dom#tohavedescription) 485 - */ 486 - toHaveDescription(text?: string | RegExp | E): R 487 - /** 488 - * @description 489 - * This allows to assert that an element has the expected [accessible description](https://w3c.github.io/accname/). 490 - * 491 - * You can pass the exact string of the expected accessible description, or you can make a 492 - * partial match passing a regular expression, or by using either 493 - * [expect.stringContaining](https://jestjs.io/docs/en/expect.html#expectnotstringcontainingstring) 494 - * or [expect.stringMatching](https://jestjs.io/docs/en/expect.html#expectstringmatchingstring-regexp). 495 - * @example 496 - * <a data-testid="link" href="/" aria-label="Home page" title="A link to start over">Start</a> 497 - * <a data-testid="extra-link" href="/about" aria-label="About page">About</a> 498 - * <img src="avatar.jpg" data-testid="avatar" alt="User profile pic" /> 499 - * <img src="logo.jpg" data-testid="logo" alt="Company logo" aria-describedby="t1" /> 500 - * <span id="t1" role="presentation">The logo of Our Company</span> 501 - * 502 - * expect(getByTestId('link')).toHaveAccessibleDescription() 503 - * expect(getByTestId('link')).toHaveAccessibleDescription('A link to start over') 504 - * expect(getByTestId('link')).not.toHaveAccessibleDescription('Home page') 505 - * expect(getByTestId('extra-link')).not.toHaveAccessibleDescription() 506 - * expect(getByTestId('avatar')).not.toHaveAccessibleDescription() 507 - * expect(getByTestId('logo')).not.toHaveAccessibleDescription('Company logo') 508 - * expect(getByTestId('logo')).toHaveAccessibleDescription('The logo of Our Company') 509 - * @see 510 - * [testing-library/jest-dom#tohaveaccessibledescription](https://github.com/testing-library/jest-dom#tohaveaccessibledescription) 511 - */ 512 - toHaveAccessibleDescription(text?: string | RegExp | E): R 5 + export interface TestingLibraryMatchers<E, R> { 6 + /** 7 + * @description 8 + * Assert whether an element is present in the document or not. 9 + * @example 10 + * <svg data-testid="svg-element"></svg> 11 + * 12 + * await expect.element(page.getByTestId('svg-element')).toBeInTheDocument() 13 + * await expect.element(page.getByTestId('does-not-exist')).not.toBeInTheDocument() 14 + * @see https://vitest.dev/guide/browser/assertion-api#tobeinthedocument 15 + */ 16 + toBeInTheDocument(): R 17 + /** 18 + * @description 19 + * This allows you to check if an element is currently visible to the user. 20 + * 21 + * An element is visible if **all** the following conditions are met: 22 + * * it does not have its css property display set to none 23 + * * it does not have its css property visibility set to either hidden or collapse 24 + * * it does not have its css property opacity set to 0 25 + * * its parent element is also visible (and so on up to the top of the DOM tree) 26 + * * it does not have the hidden attribute 27 + * * if `<details />` it has the open attribute 28 + * @example 29 + * <div 30 + * data-testid="zero-opacity" 31 + * style="opacity: 0" 32 + * > 33 + * Zero Opacity 34 + * </div> 35 + * 36 + * <div data-testid="visible">Visible Example</div> 37 + * 38 + * await expect.element(page.getByTestId('zero-opacity')).not.toBeVisible() 39 + * await expect.element(page.getByTestId('visible')).toBeVisible() 40 + * @see https://vitest.dev/guide/browser/assertion-api#tobevisible 41 + */ 42 + toBeVisible(): R 43 + /** 44 + * @description 45 + * Assert whether an element has content or not. 46 + * @example 47 + * <span data-testid="not-empty"> 48 + * <span data-testid="empty"></span> 49 + * </span> 50 + * 51 + * await expect.element(page.getByTestId('empty')).toBeEmptyDOMElement() 52 + * await expect.element(page.getByTestId('not-empty')).not.toBeEmptyDOMElement() 53 + * @see https://vitest.dev/guide/browser/assertion-api#tobeemptydomelement 54 + */ 55 + toBeEmptyDOMElement(): R 56 + /** 57 + * @description 58 + * Allows you to check whether an element is disabled from the user's perspective. 59 + * 60 + * Matches if the element is a form control and the `disabled` attribute is specified on this element or the 61 + * element is a descendant of a form element with a `disabled` attribute. 62 + * @example 63 + * <button 64 + * data-testid="button" 65 + * type="submit" 66 + * disabled 67 + * > 68 + * submit 69 + * </button> 70 + * 71 + * await expect.element(page.getByTestId('button')).toBeDisabled() 72 + * @see https://vitest.dev/guide/browser/assertion-api#tobedisabled 73 + */ 74 + toBeDisabled(): R 75 + /** 76 + * @description 77 + * Allows you to check whether an element is not disabled from the user's perspective. 78 + * 79 + * Works like `not.toBeDisabled()`. 80 + * 81 + * Use this matcher to avoid double negation in your tests. 82 + * @example 83 + * <button 84 + * data-testid="button" 85 + * type="submit" 86 + * > 87 + * submit 88 + * </button> 89 + * 90 + * await expect.element(page.getByTestId('button')).toBeEnabled() 91 + * @see https://vitest.dev/guide/browser/assertion-api#tobeenabled 92 + */ 93 + toBeEnabled(): R 94 + /** 95 + * @description 96 + * Check if a form element, or the entire `form`, is currently invalid. 97 + * 98 + * An `input`, `select`, `textarea`, or `form` element is invalid if it has an `aria-invalid` attribute with no 99 + * value or a value of "true", or if the result of `checkValidity()` is false. 100 + * @example 101 + * <input data-testid="no-aria-invalid" /> 102 + * 103 + * <form data-testid="invalid-form"> 104 + * <input required /> 105 + * </form> 106 + * 107 + * await expect(page.getByTestId('no-aria-invalid')).not.toBeInvalid() 108 + * await expect(page.getByTestId('invalid-form')).toBeInvalid() 109 + * @see https://vitest.dev/guide/browser/assertion-api#tobeinvalid 110 + */ 111 + toBeInvalid(): R 112 + /** 113 + * @description 114 + * This allows you to check if a form element is currently required. 115 + * 116 + * An element is required if it is having a `required` or `aria-required="true"` attribute. 117 + * @example 118 + * <input data-testid="required-input" required /> 119 + * <div 120 + * data-testid="supported-role" 121 + * role="tree" 122 + * required /> 123 + * 124 + * await expect.element(page.getByTestId('required-input')).toBeRequired() 125 + * await expect.element(page.getByTestId('supported-role')).not.toBeRequired() 126 + * @see https://vitest.dev/guide/browser/assertion-api#toberequired 127 + */ 128 + toBeRequired(): R 129 + /** 130 + * @description 131 + * Allows you to check if a form element is currently required. 132 + * 133 + * An `input`, `select`, `textarea`, or `form` element is invalid if it has an `aria-invalid` attribute with no 134 + * value or a value of "false", or if the result of `checkValidity()` is true. 135 + * @example 136 + * <input data-testid="aria-invalid" aria-invalid /> 137 + * 138 + * <form data-testid="valid-form"> 139 + * <input /> 140 + * </form> 141 + * 142 + * await expect.element(page.getByTestId('no-aria-invalid')).not.toBeValid() 143 + * await expect.element(page.getByTestId('invalid-form')).toBeInvalid() 144 + * @see https://vitest.dev/guide/browser/assertion-api#tobevalid 145 + */ 146 + toBeValid(): R 147 + /** 148 + * @description 149 + * Allows you to assert whether an element contains another element as a descendant or not. 150 + * @example 151 + * <span data-testid="ancestor"> 152 + * <span data-testid="descendant"></span> 153 + * </span> 154 + * 155 + * const ancestor = page.getByTestId('ancestor') 156 + * const descendant = page.getByTestId('descendant') 157 + * const nonExistentElement = page.getByTestId('does-not-exist') 158 + * await expect.element(ancestor).toContainElement(descendant) 159 + * await expect.element(descendant).not.toContainElement(ancestor) 160 + * await expect.element(ancestor).not.toContainElement(nonExistentElement) 161 + * @see https://vitest.dev/guide/browser/assertion-api#tocontainelement 162 + */ 163 + toContainElement(element: HTMLElement | SVGElement | null): R 164 + /** 165 + * @description 166 + * Assert whether a string representing a HTML element is contained in another element. 167 + * @example 168 + * <span data-testid="parent"><span data-testid="child"></span></span> 169 + * 170 + * const parent = page.getByTestId('parent') 171 + * await expect.element(parent).toContainHTML('<span data-testid="child"></span>') 172 + * @see https://vitest.dev/guide/browser/assertion-api#tocontainhtml 173 + */ 174 + toContainHTML(htmlText: string): R 175 + /** 176 + * @description 177 + * Allows you to check if a given element has an attribute or not. 178 + * 179 + * You can also optionally check that the attribute has a specific expected value or partial match using 180 + * [expect.stringContaining](https://jestjs.io/docs/en/expect.html#expectnotstringcontainingstring) or 181 + * [expect.stringMatching](https://jestjs.io/docs/en/expect.html#expectstringmatchingstring-regexp). 182 + * @example 183 + * <button 184 + * data-testid="ok-button" 185 + * type="submit" 186 + * disabled 187 + * > 188 + * ok 189 + * </button> 190 + * 191 + * await expect.element(button).toHaveAttribute('disabled') 192 + * await expect.element(button).toHaveAttribute('type', 'submit') 193 + * await expect.element(button).not.toHaveAttribute('type', 'button') 194 + * @see https://vitest.dev/guide/browser/assertion-api#tohaveattribute 195 + */ 196 + toHaveAttribute(attr: string, value?: unknown): R 197 + /** 198 + * @description 199 + * Check whether the given element has certain classes within its `class` attribute. 200 + * 201 + * You must provide at least one class, unless you are asserting that an element does not have any classes. 202 + * @example 203 + * <button 204 + * data-testid="delete-button" 205 + * class="btn xs btn-danger" 206 + * > 207 + * delete item 208 + * </button> 209 + * 210 + * <div data-testid="no-classes">no classes</div> 211 + * 212 + * const deleteButton = page.getByTestId('delete-button') 213 + * const noClasses = page.getByTestId('no-classes') 214 + * await expect.element(deleteButton).toHaveClass('btn') 215 + * await expect.element(deleteButton).toHaveClass('btn-danger xs') 216 + * await expect.element(deleteButton).toHaveClass(/danger/, 'xs') 217 + * await expect.element(deleteButton).toHaveClass('btn xs btn-danger', {exact: true}) 218 + * await expect.element(deleteButton).not.toHaveClass('btn xs btn-danger', {exact: true}) 219 + * await expect.element(noClasses).not.toHaveClass() 220 + * @see https://vitest.dev/guide/browser/assertion-api#tohaveclass 221 + */ 222 + toHaveClass(...classNames: 223 + | (string | RegExp)[] 224 + | [string, options?: {exact: boolean}] 225 + | [string, string, options?: {exact: boolean}] 226 + | [string, string, string, options?: {exact: boolean}] 227 + | [string, string, string, string, options?: {exact: boolean}] 228 + | [string, string, string, string, string, options?: {exact: boolean}] 229 + | [string, string, string, string, string, string, options?: {exact: boolean}] 230 + | [string, string, string, string, string, string, string, options?: {exact: boolean}] 231 + | [string, string, string, string, string, string, string, string, options?: {exact: boolean}] 232 + | [string, string, string, string, string, string, string, string, string, options?: {exact: boolean}] 233 + ): R 234 + /** 235 + * @description 236 + * This allows you to check whether the given form element has the specified displayed value (the one the 237 + * end user will see). It accepts <input>, <select> and <textarea> elements with the exception of <input type="checkbox"> 238 + * and <input type="radio">, which can be meaningfully matched only using toBeChecked or toHaveFormValues. 239 + * @example 240 + * <label for="input-example">First name</label> 241 + * <input type="text" id="input-example" value="Luca" /> 242 + * 243 + * <label for="textarea-example">Description</label> 244 + * <textarea id="textarea-example">An example description here.</textarea> 245 + * 246 + * <label for="single-select-example">Fruit</label> 247 + * <select id="single-select-example"> 248 + * <option value="">Select a fruit...</option> 249 + * <option value="banana">Banana</option> 250 + * <option value="ananas">Ananas</option> 251 + * <option value="avocado">Avocado</option> 252 + * </select> 253 + * 254 + * <label for="multiple-select-example">Fruits</label> 255 + * <select id="multiple-select-example" multiple> 256 + * <option value="">Select a fruit...</option> 257 + * <option value="banana" selected>Banana</option> 258 + * <option value="ananas">Ananas</option> 259 + * <option value="avocado" selected>Avocado</option> 260 + * </select> 261 + * 262 + * const input = page.getByLabelText('First name') 263 + * const textarea = page.getByLabelText('Description') 264 + * const selectSingle = page.getByLabelText('Fruit') 265 + * const selectMultiple = page.getByLabelText('Fruits') 266 + * 267 + * await expect.element(input).toHaveDisplayValue('Luca') 268 + * await expect.element(textarea).toHaveDisplayValue('An example description here.') 269 + * await expect.element(selectSingle).toHaveDisplayValue('Select a fruit...') 270 + * await expect.element(selectMultiple).toHaveDisplayValue(['Banana', 'Avocado']) 271 + * 272 + * @see https://vitest.dev/guide/browser/assertion-api#tohavedisplayvalue 273 + */ 274 + toHaveDisplayValue(value: string | number | RegExp | Array<string | RegExp | number>): R 275 + /** 276 + * @description 277 + * Assert whether an element has focus or not. 278 + * @example 279 + * <div> 280 + * <input type="text" data-testid="element-to-focus" /> 281 + * </div> 282 + * 283 + * const input = page.getByTestId('element-to-focus') 284 + * input.element().focus() 285 + * await expect.element(input).toHaveFocus() 286 + * input.element().blur() 287 + * await expect.element(input).not.toHaveFocus() 288 + * @see https://vitest.dev/guide/browser/assertion-api#tohavefocus 289 + */ 290 + toHaveFocus(): R 291 + /** 292 + * @description 293 + * Check if a form or fieldset contains form controls for each given name, and having the specified value. 294 + * 295 + * Can only be invoked on a form or fieldset element. 296 + * @example 297 + * <form data-testid="login-form"> 298 + * <input type="text" name="username" value="jane.doe" /> 299 + * <input type="password" name="password" value="123" /> 300 + * <input type="checkbox" name="rememberMe" checked /> 301 + * <button type="submit">Sign in</button> 302 + * </form> 303 + * 304 + * await expect.element(page.getByTestId('login-form')).toHaveFormValues({ 305 + * username: 'jane.doe', 306 + * rememberMe: true, 307 + * }) 308 + * @see https://vitest.dev/guide/browser/assertion-api#tohaveformvalues 309 + */ 310 + toHaveFormValues(expectedValues: Record<string, unknown>): R 311 + /** 312 + * @description 313 + * Check if an element has specific css properties with specific values applied. 314 + * 315 + * Only matches if the element has *all* the expected properties applied, not just some of them. 316 + * @example 317 + * <button 318 + * data-testid="submit-button" 319 + * style="background-color: green; display: none" 320 + * > 321 + * submit 322 + * </button> 323 + * 324 + * const button = page.getByTestId('submit-button') 325 + * await expect.element(button).toHaveStyle('background-color: green') 326 + * await expect.element(button).toHaveStyle({ 327 + * 'background-color': 'green', 328 + * display: 'none' 329 + * }) 330 + * @see https://vitest.dev/guide/browser/assertion-api#tohavestyle 331 + */ 332 + toHaveStyle(css: string | Partial<CSSStyleDeclaration>): R 333 + /** 334 + * @description 335 + * Check whether the given element has a text content or not. 336 + * 337 + * When a string argument is passed through, it will perform a partial case-sensitive match to the element 338 + * content. 339 + * 340 + * To perform a case-insensitive match, you can use a RegExp with the `/i` modifier. 341 + * 342 + * If you want to match the whole content, you can use a RegExp to do it. 343 + * @example 344 + * <span data-testid="text-content">Text Content</span> 345 + * 346 + * const element = page.getByTestId('text-content') 347 + * await expect.element(element).toHaveTextContent('Content') 348 + * // to match the whole content 349 + * await expect.element(element).toHaveTextContent(/^Text Content$/) 350 + * // to use case-insensitive match 351 + * await expect.element(element).toHaveTextContent(/content$/i) 352 + * await expect.element(element).not.toHaveTextContent('content') 353 + * @see https://vitest.dev/guide/browser/assertion-api#tohavetextcontent 354 + */ 355 + toHaveTextContent( 356 + text: string | number | RegExp, 357 + options?: {normalizeWhitespace: boolean}, 358 + ): R 359 + /** 360 + * @description 361 + * Check whether the given form element has the specified value. 362 + * 363 + * Accepts `<input>`, `<select>`, and `<textarea>` elements with the exception of `<input type="checkbox">` and 364 + * `<input type="radiobox">`, which can be matched only using 365 + * [toBeChecked](https://vitest.dev/guide/browser/assertion-api#tobechecked) or 366 + * [toHaveFormValues](https://vitest.dev/guide/browser/assertion-api#tohaveformvalues). 367 + * @example 368 + * <input 369 + * type="number" 370 + * value="5" 371 + * data-testid="input-number" /> 372 + * 373 + * const numberInput = page.getByTestId('input-number') 374 + * await expect.element(numberInput).toHaveValue(5) 375 + * @see https://vitest.dev/guide/browser/assertion-api#tohavevalue 376 + */ 377 + toHaveValue(value?: string | string[] | number | null): R 378 + /** 379 + * @description 380 + * Assert whether the given element is checked. 381 + * 382 + * It accepts an `input` of type `checkbox` or `radio` and elements with a `role` of `radio` with a valid 383 + * `aria-checked` attribute of "true" or "false". 384 + * @example 385 + * <input 386 + * type="checkbox" 387 + * checked 388 + * data-testid="input-checkbox" /> 389 + * <input 390 + * type="radio" 391 + * value="foo" 392 + * data-testid="input-radio" /> 393 + * 394 + * const inputCheckbox = page.getByTestId('input-checkbox') 395 + * const inputRadio = page.getByTestId('input-radio') 396 + * await expect.element(inputCheckbox).toBeChecked() 397 + * await expect.element(inputRadio).not.toBeChecked() 398 + * @see https://vitest.dev/guide/browser/assertion-api#tobechecked 399 + */ 400 + toBeChecked(): R 401 + /** 402 + * @description 403 + * This allows to assert that an element has the expected [accessible description](https://w3c.github.io/accname/). 404 + * 405 + * You can pass the exact string of the expected accessible description, or you can make a 406 + * partial match passing a regular expression, or by using either 407 + * [expect.stringContaining](https://jestjs.io/docs/en/expect.html#expectnotstringcontainingstring) 408 + * or [expect.stringMatching](https://jestjs.io/docs/en/expect.html#expectstringmatchingstring-regexp). 409 + * @example 410 + * <a data-testid="link" href="/" aria-label="Home page" title="A link to start over">Start</a> 411 + * <a data-testid="extra-link" href="/about" aria-label="About page">About</a> 412 + * <img src="avatar.jpg" data-testid="avatar" alt="User profile pic" /> 413 + * <img src="logo.jpg" data-testid="logo" alt="Company logo" aria-describedby="t1" /> 414 + * <span id="t1" role="presentation">The logo of Our Company</span> 415 + * 416 + * await expect.element(page.getByTestId('link')).toHaveAccessibleDescription() 417 + * await expect.element(page.getByTestId('link')).toHaveAccessibleDescription('A link to start over') 418 + * await expect.element(page.getByTestId('link')).not.toHaveAccessibleDescription('Home page') 419 + * await expect.element(page.getByTestId('extra-link')).not.toHaveAccessibleDescription() 420 + * await expect.element(page.getByTestId('avatar')).not.toHaveAccessibleDescription() 421 + * await expect.element(page.getByTestId('logo')).not.toHaveAccessibleDescription('Company logo') 422 + * await expect.element(page.getByTestId('logo')).toHaveAccessibleDescription('The logo of Our Company') 423 + * @see https://vitest.dev/guide/browser/assertion-api#tohaveaccessibledescription 424 + */ 425 + toHaveAccessibleDescription(text?: string | RegExp | E): R 513 426 514 - /** 515 - * @description 516 - * This allows you to assert that an element has the expected 517 - * [accessible error message](https://w3c.github.io/aria/#aria-errormessage). 518 - * 519 - * You can pass the exact string of the expected accessible error message. 520 - * Alternatively, you can perform a partial match by passing a regular expression 521 - * or by using either 522 - * [expect.stringContaining](https://jestjs.io/docs/en/expect.html#expectnotstringcontainingstring) 523 - * or [expect.stringMatching](https://jestjs.io/docs/en/expect.html#expectstringmatchingstring-regexp). 524 - * 525 - * @example 526 - * <input aria-label="Has Error" aria-invalid="true" aria-errormessage="error-message" /> 527 - * <div id="error-message" role="alert">This field is invalid</div> 528 - * 529 - * <input aria-label="No Error Attributes" /> 530 - * <input aria-label="Not Invalid" aria-invalid="false" aria-errormessage="error-message" /> 531 - * 532 - * // Inputs with Valid Error Messages 533 - * expect(getByRole('textbox', {name: 'Has Error'})).toHaveAccessibleErrorMessage() 534 - * expect(getByRole('textbox', {name: 'Has Error'})).toHaveAccessibleErrorMessage('This field is invalid') 535 - * expect(getByRole('textbox', {name: 'Has Error'})).toHaveAccessibleErrorMessage(/invalid/i) 536 - * expect( 537 - * getByRole('textbox', {name: 'Has Error'}), 538 - * ).not.toHaveAccessibleErrorMessage('This field is absolutely correct!') 539 - * 540 - * // Inputs without Valid Error Messages 541 - * expect( 542 - * getByRole('textbox', {name: 'No Error Attributes'}), 543 - * ).not.toHaveAccessibleErrorMessage() 544 - * 545 - * expect( 546 - * getByRole('textbox', {name: 'Not Invalid'}), 547 - * ).not.toHaveAccessibleErrorMessage() 548 - * 549 - * @see 550 - * [testing-library/jest-dom#tohaveaccessibleerrormessage](https://github.com/testing-library/jest-dom#tohaveaccessibleerrormessage) 551 - */ 552 - toHaveAccessibleErrorMessage(text?: string | RegExp | E): R 427 + /** 428 + * @description 429 + * This allows you to assert that an element has the expected 430 + * [accessible error message](https://w3c.github.io/aria/#aria-errormessage). 431 + * 432 + * You can pass the exact string of the expected accessible error message. 433 + * Alternatively, you can perform a partial match by passing a regular expression 434 + * or by using either 435 + * [expect.stringContaining](https://jestjs.io/docs/en/expect.html#expectnotstringcontainingstring) 436 + * or [expect.stringMatching](https://jestjs.io/docs/en/expect.html#expectstringmatchingstring-regexp). 437 + * 438 + * @example 439 + * <input aria-label="Has Error" aria-invalid="true" aria-errormessage="error-message" /> 440 + * <div id="error-message" role="alert">This field is invalid</div> 441 + * 442 + * <input aria-label="No Error Attributes" /> 443 + * <input aria-label="Not Invalid" aria-invalid="false" aria-errormessage="error-message" /> 444 + * 445 + * // Inputs with Valid Error Messages 446 + * await expect.element(page.getByRole('textbox', {name: 'Has Error'})).toHaveAccessibleErrorMessage() 447 + * await expect.element(page.getByRole('textbox', {name: 'Has Error'})).toHaveAccessibleErrorMessage('This field is invalid') 448 + * await expect.element(page.getByRole('textbox', {name: 'Has Error'})).toHaveAccessibleErrorMessage(/invalid/i) 449 + * await expect.element( 450 + * page.getByRole('textbox', {name: 'Has Error'}), 451 + * ).not.toHaveAccessibleErrorMessage('This field is absolutely correct!') 452 + * 453 + * // Inputs without Valid Error Messages 454 + * await expect.element( 455 + * page.getByRole('textbox', {name: 'No Error Attributes'}), 456 + * ).not.toHaveAccessibleErrorMessage() 457 + * 458 + * await expect.element( 459 + * page.getByRole('textbox', {name: 'Not Invalid'}), 460 + * ).not.toHaveAccessibleErrorMessage() 461 + * 462 + * @see https://vitest.dev/guide/browser/assertion-api#tohaveaccessibleerrormessage 463 + */ 464 + toHaveAccessibleErrorMessage(text?: string | RegExp | E): R 553 465 554 - /** 555 - * @description 556 - * This allows to assert that an element has the expected [accessible name](https://w3c.github.io/accname/). 557 - * It is useful, for instance, to assert that form elements and buttons are properly labelled. 558 - * 559 - * You can pass the exact string of the expected accessible name, or you can make a 560 - * partial match passing a regular expression, or by using either 561 - * [expect.stringContaining](https://jestjs.io/docs/en/expect.html#expectnotstringcontainingstring) 562 - * or [expect.stringMatching](https://jestjs.io/docs/en/expect.html#expectstringmatchingstring-regexp). 563 - * @example 564 - * <img data-testid="img-alt" src="" alt="Test alt" /> 565 - * <img data-testid="img-empty-alt" src="" alt="" /> 566 - * <svg data-testid="svg-title"><title>Test title</title></svg> 567 - * <button data-testid="button-img-alt"><img src="" alt="Test" /></button> 568 - * <p><img data-testid="img-paragraph" src="" alt="" /> Test content</p> 569 - * <button data-testid="svg-button"><svg><title>Test</title></svg></p> 570 - * <div><svg data-testid="svg-without-title"></svg></div> 571 - * <input data-testid="input-title" title="test" /> 572 - * 573 - * expect(getByTestId('img-alt')).toHaveAccessibleName('Test alt') 574 - * expect(getByTestId('img-empty-alt')).not.toHaveAccessibleName() 575 - * expect(getByTestId('svg-title')).toHaveAccessibleName('Test title') 576 - * expect(getByTestId('button-img-alt')).toHaveAccessibleName() 577 - * expect(getByTestId('img-paragraph')).not.toHaveAccessibleName() 578 - * expect(getByTestId('svg-button')).toHaveAccessibleName() 579 - * expect(getByTestId('svg-without-title')).not.toHaveAccessibleName() 580 - * expect(getByTestId('input-title')).toHaveAccessibleName() 581 - * @see 582 - * [testing-library/jest-dom#tohaveaccessiblename](https://github.com/testing-library/jest-dom#tohaveaccessiblename) 583 - */ 584 - toHaveAccessibleName(text?: string | RegExp | E): R 585 - /** 586 - * @description 587 - * This allows you to assert that an element has the expected 588 - * [role](https://www.w3.org/TR/html-aria/#docconformance). 589 - * 590 - * This is useful in cases where you already have access to an element via 591 - * some query other than the role itself, and want to make additional 592 - * assertions regarding its accessibility. 593 - * 594 - * The role can match either an explicit role (via the `role` attribute), or 595 - * an implicit one via the [implicit ARIA 596 - * semantics](https://www.w3.org/TR/html-aria/). 597 - * 598 - * Note: roles are matched literally by string equality, without inheriting 599 - * from the ARIA role hierarchy. As a result, querying a superclass role 600 - * like 'checkbox' will not include elements with a subclass role like 601 - * 'switch'. 602 - * 603 - * @example 604 - * <button data-testid="button">Continue</button> 605 - * <div role="button" data-testid="button-explicit">Continue</button> 606 - * <button role="switch button" data-testid="button-explicit-multiple">Continue</button> 607 - * <a href="/about" data-testid="link">About</a> 608 - * <a data-testid="link-invalid">Invalid link<a/> 609 - * 610 - * expect(getByTestId('button')).toHaveRole('button') 611 - * expect(getByTestId('button-explicit')).toHaveRole('button') 612 - * expect(getByTestId('button-explicit-multiple')).toHaveRole('button') 613 - * expect(getByTestId('button-explicit-multiple')).toHaveRole('switch') 614 - * expect(getByTestId('link')).toHaveRole('link') 615 - * expect(getByTestId('link-invalid')).not.toHaveRole('link') 616 - * expect(getByTestId('link-invalid')).toHaveRole('generic') 617 - * 618 - * @see 619 - * [testing-library/jest-dom#tohaverole](https://github.com/testing-library/jest-dom#tohaverole) 620 - */ 621 - toHaveRole( 622 - // Get autocomplete for ARIARole union types, while still supporting another string 623 - // Ref: https://github.com/microsoft/TypeScript/issues/29729#issuecomment-567871939 624 - role: ARIARole | (string & {}), 625 - ): R 626 - /** 627 - * @description 628 - * This allows you to check whether the given element is partially checked. 629 - * It accepts an input of type checkbox and elements with a role of checkbox 630 - * with a aria-checked="mixed", or input of type checkbox with indeterminate 631 - * set to true 632 - * 633 - * @example 634 - * <input type="checkbox" aria-checked="mixed" data-testid="aria-checkbox-mixed" /> 635 - * <input type="checkbox" checked data-testid="input-checkbox-checked" /> 636 - * <input type="checkbox" data-testid="input-checkbox-unchecked" /> 637 - * <div role="checkbox" aria-checked="true" data-testid="aria-checkbox-checked" /> 638 - * <div 639 - * role="checkbox" 640 - * aria-checked="false" 641 - * data-testid="aria-checkbox-unchecked" 642 - * /> 643 - * <input type="checkbox" data-testid="input-checkbox-indeterminate" /> 644 - * 645 - * const ariaCheckboxMixed = getByTestId('aria-checkbox-mixed') 646 - * const inputCheckboxChecked = getByTestId('input-checkbox-checked') 647 - * const inputCheckboxUnchecked = getByTestId('input-checkbox-unchecked') 648 - * const ariaCheckboxChecked = getByTestId('aria-checkbox-checked') 649 - * const ariaCheckboxUnchecked = getByTestId('aria-checkbox-unchecked') 650 - * const inputCheckboxIndeterminate = getByTestId('input-checkbox-indeterminate') 651 - * 652 - * expect(ariaCheckboxMixed).toBePartiallyChecked() 653 - * expect(inputCheckboxChecked).not.toBePartiallyChecked() 654 - * expect(inputCheckboxUnchecked).not.toBePartiallyChecked() 655 - * expect(ariaCheckboxChecked).not.toBePartiallyChecked() 656 - * expect(ariaCheckboxUnchecked).not.toBePartiallyChecked() 657 - * 658 - * inputCheckboxIndeterminate.indeterminate = true 659 - * expect(inputCheckboxIndeterminate).toBePartiallyChecked() 660 - * @see 661 - * [testing-library/jest-dom#tobepartiallychecked](https://github.com/testing-library/jest-dom#tobepartiallychecked) 662 - */ 663 - toBePartiallyChecked(): R 664 - /** 665 - * @deprecated 666 - * since v5.17.0 667 - * 668 - * @description 669 - * Check whether the given element has an [ARIA error message](https://www.w3.org/TR/wai-aria/#aria-errormessage) or not. 670 - * 671 - * Use the `aria-errormessage` attribute to reference another element that contains 672 - * custom error message text. Multiple ids is **NOT** allowed. Authors MUST use 673 - * `aria-invalid` in conjunction with `aria-errormessage`. Learn more from the 674 - * [`aria-errormessage` spec](https://www.w3.org/TR/wai-aria/#aria-errormessage). 675 - * 676 - * Whitespace is normalized. 677 - * 678 - * When a `string` argument is passed through, it will perform a whole 679 - * case-sensitive match to the error message text. 680 - * 681 - * To perform a case-insensitive match, you can use a `RegExp` with the `/i` 682 - * modifier. 683 - * 684 - * To perform a partial match, you can pass a `RegExp` or use 685 - * expect.stringContaining("partial string")`. 686 - * 687 - * @example 688 - * <label for="startTime"> Please enter a start time for the meeting: </label> 689 - * <input id="startTime" type="text" aria-errormessage="msgID" aria-invalid="true" value="11:30 PM" /> 690 - * <span id="msgID" aria-live="assertive" style="visibility:visible"> 691 - * Invalid time: the time must be between 9:00 AM and 5:00 PM" 692 - * </span> 693 - * 694 - * 695 - * const timeInput = getByLabel('startTime') 696 - * 697 - * expect(timeInput).toHaveErrorMessage( 698 - * 'Invalid time: the time must be between 9:00 AM and 5:00 PM', 699 - * ) 700 - * expect(timeInput).toHaveErrorMessage(/invalid time/i) // to partially match 701 - * expect(timeInput).toHaveErrorMessage(expect.stringContaining('Invalid time')) // to partially match 702 - * expect(timeInput).not.toHaveErrorMessage('Pikachu!') 703 - * @see 704 - * [testing-library/jest-dom#tohaveerrormessage](https://github.com/testing-library/jest-dom#tohaveerrormessage) 705 - */ 706 - toHaveErrorMessage(text?: string | RegExp | E): R 707 - } 466 + /** 467 + * @description 468 + * This allows to assert that an element has the expected [accessible name](https://w3c.github.io/accname/). 469 + * It is useful, for instance, to assert that form elements and buttons are properly labelled. 470 + * 471 + * You can pass the exact string of the expected accessible name, or you can make a 472 + * partial match passing a regular expression, or by using either 473 + * [expect.stringContaining](https://jestjs.io/docs/en/expect.html#expectnotstringcontainingstring) 474 + * or [expect.stringMatching](https://jestjs.io/docs/en/expect.html#expectstringmatchingstring-regexp). 475 + * @example 476 + * <img data-testid="img-alt" src="" alt="Test alt" /> 477 + * <img data-testid="img-empty-alt" src="" alt="" /> 478 + * <svg data-testid="svg-title"><title>Test title</title></svg> 479 + * <button data-testid="button-img-alt"><img src="" alt="Test" /></button> 480 + * <p><img data-testid="img-paragraph" src="" alt="" /> Test content</p> 481 + * <button data-testid="svg-button"><svg><title>Test</title></svg></p> 482 + * <div><svg data-testid="svg-without-title"></svg></div> 483 + * <input data-testid="input-title" title="test" /> 484 + * 485 + * await expect.element(page.getByTestId('img-alt')).toHaveAccessibleName('Test alt') 486 + * await expect.element(page.getByTestId('img-empty-alt')).not.toHaveAccessibleName() 487 + * await expect.element(page.getByTestId('svg-title')).toHaveAccessibleName('Test title') 488 + * await expect.element(page.getByTestId('button-img-alt')).toHaveAccessibleName() 489 + * await expect.element(page.getByTestId('img-paragraph')).not.toHaveAccessibleName() 490 + * await expect.element(page.getByTestId('svg-button')).toHaveAccessibleName() 491 + * await expect.element(page.getByTestId('svg-without-title')).not.toHaveAccessibleName() 492 + * await expect.element(page.getByTestId('input-title')).toHaveAccessibleName() 493 + * @see https://vitest.dev/guide/browser/assertion-api#tohaveaccessiblename 494 + */ 495 + toHaveAccessibleName(text?: string | RegExp | E): R 496 + /** 497 + * @description 498 + * This allows you to assert that an element has the expected 499 + * [role](https://www.w3.org/TR/html-aria/#docconformance). 500 + * 501 + * This is useful in cases where you already have access to an element via 502 + * some query other than the role itself, and want to make additional 503 + * assertions regarding its accessibility. 504 + * 505 + * The role can match either an explicit role (via the `role` attribute), or 506 + * an implicit one via the [implicit ARIA 507 + * semantics](https://www.w3.org/TR/html-aria/). 508 + * 509 + * Note: roles are matched literally by string equality, without inheriting 510 + * from the ARIA role hierarchy. As a result, querying a superclass role 511 + * like 'checkbox' will not include elements with a subclass role like 512 + * 'switch'. 513 + * 514 + * @example 515 + * <button data-testid="button">Continue</button> 516 + * <div role="button" data-testid="button-explicit">Continue</button> 517 + * <button role="switch button" data-testid="button-explicit-multiple">Continue</button> 518 + * <a href="/about" data-testid="link">About</a> 519 + * <a data-testid="link-invalid">Invalid link<a/> 520 + * 521 + * await expect.element(page.getByTestId('button')).toHaveRole('button') 522 + * await expect.element(page.getByTestId('button-explicit')).toHaveRole('button') 523 + * await expect.element(page.getByTestId('button-explicit-multiple')).toHaveRole('button') 524 + * await expect.element(page.getByTestId('button-explicit-multiple')).toHaveRole('switch') 525 + * await expect.element(page.getByTestId('link')).toHaveRole('link') 526 + * await expect.element(page.getByTestId('link-invalid')).not.toHaveRole('link') 527 + * await expect.element(page.getByTestId('link-invalid')).toHaveRole('generic') 528 + * 529 + * @see https://vitest.dev/guide/browser/assertion-api#tohaverole 530 + */ 531 + toHaveRole( 532 + // Get autocomplete for ARIARole union types, while still supporting another string 533 + // Ref: https://github.com/microsoft/TypeScript/issues/29729#issuecomment-567871939 534 + role: ARIARole | (string & {}), 535 + ): R 536 + /** 537 + * @description 538 + * This allows you to check whether the given element is partially checked. 539 + * It accepts an input of type checkbox and elements with a role of checkbox 540 + * with a aria-checked="mixed", or input of type checkbox with indeterminate 541 + * set to true 542 + * 543 + * @example 544 + * <input type="checkbox" aria-checked="mixed" data-testid="aria-checkbox-mixed" /> 545 + * <input type="checkbox" checked data-testid="input-checkbox-checked" /> 546 + * <input type="checkbox" data-testid="input-checkbox-unchecked" /> 547 + * <div role="checkbox" aria-checked="true" data-testid="aria-checkbox-checked" /> 548 + * <div 549 + * role="checkbox" 550 + * aria-checked="false" 551 + * data-testid="aria-checkbox-unchecked" 552 + * /> 553 + * <input type="checkbox" data-testid="input-checkbox-indeterminate" /> 554 + * 555 + * const ariaCheckboxMixed = getByTestId('aria-checkbox-mixed') 556 + * const inputCheckboxChecked = getByTestId('input-checkbox-checked') 557 + * const inputCheckboxUnchecked = getByTestId('input-checkbox-unchecked') 558 + * const ariaCheckboxChecked = getByTestId('aria-checkbox-checked') 559 + * const ariaCheckboxUnchecked = getByTestId('aria-checkbox-unchecked') 560 + * const inputCheckboxIndeterminate = getByTestId('input-checkbox-indeterminate') 561 + * 562 + * await expect.element(ariaCheckboxMixed).toBePartiallyChecked() 563 + * await expect.element(inputCheckboxChecked).not.toBePartiallyChecked() 564 + * await expect.element(inputCheckboxUnchecked).not.toBePartiallyChecked() 565 + * await expect.element(ariaCheckboxChecked).not.toBePartiallyChecked() 566 + * await expect.element(ariaCheckboxUnchecked).not.toBePartiallyChecked() 567 + * 568 + * inputCheckboxIndeterminate.indeterminate = true 569 + * await expect.element(inputCheckboxIndeterminate).toBePartiallyChecked() 570 + * @see https://vitest.dev/guide/browser/assertion-api#tobepartiallychecked 571 + */ 572 + toBePartiallyChecked(): R 573 + /** 574 + * @description 575 + * This allows to assert that an element has a 576 + * [text selection](https://developer.mozilla.org/en-US/docs/Web/API/Selection). 577 + * 578 + * This is useful to check if text or part of the text is selected within an 579 + * element. The element can be either an input of type text, a textarea, or any 580 + * other element that contains text, such as a paragraph, span, div etc. 581 + * 582 + * NOTE: the expected selection is a string, it does not allow to check for 583 + * selection range indeces. 584 + * 585 + * @example 586 + * <div> 587 + * <input type="text" value="text selected text" data-testid="text" /> 588 + * <textarea data-testid="textarea">text selected text</textarea> 589 + * <p data-testid="prev">prev</p> 590 + * <p data-testid="parent">text <span data-testid="child">selected</span> text</p> 591 + * <p data-testid="next">next</p> 592 + * </div> 593 + * 594 + * page.getByTestId('text').element().setSelectionRange(5, 13) 595 + * await expect.element(page.getByTestId('text')).toHaveSelection('selected') 596 + * 597 + * page.getByTestId('textarea').element().setSelectionRange(0, 5) 598 + * await expect.element('textarea').toHaveSelection('text ') 599 + * 600 + * const selection = document.getSelection() 601 + * const range = document.createRange() 602 + * selection.removeAllRanges() 603 + * selection.empty() 604 + * selection.addRange(range) 605 + * 606 + * // selection of child applies to the parent as well 607 + * range.selectNodeContents(page.getByTestId('child').element()) 608 + * await expect.element(page.getByTestId('child')).toHaveSelection('selected') 609 + * await expect.element(page.getByTestId('parent')).toHaveSelection('selected') 610 + * 611 + * // selection that applies from prev all, parent text before child, and part child. 612 + * range.setStart(page.getByTestId('prev').element(), 0) 613 + * range.setEnd(page.getByTestId('child').element().childNodes[0], 3) 614 + * await expect.element(page.queryByTestId('prev')).toHaveSelection('prev') 615 + * await expect.element(page.queryByTestId('child')).toHaveSelection('sel') 616 + * await expect.element(page.queryByTestId('parent')).toHaveSelection('text sel') 617 + * await expect.element(page.queryByTestId('next')).not.toHaveSelection() 618 + * 619 + * // selection that applies from part child, parent text after child and part next. 620 + * range.setStart(page.getByTestId('child').element().childNodes[0], 3) 621 + * range.setEnd(page.getByTestId('next').element().childNodes[0], 2) 622 + * await expect.element(page.queryByTestId('child')).toHaveSelection('ected') 623 + * await expect.element(page.queryByTestId('parent')).toHaveSelection('ected text') 624 + * await expect.element(page.queryByTestId('prev')).not.toHaveSelection() 625 + * await expect.element(page.queryByTestId('next')).toHaveSelection('ne') 626 + * 627 + * @see https://vitest.dev/guide/browser/assertion-api#tohaveselection 628 + */ 629 + toHaveSelection(selection?: string): R 708 630 } 709 - 710 - // Needs to extend Record<string, any> to be accepted by expect.extend() 711 - // as it requires a string index signature. 712 - declare const matchers: matchers.TestingLibraryMatchers<any, void> & 713 - Record<string, any> 714 - 715 - declare namespace matchers$1 { 716 - export { matchers as default }; 717 - } 718 - 719 - export { matchers$1 as default };
+4 -4
packages/browser/matchers.d.ts
··· 1 1 import type { Locator } from '@vitest/browser/context' 2 - import type jsdomMatchers from './jest-dom.js' 2 + import type { TestingLibraryMatchers } from './jest-dom.js' 3 3 import type { Assertion, ExpectPollOptions } from 'vitest' 4 4 5 5 declare module 'vitest' { 6 - interface JestAssertion<T = any> extends jsdomMatchers.default.TestingLibraryMatchers<void, T> {} 7 - interface AsymmetricMatchersContaining extends jsdomMatchers.default.TestingLibraryMatchers<void, void> {} 6 + interface JestAssertion<T = any> extends TestingLibraryMatchers<void, T> {} 7 + interface AsymmetricMatchersContaining extends TestingLibraryMatchers<void, void> {} 8 8 9 9 type Promisify<O> = { 10 10 [K in keyof O]: O[K] extends (...args: infer A) => infer R ··· 22 22 * You can set default timeout via `expect.poll.timeout` option in the config. 23 23 * @see {@link https://vitest.dev/api/expect#poll} 24 24 */ 25 - element: <T extends Element | Locator>(element: T, options?: ExpectPollOptions) => PromisifyDomAssertion<Awaited<Element | null>> 25 + element: <T extends Element | Locator | null>(element: T, options?: ExpectPollOptions) => PromisifyDomAssertion<Awaited<Element | null>> 26 26 } 27 27 } 28 28
+1 -2
packages/browser/package.json
··· 99 99 "ws": "catalog:" 100 100 }, 101 101 "devDependencies": { 102 - "@testing-library/jest-dom": "^6.6.3", 103 102 "@types/ws": "catalog:", 104 103 "@vitest/runner": "workspace:*", 105 104 "@vitest/ui": "workspace:*", ··· 108 107 "@wdio/types": "^9.10.1", 109 108 "birpc": "catalog:", 110 109 "flatted": "catalog:", 111 - "ivya": "^1.5.1", 110 + "ivya": "^1.6.0", 112 111 "mime": "^4.0.6", 113 112 "pathe": "catalog:", 114 113 "periscopic": "^4.0.2",
+6 -1
packages/browser/rollup.config.js
··· 75 75 'locators/webdriverio': './src/client/tester/locators/webdriverio.ts', 76 76 'locators/preview': './src/client/tester/locators/preview.ts', 77 77 'locators/index': './src/client/tester/locators/index.ts', 78 + 'expect-element': './src/client/tester/expect-element.ts', 78 79 'utils': './src/client/tester/public-utils.ts', 79 80 }, 80 81 output: { ··· 84 85 external, 85 86 plugins: [ 86 87 ...dtsUtilsClient.isolatedDecl(), 87 - ...plugins, 88 + ...plugins.filter(p => p.name !== 'unplugin-oxc'), 89 + oxc({ 90 + transform: { target: 'node18' }, 91 + minify: true, 92 + }), 88 93 ], 89 94 }, 90 95 {
+1
test/browser/package.json
··· 9 9 "test:playwright": "PROVIDER=playwright pnpm run test:unit", 10 10 "test:safaridriver": "PROVIDER=webdriverio BROWSER=safari pnpm run test:unit", 11 11 "test-fixtures": "vitest", 12 + "test-expect-dom": "vitest --root ./fixtures/expect-dom", 12 13 "test-mocking": "vitest --root ./fixtures/mocking", 13 14 "test-timeout": "vitest --root ./fixtures/timeout", 14 15 "test-mocking-watch": "vitest --root ./fixtures/mocking-watch",
+1010 -41
docs/guide/browser/assertion-api.md
··· 4 4 5 5 # Assertion API 6 6 7 - Vitest bundles the [`@testing-library/jest-dom`](https://github.com/testing-library/jest-dom) library to provide a wide range of DOM assertions out of the box. For detailed documentation, you can read the `jest-dom` readme: 7 + Vitest provides a wide range of DOM assertions out of the box forked from [`@testing-library/jest-dom`](https://github.com/testing-library/jest-dom) library with the added support for locators and built-in retry-ability. 8 8 9 - - [`toBeDisabled`](https://github.com/testing-library/jest-dom#toBeDisabled) 10 - - [`toBeEnabled`](https://github.com/testing-library/jest-dom#toBeEnabled) 11 - - [`toBeEmptyDOMElement`](https://github.com/testing-library/jest-dom#toBeEmptyDOMElement) 12 - - [`toBeInTheDocument`](https://github.com/testing-library/jest-dom#toBeInTheDocument) 13 - - [`toBeInvalid`](https://github.com/testing-library/jest-dom#toBeInvalid) 14 - - [`toBeRequired`](https://github.com/testing-library/jest-dom#toBeRequired) 15 - - [`toBeValid`](https://github.com/testing-library/jest-dom#toBeValid) 16 - - [`toBeVisible`](https://github.com/testing-library/jest-dom#toBeVisible) 17 - - [`toContainElement`](https://github.com/testing-library/jest-dom#toContainElement) 18 - - [`toContainHTML`](https://github.com/testing-library/jest-dom#toContainHTML) 19 - - [`toHaveAccessibleDescription`](https://github.com/testing-library/jest-dom#toHaveAccessibleDescription) 20 - - [`toHaveAccessibleErrorMessage`](https://github.com/testing-library/jest-dom#toHaveAccessibleErrorMessage) 21 - - [`toHaveAccessibleName`](https://github.com/testing-library/jest-dom#toHaveAccessibleName) 22 - - [`toHaveAttribute`](https://github.com/testing-library/jest-dom#toHaveAttribute) 23 - - [`toHaveClass`](https://github.com/testing-library/jest-dom#toHaveClass) 24 - - [`toHaveFocus`](https://github.com/testing-library/jest-dom#toHaveFocus) 25 - - [`toHaveFormValues`](https://github.com/testing-library/jest-dom#toHaveFormValues) 26 - - [`toHaveStyle`](https://github.com/testing-library/jest-dom#toHaveStyle) 27 - - [`toHaveTextContent`](https://github.com/testing-library/jest-dom#toHaveTextContent) 28 - - [`toHaveValue`](https://github.com/testing-library/jest-dom#toHaveValue) 29 - - [`toHaveDisplayValue`](https://github.com/testing-library/jest-dom#toHaveDisplayValue) 30 - - [`toBeChecked`](https://github.com/testing-library/jest-dom#toBeChecked) 31 - - [`toBePartiallyChecked`](https://github.com/testing-library/jest-dom#toBePartiallyChecked) 32 - - [`toHaveRole`](https://github.com/testing-library/jest-dom#toHaveRole) 33 - - [`toHaveErrorMessage`](https://github.com/testing-library/jest-dom#toHaveErrorMessage) 9 + ::: tip TypeScript Support 10 + If you are using [TypeScript](/guide/browser/#typescript) or want to have correct type hints in `expect`, make sure you have `@vitest/browser/context` referenced somewhere. If you never imported from there, you can add a `reference` comment in any file that's covered by your `tsconfig.json`: 34 11 35 - If you are using [TypeScript](/guide/browser/#typescript) or want to have correct type hints in `expect`, make sure you have either `@vitest/browser/providers/playwright` or `@vitest/browser/providers/webdriverio` referenced in your [setup file](/config/#setupfile) or a [config file](/config/) depending on the provider you use. If you use the default `preview` provider, you can specify `@vitest/browser/matchers` instead. 36 - 37 - ::: code-group 38 - ```ts [preview] 39 - /// <reference types="@vitest/browser/matchers" /> 40 - ``` 41 - ```ts [playwright] 42 - /// <reference types="@vitest/browser/providers/playwright" /> 43 - ``` 44 - ```ts [webdriverio] 45 - /// <reference types="@vitest/browser/providers/webdriverio" /> 12 + ```ts 13 + /// <reference types="@vitest/browser/context" /> 46 14 ``` 47 15 ::: 48 16 ··· 55 23 test('error banner is rendered', async () => { 56 24 triggerError() 57 25 58 - // @testing-library provides queries with built-in retry-ability 59 - // It will try to find the banner until it's rendered 26 + // This creates a locator that will try to find the element 27 + // when any of its methods are called. 28 + // This call by itself doesn't check the existence of the element. 60 29 const banner = page.getByRole('alert', { 61 30 name: /error/i, 62 31 }) 63 32 64 33 // Vitest provides `expect.element` with built-in retry-ability 65 - // It will check `element.textContent` until it's equal to "Error!" 34 + // It will repeatedly check that the element exists in the DOM and that 35 + // the content of `element.textContent` is equal to "Error!" 36 + // until all the conditions are met 66 37 await expect.element(banner).toHaveTextContent('Error!') 67 38 }) 39 + ``` 40 + 41 + We recommend to always use `expect.element` when working with `page.getBy*` locators to reduce test flakiness. Note that `expect.element` accepts a second option: 42 + 43 + ```ts 44 + interface ExpectPollOptions { 45 + // The interval to retry the assertion for in milliseconds 46 + // Defaults to "expect.poll.interval" config option 47 + interval?: number 48 + // Time to retry the assertion for in milliseconds 49 + // Defaults to "expect.poll.timeout" config option 50 + timeout?: number 51 + // The message printed when the assertion fails 52 + message?: string 53 + } 68 54 ``` 69 55 70 56 ::: tip 71 57 `expect.element` is a shorthand for `expect.poll(() => element)` and works in exactly the same way. 72 58 73 - `toHaveTextContent` and all other [`@testing-library/jest-dom`](https://github.com/testing-library/jest-dom) assertions are still available on a regular `expect` without a built-in retry-ability mechanism: 59 + `toHaveTextContent` and all other assertions are still available on a regular `expect` without a built-in retry-ability mechanism: 74 60 75 61 ```ts 76 62 // will fail immediately if .textContent is not `'Error!'` 77 63 expect(banner).toHaveTextContent('Error!') 78 64 ``` 79 65 ::: 66 + 67 + ## toBeDisabled 68 + 69 + ```ts 70 + function toBeDisabled(): Promise<void> 71 + ``` 72 + 73 + Allows you to check whether an element is disabled from the user's perspective. 74 + 75 + Matches if the element is a form control and the `disabled` attribute is specified on this element or the 76 + element is a descendant of a form element with a `disabled` attribute. 77 + 78 + Note that only native control elements such as HTML `button`, `input`, `select`, `textarea`, `option`, `optgroup` 79 + can be disabled by setting "disabled" attribute. "disabled" attribute on other elements is ignored, unless it's a custom element. 80 + 81 + ```html 82 + <button 83 + data-testid="button" 84 + type="submit" 85 + disabled 86 + > 87 + submit 88 + </button> 89 + ``` 90 + 91 + ```ts 92 + await expect.element(getByTestId('button')).toBeDisabled() // ✅ 93 + await expect.element(getByTestId('button')).not.toBeDisabled() // ❌ 94 + ``` 95 + 96 + ## toBeEnabled 97 + 98 + ```ts 99 + function toBeEnabled(): Promise<void> 100 + ``` 101 + 102 + Allows you to check whether an element is not disabled from the user's perspective. 103 + 104 + Works like [`not.toBeDisabled()`](#tobedisabled). Use this matcher to avoid double negation in your tests. 105 + 106 + ```html 107 + <button 108 + data-testid="button" 109 + type="submit" 110 + disabled 111 + > 112 + submit 113 + </button> 114 + ``` 115 + 116 + ```ts 117 + await expect.element(getByTestId('button')).toBeEnabled() // ✅ 118 + await expect.element(getByTestId('button')).not.toBeEnabled() // ❌ 119 + ``` 120 + 121 + ## toBeEmptyDOMElement 122 + 123 + ```ts 124 + function toBeEmptyDOMElement(): Promise<void> 125 + ``` 126 + 127 + This allows you to assert whether an element has no visible content for the user. It ignores comments but will fail if the element contains white-space. 128 + 129 + ```html 130 + <span data-testid="not-empty"><span data-testid="empty"></span></span> 131 + <span data-testid="with-whitespace"> </span> 132 + <span data-testid="with-comment"><!-- comment --></span> 133 + ``` 134 + 135 + ```ts 136 + await expect.element(getByTestId('empty')).toBeEmptyDOMElement() 137 + await expect.element(getByTestId('not-empty')).not.toBeEmptyDOMElement() 138 + await expect.element( 139 + getByTestId('with-whitespace') 140 + ).not.toBeEmptyDOMElement() 141 + ``` 142 + 143 + ## toBeInTheDocument 144 + 145 + ```ts 146 + function toBeInTheDocument(): Promise<void> 147 + ``` 148 + 149 + Assert whether an element is present in the document or not. 150 + 151 + ```html 152 + <svg data-testid="svg-element"></svg> 153 + ``` 154 + 155 + ```ts 156 + await expect.element(getByTestId('svg-element')).toBeInTheDocument() 157 + await expect.element(getByTestId('does-not-exist')).not.toBeInTheDocument() 158 + ``` 159 + 160 + ::: warning 161 + This matcher does not find detached elements. The element must be added to the document to be found by `toBeInTheDocument`. If you desire to search in a detached element, please use: [`toContainElement`](#tocontainelement). 162 + ::: 163 + 164 + ## toBeInvalid 165 + 166 + ```ts 167 + function toBeInvalid(): Promise<void> 168 + ``` 169 + 170 + This allows you to check if an element, is currently invalid. 171 + 172 + An element is invalid if it has an [`aria-invalid` attribute](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-invalid) with no value or a value of `"true"`, or if the result of [`checkValidity()`](https://developer.mozilla.org/en-US/docs/Web/HTML/Constraint_validation) is `false`. 173 + 174 + ```html 175 + <input data-testid="no-aria-invalid" /> 176 + <input data-testid="aria-invalid" aria-invalid /> 177 + <input data-testid="aria-invalid-value" aria-invalid="true" /> 178 + <input data-testid="aria-invalid-false" aria-invalid="false" /> 179 + 180 + <form data-testid="valid-form"> 181 + <input /> 182 + </form> 183 + 184 + <form data-testid="invalid-form"> 185 + <input required /> 186 + </form> 187 + ``` 188 + 189 + ```ts 190 + await expect.element(getByTestId('no-aria-invalid')).not.toBeInvalid() 191 + await expect.element(getByTestId('aria-invalid')).toBeInvalid() 192 + await expect.element(getByTestId('aria-invalid-value')).toBeInvalid() 193 + await expect.element(getByTestId('aria-invalid-false')).not.toBeInvalid() 194 + 195 + await expect.element(getByTestId('valid-form')).not.toBeInvalid() 196 + await expect.element(getByTestId('invalid-form')).toBeInvalid() 197 + ``` 198 + 199 + ## toBeRequired 200 + 201 + ```ts 202 + function toBeRequired(): Promise<void> 203 + ``` 204 + 205 + This allows you to check if a form element is currently required. 206 + 207 + An element is required if it is having a `required` or `aria-required="true"` attribute. 208 + 209 + ```html 210 + <input data-testid="required-input" required /> 211 + <input data-testid="aria-required-input" aria-required="true" /> 212 + <input data-testid="conflicted-input" required aria-required="false" /> 213 + <input data-testid="aria-not-required-input" aria-required="false" /> 214 + <input data-testid="optional-input" /> 215 + <input data-testid="unsupported-type" type="image" required /> 216 + <select data-testid="select" required></select> 217 + <textarea data-testid="textarea" required></textarea> 218 + <div data-testid="supported-role" role="tree" required></div> 219 + <div data-testid="supported-role-aria" role="tree" aria-required="true"></div> 220 + ``` 221 + 222 + ```ts 223 + await expect.element(getByTestId('required-input')).toBeRequired() 224 + await expect.element(getByTestId('aria-required-input')).toBeRequired() 225 + await expect.element(getByTestId('conflicted-input')).toBeRequired() 226 + await expect.element(getByTestId('aria-not-required-input')).not.toBeRequired() 227 + await expect.element(getByTestId('optional-input')).not.toBeRequired() 228 + await expect.element(getByTestId('unsupported-type')).not.toBeRequired() 229 + await expect.element(getByTestId('select')).toBeRequired() 230 + await expect.element(getByTestId('textarea')).toBeRequired() 231 + await expect.element(getByTestId('supported-role')).not.toBeRequired() 232 + await expect.element(getByTestId('supported-role-aria')).toBeRequired() 233 + ``` 234 + 235 + ## toBeValid 236 + 237 + ```ts 238 + function toBeValid(): Promise<void> 239 + ``` 240 + 241 + This allows you to check if the value of an element, is currently valid. 242 + 243 + An element is valid if it has no [`aria-invalid` attribute](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-invalid) or an attribute value of "false". The result of [`checkValidity()`](https://developer.mozilla.org/en-US/docs/Web/HTML/Constraint_validation) must also be `true` if it's a form element. 244 + 245 + ```html 246 + <input data-testid="no-aria-invalid" /> 247 + <input data-testid="aria-invalid" aria-invalid /> 248 + <input data-testid="aria-invalid-value" aria-invalid="true" /> 249 + <input data-testid="aria-invalid-false" aria-invalid="false" /> 250 + 251 + <form data-testid="valid-form"> 252 + <input /> 253 + </form> 254 + 255 + <form data-testid="invalid-form"> 256 + <input required /> 257 + </form> 258 + ``` 259 + 260 + ```ts 261 + await expect.element(getByTestId('no-aria-invalid')).toBeValid() 262 + await expect.element(getByTestId('aria-invalid')).not.toBeValid() 263 + await expect.element(getByTestId('aria-invalid-value')).not.toBeValid() 264 + await expect.element(getByTestId('aria-invalid-false')).toBeValid() 265 + 266 + await expect.element(getByTestId('valid-form')).toBeValid() 267 + await expect.element(getByTestId('invalid-form')).not.toBeValid() 268 + ``` 269 + 270 + ## toBeVisible 271 + 272 + ```ts 273 + function toBeVisible(): Promise<void> 274 + ``` 275 + 276 + This allows you to check if an element is currently visible to the user. 277 + 278 + Element is considered visible when it has non-empty bounding box and does not have `visibility:hidden` computed style. 279 + 280 + Note that according to this definition: 281 + 282 + - Elements of zero size **are not** considered visible. 283 + - Elements with `display:none` **are not** considered visible. 284 + - Elements with `opacity:0` **are** considered visible. 285 + 286 + To check that at least one element from the list is visible, use `locator.first()`. 287 + 288 + ```ts 289 + // A specific element is visible. 290 + await expect.element(page.getByText('Welcome')).toBeVisible() 291 + 292 + // At least one item in the list is visible. 293 + await expect.element(page.getByTestId('todo-item').first()).toBeVisible() 294 + 295 + // At least one of the two elements is visible, possibly both. 296 + await expect.element( 297 + page.getByRole('button', { name: 'Sign in' }) 298 + .or(page.getByRole('button', { name: 'Sign up' })) 299 + .first() 300 + ).toBeVisible() 301 + ``` 302 + 303 + ## toContainElement 304 + 305 + ```ts 306 + function toContainElement(element: HTMLElement | SVGElement | null): Promise<void> 307 + ``` 308 + 309 + This allows you to assert whether an element contains another element as a descendant or not. 310 + 311 + ```html 312 + <span data-testid="ancestor"><span data-testid="descendant"></span></span> 313 + ``` 314 + 315 + ```ts 316 + const ancestor = getByTestId('ancestor') 317 + const descendant = getByTestId('descendant') 318 + const nonExistantElement = getByTestId('does-not-exist') 319 + 320 + await expect.element(ancestor).toContainElement(descendant) 321 + await expect.element(descendant).not.toContainElement(ancestor) 322 + await expect.element(ancestor).not.toContainElement(nonExistantElement) 323 + ``` 324 + 325 + ## toContainHTML 326 + 327 + ```ts 328 + function toContainHTML(htmlText: string): Promise<void> 329 + ``` 330 + 331 + Assert whether a string representing a HTML element is contained in another element. The string should contain valid html, and not any incomplete html. 332 + 333 + ```html 334 + <span data-testid="parent"><span data-testid="child"></span></span> 335 + ``` 336 + 337 + ```ts 338 + // These are valid usages 339 + await expect.element(getByTestId('parent')).toContainHTML('<span data-testid="child"></span>') 340 + await expect.element(getByTestId('parent')).toContainHTML('<span data-testid="child" />') 341 + await expect.element(getByTestId('parent')).not.toContainHTML('<br />') 342 + 343 + // These won't work 344 + await expect.element(getByTestId('parent')).toContainHTML('data-testid="child"') 345 + await expect.element(getByTestId('parent')).toContainHTML('data-testid') 346 + await expect.element(getByTestId('parent')).toContainHTML('</span>') 347 + ``` 348 + 349 + ::: warning 350 + Chances are you probably do not need to use this matcher. We encourage testing from the perspective of how the user perceives the app in a browser. That's why testing against a specific DOM structure is not advised. 351 + 352 + It could be useful in situations where the code being tested renders html that was obtained from an external source, and you want to validate that that html code was used as intended. 353 + 354 + It should not be used to check DOM structure that you control. Please, use [`toContainElement`](#tocontainelement) instead. 355 + ::: 356 + 357 + ## toHaveAccessibleDescription 358 + 359 + ```ts 360 + function toHaveAccessibleDescription(description?: string | RegExp): Promise<void> 361 + ``` 362 + 363 + This allows you to assert that an element has the expected 364 + [accessible description](https://w3c.github.io/accname/). 365 + 366 + You can pass the exact string of the expected accessible description, or you can 367 + make a partial match passing a regular expression, or by using 368 + [`expect.stringContaining`](/api/expect#expect-stringcontaining) or [`expect.stringMatching`](/api/expect#expect-stringmatching). 369 + 370 + ```html 371 + <a 372 + data-testid="link" 373 + href="/" 374 + aria-label="Home page" 375 + title="A link to start over" 376 + >Start</a 377 + > 378 + <a data-testid="extra-link" href="/about" aria-label="About page">About</a> 379 + <img src="avatar.jpg" data-testid="avatar" alt="User profile pic" /> 380 + <img 381 + src="logo.jpg" 382 + data-testid="logo" 383 + alt="Company logo" 384 + aria-describedby="t1" 385 + /> 386 + <span id="t1" role="presentation">The logo of Our Company</span> 387 + <img 388 + src="logo.jpg" 389 + data-testid="logo2" 390 + alt="Company logo" 391 + aria-description="The logo of Our Company" 392 + /> 393 + ``` 394 + 395 + ```ts 396 + await expect.element(getByTestId('link')).toHaveAccessibleDescription() 397 + await expect.element(getByTestId('link')).toHaveAccessibleDescription('A link to start over') 398 + await expect.element(getByTestId('link')).not.toHaveAccessibleDescription('Home page') 399 + await expect.element(getByTestId('extra-link')).not.toHaveAccessibleDescription() 400 + await expect.element(getByTestId('avatar')).not.toHaveAccessibleDescription() 401 + await expect.element(getByTestId('logo')).not.toHaveAccessibleDescription('Company logo') 402 + await expect.element(getByTestId('logo')).toHaveAccessibleDescription( 403 + 'The logo of Our Company', 404 + ) 405 + await expect.element(getByTestId('logo2')).toHaveAccessibleDescription( 406 + 'The logo of Our Company', 407 + ) 408 + ``` 409 + 410 + ## toHaveAccessibleErrorMessage 411 + 412 + ```ts 413 + function toHaveAccessibleErrorMessage(message?: string | RegExp): Promise<void> 414 + ``` 415 + 416 + This allows you to assert that an element has the expected 417 + [accessible error message](https://w3c.github.io/aria/#aria-errormessage). 418 + 419 + You can pass the exact string of the expected accessible error message. 420 + Alternatively, you can perform a partial match by passing a regular expression 421 + or by using 422 + [`expect.stringContaining`](/api/expect#expect-stringcontaining) or [`expect.stringMatching`](/api/expect#expect-stringmatching). 423 + 424 + ```html 425 + <input 426 + aria-label="Has Error" 427 + aria-invalid="true" 428 + aria-errormessage="error-message" 429 + /> 430 + <div id="error-message" role="alert">This field is invalid</div> 431 + 432 + <input aria-label="No Error Attributes" /> 433 + <input 434 + aria-label="Not Invalid" 435 + aria-invalid="false" 436 + aria-errormessage="error-message" 437 + /> 438 + ``` 439 + 440 + ```ts 441 + // Inputs with Valid Error Messages 442 + await expect.element(getByRole('textbox', { name: 'Has Error' })).toHaveAccessibleErrorMessage() 443 + await expect.element(getByRole('textbox', { name: 'Has Error' })).toHaveAccessibleErrorMessage( 444 + 'This field is invalid', 445 + ) 446 + await expect.element(getByRole('textbox', { name: 'Has Error' })).toHaveAccessibleErrorMessage( 447 + /invalid/i, 448 + ) 449 + await expect.element( 450 + getByRole('textbox', { name: 'Has Error' }), 451 + ).not.toHaveAccessibleErrorMessage('This field is absolutely correct!') 452 + 453 + // Inputs without Valid Error Messages 454 + await expect.element( 455 + getByRole('textbox', { name: 'No Error Attributes' }), 456 + ).not.toHaveAccessibleErrorMessage() 457 + 458 + await expect.element( 459 + getByRole('textbox', { name: 'Not Invalid' }), 460 + ).not.toHaveAccessibleErrorMessage() 461 + ``` 462 + 463 + ## toHaveAccessibleName 464 + 465 + ```ts 466 + function toHaveAccessibleName(name?: string | RegExp): Promise<void> 467 + ``` 468 + 469 + This allows you to assert that an element has the expected 470 + [accessible name](https://w3c.github.io/accname/). It is useful, for instance, 471 + to assert that form elements and buttons are properly labelled. 472 + 473 + You can pass the exact string of the expected accessible name, or you can make a 474 + partial match passing a regular expression, or by using 475 + [`expect.stringContaining`](/api/expect#expect-stringcontaining) or [`expect.stringMatching`](/api/expect#expect-stringmatching). 476 + 477 + ```html 478 + <img data-testid="img-alt" src="" alt="Test alt" /> 479 + <img data-testid="img-empty-alt" src="" alt="" /> 480 + <svg data-testid="svg-title"><title>Test title</title></svg> 481 + <button data-testid="button-img-alt"><img src="" alt="Test" /></button> 482 + <p><img data-testid="img-paragraph" src="" alt="" /> Test content</p> 483 + <button data-testid="svg-button"><svg><title>Test</title></svg></p> 484 + <div><svg data-testid="svg-without-title"></svg></div> 485 + <input data-testid="input-title" title="test" /> 486 + ``` 487 + 488 + ```javascript 489 + await expect.element(getByTestId('img-alt')).toHaveAccessibleName('Test alt') 490 + await expect.element(getByTestId('img-empty-alt')).not.toHaveAccessibleName() 491 + await expect.element(getByTestId('svg-title')).toHaveAccessibleName('Test title') 492 + await expect.element(getByTestId('button-img-alt')).toHaveAccessibleName() 493 + await expect.element(getByTestId('img-paragraph')).not.toHaveAccessibleName() 494 + await expect.element(getByTestId('svg-button')).toHaveAccessibleName() 495 + await expect.element(getByTestId('svg-without-title')).not.toHaveAccessibleName() 496 + await expect.element(getByTestId('input-title')).toHaveAccessibleName() 497 + ``` 498 + 499 + ## toHaveAttribute 500 + 501 + ```ts 502 + function toHaveAttribute(attribute: string, value?: unknown): Promise<void> 503 + ``` 504 + 505 + This allows you to check whether the given element has an attribute or not. You 506 + can also optionally check that the attribute has a specific expected value or 507 + partial match using [`expect.stringContaining`](/api/expect#expect-stringcontaining) or [`expect.stringMatching`](/api/expect#expect-stringmatching). 508 + 509 + ```html 510 + <button data-testid="ok-button" type="submit" disabled>ok</button> 511 + ``` 512 + 513 + ```ts 514 + const button = getByTestId('ok-button') 515 + 516 + await expect.element(button).toHaveAttribute('disabled') 517 + await expect.element(button).toHaveAttribute('type', 'submit') 518 + await expect.element(button).not.toHaveAttribute('type', 'button') 519 + 520 + await expect.element(button).toHaveAttribute( 521 + 'type', 522 + expect.stringContaining('sub') 523 + ) 524 + await expect.element(button).toHaveAttribute( 525 + 'type', 526 + expect.not.stringContaining('but') 527 + ) 528 + ``` 529 + 530 + ## toHaveClass 531 + 532 + ```ts 533 + function toHaveClass(...classNames: string[], options?: { exact: boolean }): Promise<void> 534 + function toHaveClass(...classNames: (string | RegExp)[]): Promise<void> 535 + ``` 536 + 537 + This allows you to check whether the given element has certain classes within 538 + its `class` attribute. You must provide at least one class, unless you are 539 + asserting that an element does not have any classes. 540 + 541 + The list of class names may include strings and regular expressions. Regular 542 + expressions are matched against each individual class in the target element, and 543 + it is NOT matched against its full `class` attribute value as whole. 544 + 545 + ::: warning 546 + Note that you cannot use `exact: true` option when only regular expressions are provided. 547 + ::: 548 + 549 + ```html 550 + <button data-testid="delete-button" class="btn extra btn-danger"> 551 + Delete item 552 + </button> 553 + <button data-testid="no-classes">No Classes</button> 554 + ``` 555 + 556 + ```ts 557 + const deleteButton = getByTestId('delete-button') 558 + const noClasses = getByTestId('no-classes') 559 + 560 + await expect.element(deleteButton).toHaveClass('extra') 561 + await expect.element(deleteButton).toHaveClass('btn-danger btn') 562 + await expect.element(deleteButton).toHaveClass(/danger/, 'btn') 563 + await expect.element(deleteButton).toHaveClass('btn-danger', 'btn') 564 + await expect.element(deleteButton).not.toHaveClass('btn-link') 565 + await expect.element(deleteButton).not.toHaveClass(/link/) 566 + 567 + // ⚠️ regexp matches against individual classes, not the whole classList 568 + await expect.element(deleteButton).not.toHaveClass(/btn extra/) 569 + 570 + // the element has EXACTLY a set of classes (in any order) 571 + await expect.element(deleteButton).toHaveClass('btn-danger extra btn', { 572 + exact: true 573 + }) 574 + // if it has more than expected it is going to fail 575 + await expect.element(deleteButton).not.toHaveClass('btn-danger extra', { 576 + exact: true 577 + }) 578 + 579 + await expect.element(noClasses).not.toHaveClass() 580 + ``` 581 + 582 + ## toHaveFocus 583 + 584 + ```ts 585 + function toHaveFocus(): Promise<void> 586 + ``` 587 + 588 + This allows you to assert whether an element has focus or not. 589 + 590 + ```html 591 + <div><input type="text" data-testid="element-to-focus" /></div> 592 + ``` 593 + 594 + ```ts 595 + const input = page.getByTestId('element-to-focus') 596 + input.element().focus() 597 + await expect.element(input).toHaveFocus() 598 + input.element().blur() 599 + await expect.element(input).not.toHaveFocus() 600 + ``` 601 + 602 + ## toHaveFormValues 603 + 604 + ```ts 605 + function toHaveFormValues(expectedValues: Record<string, unknown>): Promise<void> 606 + ``` 607 + 608 + This allows you to check if a form or fieldset contains form controls for each given name, and having the specified value. 609 + 610 + ::: tip 611 + It is important to stress that this matcher can only be invoked on a [form](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement) or a [fieldset](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFieldSetElement) element. 612 + 613 + This allows it to take advantage of the [`.elements`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/elements) property in `form` and `fieldset` to reliably fetch all form controls within them. 614 + 615 + This also avoids the possibility that users provide a container that contains more than one `form`, thereby intermixing form controls that are not related, and could even conflict with one another. 616 + ::: 617 + 618 + This matcher abstracts away the particularities with which a form control value 619 + is obtained depending on the type of form control. For instance, `<input>` 620 + elements have a `value` attribute, but `<select>` elements do not. Here's a list 621 + of all cases covered: 622 + 623 + - `<input type="number">` elements return the value as a **number**, instead of 624 + a string. 625 + - `<input type="checkbox">` elements: 626 + - if there's a single one with the given `name` attribute, it is treated as a 627 + **boolean**, returning `true` if the checkbox is checked, `false` if 628 + unchecked. 629 + - if there's more than one checkbox with the same `name` attribute, they are 630 + all treated collectively as a single form control, which returns the value 631 + as an **array** containing all the values of the selected checkboxes in the 632 + collection. 633 + - `<input type="radio">` elements are all grouped by the `name` attribute, and 634 + such a group treated as a single form control. This form control returns the 635 + value as a **string** corresponding to the `value` attribute of the selected 636 + radio button within the group. 637 + - `<input type="text">` elements return the value as a **string**. This also 638 + applies to `<input>` elements having any other possible `type` attribute 639 + that's not explicitly covered in different rules above (e.g. `search`, 640 + `email`, `date`, `password`, `hidden`, etc.) 641 + - `<select>` elements without the `multiple` attribute return the value as a 642 + **string** corresponding to the `value` attribute of the selected `option`, or 643 + `undefined` if there's no selected option. 644 + - `<select multiple>` elements return the value as an **array** containing all 645 + the values of the [selected options](https://developer.mozilla.org/en-US/docs/Web/API/HTMLSelectElement/selectedOptions). 646 + - `<textarea>` elements return their value as a **string**. The value 647 + corresponds to their node content. 648 + 649 + The above rules make it easy, for instance, to switch from using a single select 650 + control to using a group of radio buttons. Or to switch from a multi select 651 + control, to using a group of checkboxes. The resulting set of form values used 652 + by this matcher to compare against would be the same. 653 + 654 + ```html 655 + <form data-testid="login-form"> 656 + <input type="text" name="username" value="jane.doe" /> 657 + <input type="password" name="password" value="12345678" /> 658 + <input type="checkbox" name="rememberMe" checked /> 659 + <button type="submit">Sign in</button> 660 + </form> 661 + ``` 662 + 663 + ```ts 664 + await expect.element(getByTestId('login-form')).toHaveFormValues({ 665 + username: 'jane.doe', 666 + rememberMe: true, 667 + }) 668 + ``` 669 + 670 + ## toHaveStyle 671 + 672 + ```ts 673 + function toHaveStyle(css: string | Partial<CSSStyleDeclaration>): Promise<void> 674 + ``` 675 + 676 + This allows you to check if a certain element has some specific css properties 677 + with specific values applied. It matches only if the element has _all_ the 678 + expected properties applied, not just some of them. 679 + 680 + ```html 681 + <button 682 + data-testid="delete-button" 683 + style="display: none; background-color: red" 684 + > 685 + Delete item 686 + </button> 687 + ``` 688 + 689 + ```ts 690 + const button = getByTestId('delete-button') 691 + 692 + await expect.element(button).toHaveStyle('display: none') 693 + await expect.element(button).toHaveStyle({ display: 'none' }) 694 + await expect.element(button).toHaveStyle(` 695 + background-color: red; 696 + display: none; 697 + `) 698 + await expect.element(button).toHaveStyle({ 699 + backgroundColor: 'red', 700 + display: 'none', 701 + }) 702 + await expect.element(button).not.toHaveStyle(` 703 + background-color: blue; 704 + display: none; 705 + `) 706 + await expect.element(button).not.toHaveStyle({ 707 + backgroundColor: 'blue', 708 + display: 'none', 709 + }) 710 + ``` 711 + 712 + This also works with rules that are applied to the element via a class name for 713 + which some rules are defined in a stylesheet currently active in the document. 714 + The usual rules of css precedence apply. 715 + 716 + ## toHaveTextContent 717 + 718 + ```ts 719 + function toHaveTextContent( 720 + text: string | RegExp, 721 + options?: { normalizeWhitespace: boolean } 722 + ): Promise<void> 723 + ``` 724 + 725 + This allows you to check whether the given node has a text content or not. This 726 + supports elements, but also text nodes and fragments. 727 + 728 + When a `string` argument is passed through, it will perform a partial 729 + case-sensitive match to the node content. 730 + 731 + To perform a case-insensitive match, you can use a `RegExp` with the `/i` 732 + modifier. 733 + 734 + If you want to match the whole content, you can use a `RegExp` to do it. 735 + 736 + ```html 737 + <span data-testid="text-content">Text Content</span> 738 + ``` 739 + 740 + ```ts 741 + const element = getByTestId('text-content') 742 + 743 + await expect.element(element).toHaveTextContent('Content') 744 + // to match the whole content 745 + await expect.element(element).toHaveTextContent(/^Text Content$/) 746 + // to use case-insensitive match 747 + await expect.element(element).toHaveTextContent(/content$/i) 748 + await expect.element(element).not.toHaveTextContent('content') 749 + ``` 750 + 751 + ## toHaveValue 752 + 753 + ```ts 754 + function toHaveValue(value: string | string[] | number | null): Promise<void> 755 + ``` 756 + 757 + This allows you to check whether the given form element has the specified value. 758 + It accepts `<input>`, `<select>` and `<textarea>` elements with the exception of 759 + `<input type="checkbox">` and `<input type="radio">`, which can be meaningfully 760 + matched only using [`toBeChecked`](#tobechecked) or 761 + [`toHaveFormValues`](#tohaveformvalues). 762 + 763 + It also accepts elements with roles `meter`, `progressbar`, `slider` or 764 + `spinbutton` and checks their `aria-valuenow` attribute (as a number). 765 + 766 + For all other form elements, the value is matched using the same algorithm as in 767 + [`toHaveFormValues`](#tohaveformvalues) does. 768 + 769 + ```html 770 + <input type="text" value="text" data-testid="input-text" /> 771 + <input type="number" value="5" data-testid="input-number" /> 772 + <input type="text" data-testid="input-empty" /> 773 + <select multiple data-testid="select-number"> 774 + <option value="first">First Value</option> 775 + <option value="second" selected>Second Value</option> 776 + <option value="third" selected>Third Value</option> 777 + </select> 778 + ``` 779 + 780 + ```ts 781 + const textInput = getByTestId('input-text') 782 + const numberInput = getByTestId('input-number') 783 + const emptyInput = getByTestId('input-empty') 784 + const selectInput = getByTestId('select-number') 785 + 786 + await expect.element(textInput).toHaveValue('text') 787 + await expect.element(numberInput).toHaveValue(5) 788 + await expect.element(emptyInput).not.toHaveValue() 789 + await expect.element(selectInput).toHaveValue(['second', 'third']) 790 + ``` 791 + 792 + ## toHaveDisplayValue 793 + 794 + ```typescript 795 + function toHaveDisplayValue( 796 + value: string | RegExp | (string | RegExp)[] 797 + ): Promise<void> 798 + ``` 799 + 800 + This allows you to check whether the given form element has the specified 801 + displayed value (the one the end user will see). It accepts `<input>`, 802 + `<select>` and `<textarea>` elements with the exception of 803 + `<input type="checkbox">` and `<input type="radio">`, which can be meaningfully 804 + matched only using [`toBeChecked`](#tobechecked) or 805 + [`toHaveFormValues`](#tohaveformvalues). 806 + 807 + ```html 808 + <label for="input-example">First name</label> 809 + <input type="text" id="input-example" value="Luca" /> 810 + 811 + <label for="textarea-example">Description</label> 812 + <textarea id="textarea-example">An example description here.</textarea> 813 + 814 + <label for="single-select-example">Fruit</label> 815 + <select id="single-select-example"> 816 + <option value="">Select a fruit...</option> 817 + <option value="banana">Banana</option> 818 + <option value="ananas">Ananas</option> 819 + <option value="avocado">Avocado</option> 820 + </select> 821 + 822 + <label for="multiple-select-example">Fruits</label> 823 + <select id="multiple-select-example" multiple> 824 + <option value="">Select a fruit...</option> 825 + <option value="banana" selected>Banana</option> 826 + <option value="ananas">Ananas</option> 827 + <option value="avocado" selected>Avocado</option> 828 + </select> 829 + ``` 830 + 831 + ```ts 832 + const input = page.getByLabelText('First name') 833 + const textarea = page.getByLabelText('Description') 834 + const selectSingle = page.getByLabelText('Fruit') 835 + const selectMultiple = page.getByLabelText('Fruits') 836 + 837 + await expect.element(input).toHaveDisplayValue('Luca') 838 + await expect.element(input).toHaveDisplayValue(/Luc/) 839 + await expect.element(textarea).toHaveDisplayValue('An example description here.') 840 + await expect.element(textarea).toHaveDisplayValue(/example/) 841 + await expect.element(selectSingle).toHaveDisplayValue('Select a fruit...') 842 + await expect.element(selectSingle).toHaveDisplayValue(/Select/) 843 + await expect.element(selectMultiple).toHaveDisplayValue([/Avocado/, 'Banana']) 844 + ``` 845 + 846 + ## toBeChecked 847 + 848 + ```ts 849 + function toBeChecked(): Promise<void> 850 + ``` 851 + 852 + This allows you to check whether the given element is checked. It accepts an 853 + `input` of type `checkbox` or `radio` and elements with a `role` of `checkbox`, 854 + `radio` or `switch` with a valid `aria-checked` attribute of `"true"` or 855 + `"false"`. 856 + 857 + ```html 858 + <input type="checkbox" checked data-testid="input-checkbox-checked" /> 859 + <input type="checkbox" data-testid="input-checkbox-unchecked" /> 860 + <div role="checkbox" aria-checked="true" data-testid="aria-checkbox-checked" /> 861 + <div 862 + role="checkbox" 863 + aria-checked="false" 864 + data-testid="aria-checkbox-unchecked" 865 + /> 866 + 867 + <input type="radio" checked value="foo" data-testid="input-radio-checked" /> 868 + <input type="radio" value="foo" data-testid="input-radio-unchecked" /> 869 + <div role="radio" aria-checked="true" data-testid="aria-radio-checked" /> 870 + <div role="radio" aria-checked="false" data-testid="aria-radio-unchecked" /> 871 + <div role="switch" aria-checked="true" data-testid="aria-switch-checked" /> 872 + <div role="switch" aria-checked="false" data-testid="aria-switch-unchecked" /> 873 + ``` 874 + 875 + ```ts 876 + const inputCheckboxChecked = getByTestId('input-checkbox-checked') 877 + const inputCheckboxUnchecked = getByTestId('input-checkbox-unchecked') 878 + const ariaCheckboxChecked = getByTestId('aria-checkbox-checked') 879 + const ariaCheckboxUnchecked = getByTestId('aria-checkbox-unchecked') 880 + await expect.element(inputCheckboxChecked).toBeChecked() 881 + await expect.element(inputCheckboxUnchecked).not.toBeChecked() 882 + await expect.element(ariaCheckboxChecked).toBeChecked() 883 + await expect.element(ariaCheckboxUnchecked).not.toBeChecked() 884 + 885 + const inputRadioChecked = getByTestId('input-radio-checked') 886 + const inputRadioUnchecked = getByTestId('input-radio-unchecked') 887 + const ariaRadioChecked = getByTestId('aria-radio-checked') 888 + const ariaRadioUnchecked = getByTestId('aria-radio-unchecked') 889 + await expect.element(inputRadioChecked).toBeChecked() 890 + await expect.element(inputRadioUnchecked).not.toBeChecked() 891 + await expect.element(ariaRadioChecked).toBeChecked() 892 + await expect.element(ariaRadioUnchecked).not.toBeChecked() 893 + 894 + const ariaSwitchChecked = getByTestId('aria-switch-checked') 895 + const ariaSwitchUnchecked = getByTestId('aria-switch-unchecked') 896 + await expect.element(ariaSwitchChecked).toBeChecked() 897 + await expect.element(ariaSwitchUnchecked).not.toBeChecked() 898 + ``` 899 + 900 + ## toBePartiallyChecked 901 + 902 + ```typescript 903 + function toBePartiallyChecked(): Promise<void> 904 + ``` 905 + 906 + This allows you to check whether the given element is partially checked. It 907 + accepts an `input` of type `checkbox` and elements with a `role` of `checkbox` 908 + with a `aria-checked="mixed"`, or `input` of type `checkbox` with 909 + `indeterminate` set to `true` 910 + 911 + ```html 912 + <input type="checkbox" aria-checked="mixed" data-testid="aria-checkbox-mixed" /> 913 + <input type="checkbox" checked data-testid="input-checkbox-checked" /> 914 + <input type="checkbox" data-testid="input-checkbox-unchecked" /> 915 + <div role="checkbox" aria-checked="true" data-testid="aria-checkbox-checked" /> 916 + <div 917 + role="checkbox" 918 + aria-checked="false" 919 + data-testid="aria-checkbox-unchecked" 920 + /> 921 + <input type="checkbox" data-testid="input-checkbox-indeterminate" /> 922 + ``` 923 + 924 + ```ts 925 + const ariaCheckboxMixed = getByTestId('aria-checkbox-mixed') 926 + const inputCheckboxChecked = getByTestId('input-checkbox-checked') 927 + const inputCheckboxUnchecked = getByTestId('input-checkbox-unchecked') 928 + const ariaCheckboxChecked = getByTestId('aria-checkbox-checked') 929 + const ariaCheckboxUnchecked = getByTestId('aria-checkbox-unchecked') 930 + const inputCheckboxIndeterminate = getByTestId('input-checkbox-indeterminate') 931 + 932 + await expect.element(ariaCheckboxMixed).toBePartiallyChecked() 933 + await expect.element(inputCheckboxChecked).not.toBePartiallyChecked() 934 + await expect.element(inputCheckboxUnchecked).not.toBePartiallyChecked() 935 + await expect.element(ariaCheckboxChecked).not.toBePartiallyChecked() 936 + await expect.element(ariaCheckboxUnchecked).not.toBePartiallyChecked() 937 + 938 + inputCheckboxIndeterminate.element().indeterminate = true 939 + await expect.element(inputCheckboxIndeterminate).toBePartiallyChecked() 940 + ``` 941 + 942 + ## toHaveRole 943 + 944 + ```ts 945 + function toHaveRole(role: ARIARole): Promise<void> 946 + ``` 947 + 948 + This allows you to assert that an element has the expected [role](https://www.w3.org/TR/html-aria/#docconformance). 949 + 950 + This is useful in cases where you already have access to an element via some query other than the role itself, and want to make additional assertions regarding its accessibility. 951 + 952 + The role can match either an explicit role (via the `role` attribute), or an implicit one via the [implicit ARIA semantics](https://www.w3.org/TR/html-aria/#docconformance). 953 + 954 + ```html 955 + <button data-testid="button">Continue</button> 956 + <div role="button" data-testid="button-explicit">Continue</button> 957 + <button role="switch button" data-testid="button-explicit-multiple">Continue</button> 958 + <a href="/about" data-testid="link">About</a> 959 + <a data-testid="link-invalid">Invalid link<a/> 960 + ``` 961 + 962 + ```ts 963 + await expect.element(getByTestId('button')).toHaveRole('button') 964 + await expect.element(getByTestId('button-explicit')).toHaveRole('button') 965 + await expect.element(getByTestId('button-explicit-multiple')).toHaveRole('button') 966 + await expect.element(getByTestId('button-explicit-multiple')).toHaveRole('switch') 967 + await expect.element(getByTestId('link')).toHaveRole('link') 968 + await expect.element(getByTestId('link-invalid')).not.toHaveRole('link') 969 + await expect.element(getByTestId('link-invalid')).toHaveRole('generic') 970 + ``` 971 + 972 + ::: warning 973 + Roles are matched literally by string equality, without inheriting from the ARIA role hierarchy. As a result, querying a superclass role like `checkbox` will not include elements with a subclass role like `switch`. 974 + 975 + Also note that unlike `testing-library`, Vitest ignores all custom roles except the first valid one, following Playwright's behaviour: 976 + 977 + ```jsx 978 + <div data-testid="switch" role="switch alert"></div> 979 + 980 + await expect.element(getByTestId('switch')).toHaveRole('switch') // ✅ 981 + await expect.element(getByTestId('switch')).toHaveRole('alert') // ❌ 982 + ``` 983 + ::: 984 + 985 + ## toHaveSelection 986 + 987 + ```ts 988 + function toHaveSelection(selection?: string): Promise<void> 989 + ``` 990 + 991 + This allows to assert that an element has a 992 + [text selection](https://developer.mozilla.org/en-US/docs/Web/API/Selection). 993 + 994 + This is useful to check if text or part of the text is selected within an 995 + element. The element can be either an input of type text, a textarea, or any 996 + other element that contains text, such as a paragraph, span, div etc. 997 + 998 + ::: warning 999 + The expected selection is a string, it does not allow to check for 1000 + selection range indeces. 1001 + ::: 1002 + 1003 + ```html 1004 + <div> 1005 + <input type="text" value="text selected text" data-testid="text" /> 1006 + <textarea data-testid="textarea">text selected text</textarea> 1007 + <p data-testid="prev">prev</p> 1008 + <p data-testid="parent"> 1009 + text <span data-testid="child">selected</span> text 1010 + </p> 1011 + <p data-testid="next">next</p> 1012 + </div> 1013 + ``` 1014 + 1015 + ```ts 1016 + getByTestId('text').element().setSelectionRange(5, 13) 1017 + await expect.element(getByTestId('text')).toHaveSelection('selected') 1018 + 1019 + getByTestId('textarea').element().setSelectionRange(0, 5) 1020 + await expect.element('textarea').toHaveSelection('text ') 1021 + 1022 + const selection = document.getSelection() 1023 + const range = document.createRange() 1024 + selection.removeAllRanges() 1025 + selection.empty() 1026 + selection.addRange(range) 1027 + 1028 + // selection of child applies to the parent as well 1029 + range.selectNodeContents(getByTestId('child').element()) 1030 + await expect.element(getByTestId('child')).toHaveSelection('selected') 1031 + await expect.element(getByTestId('parent')).toHaveSelection('selected') 1032 + 1033 + // selection that applies from prev all, parent text before child, and part child. 1034 + range.setStart(getByTestId('prev').element(), 0) 1035 + range.setEnd(getByTestId('child').element().childNodes[0], 3) 1036 + await expect.element(queryByTestId('prev')).toHaveSelection('prev') 1037 + await expect.element(queryByTestId('child')).toHaveSelection('sel') 1038 + await expect.element(queryByTestId('parent')).toHaveSelection('text sel') 1039 + await expect.element(queryByTestId('next')).not.toHaveSelection() 1040 + 1041 + // selection that applies from part child, parent text after child and part next. 1042 + range.setStart(getByTestId('child').element().childNodes[0], 3) 1043 + range.setEnd(getByTestId('next').element().childNodes[0], 2) 1044 + await expect.element(queryByTestId('child')).toHaveSelection('ected') 1045 + await expect.element(queryByTestId('parent')).toHaveSelection('ected text') 1046 + await expect.element(queryByTestId('prev')).not.toHaveSelection() 1047 + await expect.element(queryByTestId('next')).toHaveSelection('ne') 1048 + ```
+1 -1
docs/guide/browser/index.md
··· 401 401 402 402 If your framework is not represented, feel free to create your own package - it is a simple wrapper around the framework renderer and `page.elementLocator` API. We will add a link to it on this page. Make sure it has a name starting with `vitest-browser-`. 403 403 404 - Besides rendering components and locating elements, you will also need to make assertions. Vitest bundles the [`@testing-library/jest-dom`](https://github.com/testing-library/jest-dom) library to provide a wide range of DOM assertions out of the box. Read more at the [Assertions API](/guide/browser/assertion-api). 404 + Besides rendering components and locating elements, you will also need to make assertions. Vitest forks the [`@testing-library/jest-dom`](https://github.com/testing-library/jest-dom) library to provide a wide range of DOM assertions out of the box. Read more at the [Assertions API](/guide/browser/assertion-api). 405 405 406 406 ```ts 407 407 import { expect } from 'vitest'
+6 -2
docs/guide/browser/locators.md
··· 5 5 6 6 # Locators 7 7 8 - A locator is a representation of an element or a number of elements. Every locator is defined by a string called a selector. Vitest abstracts this selector by providing convenient methods that generate those selectors behind the scenes. 8 + A locator is a representation of an element or a number of elements. Every locator is defined by a string called a selector. Vitest abstracts this selector by providing convenient methods that generate them behind the scenes. 9 9 10 - The locator API uses a fork of [Playwright's locators](https://playwright.dev/docs/api/class-locator) called [Ivya](https://npmjs.com/ivya). However, Vitest provides this API to every [provider](/guide/browser/config.html#browser-provider). 10 + The locator API uses a fork of [Playwright's locators](https://playwright.dev/docs/api/class-locator) called [Ivya](https://npmjs.com/ivya). However, Vitest provides this API to every [provider](/guide/browser/config.html#browser-provider), not just playwright. 11 + 12 + ::: tip 13 + This page covers API usage. To better understand locators and their usage, read [Playwright's "Locators" documentation](https://playwright.dev/docs/locators). 14 + ::: 11 15 12 16 ## getByRole 13 17
+16
packages/expect/src/jest-matcher-utils.ts
··· 107 107 printReceived: typeof printReceived 108 108 printExpected: typeof printExpected 109 109 printDiffOrStringify: typeof printDiffOrStringify 110 + printWithType: typeof printWithType 110 111 } { 111 112 return { 112 113 EXPECTED_COLOR, ··· 120 121 printReceived, 121 122 printExpected, 122 123 printDiffOrStringify, 124 + printWithType, 123 125 } 126 + } 127 + 128 + export function printWithType<T>( 129 + name: string, 130 + value: T, 131 + print: (value: T) => string, 132 + ): string { 133 + const type = getType(value) 134 + const hasType 135 + = type !== 'null' && type !== 'undefined' 136 + ? `${name} has type: ${type}\n` 137 + : '' 138 + const hasValue = `${name} has value: ${print(value)}` 139 + return hasType + hasValue 124 140 } 125 141 126 142 export function addCustomEqualityTesters(newTesters: Array<Tester>): void {
+21
test/browser/specs/expect-dom.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + import { instances, runBrowserTests } from './utils' 3 + 4 + const testNames = Object.keys(import.meta.glob('../fixtures/expect-dom/*.test.ts', { 5 + eager: false, 6 + })).map(path => path.slice('../fixtures/expect-dom/'.length)) 7 + 8 + test('expect-dom works correctly', async () => { 9 + const { stderr, stdout } = await runBrowserTests({ 10 + root: './fixtures/expect-dom', 11 + }) 12 + 13 + expect(stderr).toReportNoErrors() 14 + instances.forEach(({ browser }) => { 15 + testNames.forEach((name) => { 16 + expect(stdout).toReportPassedTest(name, browser) 17 + }) 18 + }) 19 + 20 + expect(stdout).toContain(`Test Files ${instances.length * testNames.length} passed`) 21 + })
+8
packages/browser/src/node/plugin.ts
··· 505 505 }, 506 506 injectTo: 'head' as const, 507 507 }, 508 + { 509 + tag: 'script', 510 + attrs: { 511 + type: 'module', 512 + src: parentServer.matchersUrl, 513 + }, 514 + injectTo: 'head' as const, 515 + }, 508 516 parentServer.locatorsUrl 509 517 ? { 510 518 tag: 'script',
-1
packages/browser/src/node/project.ts
··· 19 19 export class ProjectBrowser implements IProjectBrowser { 20 20 public testerHtml: Promise<string> | string 21 21 public testerFilepath: string 22 - public locatorsUrl: string | undefined 23 22 24 23 public provider!: BrowserProvider 25 24 public vitest: Vitest
+2
packages/browser/src/node/projectParent.ts
··· 35 35 public injectorJs: Promise<string> | string 36 36 public errorCatcherUrl: string 37 37 public locatorsUrl: string | undefined 38 + public matchersUrl: string 38 39 public stateJs: Promise<string> | string 39 40 40 41 public commands: Record<string, BrowserCommand<any>> = {} ··· 131 132 if (builtinProviders.includes(providerName)) { 132 133 this.locatorsUrl = join('/@fs/', distRoot, 'locators', `${providerName}.js`) 133 134 } 135 + this.matchersUrl = join('/@fs/', distRoot, 'expect-element.js') 134 136 this.stateJs = readFile( 135 137 resolve(distRoot, 'state.js'), 136 138 'utf-8',
+1 -1
packages/vitest/src/node/project.ts
··· 521 521 [ 522 522 ...MocksPlugins({ 523 523 filter(id) { 524 - if (id.includes(distRoot)) { 524 + if (id.includes(distRoot) || id.includes(browser.vite.config.cacheDir)) { 525 525 return false 526 526 } 527 527 return true
+21
test/browser/fixtures/expect-dom/setup.ts
··· 1 + import { expect } from 'vitest'; 2 + 3 + // Valid string terminator sequences are BEL, ESC\, and 0x9c 4 + const ST = '(?:\\u0007|\\u001B\\u005C|\\u009C)'; 5 + const pattern = [ 6 + `[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?${ST})`, 7 + '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))', 8 + ].join('|'); 9 + 10 + const ansiRegexp = new RegExp(pattern, 'g'); 11 + 12 + function stripAnsi(string: string) { 13 + return string.replace(ansiRegexp, ''); 14 + } 15 + 16 + expect.addSnapshotSerializer({ 17 + test: (val) => typeof val === 'string' || val instanceof Error, 18 + print: (val: string | Error) => typeof val === 'string' 19 + ? stripAnsi(val) 20 + : `[${val.name}: ${stripAnsi(val.message)}]`, 21 + })
+207
test/browser/fixtures/expect-dom/toBeChecked.test.ts
··· 1 + import { describe, expect, test } from 'vitest' 2 + import { render } from './utils' 3 + 4 + describe('.toBeChecked', () => { 5 + test('handles checkbox input', () => { 6 + const {queryByTestId} = render(` 7 + <input type="checkbox" checked data-testid="input-checkbox-checked" /> 8 + <input type="checkbox" data-testid="input-checkbox-unchecked" /> 9 + `) 10 + 11 + expect(queryByTestId('input-checkbox-checked')).toBeChecked() 12 + expect(queryByTestId('input-checkbox-unchecked')).not.toBeChecked() 13 + }) 14 + 15 + test('handles radio input', () => { 16 + const {queryByTestId} = render(` 17 + <input type="radio" checked value="foo" data-testid="input-radio-checked" /> 18 + <input type="radio" value="foo" data-testid="input-radio-unchecked" /> 19 + `) 20 + 21 + expect(queryByTestId('input-radio-checked')).toBeChecked() 22 + expect(queryByTestId('input-radio-unchecked')).not.toBeChecked() 23 + }) 24 + 25 + test('handles element with role="checkbox"', () => { 26 + const {queryByTestId} = render(` 27 + <div role="checkbox" aria-checked="true" data-testid="aria-checkbox-checked" /> 28 + <div role="checkbox" aria-checked="false" data-testid="aria-checkbox-unchecked" /> 29 + `) 30 + 31 + expect(queryByTestId('aria-checkbox-checked')).toBeChecked() 32 + expect(queryByTestId('aria-checkbox-unchecked')).not.toBeChecked() 33 + }) 34 + 35 + test('handles element with role="radio"', () => { 36 + const {queryByTestId} = render(` 37 + <div role="radio" aria-checked="true" data-testid="aria-radio-checked" /> 38 + <div role="radio" aria-checked="false" data-testid="aria-radio-unchecked" /> 39 + `) 40 + 41 + expect(queryByTestId('aria-radio-checked')).toBeChecked() 42 + expect(queryByTestId('aria-radio-unchecked')).not.toBeChecked() 43 + }) 44 + 45 + test('handles element with role="switch"', () => { 46 + const {queryByTestId} = render(` 47 + <div role="switch" aria-checked="true" data-testid="aria-switch-checked" /> 48 + <div role="switch" aria-checked="false" data-testid="aria-switch-unchecked" /> 49 + `) 50 + 51 + expect(queryByTestId('aria-switch-checked')).toBeChecked() 52 + expect(queryByTestId('aria-switch-unchecked')).not.toBeChecked() 53 + }) 54 + 55 + test('handles element with role="menuitemcheckbox"', () => { 56 + const {queryByTestId} = render(` 57 + <div role="menuitemcheckbox" aria-checked="true" data-testid="aria-menuitemcheckbox-checked" /> 58 + <div role="menuitemcheckbox" aria-checked="false" data-testid="aria-menuitemcheckbox-unchecked" /> 59 + `) 60 + 61 + expect(queryByTestId('aria-menuitemcheckbox-checked')).toBeChecked() 62 + expect(queryByTestId('aria-menuitemcheckbox-unchecked')).not.toBeChecked() 63 + }) 64 + 65 + test('throws when checkbox input is checked but expected not to be', () => { 66 + const {queryByTestId} = render( 67 + `<input type="checkbox" checked data-testid="input-checked" />`, 68 + ) 69 + 70 + expect(() => 71 + expect(queryByTestId('input-checked')).not.toBeChecked(), 72 + ).toThrowError() 73 + }) 74 + 75 + test('throws when input checkbox is not checked but expected to be', () => { 76 + const {queryByTestId} = render( 77 + `<input type="checkbox" data-testid="input-empty" />`, 78 + ) 79 + 80 + expect(() => 81 + expect(queryByTestId('input-empty')).toBeChecked(), 82 + ).toThrowError() 83 + }) 84 + 85 + test('throws when element with role="checkbox" is checked but expected not to be', () => { 86 + const {queryByTestId} = render( 87 + `<div role="checkbox" aria-checked="true" data-testid="aria-checkbox-checked" />`, 88 + ) 89 + 90 + expect(() => 91 + expect(queryByTestId('aria-checkbox-checked')).not.toBeChecked(), 92 + ).toThrowError() 93 + }) 94 + 95 + test('throws when element with role="checkbox" is not checked but expected to be', () => { 96 + const {queryByTestId} = render( 97 + `<div role="checkbox" aria-checked="false" data-testid="aria-checkbox-unchecked" />`, 98 + ) 99 + 100 + expect(() => 101 + expect(queryByTestId('aria-checkbox-unchecked')).toBeChecked(), 102 + ).toThrowError() 103 + }) 104 + 105 + test('throws when radio input is checked but expected not to be', () => { 106 + const {queryByTestId} = render( 107 + `<input type="radio" checked data-testid="input-radio-checked" />`, 108 + ) 109 + 110 + expect(() => 111 + expect(queryByTestId('input-radio-checked')).not.toBeChecked(), 112 + ).toThrowError() 113 + }) 114 + 115 + test('throws when input radio is not checked but expected to be', () => { 116 + const {queryByTestId} = render( 117 + `<input type="radio" data-testid="input-radio-unchecked" />`, 118 + ) 119 + 120 + expect(() => 121 + expect(queryByTestId('input-radio-unchecked')).toBeChecked(), 122 + ).toThrowError() 123 + }) 124 + 125 + test('throws when element with role="radio" is checked but expected not to be', () => { 126 + const {queryByTestId} = render( 127 + `<div role="radio" aria-checked="true" data-testid="aria-radio-checked" />`, 128 + ) 129 + 130 + expect(() => 131 + expect(queryByTestId('aria-radio-checked')).not.toBeChecked(), 132 + ).toThrowError() 133 + }) 134 + 135 + test('throws when element with role="radio" is not checked but expected to be', () => { 136 + const {queryByTestId} = render( 137 + `<div role="radio" aria-checked="false" data-testid="aria-radio-unchecked" />`, 138 + ) 139 + 140 + expect(() => 141 + expect(queryByTestId('aria-checkbox-unchecked')).toBeChecked(), 142 + ).toThrowError() 143 + }) 144 + 145 + test('throws when element with role="switch" is checked but expected not to be', () => { 146 + const {queryByTestId} = render( 147 + `<div role="switch" aria-checked="true" data-testid="aria-switch-checked" />`, 148 + ) 149 + 150 + expect(() => 151 + expect(queryByTestId('aria-switch-checked')).not.toBeChecked(), 152 + ).toThrowError() 153 + }) 154 + 155 + test('throws when element with role="switch" is not checked but expected to be', () => { 156 + const {queryByTestId} = render( 157 + `<div role="switch" aria-checked="false" data-testid="aria-switch-unchecked" />`, 158 + ) 159 + 160 + expect(() => 161 + expect(queryByTestId('aria-switch-unchecked')).toBeChecked(), 162 + ).toThrowError() 163 + }) 164 + 165 + test('throws when element with role="checkbox" has an invalid aria-checked attribute', () => { 166 + const {queryByTestId} = render( 167 + `<div role="checkbox" aria-checked="something" data-testid="aria-checkbox-invalid" />`, 168 + ) 169 + 170 + expect(() => 171 + expect(queryByTestId('aria-checkbox-invalid')).toBeChecked(), 172 + ).toThrowError( 173 + /only inputs with .* a valid aria-checked attribute can be used/, 174 + ) 175 + }) 176 + 177 + test('throws when element with role="radio" has an invalid aria-checked attribute', () => { 178 + const {queryByTestId} = render( 179 + `<div role="radio" aria-checked="something" data-testid="aria-radio-invalid" />`, 180 + ) 181 + 182 + expect(() => 183 + expect(queryByTestId('aria-radio-invalid')).toBeChecked(), 184 + ).toThrowError( 185 + /only inputs with .* a valid aria-checked attribute can be used/, 186 + ) 187 + }) 188 + 189 + test('throws when element with role="switch" has an invalid aria-checked attribute', () => { 190 + const {queryByTestId} = render( 191 + `<div role="switch" aria-checked="something" data-testid="aria-switch-invalid" />`, 192 + ) 193 + 194 + expect(() => 195 + expect(queryByTestId('aria-switch-invalid')).toBeChecked(), 196 + ).toThrowError( 197 + /only inputs with .* a valid aria-checked attribute can be used/, 198 + ) 199 + }) 200 + 201 + test('throws when the element is not an input', () => { 202 + const {queryByTestId} = render(`<select data-testid="select"></select>`) 203 + expect(() => expect(queryByTestId('select')).toBeChecked()).toThrowError( 204 + /only inputs with type="checkbox" or type="radio" or elements with.* role="checkbox".* role="menuitemcheckbox".* role="option".* role="radio".* role="switch".* role="menuitemradio".* role="treeitem" .* can be used/, 205 + ) 206 + }) 207 + })
+291
test/browser/fixtures/expect-dom/toBeDisabled.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + import { render } from './utils' 3 + 4 + const window = document.defaultView 5 + 6 + window.customElements.define( 7 + 'custom-element', 8 + class extends window.HTMLElement {}, 9 + ) 10 + 11 + test('.toBeDisabled', () => { 12 + const {queryByTestId} = render(` 13 + <div> 14 + <button disabled={true} data-testid="button-element">x</button> 15 + <textarea disabled={true} data-testid="textarea-element"></textarea> 16 + <input type="checkbox" disabled={true} data-testid="input-element" /> 17 + 18 + <fieldset disabled={true} data-testid="fieldset-element"> 19 + <button data-testid="fieldset-child-element">x</button> 20 + </fieldset> 21 + 22 + <div disabled={true} data-testid="div-element"> 23 + <button data-testid="div-child-element">x</button> 24 + </div> 25 + 26 + <fieldset disabled={true}> 27 + <div> 28 + <button data-testid="nested-form-element">x</button> 29 + 30 + <select data-testid="deep-select-element"> 31 + <optgroup data-testid="deep-optgroup-element"> 32 + <option data-testid="deep-option-element">x</option> 33 + </optgroup> 34 + </select> 35 + </div> 36 + <a href="http://github.com" data-testid="deep-a-element">x</a> 37 + </fieldset> 38 + 39 + <a href="http://github.com" disabled={true} data-testid="a-element">x</a> 40 + </div> 41 + `) 42 + 43 + expect(queryByTestId('button-element')).toBeDisabled() 44 + expect(() => 45 + expect(queryByTestId('button-element')).not.toBeDisabled(), 46 + ).toThrowError() 47 + expect(queryByTestId('textarea-element')).toBeDisabled() 48 + expect(queryByTestId('input-element')).toBeDisabled() 49 + 50 + // technically, everything inside a disabled fieldset is disabled, 51 + // but the fieldset itself is not considered disabled, because its 52 + // native tag is not part of 53 + // https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings 54 + // NOTE: this is different from jest-dom, but closer to how PW works 55 + expect(queryByTestId('fieldset-element')).not.toBeDisabled() 56 + expect(queryByTestId('fieldset-child-element')).toBeDisabled() 57 + 58 + expect(queryByTestId('div-element')).not.toBeDisabled() 59 + expect(queryByTestId('div-child-element')).not.toBeDisabled() 60 + 61 + expect(queryByTestId('nested-form-element')).toBeDisabled() 62 + expect(queryByTestId('deep-select-element')).toBeDisabled() 63 + expect(queryByTestId('deep-optgroup-element')).toBeDisabled() 64 + expect(queryByTestId('deep-option-element')).toBeDisabled() 65 + 66 + expect(queryByTestId('a-element')).not.toBeDisabled() 67 + expect(queryByTestId('deep-a-element')).not.toBeDisabled() 68 + expect(() => expect(queryByTestId('a-element')).toBeDisabled()).toThrowError() 69 + expect(() => 70 + expect(queryByTestId('deep-a-element')).toBeDisabled(), 71 + ).toThrowError() 72 + }) 73 + 74 + test('.toBeDisabled fieldset>legend', () => { 75 + const {queryByTestId} = render(` 76 + <div> 77 + <fieldset disabled={true}> 78 + <button data-testid="inherited-element">x</button> 79 + </fieldset> 80 + 81 + <fieldset disabled={true}> 82 + <legend> 83 + <button data-testid="inside-legend-element">x</button> 84 + </legend> 85 + </fieldset> 86 + 87 + <fieldset disabled={true}> 88 + <legend> 89 + <div> 90 + <button data-testid="nested-inside-legend-element">x</button> 91 + </div> 92 + </legend> 93 + </fieldset> 94 + 95 + <fieldset disabled={true}> 96 + <div></div> 97 + <legend> 98 + <button data-testid="first-legend-element">x</button> 99 + </legend> 100 + <legend> 101 + <button data-testid="second-legend-element">x</button> 102 + </legend> 103 + </fieldset> 104 + 105 + <fieldset disabled={true}> 106 + <fieldset> 107 + <legend> 108 + <button data-testid="outer-fieldset-element">x</button> 109 + </legend> 110 + </fieldset> 111 + </fieldset> 112 + </div> 113 + `) 114 + 115 + expect(queryByTestId('inherited-element')).toBeDisabled() 116 + expect(queryByTestId('inside-legend-element')).not.toBeDisabled() 117 + expect(queryByTestId('nested-inside-legend-element')).not.toBeDisabled() 118 + 119 + expect(queryByTestId('first-legend-element')).not.toBeDisabled() 120 + expect(queryByTestId('second-legend-element')).toBeDisabled() 121 + 122 + expect(queryByTestId('outer-fieldset-element')).toBeDisabled() 123 + }) 124 + 125 + test('.toBeDisabled custom element', () => { 126 + const {queryByTestId} = render(` 127 + <custom-element data-testid="disabled-custom-element" disabled=""></custom-element> 128 + <custom-element data-testid="enabled-custom-element"></custom-element> 129 + `) 130 + 131 + expect(queryByTestId('disabled-custom-element')).toBeDisabled() 132 + expect(() => { 133 + expect(queryByTestId('disabled-custom-element')).not.toBeDisabled() 134 + }).toThrowError('element is disabled') 135 + 136 + expect(queryByTestId('enabled-custom-element')).not.toBeDisabled() 137 + expect(() => { 138 + expect(queryByTestId('enabled-custom-element')).toBeDisabled() 139 + }).toThrowError('element is not disabled') 140 + }) 141 + 142 + test('.toBeEnabled', () => { 143 + const {queryByTestId} = render(` 144 + <div> 145 + <button disabled={true} data-testid="button-element">x</button> 146 + <textarea disabled={true} data-testid="textarea-element"></textarea> 147 + <input type="checkbox" disabled={true} data-testid="input-element" /> 148 + 149 + <fieldset disabled={true} data-testid="fieldset-element"> 150 + <button data-testid="fieldset-child-element">x</button> 151 + </fieldset> 152 + 153 + <div disabled={true} data-testid="div-element"> 154 + <button data-testid="div-child-element">x</button> 155 + </div> 156 + 157 + <fieldset disabled={true}> 158 + <div> 159 + <button data-testid="nested-form-element">x</button> 160 + 161 + <select data-testid="deep-select-element"> 162 + <optgroup data-testid="deep-optgroup-element"> 163 + <option data-testid="deep-option-element">x</option> 164 + </optgroup> 165 + </select> 166 + </div> 167 + <a href="http://github.com" data-testid="deep-a-element">x</a> 168 + </fieldset> 169 + 170 + <a href="http://github.com" disabled={true} data-testid="a-element">x</a> 171 + </div> 172 + `) 173 + 174 + expect(() => { 175 + expect(queryByTestId('button-element')).toBeEnabled() 176 + }).toThrowError() 177 + expect(queryByTestId('button-element')).not.toBeEnabled() 178 + expect(() => { 179 + expect(queryByTestId('textarea-element')).toBeEnabled() 180 + }).toThrowError() 181 + expect(() => { 182 + expect(queryByTestId('input-element')).toBeEnabled() 183 + }).toThrowError() 184 + 185 + expect(() => { 186 + // fieldset elements can't be considered disabled, only their children 187 + expect(queryByTestId('fieldset-element')).toBeDisabled() 188 + }).toThrowError() 189 + expect(() => { 190 + expect(queryByTestId('fieldset-child-element')).toBeEnabled() 191 + }).toThrowError() 192 + 193 + expect(queryByTestId('div-element')).toBeEnabled() 194 + expect(queryByTestId('div-child-element')).toBeEnabled() 195 + 196 + expect(() => { 197 + expect(queryByTestId('nested-form-element')).toBeEnabled() 198 + }).toThrowError() 199 + expect(() => { 200 + expect(queryByTestId('deep-select-element')).toBeEnabled() 201 + }).toThrowError() 202 + expect(() => { 203 + expect(queryByTestId('deep-optgroup-element')).toBeEnabled() 204 + }).toThrowError() 205 + expect(() => { 206 + expect(queryByTestId('deep-option-element')).toBeEnabled() 207 + }).toThrowError() 208 + 209 + expect(queryByTestId('a-element')).toBeEnabled() 210 + expect(() => 211 + expect(queryByTestId('a-element')).not.toBeEnabled(), 212 + ).toThrowError() 213 + expect(queryByTestId('deep-a-element')).toBeEnabled() 214 + expect(() => 215 + expect(queryByTestId('deep-a-element')).not.toBeEnabled(), 216 + ).toThrowError() 217 + }) 218 + 219 + test('.toBeEnabled fieldset>legend', () => { 220 + const {queryByTestId} = render(` 221 + <div> 222 + <fieldset disabled={true}> 223 + <button data-testid="inherited-element">x</button> 224 + </fieldset> 225 + 226 + <fieldset disabled={true}> 227 + <legend> 228 + <button data-testid="inside-legend-element">x</button> 229 + </legend> 230 + </fieldset> 231 + 232 + <fieldset disabled={true}> 233 + <legend> 234 + <div> 235 + <button data-testid="nested-inside-legend-element">x</button> 236 + </div> 237 + </legend> 238 + </fieldset> 239 + 240 + <fieldset disabled={true}> 241 + <div></div> 242 + <legend> 243 + <button data-testid="first-legend-element">x</button> 244 + </legend> 245 + <legend> 246 + <button data-testid="second-legend-element">x</button> 247 + </legend> 248 + </fieldset> 249 + 250 + <fieldset disabled={true}> 251 + <fieldset> 252 + <legend> 253 + <button data-testid="outer-fieldset-element">x</button> 254 + </legend> 255 + </fieldset> 256 + </fieldset> 257 + </div> 258 + `) 259 + 260 + expect(() => { 261 + expect(queryByTestId('inherited-element')).toBeEnabled() 262 + }).toThrowError() 263 + expect(queryByTestId('inside-legend-element')).toBeEnabled() 264 + expect(queryByTestId('nested-inside-legend-element')).toBeEnabled() 265 + 266 + expect(queryByTestId('first-legend-element')).toBeEnabled() 267 + expect(() => { 268 + expect(queryByTestId('second-legend-element')).toBeEnabled() 269 + }).toThrowError() 270 + 271 + expect(() => { 272 + expect(queryByTestId('outer-fieldset-element')).toBeEnabled() 273 + }).toThrowError() 274 + }) 275 + 276 + test('.toBeEnabled custom element', () => { 277 + const {queryByTestId} = render(` 278 + <custom-element data-testid="disabled-custom-element" disabled=""></custom-element> 279 + <custom-element data-testid="enabled-custom-element"></custom-element> 280 + `) 281 + 282 + expect(queryByTestId('disabled-custom-element')).not.toBeEnabled() 283 + expect(() => { 284 + expect(queryByTestId('disabled-custom-element')).toBeEnabled() 285 + }).toThrowError('element is not enabled') 286 + 287 + expect(queryByTestId('enabled-custom-element')).toBeEnabled() 288 + expect(() => { 289 + expect(queryByTestId('enabled-custom-element')).not.toBeEnabled() 290 + }).toThrowError('element is enabled') 291 + })
+63
test/browser/fixtures/expect-dom/toBeEmptyDOMElement.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + import { render } from './utils' 3 + 4 + test('.toBeEmptyDOMElement', () => { 5 + const {queryByTestId} = render(` 6 + <span data-testid="not-empty"> 7 + <span data-testid="empty"></span> 8 + <svg data-testid="svg-empty"></svg> 9 + </span> 10 + <span data-testid="with-comment"><!-- This Comment --></span> 11 + <span data-testid="with-multiple-comments"><!-- Comment1 --><!-- Comment2 --></span> 12 + <span data-testid="with-element"><span></span></span> 13 + <span data-testid="with-element-and-comment"><!--Comment--><span></span></span> 14 + <span data-testid="with-whitespace"> </span> 15 + <span data-testid="with-text">Text</span>`) 16 + 17 + const empty = queryByTestId('empty') 18 + const notEmpty = queryByTestId('not-empty') 19 + const svgEmpty = queryByTestId('svg-empty') 20 + const withComment = queryByTestId('with-comment') 21 + const withMultipleComments = queryByTestId('with-multiple-comments') 22 + const withElement = queryByTestId('with-element') 23 + const withElementAndComment = queryByTestId('with-element-and-comment') 24 + const withWhitespace = queryByTestId('with-whitespace') 25 + const withText = queryByTestId('with-whitespace') 26 + const nonExistantElement = queryByTestId('not-exists') 27 + const fakeElement = {thisIsNot: 'an html element'} 28 + 29 + expect(empty).toBeEmptyDOMElement() 30 + expect(svgEmpty).toBeEmptyDOMElement() 31 + expect(notEmpty).not.toBeEmptyDOMElement() 32 + expect(withComment).toBeEmptyDOMElement() 33 + expect(withMultipleComments).toBeEmptyDOMElement() 34 + expect(withElement).not.toBeEmptyDOMElement() 35 + expect(withElementAndComment).not.toBeEmptyDOMElement() 36 + expect(withWhitespace).not.toBeEmptyDOMElement() 37 + expect(withText).not.toBeEmptyDOMElement() 38 + 39 + // negative test cases wrapped in throwError assertions for coverage. 40 + expect(() => expect(empty).not.toBeEmptyDOMElement()).toThrowError() 41 + 42 + expect(() => expect(svgEmpty).not.toBeEmptyDOMElement()).toThrowError() 43 + 44 + expect(() => expect(notEmpty).toBeEmptyDOMElement()).toThrowError() 45 + 46 + expect(() => expect(withComment).not.toBeEmptyDOMElement()).toThrowError() 47 + 48 + expect(() => expect(withMultipleComments).not.toBeEmptyDOMElement()).toThrowError() 49 + 50 + expect(() => expect(withElement).toBeEmptyDOMElement()).toThrowError() 51 + 52 + expect(() => expect(withElementAndComment).toBeEmptyDOMElement()).toThrowError() 53 + 54 + expect(() => expect(withWhitespace).toBeEmptyDOMElement()).toThrowError() 55 + 56 + expect(() => expect(withText).toBeEmptyDOMElement()).toThrowError() 57 + 58 + expect(() => expect(fakeElement).toBeEmptyDOMElement()).toThrowError() 59 + 60 + expect(() => { 61 + expect(nonExistantElement).toBeEmptyDOMElement() 62 + }).toThrowError() 63 + })
+63
test/browser/fixtures/expect-dom/toBeInTheDocument.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + 3 + test('.toBeInTheDocument', () => { 4 + const window = document.defaultView 5 + 6 + window.customElements.define( 7 + 'custom-element-document', 8 + class extends window.HTMLElement { 9 + constructor() { 10 + super() 11 + this.attachShadow({mode: 'open'}).innerHTML = 12 + '<div data-testid="custom-element-child"></div>' 13 + } 14 + }, 15 + ) 16 + 17 + document.body.innerHTML = ` 18 + <span data-testid="html-element"><span>Html Element</span></span> 19 + <svg data-testid="svg-element"></svg> 20 + <custom-element-document data-testid="custom-element"></custom-element-document>` 21 + 22 + const htmlElement = document.querySelector('[data-testid="html-element"]') 23 + const svgElement = document.querySelector('[data-testid="svg-element"]') 24 + const customElementChild = document 25 + .querySelector('[data-testid="custom-element"]') 26 + .shadowRoot.querySelector('[data-testid="custom-element-child"]') 27 + const detachedElement = document.createElement('div') 28 + const fakeElement = {thisIsNot: 'an html element'} 29 + const undefinedElement = undefined 30 + const nullElement = null 31 + 32 + expect(htmlElement).toBeInTheDocument() 33 + expect(svgElement).toBeInTheDocument() 34 + expect(customElementChild).toBeInTheDocument() 35 + expect(detachedElement).not.toBeInTheDocument() 36 + expect(nullElement).not.toBeInTheDocument() 37 + 38 + // negative test cases wrapped in throwError assertions for coverage. 39 + const expectToBe = /expect.*\.toBeInTheDocument/ 40 + const expectNotToBe = /expect.*not\.toBeInTheDocument/ 41 + const userInputNode = /an HTMLElement or an SVGElement/ 42 + expect(() => expect(htmlElement).not.toBeInTheDocument()).toThrowError( 43 + expectNotToBe, 44 + ) 45 + expect(() => expect(svgElement).not.toBeInTheDocument()).toThrowError( 46 + expectNotToBe, 47 + ) 48 + expect(() => expect(detachedElement).toBeInTheDocument()).toThrowError( 49 + expectToBe, 50 + ) 51 + expect(() => expect(fakeElement).toBeInTheDocument()).toThrowError( 52 + userInputNode, 53 + ) 54 + expect(() => expect(nullElement).toBeInTheDocument()).toThrowError( 55 + userInputNode, 56 + ) 57 + expect(() => expect(undefinedElement).toBeInTheDocument()).toThrowError( 58 + userInputNode, 59 + ) 60 + expect(() => expect(undefinedElement).not.toBeInTheDocument()).toThrowError( 61 + userInputNode, 62 + ) 63 + })
+180
test/browser/fixtures/expect-dom/toBeInvalid.test.ts
··· 1 + import { describe, expect, test } from 'vitest' 2 + import { render } from './utils' 3 + 4 + function getDOMElement(htmlString: string, selector: string) { 5 + const doc = document.implementation.createHTMLDocument('') 6 + doc.body.innerHTML = htmlString 7 + return doc.querySelector(selector) 8 + } 9 + 10 + // A required field without a value is invalid 11 + const invalidInputHtml = `<input required>` 12 + 13 + const invalidInputNode = getDOMElement(invalidInputHtml, 'input') 14 + 15 + // A form is invalid if it contains an invalid input 16 + const invalidFormHtml = `<form>${invalidInputHtml}</form>` 17 + 18 + const invalidFormNode = getDOMElement(invalidFormHtml, 'form') 19 + 20 + describe('.toBeInvalid', () => { 21 + test('handles <input/>', () => { 22 + const {queryByTestId} = render(` 23 + <div> 24 + <input data-testid="no-aria-invalid"> 25 + <input data-testid="aria-invalid" aria-invalid> 26 + <input data-testid="aria-invalid-value" aria-invalid="true"> 27 + <input data-testid="aria-invalid-false" aria-invalid="false"> 28 + </div> 29 + `) 30 + 31 + expect(queryByTestId('no-aria-invalid')).not.toBeInvalid() 32 + expect(queryByTestId('aria-invalid')).toBeInvalid() 33 + expect(queryByTestId('aria-invalid-value')).toBeInvalid() 34 + expect(queryByTestId('aria-invalid-false')).not.toBeInvalid() 35 + expect(invalidInputNode).toBeInvalid() 36 + 37 + // negative test cases wrapped in throwError assertions for coverage. 38 + expect(() => 39 + expect(queryByTestId('no-aria-invalid')).toBeInvalid(), 40 + ).toThrowError() 41 + expect(() => 42 + expect(queryByTestId('aria-invalid')).not.toBeInvalid(), 43 + ).toThrowError() 44 + expect(() => 45 + expect(queryByTestId('aria-invalid-value')).not.toBeInvalid(), 46 + ).toThrowError() 47 + expect(() => 48 + expect(queryByTestId('aria-invalid-false')).toBeInvalid(), 49 + ).toThrowError() 50 + expect(() => expect(invalidInputNode).not.toBeInvalid()).toThrowError() 51 + }) 52 + 53 + test('handles <form/>', () => { 54 + const {queryByTestId} = render(` 55 + <form data-testid="valid"> 56 + <input> 57 + </form> 58 + `) 59 + 60 + expect(queryByTestId('valid')).not.toBeInvalid() 61 + expect(invalidFormNode).toBeInvalid() 62 + 63 + // negative test cases wrapped in throwError assertions for coverage. 64 + expect(() => expect(queryByTestId('valid')).toBeInvalid()).toThrowError() 65 + expect(() => expect(invalidFormNode).not.toBeInvalid()).toThrowError() 66 + }) 67 + 68 + test('handles any element', () => { 69 + const {queryByTestId} = render(` 70 + <ol data-testid="valid"> 71 + <li data-testid="no-aria-invalid" > </li> 72 + <li data-testid="aria-invalid" aria-invalid> </li> 73 + <li data-testid="aria-invalid-value" aria-invalid="true"> </li> 74 + <li data-testid="aria-invalid-false" aria-invalid="false"> </li> 75 + </ol> 76 + `) 77 + 78 + expect(queryByTestId('valid')).not.toBeInvalid() 79 + expect(queryByTestId('no-aria-invalid')).not.toBeInvalid() 80 + expect(queryByTestId('aria-invalid')).toBeInvalid() 81 + expect(queryByTestId('aria-invalid-value')).toBeInvalid() 82 + expect(queryByTestId('aria-invalid-false')).not.toBeInvalid() 83 + 84 + // negative test cases wrapped in throwError assertions for coverage. 85 + expect(() => expect(queryByTestId('valid')).toBeInvalid()).toThrowError() 86 + expect(() => 87 + expect(queryByTestId('no-aria-invalid')).toBeInvalid(), 88 + ).toThrowError() 89 + expect(() => 90 + expect(queryByTestId('aria-invalid')).not.toBeInvalid(), 91 + ).toThrowError() 92 + expect(() => 93 + expect(queryByTestId('aria-invalid-value')).not.toBeInvalid(), 94 + ).toThrowError() 95 + expect(() => 96 + expect(queryByTestId('aria-invalid-false')).toBeInvalid(), 97 + ).toThrowError() 98 + }) 99 + }) 100 + 101 + describe('.toBeValid', () => { 102 + test('handles <input/>', () => { 103 + const {queryByTestId} = render(` 104 + <div> 105 + <input data-testid="no-aria-invalid"> 106 + <input data-testid="aria-invalid" aria-invalid> 107 + <input data-testid="aria-invalid-value" aria-invalid="true"> 108 + <input data-testid="aria-invalid-false" aria-invalid="false"> 109 + </div> 110 + `) 111 + 112 + expect(queryByTestId('no-aria-invalid')).toBeValid() 113 + expect(queryByTestId('aria-invalid')).not.toBeValid() 114 + expect(queryByTestId('aria-invalid-value')).not.toBeValid() 115 + expect(queryByTestId('aria-invalid-false')).toBeValid() 116 + expect(invalidInputNode).not.toBeValid() 117 + 118 + // negative test cases wrapped in throwError assertions for coverage. 119 + expect(() => 120 + expect(queryByTestId('no-aria-invalid')).not.toBeValid(), 121 + ).toThrowError() 122 + expect(() => 123 + expect(queryByTestId('aria-invalid')).toBeValid(), 124 + ).toThrowError() 125 + expect(() => 126 + expect(queryByTestId('aria-invalid-value')).toBeValid(), 127 + ).toThrowError() 128 + expect(() => 129 + expect(queryByTestId('aria-invalid-false')).not.toBeValid(), 130 + ).toThrowError() 131 + expect(() => expect(invalidInputNode).toBeValid()).toThrowError() 132 + }) 133 + 134 + test('handles <form/>', () => { 135 + const {queryByTestId} = render(` 136 + <form data-testid="valid"> 137 + <input> 138 + </form> 139 + `) 140 + 141 + expect(queryByTestId('valid')).toBeValid() 142 + expect(invalidFormNode).not.toBeValid() 143 + 144 + // negative test cases wrapped in throwError assertions for coverage. 145 + expect(() => expect(queryByTestId('valid')).not.toBeValid()).toThrowError() 146 + expect(() => expect(invalidFormNode).toBeValid()).toThrowError() 147 + }) 148 + 149 + test('handles any element', () => { 150 + const {queryByTestId} = render(` 151 + <ol data-testid="valid"> 152 + <li data-testid="no-aria-invalid" > </li> 153 + <li data-testid="aria-invalid" aria-invalid> </li> 154 + <li data-testid="aria-invalid-value" aria-invalid="true"> </li> 155 + <li data-testid="aria-invalid-false" aria-invalid="false"> </li> 156 + </ol> 157 + `) 158 + 159 + expect(queryByTestId('valid')).toBeValid() 160 + expect(queryByTestId('no-aria-invalid')).toBeValid() 161 + expect(queryByTestId('aria-invalid')).not.toBeValid() 162 + expect(queryByTestId('aria-invalid-value')).not.toBeValid() 163 + expect(queryByTestId('aria-invalid-false')).toBeValid() 164 + 165 + // negative test cases wrapped in throwError assertions for coverage. 166 + expect(() => expect(queryByTestId('valid')).not.toBeValid()).toThrowError() 167 + expect(() => 168 + expect(queryByTestId('no-aria-invalid')).not.toBeValid(), 169 + ).toThrowError() 170 + expect(() => 171 + expect(queryByTestId('aria-invalid')).toBeValid(), 172 + ).toThrowError() 173 + expect(() => 174 + expect(queryByTestId('aria-invalid-value')).toBeValid(), 175 + ).toThrowError() 176 + expect(() => 177 + expect(queryByTestId('aria-invalid-false')).not.toBeValid(), 178 + ).toThrowError() 179 + }) 180 + })
+126
test/browser/fixtures/expect-dom/toBePartiallyChecked.test.ts
··· 1 + import { describe, expect, test } from 'vitest' 2 + import { render } from './utils' 3 + 4 + // FIXME 5 + // playwright prioritizes native checked to aria-checked for "checkbox" elements 6 + // jest-dom checks aria-checked="mixed" anyway 7 + describe('.toBePartiallyChecked', () => { 8 + test('handles input checkbox with aria-checked', () => { 9 + const {queryByTestId} = render(` 10 + <input type="checkbox" aria-checked="mixed" data-testid="checkbox-mixed" /> 11 + <input type="checkbox" checked data-testid="checkbox-checked" /> 12 + <input type="checkbox" data-testid="checkbox-unchecked" /> 13 + `) 14 + 15 + expect(queryByTestId('checkbox-mixed')).toBePartiallyChecked() 16 + expect(queryByTestId('checkbox-checked')).not.toBePartiallyChecked() 17 + expect(queryByTestId('checkbox-unchecked')).not.toBePartiallyChecked() 18 + }) 19 + 20 + test('handles input checkbox set as indeterminate', () => { 21 + const {queryByTestId} = render(` 22 + <input type="checkbox" data-testid="checkbox-mixed" /> 23 + <input type="checkbox" checked data-testid="checkbox-checked" /> 24 + <input type="checkbox" data-testid="checkbox-unchecked" /> 25 + `) 26 + 27 + ;(queryByTestId('checkbox-mixed') as HTMLInputElement).indeterminate = true 28 + 29 + expect(queryByTestId('checkbox-mixed')).toBePartiallyChecked() 30 + expect(queryByTestId('checkbox-checked')).not.toBePartiallyChecked() 31 + expect(queryByTestId('checkbox-unchecked')).not.toBePartiallyChecked() 32 + }) 33 + 34 + test('handles element with role="checkbox"', () => { 35 + const {queryByTestId} = render(` 36 + <div role="checkbox" aria-checked="mixed" data-testid="aria-checkbox-mixed" /> 37 + <div role="checkbox" aria-checked="true" data-testid="aria-checkbox-checked" /> 38 + <div role="checkbox" aria-checked="false" data-testid="aria-checkbox-unchecked" /> 39 + `) 40 + 41 + expect(queryByTestId('aria-checkbox-mixed')).toBePartiallyChecked() 42 + expect(queryByTestId('aria-checkbox-checked')).not.toBePartiallyChecked() 43 + expect(queryByTestId('aria-checkbox-unchecked')).not.toBePartiallyChecked() 44 + }) 45 + 46 + test('throws when input checkbox is mixed but expected not to be', () => { 47 + const {queryByTestId} = render( 48 + `<input type="checkbox" aria-checked="mixed" data-testid="checkbox-mixed" />`, 49 + ) 50 + 51 + expect(() => 52 + expect(queryByTestId('checkbox-mixed')).not.toBePartiallyChecked(), 53 + ).toThrowError() 54 + }) 55 + 56 + test('throws when input checkbox is indeterminate but expected not to be', () => { 57 + const {queryByTestId} = render( 58 + `<input type="checkbox" data-testid="checkbox-mixed" />`, 59 + ) 60 + 61 + ;(queryByTestId('checkbox-mixed') as HTMLInputElement).indeterminate = true 62 + 63 + expect(() => 64 + expect(queryByTestId('input-mixed')).not.toBePartiallyChecked(), 65 + ).toThrowError() 66 + }) 67 + 68 + test('throws when input checkbox is not checked but expected to be', () => { 69 + const {queryByTestId} = render( 70 + `<input type="checkbox" data-testid="checkbox-empty" />`, 71 + ) 72 + 73 + expect(() => 74 + expect(queryByTestId('checkbox-empty')).toBePartiallyChecked(), 75 + ).toThrowError() 76 + }) 77 + 78 + test('throws when element with role="checkbox" is partially checked but expected not to be', () => { 79 + const {queryByTestId} = render( 80 + `<div role="checkbox" aria-checked="mixed" data-testid="aria-checkbox-mixed" />`, 81 + ) 82 + 83 + expect(() => 84 + expect(queryByTestId('aria-checkbox-mixed')).not.toBePartiallyChecked(), 85 + ).toThrowError() 86 + }) 87 + 88 + test('throws when element with role="checkbox" is checked but expected to be partially checked', () => { 89 + const {queryByTestId} = render( 90 + `<div role="checkbox" aria-checked="true" data-testid="aria-checkbox-checked" />`, 91 + ) 92 + 93 + expect(() => 94 + expect(queryByTestId('aria-checkbox-checked')).toBePartiallyChecked(), 95 + ).toThrowError() 96 + }) 97 + 98 + test('throws when element with role="checkbox" is not checked but expected to be', () => { 99 + const {queryByTestId} = render( 100 + `<div role="checkbox" aria-checked="false" data-testid="aria-checkbox" />`, 101 + ) 102 + 103 + expect(() => 104 + expect(queryByTestId('aria-checkbox')).toBePartiallyChecked(), 105 + ).toThrowError() 106 + }) 107 + 108 + test('throws when element with role="checkbox" has an invalid aria-checked attribute', () => { 109 + const {queryByTestId} = render( 110 + `<div role="checkbox" aria-checked="something" data-testid="aria-checkbox-invalid" />`, 111 + ) 112 + 113 + expect(() => 114 + expect(queryByTestId('aria-checkbox-invalid')).toBePartiallyChecked(), 115 + ).toThrowError() 116 + }) 117 + 118 + test('throws when the element is not a checkbox', () => { 119 + const {queryByTestId} = render(`<select data-testid="select"></select>`) 120 + expect(() => 121 + expect(queryByTestId('select')).toBePartiallyChecked(), 122 + ).toThrowError( 123 + 'only inputs with type="checkbox" or elements with role="checkbox" and a valid aria-checked attribute can be used with .toBePartiallyChecked(). Use .toHaveValue() instead', 124 + ) 125 + }) 126 + })
+62
test/browser/fixtures/expect-dom/toBeRequired.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + import { render } from './utils' 3 + 4 + test('.toBeRequired', () => { 5 + const {queryByTestId} = render(` 6 + <div> 7 + <input data-testid="required-input" required> 8 + <input data-testid="aria-required-input" aria-required="true"> 9 + <input data-testid="conflicted-input" required aria-required="false"> 10 + <input data-testid="not-required-input" aria-required="false"> 11 + <input data-testid="basic-input"> 12 + <input data-testid="unsupported-type" type="image" required> 13 + <select data-testid="select" required></select> 14 + <textarea data-testid="textarea" required></textarea> 15 + <div data-testid="supported-role" role="tree" required></div> 16 + <div data-testid="supported-role-aria" role="tree" aria-required="true"></div> 17 + </div> 18 + `) 19 + 20 + expect(queryByTestId('required-input')).toBeRequired() 21 + expect(queryByTestId('aria-required-input')).toBeRequired() 22 + expect(queryByTestId('conflicted-input')).toBeRequired() 23 + expect(queryByTestId('not-required-input')).not.toBeRequired() 24 + expect(queryByTestId('basic-input')).not.toBeRequired() 25 + expect(queryByTestId('unsupported-type')).not.toBeRequired() 26 + expect(queryByTestId('select')).toBeRequired() 27 + expect(queryByTestId('textarea')).toBeRequired() 28 + expect(queryByTestId('supported-role')).not.toBeRequired() 29 + expect(queryByTestId('supported-role-aria')).toBeRequired() 30 + 31 + // negative test cases wrapped in throwError assertions for coverage. 32 + expect(() => 33 + expect(queryByTestId('required-input')).not.toBeRequired(), 34 + ).toThrowError() 35 + expect(() => 36 + expect(queryByTestId('aria-required-input')).not.toBeRequired(), 37 + ).toThrowError() 38 + expect(() => 39 + expect(queryByTestId('conflicted-input')).not.toBeRequired(), 40 + ).toThrowError() 41 + expect(() => 42 + expect(queryByTestId('not-required-input')).toBeRequired(), 43 + ).toThrowError() 44 + expect(() => 45 + expect(queryByTestId('basic-input')).toBeRequired(), 46 + ).toThrowError() 47 + expect(() => 48 + expect(queryByTestId('unsupported-type')).toBeRequired(), 49 + ).toThrowError() 50 + expect(() => 51 + expect(queryByTestId('select')).not.toBeRequired(), 52 + ).toThrowError() 53 + expect(() => 54 + expect(queryByTestId('textarea')).not.toBeRequired(), 55 + ).toThrowError() 56 + expect(() => 57 + expect(queryByTestId('supported-role')).toBeRequired(), 58 + ).toThrowError() 59 + expect(() => 60 + expect(queryByTestId('supported-role-aria')).not.toBeRequired(), 61 + ).toThrowError() 62 + })
+427
test/browser/fixtures/expect-dom/toBeVisible.test.ts
··· 1 + import { afterEach, beforeEach, describe, expect, it } from 'vitest' 2 + import { render } from './utils' 3 + 4 + describe('.toBeVisible', () => { 5 + it('returns the visibility of an element', () => { 6 + const {container} = render(` 7 + <div> 8 + <header> 9 + <h1 style="display: none">Main title</h1> 10 + <h2 style="visibility: hidden">Secondary title</h2> 11 + <h3 style="visibility: collapse">Secondary title</h3> 12 + <h4 style="opacity: 0">Secondary title</h4> 13 + <h5 style="opacity: 0.1">Secondary title</h5> 14 + </header> 15 + <button hidden>Hidden button</button> 16 + <section style="display: block; visibility: hidden"> 17 + <p>Hello <strong>World</strong></p> 18 + </section> 19 + </div> 20 + `) 21 + 22 + expect(container.querySelector('header')).toBeVisible() 23 + expect(container.querySelector('h1')).not.toBeVisible() 24 + expect(container.querySelector('h2')).not.toBeVisible() 25 + expect(container.querySelector('h3')).not.toBeVisible() 26 + expect(container.querySelector('h4')).toBeVisible() // element.checkVisibility() returns true for opacity: 0 27 + expect(container.querySelector('h5')).toBeVisible() 28 + expect(container.querySelector('button')).not.toBeVisible() 29 + expect(container.querySelector('strong')).not.toBeVisible() 30 + 31 + expect(() => 32 + expect(container.querySelector('header')).not.toBeVisible(), 33 + ).toThrowError() 34 + expect(() => 35 + expect(container.querySelector('p')).toBeVisible(), 36 + ).toThrowError() 37 + }) 38 + 39 + it('detached element is not visible', () => { 40 + const subject = document.createElement('div') 41 + expect(subject).not.toBeVisible() 42 + expect(() => expect(subject).toBeVisible()).toThrowError() 43 + }) 44 + 45 + describe('with a <details /> element', () => { 46 + let subject 47 + 48 + afterEach(() => { 49 + subject = undefined 50 + }) 51 + 52 + describe('when the details is opened', () => { 53 + beforeEach(() => { 54 + subject = render(` 55 + <details open> 56 + <summary>Title of visible</summary> 57 + <div>Visible <small>details</small></div> 58 + </details> 59 + `) 60 + }) 61 + 62 + it('returns true to the details content', () => { 63 + expect(subject.container.querySelector('div')).toBeVisible() 64 + }) 65 + 66 + it('returns true to the most inner details content', () => { 67 + expect(subject.container.querySelector('small')).toBeVisible() 68 + }) 69 + 70 + it('returns true to the details summary', () => { 71 + expect(subject.container.querySelector('summary')).toBeVisible() 72 + }) 73 + 74 + describe('when the user clicks on the summary', () => { 75 + beforeEach(() => subject.container.querySelector('summary').click()) 76 + 77 + it('returns false to the details content', () => { 78 + expect(subject.container.querySelector('div')).not.toBeVisible() 79 + }) 80 + 81 + it('returns true to the details summary', () => { 82 + expect(subject.container.querySelector('summary')).toBeVisible() 83 + }) 84 + }) 85 + }) 86 + 87 + describe('when the details is not opened', () => { 88 + beforeEach(() => { 89 + subject = render(` 90 + <details> 91 + <summary>Title of hidden</summary> 92 + <div>Hidden details</div> 93 + </details> 94 + `) 95 + }) 96 + 97 + it('returns false to the details content', () => { 98 + expect(subject.container.querySelector('div')).not.toBeVisible() 99 + }) 100 + 101 + it('returns true to the summary content', () => { 102 + expect(subject.container.querySelector('summary')).toBeVisible() 103 + }) 104 + 105 + describe('when the user clicks on the summary', () => { 106 + beforeEach(() => subject.container.querySelector('summary').click()) 107 + 108 + it('returns true to the details content', () => { 109 + expect(subject.container.querySelector('div')).toBeVisible() 110 + }) 111 + 112 + it('returns true to the details summary', () => { 113 + expect(subject.container.querySelector('summary')).toBeVisible() 114 + }) 115 + }) 116 + }) 117 + 118 + describe('when the details is opened but it is hidden', () => { 119 + beforeEach(() => { 120 + subject = render(` 121 + <details open hidden> 122 + <summary>Title of visible</summary> 123 + <div>Visible details</div> 124 + </details> 125 + `) 126 + }) 127 + 128 + it('returns false to the details content', () => { 129 + expect(subject.container.querySelector('div')).not.toBeVisible() 130 + }) 131 + 132 + it('returns false to the details summary', () => { 133 + expect(subject.container.querySelector('summary')).not.toBeVisible() 134 + }) 135 + }) 136 + 137 + describe('when the <details /> inner text does not have an enclosing element', () => { 138 + describe('when the details is not opened', () => { 139 + beforeEach(() => { 140 + subject = render(` 141 + <details> 142 + <summary>Title of hidden innerText</summary> 143 + hidden innerText 144 + </details> 145 + `) 146 + }) 147 + 148 + it('returns true to the details content', () => { 149 + expect(subject.container.querySelector('details')).toBeVisible() 150 + }) 151 + 152 + it('returns true to the details summary', () => { 153 + expect(subject.container.querySelector('summary')).toBeVisible() 154 + }) 155 + 156 + describe('when the user clicks on the summary', () => { 157 + beforeEach(() => subject.container.querySelector('summary').click()) 158 + 159 + it('returns true to the details content', () => { 160 + expect(subject.container.querySelector('details')).toBeVisible() 161 + }) 162 + 163 + it('returns true to the details summary', () => { 164 + expect(subject.container.querySelector('summary')).toBeVisible() 165 + }) 166 + }) 167 + }) 168 + 169 + describe('when the details is opened', () => { 170 + beforeEach(() => { 171 + subject = render(` 172 + <details open> 173 + <summary>Title of visible innerText</summary> 174 + visible <small>innerText</small> 175 + </details> 176 + `) 177 + }) 178 + 179 + it('returns true to the details content', () => { 180 + expect(subject.container.querySelector('details')).toBeVisible() 181 + }) 182 + 183 + it('returns true to inner small content', () => { 184 + expect(subject.container.querySelector('small')).toBeVisible() 185 + }) 186 + 187 + describe('when the user clicks on the summary', () => { 188 + beforeEach(() => subject.container.querySelector('summary').click()) 189 + 190 + it('returns true to the details content', () => { 191 + expect(subject.container.querySelector('details')).toBeVisible() 192 + }) 193 + 194 + it('returns false to the inner small content', () => { 195 + expect(subject.container.querySelector('small')).not.toBeVisible() 196 + }) 197 + 198 + it('returns true to the details summary', () => { 199 + expect(subject.container.querySelector('summary')).toBeVisible() 200 + }) 201 + }) 202 + }) 203 + }) 204 + 205 + describe('with a nested <details /> element', () => { 206 + describe('when the nested <details /> is opened', () => { 207 + beforeEach(() => { 208 + subject = render(` 209 + <details open> 210 + <summary>Title of visible</summary> 211 + <div>Outer content</div> 212 + <details open> 213 + <summary>Title of nested details</summary> 214 + <div>Inner content</div> 215 + </details> 216 + </details> 217 + `) 218 + }) 219 + 220 + it('returns true to the nested details content', () => { 221 + expect( 222 + subject.container.querySelector('details > details > div'), 223 + ).toBeVisible() 224 + }) 225 + 226 + it('returns true to the nested details summary', () => { 227 + expect( 228 + subject.container.querySelector('details > details > summary'), 229 + ).toBeVisible() 230 + }) 231 + 232 + it('returns true to the outer details content', () => { 233 + expect(subject.container.querySelector('details > div')).toBeVisible() 234 + }) 235 + 236 + it('returns true to the outer details summary', () => { 237 + expect( 238 + subject.container.querySelector('details > summary'), 239 + ).toBeVisible() 240 + }) 241 + }) 242 + 243 + describe('when the nested <details /> is not opened', () => { 244 + beforeEach(() => { 245 + subject = render(` 246 + <details open> 247 + <summary>Title of visible</summary> 248 + <div>Outer content</div> 249 + <details> 250 + <summary>Title of nested details</summary> 251 + <div>Inner content</div> 252 + </details> 253 + </details> 254 + `) 255 + }) 256 + 257 + it('returns false to the nested details content', () => { 258 + expect( 259 + subject.container.querySelector('details > details > div'), 260 + ).not.toBeVisible() 261 + }) 262 + 263 + it('returns true to the nested details summary', () => { 264 + expect( 265 + subject.container.querySelector('details > details > summary'), 266 + ).toBeVisible() 267 + }) 268 + 269 + it('returns true to the outer details content', () => { 270 + expect(subject.container.querySelector('details > div')).toBeVisible() 271 + }) 272 + 273 + it('returns true to the outer details summary', () => { 274 + expect( 275 + subject.container.querySelector('details > summary'), 276 + ).toBeVisible() 277 + }) 278 + }) 279 + 280 + describe('when the outer <details /> is not opened and the nested one is opened', () => { 281 + beforeEach(() => { 282 + subject = render(` 283 + <details> 284 + <summary>Title of visible</summary> 285 + <div>Outer content</div> 286 + <details open> 287 + <summary>Title of nested details</summary> 288 + <div>Inner content</div> 289 + </details> 290 + </details> 291 + `) 292 + }) 293 + 294 + it('returns false to the nested details content', () => { 295 + expect( 296 + subject.container.querySelector('details > details > div'), 297 + ).not.toBeVisible() 298 + }) 299 + 300 + it('returns false to the nested details summary', () => { 301 + expect( 302 + subject.container.querySelector('details > details > summary'), 303 + ).not.toBeVisible() 304 + }) 305 + 306 + it('returns false to the outer details content', () => { 307 + expect( 308 + subject.container.querySelector('details > div'), 309 + ).not.toBeVisible() 310 + }) 311 + 312 + it('returns true to the outer details summary', () => { 313 + expect( 314 + subject.container.querySelector('details > summary'), 315 + ).toBeVisible() 316 + }) 317 + }) 318 + 319 + describe('with nested details (unenclosed outer, enclosed inner)', () => { 320 + describe('when both outer and inner are opened', () => { 321 + beforeEach(() => { 322 + subject = render(` 323 + <details open> 324 + <summary>Title of outer unenclosed</summary> 325 + Unenclosed innerText 326 + <details open> 327 + <summary>Title of inner enclosed</summary> 328 + <div>Enclosed innerText</div> 329 + </details> 330 + </details> 331 + `) 332 + }) 333 + 334 + it('returns true to outer unenclosed innerText', () => { 335 + expect(subject.container.querySelector('details')).toBeVisible() 336 + }) 337 + 338 + it('returns true to outer summary', () => { 339 + expect(subject.container.querySelector('summary')).toBeVisible() 340 + }) 341 + 342 + it('returns true to inner enclosed innerText', () => { 343 + expect( 344 + subject.container.querySelector('details > details > div'), 345 + ).toBeVisible() 346 + }) 347 + 348 + it('returns true to inner summary', () => { 349 + expect( 350 + subject.container.querySelector('details > details > summary'), 351 + ).toBeVisible() 352 + }) 353 + }) 354 + 355 + describe('when outer is opened and inner is not opened', () => { 356 + beforeEach(() => { 357 + subject = render(` 358 + <details open> 359 + <summary>Title of outer unenclosed</summary> 360 + Unenclosed innerText 361 + <details> 362 + <summary>Title of inner enclosed</summary> 363 + <div>Enclosed innerText</div> 364 + </details> 365 + </details> 366 + `) 367 + }) 368 + 369 + it('returns true to outer unenclosed innerText', () => { 370 + expect(subject.container.querySelector('details')).toBeVisible() 371 + }) 372 + 373 + it('returns true to outer summary', () => { 374 + expect(subject.container.querySelector('summary')).toBeVisible() 375 + }) 376 + 377 + it('returns false to inner enclosed innerText', () => { 378 + expect( 379 + subject.container.querySelector('details > details > div'), 380 + ).not.toBeVisible() 381 + }) 382 + 383 + it('returns true to inner summary', () => { 384 + expect( 385 + subject.container.querySelector('details > details > summary'), 386 + ).toBeVisible() 387 + }) 388 + }) 389 + 390 + describe('when outer is not opened and inner is opened', () => { 391 + beforeEach(() => { 392 + subject = render(` 393 + <details> 394 + <summary>Title of outer unenclosed</summary> 395 + Unenclosed innerText 396 + <details open> 397 + <summary>Title of inner enclosed</summary> 398 + <div>Enclosed innerText</div> 399 + </details> 400 + </details> 401 + `) 402 + }) 403 + 404 + it('returns false to outer unenclosed innerText', () => { 405 + expect(subject.container.querySelector('details')).toBeVisible() 406 + }) 407 + 408 + it('returns true to outer summary', () => { 409 + expect(subject.container.querySelector('summary')).toBeVisible() 410 + }) 411 + 412 + it('returns false to inner enclosed innerText', () => { 413 + expect( 414 + subject.container.querySelector('details > details > div'), 415 + ).not.toBeVisible() 416 + }) 417 + 418 + it('returns true to inner summary', () => { 419 + expect( 420 + subject.container.querySelector('details > details > summary'), 421 + ).not.toBeVisible() 422 + }) 423 + }) 424 + }) 425 + }) 426 + }) 427 + })
+69
test/browser/fixtures/expect-dom/toContainElement.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + import { render } from './utils' 3 + 4 + const {queryByTestId} = render(` 5 + <span data-testid="grandparent"> 6 + <span data-testid="parent"> 7 + <span data-testid="child"></span> 8 + </span> 9 + <svg data-testid="svg-element"></svg> 10 + </span> 11 + `) 12 + 13 + const grandparent = queryByTestId('grandparent') 14 + const parent = queryByTestId('parent') 15 + const child = queryByTestId('child') 16 + const svgElement = queryByTestId('svg-element') 17 + const nonExistantElement = queryByTestId('not-exists') 18 + const fakeElement = {thisIsNot: 'an html element'} 19 + 20 + test('.toContainElement positive test cases', () => { 21 + expect(grandparent).toContainElement(parent) 22 + expect(grandparent).toContainElement(child) 23 + expect(grandparent).toContainElement(svgElement) 24 + expect(parent).toContainElement(child) 25 + expect(parent).not.toContainElement(grandparent) 26 + expect(parent).not.toContainElement(svgElement) 27 + expect(child).not.toContainElement(parent) 28 + expect(child).not.toContainElement(grandparent) 29 + expect(child).not.toContainElement(svgElement) 30 + expect(grandparent).not.toContainElement(nonExistantElement) 31 + }) 32 + 33 + test('.toContainElement negative test cases', () => { 34 + expect(() => 35 + expect(nonExistantElement).not.toContainElement(child), 36 + ).toThrowError() 37 + expect(() => expect(parent).toContainElement(grandparent)).toThrowError() 38 + expect(() => 39 + expect(nonExistantElement).toContainElement(grandparent), 40 + ).toThrowError() 41 + expect(() => 42 + expect(grandparent).toContainElement(nonExistantElement), 43 + ).toThrowError() 44 + expect(() => 45 + expect(nonExistantElement).toContainElement(nonExistantElement), 46 + ).toThrowError() 47 + expect(() => 48 + // @ts-expect-error testing invalid assertion 49 + expect(nonExistantElement).toContainElement(fakeElement), 50 + ).toThrowError() 51 + expect(() => 52 + expect(fakeElement).toContainElement(nonExistantElement), 53 + ).toThrowError() 54 + expect(() => 55 + expect(fakeElement).not.toContainElement(nonExistantElement), 56 + ).toThrowError() 57 + expect(() => expect(fakeElement).toContainElement(grandparent)).toThrowError() 58 + // @ts-expect-error testing invalid assertion 59 + expect(() => expect(grandparent).toContainElement(fakeElement)).toThrowError() 60 + // @ts-expect-error testing invalid assertion 61 + expect(() => expect(fakeElement).toContainElement(fakeElement)).toThrowError() 62 + expect(() => expect(grandparent).not.toContainElement(child)).toThrowError() 63 + expect(() => 64 + expect(grandparent).not.toContainElement(svgElement), 65 + ).toThrowError() 66 + expect(() => 67 + expect(grandparent).not.toContainElement(undefined), 68 + ).toThrowError() 69 + })
+120
test/browser/fixtures/expect-dom/toContainHTML.test.ts
··· 1 + import { describe, expect, test } from 'vitest' 2 + import { render } from './utils' 3 + 4 + /* eslint-disable max-statements */ 5 + describe('.toContainHTML', () => { 6 + test('handles positive and negative cases', () => { 7 + const {queryByTestId} = render(` 8 + <span data-testid="grandparent"> 9 + <span data-testid="parent"> 10 + <span data-testid="child"></span> 11 + </span> 12 + <svg data-testid="svg-element"></svg> 13 + </span> 14 + `) 15 + 16 + const grandparent = queryByTestId('grandparent') 17 + const parent = queryByTestId('parent') 18 + const child = queryByTestId('child') 19 + const nonExistantElement = queryByTestId('not-exists') 20 + const fakeElement = {thisIsNot: 'an html element'} 21 + const stringChildElement = '<span data-testid="child"></span>' 22 + const stringChildElementSelfClosing = '<span data-testid="child" />' 23 + const incorrectStringHtml = '<span data-testid="child"></div>' 24 + const nonExistantString = '<span> Does not exists </span>' 25 + const svgElement = queryByTestId('svg-element') 26 + 27 + expect(grandparent).toContainHTML(stringChildElement) 28 + expect(parent).toContainHTML(stringChildElement) 29 + expect(child).toContainHTML(stringChildElement) 30 + expect(child).toContainHTML(stringChildElementSelfClosing) 31 + expect(grandparent).not.toContainHTML(nonExistantString) 32 + expect(parent).not.toContainHTML(nonExistantString) 33 + expect(child).not.toContainHTML(nonExistantString) 34 + expect(child).not.toContainHTML(nonExistantString) 35 + expect(grandparent).toContainHTML(incorrectStringHtml) 36 + expect(parent).toContainHTML(incorrectStringHtml) 37 + expect(child).toContainHTML(incorrectStringHtml) 38 + 39 + // negative test cases wrapped in throwError assertions for coverage. 40 + expect(() => 41 + expect(nonExistantElement).not.toContainHTML(stringChildElement), 42 + ).toThrowError() 43 + expect(() => 44 + // @ts-expect-error testing invalid input 45 + expect(nonExistantElement).not.toContainHTML(nonExistantElement), 46 + ).toThrowError() 47 + expect(() => 48 + // @ts-expect-error testing invalid input 49 + expect(stringChildElement).not.toContainHTML(fakeElement), 50 + ).toThrowError() 51 + expect(() => 52 + expect(svgElement).toContainHTML(stringChildElement), 53 + ).toThrowError() 54 + expect(() => 55 + expect(grandparent).not.toContainHTML(stringChildElement), 56 + ).toThrowError() 57 + expect(() => 58 + expect(parent).not.toContainHTML(stringChildElement), 59 + ).toThrowError() 60 + expect(() => 61 + expect(child).not.toContainHTML(stringChildElement), 62 + ).toThrowError() 63 + expect(() => 64 + expect(child).not.toContainHTML(stringChildElement), 65 + ).toThrowError() 66 + expect(() => 67 + expect(child).not.toContainHTML(stringChildElementSelfClosing), 68 + ).toThrowError() 69 + expect(() => expect(child).toContainHTML(nonExistantString)).toThrowError() 70 + expect(() => expect(parent).toContainHTML(nonExistantString)).toThrowError() 71 + expect(() => 72 + expect(grandparent).toContainHTML(nonExistantString), 73 + ).toThrowError() 74 + // @ts-expect-error testing invalid input 75 + expect(() => expect(child).toContainHTML(nonExistantElement)).toThrowError() 76 + expect(() => 77 + // @ts-expect-error testing invalid input 78 + expect(parent).toContainHTML(nonExistantElement), 79 + ).toThrowError() 80 + expect(() => 81 + // @ts-expect-error testing invalid input 82 + expect(grandparent).toContainHTML(nonExistantElement), 83 + ).toThrowError() 84 + expect(() => 85 + expect(nonExistantElement).not.toContainHTML(incorrectStringHtml), 86 + ).toThrowError() 87 + expect(() => 88 + expect(grandparent).not.toContainHTML(incorrectStringHtml), 89 + ).toThrowError() 90 + expect(() => 91 + expect(child).not.toContainHTML(incorrectStringHtml), 92 + ).toThrowError() 93 + expect(() => 94 + expect(parent).not.toContainHTML(incorrectStringHtml), 95 + ).toThrowError() 96 + }) 97 + 98 + test('throws with an expected text', async () => { 99 + const {queryByTestId} = render('<span data-testid="child"></span>') 100 + const htmlElement = queryByTestId('child') 101 + const nonExistantString = '<div> non-existant element </div>' 102 + 103 + let errorMessage 104 + try { 105 + expect(htmlElement).toContainHTML(nonExistantString) 106 + } catch (error) { 107 + errorMessage = error.message 108 + } 109 + 110 + expect(errorMessage).toMatchInlineSnapshot(` 111 + expect(element).toContainHTML() 112 + Expected: 113 + <div> non-existant element </div> 114 + Received: 115 + <span 116 + data-testid="child" 117 + /> 118 + `) 119 + }) 120 + })
+130
test/browser/fixtures/expect-dom/toHaveAccessibleDescription.test.ts
··· 1 + import { describe, expect, it } from 'vitest' 2 + import { render } from './utils' 3 + 4 + describe('.toHaveAccessibleDescription', () => { 5 + it('works with the link title attribute', () => { 6 + const {queryByTestId} = render(` 7 + <div> 8 + <a data-testid="link" href="/" aria-label="Home page" title="A link to start over">Start</a> 9 + <a data-testid="extra-link" href="/about" aria-label="About page">About</a> 10 + </div> 11 + `) 12 + 13 + const link = queryByTestId('link') 14 + expect(link).toHaveAccessibleDescription() 15 + expect(link).toHaveAccessibleDescription('A link to start over') 16 + expect(link).not.toHaveAccessibleDescription('Home page') 17 + expect(() => { 18 + expect(link).toHaveAccessibleDescription('Invalid description') 19 + }).toThrow(/expected element to have accessible description/i) 20 + expect(() => { 21 + expect(link).not.toHaveAccessibleDescription() 22 + }).toThrow(/expected element not to have accessible description/i) 23 + 24 + const extraLink = queryByTestId('extra-link') 25 + expect(extraLink).not.toHaveAccessibleDescription() 26 + expect(() => { 27 + expect(extraLink).toHaveAccessibleDescription() 28 + }).toThrow(/expected element to have accessible description/i) 29 + }) 30 + 31 + it('works with aria-describedby attributes', () => { 32 + const {queryByTestId} = render(` 33 + <div> 34 + <img src="avatar.jpg" data-testid="avatar" alt="User profile pic"> 35 + <img src="logo.jpg" data-testid="logo" alt="Company logo" aria-describedby="t1"> 36 + <span id="t1" role="presentation">The logo of Our Company</span> 37 + </div> 38 + `) 39 + 40 + const avatar = queryByTestId('avatar') 41 + expect(avatar).not.toHaveAccessibleDescription() 42 + expect(() => { 43 + expect(avatar).toHaveAccessibleDescription('User profile pic') 44 + }).toThrow(/expected element to have accessible description/i) 45 + 46 + const logo = queryByTestId('logo') 47 + expect(logo).not.toHaveAccessibleDescription('Company logo') 48 + expect(logo).toHaveAccessibleDescription('The logo of Our Company') 49 + expect(logo).toHaveAccessibleDescription(/logo of our company/i) 50 + expect(logo).toHaveAccessibleDescription( 51 + expect.stringContaining('logo of Our Company'), 52 + ) 53 + expect(() => { 54 + expect(logo).toHaveAccessibleDescription("Our company's logo") 55 + }).toThrow(/expected element to have accessible description/i) 56 + expect(() => { 57 + expect(logo).not.toHaveAccessibleDescription('The logo of Our Company') 58 + }).toThrow(/expected element not to have accessible description/i) 59 + }) 60 + 61 + it('works with aria-description attribute', () => { 62 + const {queryByTestId} = render(` 63 + <img src="logo.jpg" data-testid="logo" alt="Company logo" aria-description="The logo of Our Company"> 64 + `) 65 + 66 + const logo = queryByTestId('logo') 67 + expect(logo).not.toHaveAccessibleDescription('Company logo') 68 + expect(logo).toHaveAccessibleDescription('The logo of Our Company') 69 + expect(logo).toHaveAccessibleDescription(/logo of our company/i) 70 + expect(logo).toHaveAccessibleDescription( 71 + expect.stringContaining('logo of Our Company'), 72 + ) 73 + expect(() => { 74 + expect(logo).toHaveAccessibleDescription("Our company's logo") 75 + }).toThrow(/expected element to have accessible description/i) 76 + expect(() => { 77 + expect(logo).not.toHaveAccessibleDescription('The logo of Our Company') 78 + }).toThrow(/expected element not to have accessible description/i) 79 + }) 80 + 81 + it('handles multiple ids', () => { 82 + const {queryByTestId} = render(` 83 + <div> 84 + <div id="first">First description</div> 85 + <div id="second">Second description</div> 86 + <div id="third">Third description</div> 87 + 88 + <div data-testid="multiple" aria-describedby="first second third"></div> 89 + </div> 90 + `) 91 + 92 + expect(queryByTestId('multiple')).toHaveAccessibleDescription( 93 + 'First description Second description Third description', 94 + ) 95 + expect(queryByTestId('multiple')).toHaveAccessibleDescription( 96 + /Second description Third/, 97 + ) 98 + expect(queryByTestId('multiple')).toHaveAccessibleDescription( 99 + expect.stringContaining('Second description Third'), 100 + ) 101 + expect(queryByTestId('multiple')).toHaveAccessibleDescription( 102 + expect.stringMatching(/Second description Third/), 103 + ) 104 + expect(queryByTestId('multiple')).not.toHaveAccessibleDescription( 105 + 'Something else', 106 + ) 107 + expect(queryByTestId('multiple')).not.toHaveAccessibleDescription('First') 108 + }) 109 + 110 + it('normalizes whitespace', () => { 111 + const {queryByTestId} = render(` 112 + <div id="first"> 113 + Step 114 + 1 115 + of 116 + 4 117 + </div> 118 + <div id="second"> 119 + And 120 + extra 121 + description 122 + </div> 123 + <div data-testid="target" aria-describedby="first second"></div> 124 + `) 125 + 126 + expect(queryByTestId('target')).toHaveAccessibleDescription( 127 + 'Step 1 of 4 And extra description', 128 + ) 129 + }) 130 + })
+237
test/browser/fixtures/expect-dom/toHaveAccessibleErrorMessage.test.ts
··· 1 + import { describe, expect, it } from 'vitest' 2 + import { render } from './utils' 3 + 4 + describe('.toHaveAccessibleErrorMessage', () => { 5 + const input = 'input' 6 + const errorId = 'error-id' 7 + const error = 'This field is invalid' 8 + const strings = {true: String(true), false: String(false)} 9 + 10 + describe('Positive Test Cases', () => { 11 + it("Fails the test if an invalid `id` is provided for the target element's `aria-errormessage`", () => { 12 + const secondId = 'id2' 13 + const secondError = 'LISTEN TO ME!!!' 14 + 15 + const {queryByTestId} = render(` 16 + <div> 17 + <${input} data-testid="${input}" aria-invalid="${strings.true}" aria-errormessage="${errorId} ${secondId}" /> 18 + <div data-testid="${errorId}" id="${errorId}" role="alert">${error}</div> 19 + <div data-testid="${secondId}" id="${secondId}" role="alert">${secondError}</div> 20 + </div> 21 + `) 22 + 23 + const field = queryByTestId('input') 24 + expect(field).toHaveAccessibleErrorMessage() 25 + expect(field).toHaveAccessibleErrorMessage(new RegExp(error[0])) 26 + 27 + expect(field).toHaveAccessibleErrorMessage(error + ' ' + secondError) 28 + 29 + expect(() => expect(field).toHaveAccessibleErrorMessage(error)).toThrow() 30 + expect(() => 31 + expect(field).toHaveAccessibleErrorMessage(secondError), 32 + ).toThrow() 33 + 34 + expect(field).toHaveAccessibleErrorMessage(new RegExp(secondError[0])) 35 + }) 36 + 37 + it('Fails the test if the target element is valid according to the WAI-ARIA spec', () => { 38 + const noAriaInvalidAttribute = 'no-aria-invalid-attribute' 39 + const validFieldState = 'false' 40 + const invalidFieldStates = [ 41 + 'true', 42 + // difference with jest-dom 43 + // https://www.w3.org/TR/wai-aria-1.2/#aria-invalid 44 + // If the attribute is not present, or its value is false, or its value 45 + // is an EMPTY STRING, the default value of false applies. 46 + // '', 47 + 'grammar', 48 + 'spelling', 49 + 'asfdafbasdfasa', 50 + ] 51 + 52 + function renderFieldWithState(state) { 53 + return render(` 54 + <div> 55 + <${input} data-testid="${input}" aria-invalid="${state}" aria-errormessage="${errorId}" /> 56 + <div data-testid="${errorId}" id="${errorId}" role="alert">${error}</div> 57 + <input data-testid="${noAriaInvalidAttribute}" aria-errormessage="${errorId}" /> 58 + </div> 59 + `) 60 + } 61 + 62 + // Success Cases 63 + invalidFieldStates.forEach(invalidState => { 64 + const {queryByTestId} = renderFieldWithState(invalidState) 65 + const field = queryByTestId('input') 66 + 67 + expect(field).toHaveAccessibleErrorMessage() 68 + expect(field).toHaveAccessibleErrorMessage(error) 69 + }) 70 + 71 + // Failure Case 72 + const {queryByTestId} = renderFieldWithState(validFieldState) 73 + const field = queryByTestId('input') 74 + const fieldWithoutAttribute = queryByTestId(noAriaInvalidAttribute) 75 + 76 + expect(() => expect(fieldWithoutAttribute).toHaveAccessibleErrorMessage()) 77 + .toThrowErrorMatchingInlineSnapshot(` 78 + [Error: expect(element).toHaveAccessibleErrorMessage() 79 + 80 + Expected element to have accessible error message, but got nothing] 81 + `) 82 + 83 + expect(() => expect(field).toHaveAccessibleErrorMessage()) 84 + .toThrowErrorMatchingInlineSnapshot(` 85 + [Error: expect(element).toHaveAccessibleErrorMessage() 86 + 87 + Expected element to have accessible error message, but got nothing] 88 + `) 89 + 90 + // Assume the remaining error messages are the EXACT same as above 91 + expect(() => expect(field).toHaveAccessibleErrorMessage(error)).toThrow() 92 + expect(() => 93 + expect(field).toHaveAccessibleErrorMessage(new RegExp(error, 'i')), 94 + ).toThrow() 95 + }) 96 + 97 + it('Passes the test if the target element has ANY recognized, non-empty error message', () => { 98 + const {queryByTestId} = render(` 99 + <div> 100 + <${input} data-testid="${input}" aria-invalid="${strings.true}" aria-errormessage="${errorId}" /> 101 + <div data-testid="${errorId}" id="${errorId}" role="alert">${error}</div> 102 + </div> 103 + `) 104 + 105 + const field = queryByTestId(input) 106 + expect(field).toHaveAccessibleErrorMessage() 107 + }) 108 + 109 + it('Fails the test if NO recognized, non-empty error message was found for the target element', () => { 110 + const empty = 'empty' 111 + const emptyErrorId = 'empty-error' 112 + const missing = 'missing' 113 + 114 + const {queryByTestId} = render(` 115 + <div> 116 + <input data-testid="${empty}" aria-invalid="${strings.true}" aria-errormessage="${emptyErrorId}" /> 117 + <div data-testid="${emptyErrorId}" id="${emptyErrorId}" role="alert"></div> 118 + 119 + <input data-testid="${missing}" aria-invalid="${strings.true}" aria-errormessage="${missing}-error" /> 120 + </div> 121 + `) 122 + 123 + const fieldWithEmptyError = queryByTestId(empty) 124 + const fieldMissingError = queryByTestId(missing) 125 + 126 + expect(() => expect(fieldWithEmptyError).toHaveAccessibleErrorMessage()) 127 + .toThrowErrorMatchingInlineSnapshot(` 128 + [Error: expect(element).toHaveAccessibleErrorMessage() 129 + 130 + Expected element to have accessible error message, but got nothing] 131 + `) 132 + 133 + expect(() => expect(fieldMissingError).toHaveAccessibleErrorMessage()) 134 + .toThrowErrorMatchingInlineSnapshot(` 135 + [Error: expect(element).toHaveAccessibleErrorMessage() 136 + 137 + Expected element to have accessible error message, but got nothing] 138 + `) 139 + }) 140 + 141 + it('Passes the test if the target element has the error message that was SPECIFIED', () => { 142 + const {queryByTestId} = render(` 143 + <div> 144 + <${input} data-testid="${input}" aria-invalid="${strings.true}" aria-errormessage="${errorId}" /> 145 + <div data-testid="${errorId}" id="${errorId}" role="alert">${error}</div> 146 + </div> 147 + `) 148 + 149 + const field = queryByTestId(input) 150 + const halfOfError = error.slice(0, Math.floor(error.length * 0.5)) 151 + 152 + expect(field).toHaveAccessibleErrorMessage(error) 153 + expect(field).toHaveAccessibleErrorMessage(new RegExp(halfOfError, 'i')) 154 + expect(field).toHaveAccessibleErrorMessage( 155 + expect.stringContaining(halfOfError), 156 + ) 157 + expect(field).toHaveAccessibleErrorMessage( 158 + expect.stringMatching(new RegExp(halfOfError, 'i')), 159 + ) 160 + }) 161 + 162 + it('Fails the test if the target element DOES NOT have the error message that was SPECIFIED', () => { 163 + const {queryByTestId} = render(` 164 + <div> 165 + <${input} data-testid="${input}" aria-invalid="${strings.true}" aria-errormessage="${errorId}" /> 166 + <div data-testid="${errorId}" id="${errorId}" role="alert">${error}</div> 167 + </div> 168 + `) 169 + 170 + const field = queryByTestId(input) 171 + const msg = 'asdflkje2984fguyvb bnafdsasfa;lj' 172 + 173 + expect(() => expect(field).toHaveAccessibleErrorMessage('')) 174 + .toThrowErrorMatchingInlineSnapshot(` 175 + [Error: expect(element).toHaveAccessibleErrorMessage() 176 + 177 + Expected element to have accessible error message: 178 + 179 + Received: 180 + This field is invalid] 181 + `) 182 + 183 + // Assume this error is SIMILAR to the error above 184 + expect(() => expect(field).toHaveAccessibleErrorMessage(msg)).toThrow() 185 + expect(() => 186 + expect(field).toHaveAccessibleErrorMessage( 187 + error.slice(0, Math.floor(error.length * 0.5)), 188 + ), 189 + ).toThrow() 190 + 191 + expect(() => 192 + expect(field).toHaveAccessibleErrorMessage(new RegExp(msg, 'i')), 193 + ).toThrowErrorMatchingInlineSnapshot(` 194 + [Error: expect(element).toHaveAccessibleErrorMessage() 195 + 196 + Expected element to have accessible error message: 197 + /asdflkje2984fguyvb bnafdsasfa;lj/i 198 + Received: 199 + This field is invalid] 200 + `) 201 + }) 202 + 203 + it('Normalizes the whitespace of the received error message', () => { 204 + const {queryByTestId} = render(` 205 + <div> 206 + <${input} data-testid="${input}" aria-invalid="${strings.true}" aria-errormessage="${errorId}" /> 207 + <div data-testid="${errorId}" id="${errorId}" role="alert"> 208 + Step 209 + 1 210 + of 211 + 9000 212 + </div> 213 + </div> 214 + `) 215 + 216 + const field = queryByTestId(input) 217 + expect(field).toHaveAccessibleErrorMessage('Step 1 of 9000') 218 + }) 219 + }) 220 + 221 + // These tests for the `.not` use cases will help us cover our bases and complete test coverage 222 + describe('Negated Test Cases', () => { 223 + it('Passes the test if the target element is valid according to the WAI-ARIA spec', () => { 224 + const {queryByTestId} = render(` 225 + <div> 226 + <${input} data-testid="${input}" aria-errormessage="${errorId}" /> 227 + <div data-testid="${errorId}" id="${errorId}" role="alert">${error}</div> 228 + </div> 229 + `) 230 + 231 + const field = queryByTestId(input) 232 + expect(field).not.toHaveAccessibleErrorMessage() 233 + expect(field).not.toHaveAccessibleErrorMessage(error) 234 + expect(field).not.toHaveAccessibleErrorMessage(new RegExp(error[0])) 235 + }) 236 + }) 237 + })
+64
test/browser/fixtures/expect-dom/toHaveAttribute.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + import { render } from './utils' 3 + 4 + test('.toHaveAttribute', () => { 5 + const {queryByTestId} = render(` 6 + <button data-testid="ok-button" type="submit" disabled> 7 + OK 8 + </button> 9 + <svg data-testid="svg-element" width="12"></svg> 10 + `) 11 + 12 + expect(queryByTestId('ok-button')).toHaveAttribute('disabled') 13 + expect(queryByTestId('ok-button')).toHaveAttribute('type') 14 + expect(queryByTestId('ok-button')).not.toHaveAttribute('class') 15 + expect(queryByTestId('ok-button')).toHaveAttribute('type', 'submit') 16 + expect(queryByTestId('ok-button')).not.toHaveAttribute('type', 'button') 17 + expect(queryByTestId('svg-element')).toHaveAttribute('width') 18 + expect(queryByTestId('svg-element')).toHaveAttribute('width', '12') 19 + expect(queryByTestId('ok-button')).not.toHaveAttribute('height') 20 + 21 + expect(() => 22 + expect(queryByTestId('ok-button')).not.toHaveAttribute('disabled'), 23 + ).toThrowError() 24 + expect(() => 25 + expect(queryByTestId('ok-button')).not.toHaveAttribute('type'), 26 + ).toThrowError() 27 + expect(() => 28 + expect(queryByTestId('ok-button')).toHaveAttribute('class'), 29 + ).toThrowError() 30 + expect(() => 31 + expect(queryByTestId('ok-button')).not.toHaveAttribute('type', 'submit'), 32 + ).toThrowError() 33 + expect(() => 34 + expect(queryByTestId('ok-button')).toHaveAttribute('type', 'button'), 35 + ).toThrowError() 36 + expect(() => 37 + expect(queryByTestId('svg-element')).not.toHaveAttribute('width'), 38 + ).toThrowError() 39 + expect(() => 40 + expect(queryByTestId('svg-element')).not.toHaveAttribute('width', '12'), 41 + ).toThrowError() 42 + expect(() => 43 + // @ts-expect-error invalid signature 44 + expect({thisIsNot: 'an html element'}).not.toHaveAttribute(), 45 + ).toThrowError() 46 + 47 + // Asymmetric matchers 48 + expect(queryByTestId('ok-button')).toHaveAttribute( 49 + 'type', 50 + expect.stringContaining('sub'), 51 + ) 52 + expect(queryByTestId('ok-button')).toHaveAttribute( 53 + 'type', 54 + expect.stringMatching(/sub*/), 55 + ) 56 + expect(queryByTestId('ok-button')).toHaveAttribute('type', expect.anything()) 57 + 58 + expect(() => 59 + expect(queryByTestId('ok-button')).toHaveAttribute( 60 + 'type', 61 + expect.not.stringContaining('sub'), 62 + ), 63 + ).toThrowError() 64 + })
+233
test/browser/fixtures/expect-dom/toHaveClass.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + import { render } from './utils' 3 + 4 + const renderElementWithClasses = () => 5 + render(` 6 + <div> 7 + <button data-testid="delete-button" class="btn extra btn-danger"> 8 + Delete item 9 + </button> 10 + <button data-testid="cancel-button"> 11 + Cancel 12 + </button> 13 + <svg data-testid="svg-spinner" class="spinner clockwise"> 14 + <path /> 15 + </svg> 16 + <div data-testid="only-one-class" class="alone"></div> 17 + <div data-testid="no-classes"></div> 18 + </div> 19 + `) 20 + 21 + test('.toHaveClass', () => { 22 + const {queryByTestId} = renderElementWithClasses() 23 + 24 + expect(queryByTestId('delete-button')).toHaveClass('btn') 25 + expect(queryByTestId('delete-button')).toHaveClass('btn-danger') 26 + expect(queryByTestId('delete-button')).toHaveClass('extra') 27 + expect(queryByTestId('delete-button')).not.toHaveClass('xtra') 28 + expect(queryByTestId('delete-button')).not.toHaveClass('btn xtra') 29 + expect(queryByTestId('delete-button')).not.toHaveClass('btn', 'xtra') 30 + expect(queryByTestId('delete-button')).not.toHaveClass('btn', 'extra xtra') 31 + expect(queryByTestId('delete-button')).toHaveClass('btn btn-danger') 32 + expect(queryByTestId('delete-button')).toHaveClass('btn', 'btn-danger') 33 + expect(queryByTestId('delete-button')).toHaveClass( 34 + 'btn extra', 35 + 'btn-danger extra', 36 + ) 37 + expect(queryByTestId('delete-button')).not.toHaveClass('btn-link') 38 + expect(queryByTestId('cancel-button')).not.toHaveClass('btn-danger') 39 + expect(queryByTestId('svg-spinner')).toHaveClass('spinner') 40 + expect(queryByTestId('svg-spinner')).toHaveClass('clockwise') 41 + expect(queryByTestId('svg-spinner')).not.toHaveClass('wise') 42 + expect(queryByTestId('no-classes')).not.toHaveClass() 43 + expect(queryByTestId('no-classes')).not.toHaveClass(' ') 44 + 45 + expect(() => 46 + expect(queryByTestId('delete-button')).not.toHaveClass('btn'), 47 + ).toThrowError() 48 + expect(() => 49 + expect(queryByTestId('delete-button')).not.toHaveClass('btn-danger'), 50 + ).toThrowError() 51 + expect(() => 52 + expect(queryByTestId('delete-button')).not.toHaveClass('extra'), 53 + ).toThrowError() 54 + expect(() => 55 + expect(queryByTestId('delete-button')).toHaveClass('xtra'), 56 + ).toThrowError() 57 + expect(() => 58 + expect(queryByTestId('delete-button')).toHaveClass('btn', 'extra xtra'), 59 + ).toThrowError() 60 + expect(() => 61 + expect(queryByTestId('delete-button')).not.toHaveClass('btn btn-danger'), 62 + ).toThrowError() 63 + expect(() => 64 + expect(queryByTestId('delete-button')).not.toHaveClass('btn', 'btn-danger'), 65 + ).toThrowError() 66 + expect(() => 67 + expect(queryByTestId('delete-button')).toHaveClass('btn-link'), 68 + ).toThrowError() 69 + expect(() => 70 + expect(queryByTestId('cancel-button')).toHaveClass('btn-danger'), 71 + ).toThrowError() 72 + expect(() => 73 + expect(queryByTestId('svg-spinner')).not.toHaveClass('spinner'), 74 + ).toThrowError() 75 + expect(() => 76 + expect(queryByTestId('svg-spinner')).toHaveClass('wise'), 77 + ).toThrowError() 78 + expect(() => 79 + expect(queryByTestId('delete-button')).toHaveClass(), 80 + ).toThrowError(/At least one expected class must be provided/) 81 + expect(() => 82 + expect(queryByTestId('delete-button')).toHaveClass(''), 83 + ).toThrowError(/At least one expected class must be provided/) 84 + expect(() => expect(queryByTestId('no-classes')).toHaveClass()).toThrowError( 85 + /At least one expected class must be provided/, 86 + ) 87 + expect(() => 88 + expect(queryByTestId('delete-button')).not.toHaveClass(), 89 + ).toThrowError(/(none)/) 90 + expect(() => 91 + expect(queryByTestId('delete-button')).not.toHaveClass(' '), 92 + ).toThrowError(/(none)/) 93 + }) 94 + 95 + test('.toHaveClass with regular expressions', () => { 96 + const {queryByTestId} = renderElementWithClasses() 97 + 98 + expect(queryByTestId('delete-button')).toHaveClass(/btn/) 99 + expect(queryByTestId('delete-button')).toHaveClass(/danger/) 100 + expect(queryByTestId('delete-button')).toHaveClass( 101 + /-danger$/, 102 + 'extra', 103 + /^btn-[a-z]+$/, 104 + /\bbtn/, 105 + ) 106 + 107 + // It does not match with "btn extra", even though it is a substring of the 108 + // class "btn extra btn-danger". This is because the regular expression is 109 + // matched against each class individually. 110 + expect(queryByTestId('delete-button')).not.toHaveClass(/btn extra/) 111 + 112 + expect(() => 113 + expect(queryByTestId('delete-button')).not.toHaveClass(/danger/), 114 + ).toThrowError() 115 + 116 + expect(() => 117 + expect(queryByTestId('delete-button')).toHaveClass(/dangerous/), 118 + ).toThrowError() 119 + }) 120 + 121 + test('.toHaveClass with exact mode option', () => { 122 + const {queryByTestId} = renderElementWithClasses() 123 + 124 + expect(queryByTestId('delete-button')).toHaveClass('btn extra btn-danger', { 125 + exact: true, 126 + }) 127 + expect(queryByTestId('delete-button')).not.toHaveClass('btn extra', { 128 + exact: true, 129 + }) 130 + expect(queryByTestId('delete-button')).not.toHaveClass( 131 + 'btn extra btn-danger foo', 132 + {exact: true}, 133 + ) 134 + 135 + expect(queryByTestId('delete-button')).toHaveClass('btn extra btn-danger', { 136 + exact: false, 137 + }) 138 + expect(queryByTestId('delete-button')).toHaveClass('btn extra', { 139 + exact: false, 140 + }) 141 + expect(queryByTestId('delete-button')).not.toHaveClass( 142 + 'btn extra btn-danger foo', 143 + {exact: false}, 144 + ) 145 + 146 + expect(queryByTestId('delete-button')).toHaveClass( 147 + 'btn', 148 + 'extra', 149 + 'btn-danger', 150 + {exact: true}, 151 + ) 152 + expect(queryByTestId('delete-button')).not.toHaveClass('btn', 'extra', { 153 + exact: true, 154 + }) 155 + expect(queryByTestId('delete-button')).not.toHaveClass( 156 + 'btn', 157 + 'extra', 158 + 'btn-danger', 159 + 'foo', 160 + {exact: true}, 161 + ) 162 + 163 + expect(queryByTestId('delete-button')).toHaveClass( 164 + 'btn', 165 + 'extra', 166 + 'btn-danger', 167 + {exact: false}, 168 + ) 169 + expect(queryByTestId('delete-button')).toHaveClass('btn', 'extra', { 170 + exact: false, 171 + }) 172 + expect(queryByTestId('delete-button')).not.toHaveClass( 173 + 'btn', 174 + 'extra', 175 + 'btn-danger', 176 + 'foo', 177 + {exact: false}, 178 + ) 179 + 180 + expect(queryByTestId('only-one-class')).toHaveClass('alone', {exact: true}) 181 + expect(queryByTestId('only-one-class')).not.toHaveClass('alone foo', { 182 + exact: true, 183 + }) 184 + expect(queryByTestId('only-one-class')).not.toHaveClass('alone', 'foo', { 185 + exact: true, 186 + }) 187 + 188 + expect(queryByTestId('only-one-class')).toHaveClass('alone', {exact: false}) 189 + expect(queryByTestId('only-one-class')).not.toHaveClass('alone foo', { 190 + exact: false, 191 + }) 192 + expect(queryByTestId('only-one-class')).not.toHaveClass('alone', 'foo', { 193 + exact: false, 194 + }) 195 + 196 + expect(() => 197 + expect(queryByTestId('only-one-class')).not.toHaveClass('alone', { 198 + exact: true, 199 + }), 200 + ).toThrowError(/Expected the element not to have EXACTLY defined classes/) 201 + 202 + expect(() => 203 + expect(queryByTestId('only-one-class')).toHaveClass('alone', 'foo', { 204 + exact: true, 205 + }), 206 + ).toThrowError(/Expected the element to have EXACTLY defined classes/) 207 + }) 208 + 209 + test('.toHaveClass combining {exact:true} and regular expressions throws an error', () => { 210 + const {queryByTestId} = renderElementWithClasses() 211 + 212 + expect(() => 213 + // @ts-expect-error regexp is not supported with exact 214 + expect(queryByTestId('delete-button')).not.toHaveClass(/btn/, { 215 + exact: true, 216 + }), 217 + ).toThrowError() 218 + 219 + expect(() => 220 + expect(queryByTestId('delete-button')).not.toHaveClass( 221 + // @ts-expect-error regexp is not supported with exact 222 + /-danger$/, 223 + 'extra', 224 + /\bbtn/, 225 + {exact: true}, 226 + ), 227 + ).toThrowError() 228 + 229 + expect(() => 230 + // @ts-expect-error regexp is not supported with exact 231 + expect(queryByTestId('delete-button')).toHaveClass(/danger/, {exact: true}), 232 + ).toThrowError() 233 + })
+166
test/browser/fixtures/expect-dom/toHaveDisplayValue.test.ts
··· 1 + import { describe, expect, it } from 'vitest' 2 + import { render } from './utils' 3 + 4 + it('should work as expected', () => { 5 + const {getInputByTestId} = render(` 6 + <select id="fruits" data-testid="select"> 7 + <option value="">Select a fruit...</option> 8 + <option value="ananas">Ananas</option> 9 + <option value="banana">Banana</option> 10 + <option value="avocado">Avocado</option> 11 + </select> 12 + `) 13 + 14 + expect(getInputByTestId('select')).toHaveDisplayValue('Select a fruit...') 15 + expect(getInputByTestId('select')).not.toHaveDisplayValue('Select') 16 + expect(getInputByTestId('select')).not.toHaveDisplayValue('Banana') 17 + expect(() => 18 + expect(getInputByTestId('select')).not.toHaveDisplayValue('Select a fruit...'), 19 + ).toThrow() 20 + expect(() => 21 + expect(getInputByTestId('select')).toHaveDisplayValue('Ananas'), 22 + ).toThrow() 23 + 24 + getInputByTestId('select').value = 'banana' 25 + expect(getInputByTestId('select')).toHaveDisplayValue('Banana') 26 + expect(getInputByTestId('select')).toHaveDisplayValue(/[bB]ana/) 27 + }) 28 + 29 + describe('with multiple select', () => { 30 + function mount() { 31 + return render(` 32 + <select id="fruits" data-testid="select" multiple> 33 + <option value="">Select a fruit...</option> 34 + <option value="ananas" selected>Ananas</option> 35 + <option value="banana">Banana</option> 36 + <option value="avocado" selected>Avocado</option> 37 + </select> 38 + `) 39 + } 40 + 41 + it('matches only when all the multiple selected values are equal to all the expected values', () => { 42 + const subject = mount() 43 + expect(subject.queryByTestId('select')).toHaveDisplayValue([ 44 + 'Ananas', 45 + 'Avocado', 46 + ]) 47 + expect(() => 48 + expect(subject.queryByTestId('select')).not.toHaveDisplayValue([ 49 + 'Ananas', 50 + 'Avocado', 51 + ]), 52 + ).toThrow() 53 + expect(subject.queryByTestId('select')).not.toHaveDisplayValue([ 54 + 'Ananas', 55 + 'Avocado', 56 + 'Orange', 57 + ]) 58 + expect(subject.queryByTestId('select')).not.toHaveDisplayValue('Ananas') 59 + expect(() => 60 + expect(subject.queryByTestId('select')).toHaveDisplayValue('Ananas'), 61 + ).toThrow() 62 + 63 + Array.from((subject.queryByTestId('select') as HTMLSelectElement).options).forEach(option => { 64 + option.selected = ['ananas', 'banana'].includes(option.value) 65 + }) 66 + 67 + expect(subject.queryByTestId('select')).toHaveDisplayValue([ 68 + 'Ananas', 69 + 'Banana', 70 + ]) 71 + }) 72 + 73 + it('matches even when the expected values are unordered', () => { 74 + const subject = mount() 75 + expect(subject.queryByTestId('select')).toHaveDisplayValue([ 76 + 'Avocado', 77 + 'Ananas', 78 + ]) 79 + }) 80 + 81 + it('matches with regex expected values', () => { 82 + const subject = mount() 83 + expect(subject.queryByTestId('select')).toHaveDisplayValue([ 84 + /[Aa]nanas/, 85 + 'Avocado', 86 + ]) 87 + }) 88 + }) 89 + 90 + it('should work with input elements', () => { 91 + const {getInputByTestId} = render(` 92 + <input type="text" data-testid="input" value="Luca" /> 93 + `) 94 + 95 + expect(getInputByTestId('input')).toHaveDisplayValue('Luca') 96 + expect(getInputByTestId('input')).toHaveDisplayValue(/Luc/) 97 + 98 + getInputByTestId('input').value = 'Piero' 99 + expect(getInputByTestId('input')).toHaveDisplayValue('Piero') 100 + }) 101 + 102 + it('should work with textarea elements', () => { 103 + const {getInputByTestId} = render( 104 + '<textarea data-testid="textarea-example">An example description here.</textarea>', 105 + ) 106 + 107 + expect(getInputByTestId('textarea-example')).toHaveDisplayValue( 108 + 'An example description here.', 109 + ) 110 + expect(getInputByTestId('textarea-example')).toHaveDisplayValue(/example/) 111 + 112 + getInputByTestId('textarea-example').value = 'Another example' 113 + expect(getInputByTestId('textarea-example')).toHaveDisplayValue( 114 + 'Another example', 115 + ) 116 + }) 117 + 118 + it('should throw if element is not valid', () => { 119 + const {queryByTestId} = render(` 120 + <div data-testid="div">Banana</div> 121 + <input type="radio" data-testid="radio" value="Something" /> 122 + <input type="checkbox" data-testid="checkbox" /> 123 + `) 124 + 125 + let errorMessage 126 + try { 127 + expect(queryByTestId('div')).toHaveDisplayValue('Banana') 128 + } catch (err) { 129 + errorMessage = err.message 130 + } 131 + 132 + expect(errorMessage).toMatchInlineSnapshot( 133 + `.toHaveDisplayValue() currently supports only input, textarea or select elements, try with another matcher instead.`, 134 + ) 135 + 136 + try { 137 + expect(queryByTestId('radio')).toHaveDisplayValue('Something') 138 + } catch (err) { 139 + errorMessage = err.message 140 + } 141 + 142 + expect(errorMessage).toMatchInlineSnapshot( 143 + `.toHaveDisplayValue() currently does not support input[type="radio"], try with another matcher instead.`, 144 + ) 145 + 146 + try { 147 + // @ts-expect-error 148 + expect(queryByTestId('checkbox')).toHaveDisplayValue(true) 149 + } catch (err) { 150 + errorMessage = err.message 151 + } 152 + 153 + expect(errorMessage).toMatchInlineSnapshot( 154 + `.toHaveDisplayValue() currently does not support input[type="checkbox"], try with another matcher instead.`, 155 + ) 156 + }) 157 + 158 + it('should work with numbers', () => { 159 + const {queryByTestId} = render(` 160 + <select data-testid="select"> 161 + <option value="">1</option> 162 + </select> 163 + `) 164 + 165 + expect(queryByTestId('select')).toHaveDisplayValue(1) 166 + })
+23
test/browser/fixtures/expect-dom/toHaveFocus.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + import { render } from './utils' 3 + 4 + test('.toHaveFocus', () => { 5 + const {container} = render(` 6 + <div> 7 + <label for="focused">test</label> 8 + <input id="focused" type="text" /> 9 + <button type="submit" id="not-focused">Not Focused</button> 10 + </div>`) 11 + 12 + const focused = container.querySelector('#focused') as HTMLInputElement 13 + const notFocused = container.querySelector('#not-focused') 14 + 15 + document.body.appendChild(container) 16 + focused.focus() 17 + 18 + expect(focused).toHaveFocus() 19 + expect(notFocused).not.toHaveFocus() 20 + 21 + expect(() => expect(focused).not.toHaveFocus()).toThrowError() 22 + expect(() => expect(notFocused).toHaveFocus()).toThrowError() 23 + })
+352
test/browser/fixtures/expect-dom/toHaveFormValues.test.ts
··· 1 + import { describe, expect, it } from 'vitest' 2 + import { render } from './utils' 3 + 4 + const categories = [ 5 + {value: '', label: '–'}, 6 + {value: 'design', label: 'Design'}, 7 + {value: 'ux', label: 'User Experience'}, 8 + {value: 'programming', label: 'Programming'}, 9 + ] 10 + 11 + const skills = [ 12 + {value: 'c-sharp', label: 'C#'}, 13 + {value: 'graphql', label: 'GraphQl'}, 14 + {value: 'javascript', label: 'JavaScript'}, 15 + {value: 'ruby-on-rails', label: 'Ruby on Rails'}, 16 + {value: 'python', label: 'Python'}, 17 + ] 18 + 19 + const defaultValues = { 20 + title: 'Full-stack developer', 21 + salary: 12345, 22 + category: 'programming', 23 + skills: ['javascript', 'ruby-on-rails'], 24 + description: 'You need to know your stuff', 25 + remote: true, 26 + freelancing: false, 27 + 'is%Private^': true, 28 + 'benefits[0]': 'Fruit & free drinks everyday', 29 + 'benefits[1]': 'Multicultural environment', 30 + } 31 + 32 + function renderForm({ 33 + selectSingle = renderSelectSingle, 34 + selectMultiple = renderSelectMultiple, 35 + values: valueOverrides = {}, 36 + } = {}) { 37 + const values = { 38 + ...defaultValues, 39 + ...valueOverrides, 40 + } 41 + const {container} = render(` 42 + <form> 43 + <label for="title">Job title</label> 44 + <input 45 + type="text" 46 + id="title" 47 + name="title" 48 + value="${values.title || ''}" 49 + /> 50 + 51 + <label for="salary">Salary</label> 52 + <input 53 + type="number" 54 + id="salary" 55 + name="salary" 56 + value="${values.salary}" 57 + /> 58 + 59 + <label for="description">Description</label> 60 + <textarea id="description" name="description">${ 61 + values.description 62 + }</textarea> 63 + 64 + <input 65 + type="checkbox" 66 + id="remote" 67 + name="remote" ${values.remote ? 'checked' : ''} 68 + /> 69 + <label for="remote">Can work remotely?</label> 70 + 71 + <input 72 + type="checkbox" 73 + id="freelancing" 74 + name="freelancing" ${values.freelancing ? 'checked' : ''} 75 + /> 76 + <label for="freelancing">Freelancing?</label> 77 + 78 + <fieldset> 79 + <legend>Benefits</legend> 80 + <input 81 + type="text" 82 + id="benefits[0]" 83 + name="benefits[0]" 84 + value="${values['benefits[0]']}" 85 + /> 86 + <input 87 + type="text" 88 + id="benefits[1]" 89 + name="benefits[1]" 90 + value="${values['benefits[1]']}" 91 + /> 92 + </fieldset> 93 + 94 + <label for="is%Private^">Is Private</label> 95 + <input 96 + type="checkbox" 97 + id="is%Private^" 98 + name="is%Private^" 99 + name="isPrivate" ${values['is%Private^'] ? 'checked' : ''} 100 + /> 101 + 102 + ${selectSingle('category', 'Category', categories, values.category)} 103 + ${selectMultiple('skills', 'Skills', skills, values.skills)} 104 + </form> 105 + `) 106 + return container.querySelector('form') 107 + } 108 + 109 + describe('.toHaveFormValues', () => { 110 + it('works as expected', () => { 111 + expect(renderForm()).toHaveFormValues(defaultValues) 112 + }) 113 + 114 + it('allows to match partially', () => { 115 + expect(renderForm()).toHaveFormValues({ 116 + category: 'programming', 117 + salary: 12345, 118 + }) 119 + }) 120 + 121 + it('supports checkboxes for multiple selection', () => { 122 + expect(renderForm({selectMultiple: renderCheckboxes})).toHaveFormValues({ 123 + skills: ['javascript', 'ruby-on-rails'], 124 + }) 125 + }) 126 + 127 + it('supports radio-buttons for single selection', () => { 128 + expect(renderForm({selectSingle: renderRadioButtons})).toHaveFormValues({ 129 + category: 'programming', 130 + }) 131 + }) 132 + 133 + it('matches sets of selected values regardless of the order', () => { 134 + const form = renderForm() 135 + expect(form).toHaveFormValues({ 136 + skills: ['ruby-on-rails', 'javascript'], 137 + }) 138 + expect(form).toHaveFormValues({ 139 + skills: ['javascript', 'ruby-on-rails'], 140 + }) 141 + }) 142 + 143 + it('correctly handles empty values', () => { 144 + expect( 145 + renderForm({ 146 + values: { 147 + title: '', 148 + salary: null, 149 + category: null, 150 + skills: [], 151 + description: '', 152 + }, 153 + }), 154 + ).toHaveFormValues({ 155 + title: '', 156 + salary: null, 157 + category: '', 158 + skills: [], 159 + description: '', 160 + }) 161 + }) 162 + 163 + it('handles <input type="number"> values correctly', () => { 164 + expect(renderForm({values: {salary: 123.456}})).toHaveFormValues({ 165 + salary: 123.456, 166 + }) 167 + expect(renderForm({values: {salary: '1e5'}})).toHaveFormValues({ 168 + salary: 1e5, 169 + }) 170 + expect(renderForm({values: {salary: '1.35e5'}})).toHaveFormValues({ 171 + salary: 135000, 172 + }) 173 + expect(renderForm({values: {salary: '-5.9'}})).toHaveFormValues({ 174 + salary: -5.9, 175 + }) 176 + }) 177 + 178 + describe('edge cases', () => { 179 + // This is also to ensure 100% code coverage for edge cases 180 + it('detects multiple elements with the same name but different type', () => { 181 + const {container} = render(` 182 + <form> 183 + <input type="checkbox" name="accept"> 184 + <input type="radio" name="accept"> 185 + </form> 186 + `) 187 + const form = container.querySelector('form') 188 + expect(() => { 189 + expect(form).toHaveFormValues({}) 190 + }).toThrowError(/must be of the same type/) 191 + }) 192 + 193 + it('detects multiple elements with the same type and name', () => { 194 + const {container} = render(` 195 + <form> 196 + <input type="text" name="title" value="one"> 197 + <input type="text" name="title" value="two"> 198 + </form> 199 + `) 200 + const form = container.querySelector('form') 201 + expect(form).toHaveFormValues({ 202 + title: ['one', 'two'], 203 + }) 204 + }) 205 + 206 + it('supports radio buttons with none selected', () => { 207 + expect( 208 + renderForm({ 209 + selectSingle: renderRadioButtons, 210 + values: {category: undefined}, 211 + }), 212 + ).toHaveFormValues({ 213 + category: undefined, 214 + }) 215 + }) 216 + 217 + it('supports being called only on form and fieldset elements', () => { 218 + const expectedValues = {title: 'one', description: 'two'} 219 + const {container} = render(` 220 + <form> 221 + <input type="text" name="title" value="one"> 222 + <input type="text" name="description" value="two"> 223 + </form> 224 + `) 225 + const form = container.querySelector('form') 226 + expect(() => { 227 + expect(container).toHaveFormValues(expectedValues) 228 + }).toThrowError(/a form or a fieldset/) 229 + expect(() => { 230 + expect(form).toHaveFormValues(expectedValues) 231 + }).not.toThrowError() 232 + }) 233 + 234 + it('matches change in selected value of select', () => { 235 + const oldValue = '' 236 + const newValue = 'design' 237 + 238 + const {container} = render(` 239 + <form> 240 + ${renderSelectSingle('category', 'Category', categories, oldValue)} 241 + </form> 242 + `) 243 + 244 + const form = container.querySelector('form') 245 + const select = container.querySelector('select') 246 + expect(form).toHaveFormValues({category: oldValue}) 247 + 248 + select.value = newValue 249 + expect(form).toHaveFormValues({category: newValue}) 250 + }) 251 + }) 252 + 253 + describe('failed assertions', () => { 254 + it('work as expected', () => { 255 + expect(() => { 256 + expect(renderForm()).not.toHaveFormValues(defaultValues) 257 + }).toThrowError(/Expected the element not to have form values/) 258 + expect(() => { 259 + expect(renderForm()).toHaveFormValues({something: 'missing'}) 260 + }).toThrowError(/Expected the element to have form values/) 261 + }) 262 + }) 263 + }) 264 + 265 + // Form control renderers 266 + 267 + function isSelected(value, option) { 268 + return Array.isArray(value) && value.indexOf(option.value) >= 0 269 + } 270 + 271 + function renderCheckboxes(name, label, options, value = []) { 272 + return ` 273 + <fieldset> 274 + <legend>${label}</legend> 275 + ${renderList( 276 + options, 277 + option => ` 278 + <div> 279 + <input 280 + type="checkbox" 281 + name="${name}[]" 282 + id="${option.value}" 283 + value="${option.value}" 284 + ${isSelected(value, option) ? 'checked' : ''} 285 + /> 286 + <label for="${option.value}">${option.label}</label> 287 + </div> 288 + `, 289 + )} 290 + </fieldset> 291 + ` 292 + } 293 + 294 + function renderRadioButtons(name, label, options, value = undefined) { 295 + return ` 296 + <fieldset> 297 + <legend>${label}</legend> 298 + ${renderList( 299 + options, 300 + option => ` 301 + <div> 302 + <input 303 + type="radio" 304 + name="${name}" 305 + id="${option.value}" 306 + value="${option.value}" 307 + ${value === option.value ? 'checked' : ''} 308 + /> 309 + <label for="${option.value}">${option.label}</label> 310 + </div> 311 + `, 312 + )} 313 + </fieldset> 314 + ` 315 + } 316 + 317 + function renderSelect(name, label, options, value, multiple) { 318 + return ` 319 + <label for="${name}">${label}</label> 320 + <select id="${name}" name="${name}" ${multiple ? 'multiple' : ''}> 321 + ${renderList( 322 + options, 323 + option => ` 324 + <option 325 + value="${option.value}" 326 + ${isSelected(value, option) ? 'selected' : ''} 327 + > 328 + ${option.label} 329 + </option> 330 + `, 331 + )} 332 + </select> 333 + ` 334 + } 335 + 336 + function renderSelectSingle(name, label, options, value = undefined) { 337 + return renderSelect( 338 + name, 339 + label, 340 + options, 341 + value === undefined || value === null ? [] : [value], 342 + false, 343 + ) 344 + } 345 + 346 + function renderSelectMultiple(name, label, options, value = []) { 347 + return renderSelect(name, label, options, value, true) 348 + } 349 + 350 + function renderList(items, mapper) { 351 + return items.map(mapper).join('') 352 + }
+65
test/browser/fixtures/expect-dom/toHaveRole.test.ts
··· 1 + import { describe, expect, it } from 'vitest' 2 + import { render } from './utils' 3 + 4 + describe('.toHaveRole', () => { 5 + it('matches implicit role', () => { 6 + const {queryByTestId} = render(` 7 + <div> 8 + <button data-testid="continue-button">Continue</button> 9 + </div> 10 + `) 11 + 12 + const continueButton = queryByTestId('continue-button') 13 + 14 + expect(continueButton).not.toHaveRole('listitem') 15 + expect(continueButton).toHaveRole('button') 16 + 17 + expect(() => { 18 + expect(continueButton).toHaveRole('listitem') 19 + }).toThrow(/expected element to have role/i) 20 + expect(() => { 21 + expect(continueButton).not.toHaveRole('button') 22 + }).toThrow(/expected element not to have role/i) 23 + }) 24 + 25 + it('matches explicit role', () => { 26 + const {queryByTestId} = render(` 27 + <div> 28 + <div role="button" data-testid="continue-button">Continue</div> 29 + </div> 30 + `) 31 + 32 + const continueButton = queryByTestId('continue-button') 33 + 34 + expect(continueButton).not.toHaveRole('listitem') 35 + expect(continueButton).toHaveRole('button') 36 + 37 + expect(() => { 38 + expect(continueButton).toHaveRole('listitem') 39 + }).toThrow(/expected element to have role/i) 40 + expect(() => { 41 + expect(continueButton).not.toHaveRole('button') 42 + }).toThrow(/expected element not to have role/i) 43 + }) 44 + 45 + it('matches multiple explicit roles', () => { 46 + const {queryByTestId} = render(` 47 + <div> 48 + <div role="button switch" data-testid="continue-button">Continue</div> 49 + </div> 50 + `) 51 + 52 + const continueButton = queryByTestId('continue-button') 53 + 54 + expect(continueButton).not.toHaveRole('listitem') 55 + expect(continueButton).toHaveRole('button') 56 + expect(continueButton).not.toHaveRole('switch') 57 + 58 + expect(() => { 59 + expect(continueButton).toHaveRole('listitem') 60 + }).toThrow(/expected element to have role/i) 61 + expect(() => { 62 + expect(continueButton).not.toHaveRole('button') 63 + }).toThrow(/expected element not to have role/i) 64 + }) 65 + })
+183
test/browser/fixtures/expect-dom/toHaveSelection.test.ts
··· 1 + import { describe, expect, test } from 'vitest' 2 + import { render } from './utils' 3 + 4 + describe('.toHaveSelection', () => { 5 + test.each(['text', 'password', 'textarea'])( 6 + 'handles selection within form elements', 7 + testId => { 8 + const {getInputByTestId} = render(` 9 + <input type="text" value="text selected text" data-testid="text" /> 10 + <input type="password" value="text selected text" data-testid="password" /> 11 + <textarea data-testid="textarea">text selected text</textarea> 12 + `) 13 + 14 + getInputByTestId(testId).setSelectionRange(5, 13) 15 + expect(getInputByTestId(testId)).toHaveSelection('selected') 16 + 17 + getInputByTestId(testId).select() 18 + expect(getInputByTestId(testId)).toHaveSelection('text selected text') 19 + }, 20 + ) 21 + 22 + test.each(['checkbox', 'radio'])( 23 + 'returns empty string for form elements without text', 24 + testId => { 25 + const {getInputByTestId} = render(` 26 + <input type="checkbox" value="checkbox" data-testid="checkbox" /> 27 + <input type="radio" value="radio" data-testid="radio" /> 28 + `) 29 + 30 + getInputByTestId(testId).select() 31 + expect(getInputByTestId(testId)).toHaveSelection('') 32 + }, 33 + ) 34 + 35 + test('does not match subset string', () => { 36 + const {getInputByTestId} = render(` 37 + <input type="text" value="text selected text" data-testid="text" /> 38 + `) 39 + 40 + getInputByTestId('text').setSelectionRange(5, 13) 41 + expect(getInputByTestId('text')).not.toHaveSelection('select') 42 + expect(getInputByTestId('text')).toHaveSelection('selected') 43 + }) 44 + 45 + test('accepts any selection when expected selection is missing', () => { 46 + const {getInputByTestId} = render(` 47 + <input type="text" value="text selected text" data-testid="text" /> 48 + `) 49 + 50 + expect(getInputByTestId('text')).not.toHaveSelection() 51 + 52 + getInputByTestId('text').setSelectionRange(5, 13) 53 + 54 + expect(getInputByTestId('text')).toHaveSelection() 55 + }) 56 + 57 + test('throws when form element is not selected', () => { 58 + const {queryByTestId} = render(` 59 + <input type="text" value="text selected text" data-testid="text" /> 60 + `) 61 + 62 + expect(() => 63 + expect(queryByTestId('text')).toHaveSelection(), 64 + ).toThrowErrorMatchingInlineSnapshot(` 65 + [Error: expect(element).toHaveSelection(expected) 66 + 67 + Expected the element to have selection: 68 + (any) 69 + Received: 70 + ] 71 + `) 72 + }) 73 + 74 + test('throws when form element is selected', () => { 75 + const {getInputByTestId} = render(` 76 + <input type="text" value="text selected text" data-testid="text" /> 77 + `) 78 + getInputByTestId('text').setSelectionRange(5, 13) 79 + 80 + expect(() => 81 + expect(getInputByTestId('text')).not.toHaveSelection(), 82 + ).toThrowErrorMatchingInlineSnapshot(` 83 + [Error: expect(element).not.toHaveSelection(expected) 84 + 85 + Expected the element not to have selection: 86 + (any) 87 + Received: 88 + selected] 89 + `) 90 + }) 91 + 92 + test('throws when element is not selected', () => { 93 + const {queryByTestId} = render(` 94 + <div data-testid="text">text</div> 95 + `) 96 + 97 + expect(() => 98 + expect(queryByTestId('text')).toHaveSelection(), 99 + ).toThrowErrorMatchingInlineSnapshot(` 100 + [Error: expect(element).toHaveSelection(expected) 101 + 102 + Expected the element to have selection: 103 + (any) 104 + Received: 105 + ] 106 + `) 107 + }) 108 + 109 + test('throws when element selection does not match', () => { 110 + const {getInputByTestId} = render(` 111 + <input type="text" value="text selected text" data-testid="text" /> 112 + `) 113 + getInputByTestId('text').setSelectionRange(0, 4) 114 + 115 + expect(() => 116 + expect(getInputByTestId('text')).toHaveSelection('no match'), 117 + ).toThrowErrorMatchingInlineSnapshot(` 118 + [Error: expect(element).toHaveSelection(no match) 119 + 120 + Expected the element to have selection: 121 + no match 122 + Received: 123 + text] 124 + `) 125 + }) 126 + 127 + test('handles selection within text nodes', () => { 128 + const {queryByTestId} = render(` 129 + <div> 130 + <div data-testid="prev">prev</div> 131 + <div data-testid="parent">text <span data-testid="child">selected</span> text</div> 132 + <div data-testid="next">next</div> 133 + </div> 134 + `) 135 + 136 + const selection = queryByTestId('child').ownerDocument.getSelection() 137 + const range = queryByTestId('child').ownerDocument.createRange() 138 + selection.removeAllRanges() 139 + selection.empty() 140 + selection.addRange(range) 141 + 142 + range.selectNodeContents(queryByTestId('child')) 143 + 144 + expect(queryByTestId('child')).toHaveSelection('selected') 145 + expect(queryByTestId('parent')).toHaveSelection('selected') 146 + 147 + range.selectNodeContents(queryByTestId('parent')) 148 + 149 + expect(queryByTestId('child')).toHaveSelection('selected') 150 + expect(queryByTestId('parent')).toHaveSelection('text selected text') 151 + 152 + range.setStart(queryByTestId('prev'), 0) 153 + range.setEnd(queryByTestId('child').childNodes[0], 3) 154 + 155 + expect(queryByTestId('prev')).toHaveSelection('prev') 156 + expect(queryByTestId('child')).toHaveSelection('sel') 157 + expect(queryByTestId('parent')).toHaveSelection('text sel') 158 + expect(queryByTestId('next')).not.toHaveSelection() 159 + 160 + range.setStart(queryByTestId('child').childNodes[0], 3) 161 + range.setEnd(queryByTestId('next').childNodes[0], 2) 162 + 163 + expect(queryByTestId('child')).toHaveSelection('ected') 164 + expect(queryByTestId('parent')).toHaveSelection('ected text') 165 + expect(queryByTestId('prev')).not.toHaveSelection() 166 + expect(queryByTestId('next')).toHaveSelection('ne') 167 + }) 168 + 169 + test('throws with information when the expected selection is not string', () => { 170 + const {container} = render(`<div>1</div>`) 171 + const element = container.firstChild 172 + const range = element.ownerDocument.createRange() 173 + range.selectNodeContents(element) 174 + element.ownerDocument.getSelection().addRange(range) 175 + 176 + expect(() => 177 + // @ts-expect-error wrong type 178 + expect(element).toHaveSelection(1), 179 + ).toThrowErrorMatchingInlineSnapshot( 180 + `[Error: expected selection must be a string or undefined]` 181 + ) 182 + }) 183 + })
+283
test/browser/fixtures/expect-dom/toHaveStyle.test.ts
··· 1 + import { describe, expect, test } from 'vitest' 2 + import { render } from './utils' 3 + 4 + describe('.toHaveStyle', () => { 5 + test('handles positive test cases', () => { 6 + const { container } = render(` 7 + <div class="label" style="background-color: blue; height: 100%;"> 8 + Hello World 9 + </div> 10 + `) 11 + 12 + const style = document.createElement('style') 13 + style.innerHTML = ` 14 + .label { 15 + align-items: center; 16 + background-color: black; 17 + color: white; 18 + float: left; 19 + transition: opacity 0.2s ease-out, top 0.3s cubic-bezier(0.1, 0.7, 1.0, 0.1); 20 + transform: translateX(0px); 21 + } 22 + ` 23 + document.body.appendChild(style) 24 + document.body.appendChild(container) 25 + 26 + // border: fakefake doesn't exist 27 + expect(() => { 28 + expect(container.querySelector('.label')).toHaveStyle('border: fakefake') 29 + }).toThrowError() 30 + 31 + expect(container.querySelector('.label')).toHaveStyle(` 32 + height: 100%; 33 + color: white; 34 + background-color: blue; 35 + `) 36 + 37 + expect(container.querySelector('.label')).toHaveStyle(` 38 + background-color: blue; 39 + color: white; 40 + `) 41 + 42 + expect(container.querySelector('.label')).toHaveStyle( 43 + 'transition: opacity 0.2s ease-out, top 0.3s cubic-bezier(0.1, 0.7, 1.0, 0.1)', 44 + ) 45 + 46 + expect(container.querySelector('.label')).toHaveStyle( 47 + 'background-color:blue;color:white', 48 + ) 49 + 50 + expect(container.querySelector('.label')).not.toHaveStyle(` 51 + color: white; 52 + font-weight: bold; 53 + `) 54 + 55 + expect(container.querySelector('.label')).toHaveStyle(` 56 + Align-items: center; 57 + `) 58 + 59 + expect(container.querySelector('.label')).toHaveStyle(` 60 + transform: translateX(0px); 61 + `) 62 + }) 63 + 64 + test('handles negative test cases', () => { 65 + const { container } = render(` 66 + <div class="label" style="background-color: blue; height: 100%"> 67 + Hello World 68 + </div> 69 + `) 70 + 71 + const style = document.createElement('style') 72 + style.innerHTML = ` 73 + .label { 74 + background-color: black; 75 + color: white; 76 + float: left; 77 + --var-name: 0px; 78 + transition: opacity 0.2s ease-out, top 0.3s cubic-bezier(1.175, 0.885, 0.32, 1.275); 79 + } 80 + ` 81 + document.body.appendChild(style) 82 + document.body.appendChild(container) 83 + 84 + // CSS parser is forgiving, it doesn't throw 85 + // expect(() => 86 + // expect(container.querySelector('.label')).not.toHaveStyle( 87 + // 'font-weight bold', 88 + // ), 89 + // ).toThrowError() 90 + 91 + expect(() => 92 + expect(container.querySelector('.label')).toHaveStyle( 93 + 'font-weight: bold', 94 + ), 95 + ).toThrowError() 96 + 97 + expect(() => 98 + expect(container.querySelector('.label')).not.toHaveStyle('color: white'), 99 + ).toThrowError() 100 + 101 + expect(() => 102 + expect(container.querySelector('.label')).toHaveStyle( 103 + 'transition: all 0.7s ease, width 1.0s cubic-bezier(3, 4, 5, 6);', 104 + ), 105 + ).toThrowError() 106 + 107 + // Custom property names are case sensitive 108 + expect(() => 109 + expect(container.querySelector('.label')).toHaveStyle('--VAR-NAME: 0px;'), 110 + ).toThrowError() 111 + 112 + expect(() => 113 + expect(container.querySelector('.label')).toHaveStyle('color white'), 114 + ).toThrowError() 115 + 116 + expect(() => 117 + expect(container.querySelector('.label')).toHaveStyle('--color: black'), 118 + ).toThrowError() 119 + document.body.removeChild(style) 120 + document.body.removeChild(container) 121 + }) 122 + 123 + test('properly normalizes colors', () => { 124 + const { queryByTestId } = render(` 125 + <span data-testid="color-example" style="background-color: #123456">Hello World</span> 126 + `) 127 + expect(queryByTestId('color-example')).toHaveStyle( 128 + 'background-color: #123456', 129 + ) 130 + }) 131 + 132 + test('handles inline custom properties (with uppercase letters)', () => { 133 + const { queryByTestId } = render(` 134 + <span data-testid="color-example" style="--accentColor: blue">Hello World</span> 135 + `) 136 + expect(queryByTestId('color-example')).toHaveStyle('--accentColor: blue') 137 + }) 138 + 139 + test('handles global custom properties', () => { 140 + const style = document.createElement('style') 141 + style.innerHTML = ` 142 + div { 143 + --color: blue; 144 + } 145 + ` 146 + 147 + const { container } = render(` 148 + <div> 149 + Hello world 150 + </div> 151 + `) 152 + 153 + document.body.appendChild(style) 154 + document.body.appendChild(container) 155 + 156 + expect(container).toHaveStyle(`--color: blue`) 157 + }) 158 + 159 + test('properly normalizes colors for border', () => { 160 + const { queryByTestId } = render(` 161 + <span data-testid="color-example" style="border: 1px solid #fff">Hello World</span> 162 + `) 163 + expect(queryByTestId('color-example')).toHaveStyle('border: 1px solid #fff') 164 + }) 165 + 166 + test('handles different color declaration formats', () => { 167 + const { queryByTestId } = render(` 168 + <span data-testid="color-example" style="color: rgba(0, 0, 0, 1); background-color: #000000">Hello World</span> 169 + `) 170 + 171 + expect(queryByTestId('color-example')).toHaveStyle('color: #000000') 172 + expect(queryByTestId('color-example')).toHaveStyle( 173 + 'background-color: rgba(0, 0, 0, 1)', 174 + ) 175 + }) 176 + 177 + test('handles nonexistent styles', () => { 178 + const { container } = render(` 179 + <div class="label" style="background-color: blue; height: 100%"> 180 + Hello World 181 + </div> 182 + `) 183 + 184 + expect(container.querySelector('.label')).not.toHaveStyle( 185 + 'whatever: anything', 186 + ) 187 + }) 188 + 189 + describe('object syntax', () => { 190 + test('handles styles as object', () => { 191 + const { container } = render(` 192 + <div class="label" style="background-color: blue; height: 100%"> 193 + Hello World 194 + </div> 195 + `) 196 + 197 + expect(container.querySelector('.label')).toHaveStyle({ 198 + backgroundColor: 'blue', 199 + }) 200 + expect(container.querySelector('.label')).toHaveStyle({ 201 + backgroundColor: 'blue', 202 + height: '100%', 203 + }) 204 + expect(container.querySelector('.label')).not.toHaveStyle({ 205 + backgroundColor: 'red', 206 + height: '100%', 207 + }) 208 + expect(container.querySelector('.label')).not.toHaveStyle({ 209 + whatever: 'anything', 210 + }) 211 + }) 212 + 213 + // https://github.com/testing-library/jest-dom/issues/350 214 + test('Uses correct computed values', () => { 215 + const { container } = render(` 216 + <div class="label"> 217 + Hello World 218 + </div> 219 + `) 220 + 221 + const style = document.createElement('style') 222 + style.innerHTML = ` 223 + .label { 224 + color: #fff; 225 + background-color: #000; 226 + } 227 + ` 228 + document.body.appendChild(style) 229 + document.body.appendChild(container) 230 + 231 + expect(container.querySelector('.label')).toHaveStyle('color: #FFF') 232 + }) 233 + 234 + test('Uses px as the default unit', () => { 235 + const { queryByTestId } = render(` 236 + <span data-testid="color-example" style="font-size: 12px">Hello World</span> 237 + `) 238 + expect(queryByTestId('color-example')).toHaveStyle({ 239 + // in jest-dom '12' is converted to 12px 240 + // but in the browser setting the style to 12 wil have no effect, 241 + // so Vitest prioritizes the browser behavior 242 + // fontSize: 12, 243 + fontSize: '12px', 244 + }) 245 + }) 246 + 247 + test('Fails with an invalid unit', () => { 248 + const { queryByTestId } = render(` 249 + <span data-testid="color-example" style="font-size: 12rem">Hello World</span> 250 + `) 251 + expect(() => { 252 + expect(queryByTestId('color-example')).toHaveStyle({ fontSize: '12px' }) 253 + }).toThrowError() 254 + }) 255 + 256 + test('supports dash-cased property names', () => { 257 + const { container } = render(` 258 + <div class="label" style="background-color: blue; height: 100%"> 259 + Hello World 260 + </div> 261 + `) 262 + expect(container.querySelector('.label')).toHaveStyle({ 263 + 'background-color': 'blue', 264 + }) 265 + }) 266 + 267 + test('requires strict empty properties matching', () => { 268 + const { container } = render(` 269 + <div class="label" style="width: 100%;height: 100%"> 270 + Hello World 271 + </div> 272 + `) 273 + expect(container.querySelector('.label')).not.toHaveStyle({ 274 + width: '100%', 275 + height: '', 276 + }) 277 + expect(container.querySelector('.label')).not.toHaveStyle({ 278 + width: '', 279 + height: '', 280 + }) 281 + }) 282 + }) 283 + })
+109
test/browser/fixtures/expect-dom/toHaveTextContent.test.ts
··· 1 + import { describe, expect, test } from 'vitest' 2 + import { render } from './utils' 3 + 4 + describe('.toHaveTextContent', () => { 5 + test('handles positive test cases', () => { 6 + const {queryByTestId} = render(`<span data-testid="count-value">2</span>`) 7 + 8 + expect(queryByTestId('count-value')).toHaveTextContent('2') 9 + expect(queryByTestId('count-value')).toHaveTextContent(2) 10 + expect(queryByTestId('count-value')).toHaveTextContent(/2/) 11 + expect(queryByTestId('count-value')).not.toHaveTextContent('21') 12 + }) 13 + 14 + test('handles text nodes', () => { 15 + const {container} = render(`<span>example</span>`) 16 + 17 + expect(container.querySelector('span').firstChild).toHaveTextContent( 18 + 'example', 19 + ) 20 + }) 21 + 22 + test('handles fragments', () => { 23 + const {asFragment} = render(`<span>example</span>`) 24 + 25 + expect(asFragment()).toHaveTextContent('example') 26 + }) 27 + 28 + test('handles negative test cases', () => { 29 + const {queryByTestId} = render(`<span data-testid="count-value">2</span>`) 30 + 31 + expect(() => 32 + expect(queryByTestId('count-value2')).toHaveTextContent('2'), 33 + ).toThrowError() 34 + 35 + expect(() => 36 + expect(queryByTestId('count-value')).toHaveTextContent('3'), 37 + ).toThrowError() 38 + expect(() => 39 + expect(queryByTestId('count-value')).not.toHaveTextContent('2'), 40 + ).toThrowError() 41 + }) 42 + 43 + test('normalizes whitespace by default', () => { 44 + const {container} = render(` 45 + <span> 46 + Step 47 + 1 48 + of 49 + 4 50 + </span> 51 + `) 52 + 53 + expect(container.querySelector('span')).toHaveTextContent('Step 1 of 4') 54 + }) 55 + 56 + test('allows whitespace normalization to be turned off', () => { 57 + const {container} = render(`<span>&nbsp;&nbsp;Step 1 of 4</span>`) 58 + 59 + expect(container.querySelector('span')).toHaveTextContent(' Step 1 of 4', { 60 + normalizeWhitespace: false, 61 + }) 62 + }) 63 + 64 + test('can handle multiple levels', () => { 65 + const {container} = render(`<span id="parent"><span>Step 1 66 + 67 + of 4</span></span>`) 68 + 69 + expect(container.querySelector('#parent')).toHaveTextContent('Step 1 of 4') 70 + }) 71 + 72 + test('can handle multiple levels with content spread across decendants', () => { 73 + const {container} = render(` 74 + <span id="parent"> 75 + <span>Step</span> 76 + <span> 1</span> 77 + <span><span>of</span></span> 78 + 79 + 80 + 4</span> 81 + </span> 82 + `) 83 + 84 + expect(container.querySelector('#parent')).toHaveTextContent('Step 1 of 4') 85 + }) 86 + 87 + test('does not throw error with empty content', () => { 88 + const {container} = render(`<span></span>`) 89 + expect(container.querySelector('span')).toHaveTextContent('') 90 + }) 91 + 92 + test('is case-sensitive', () => { 93 + const {container} = render('<span>Sensitive text</span>') 94 + 95 + expect(container.querySelector('span')).toHaveTextContent('Sensitive text') 96 + expect(container.querySelector('span')).not.toHaveTextContent( 97 + 'sensitive text', 98 + ) 99 + }) 100 + 101 + test('when matching with empty string and element with content, suggest using toBeEmptyDOMElement instead', () => { 102 + // https://github.com/testing-library/jest-dom/issues/104 103 + const {container} = render('<span>not empty</span>') 104 + 105 + expect(() => 106 + expect(container.querySelector('span')).toHaveTextContent(''), 107 + ).toThrowError(/toBeEmptyDOMElement\(\)/) 108 + }) 109 + })
+221
test/browser/fixtures/expect-dom/toHaveValue.test.ts
··· 1 + import { describe, expect, test } from 'vitest' 2 + import { render } from './utils' 3 + 4 + describe('.toHaveValue', () => { 5 + test('handles value of text input', () => { 6 + const {queryByTestId, getInputByTestId} = render(` 7 + <input type="text" value="foo" data-testid="value" /> 8 + <input type="text" value="" data-testid="empty" /> 9 + <input type="text" data-testid="without" /> 10 + `) 11 + 12 + expect(queryByTestId('value')).toHaveValue('foo') 13 + expect(queryByTestId('value')).toHaveValue() 14 + expect(queryByTestId('value')).not.toHaveValue('bar') 15 + expect(queryByTestId('value')).not.toHaveValue('') 16 + 17 + expect(queryByTestId('empty')).toHaveValue('') 18 + expect(queryByTestId('empty')).not.toHaveValue() 19 + expect(queryByTestId('empty')).not.toHaveValue('foo') 20 + 21 + expect(queryByTestId('without')).toHaveValue('') 22 + expect(queryByTestId('without')).not.toHaveValue() 23 + expect(queryByTestId('without')).not.toHaveValue('foo') 24 + getInputByTestId('without').value = 'bar' 25 + expect(queryByTestId('without')).toHaveValue('bar') 26 + }) 27 + 28 + test('handles value of number input', () => { 29 + const {queryByTestId, getInputByTestId} = render(` 30 + <input type="number" value="5" data-testid="number" /> 31 + <input type="number" value="" data-testid="empty" /> 32 + <input type="number" data-testid="without" /> 33 + `) 34 + 35 + expect(queryByTestId('number')).toHaveValue(5) 36 + expect(queryByTestId('number')).toHaveValue() 37 + expect(queryByTestId('number')).not.toHaveValue(4) 38 + expect(queryByTestId('number')).not.toHaveValue('5') 39 + 40 + expect(queryByTestId('empty')).toHaveValue(null) 41 + expect(queryByTestId('empty')).not.toHaveValue() 42 + expect(queryByTestId('empty')).not.toHaveValue('5') 43 + 44 + expect(queryByTestId('without')).toHaveValue(null) 45 + expect(queryByTestId('without')).not.toHaveValue() 46 + expect(queryByTestId('without')).not.toHaveValue('10') 47 + // @ts-expect-error ts doesn't allow value to be a number, but browser will convert it 48 + getInputByTestId('without').value = 10 49 + expect(queryByTestId('without')).toHaveValue(10) 50 + }) 51 + 52 + test('handles value of select element', () => { 53 + const {queryByTestId} = render(` 54 + <select data-testid="single"> 55 + <option value="first">First Value</option> 56 + <option value="second" selected>Second Value</option> 57 + <option value="third">Third Value</option> 58 + </select> 59 + 60 + <select data-testid="multiple" multiple> 61 + <option value="first">First Value</option> 62 + <option value="second" selected>Second Value</option> 63 + <option value="third" selected>Third Value</option> 64 + </select> 65 + 66 + <select data-testid="not-selected" > 67 + <option value="" disabled selected>- Select some value - </option> 68 + <option value="first">First Value</option> 69 + <option value="second">Second Value</option> 70 + <option value="third">Third Value</option> 71 + </select> 72 + `) 73 + 74 + expect(queryByTestId('single')).toHaveValue('second') 75 + expect(queryByTestId('single')).toHaveValue() 76 + 77 + expect(queryByTestId('multiple')).toHaveValue(['second', 'third']) 78 + expect(queryByTestId('multiple')).toHaveValue() 79 + 80 + expect(queryByTestId('not-selected')).not.toHaveValue() 81 + expect(queryByTestId('not-selected')).toHaveValue('') 82 + 83 + queryByTestId('single').children[0].setAttribute('selected', 'true') 84 + expect(queryByTestId('single')).toHaveValue('first') 85 + }) 86 + 87 + test('handles value of textarea element', () => { 88 + const {queryByTestId} = render(` 89 + <textarea data-testid="textarea">text value</textarea> 90 + `) 91 + expect(queryByTestId('textarea')).toHaveValue('text value') 92 + }) 93 + 94 + test('throws when passed checkbox or radio', () => { 95 + const {queryByTestId} = render(` 96 + <input data-testid="checkbox" type="checkbox" name="checkbox" value="val" checked /> 97 + <input data-testid="radio" type="radio" name="radio" value="val" checked /> 98 + `) 99 + 100 + expect(() => { 101 + expect(queryByTestId('checkbox')).toHaveValue('') 102 + }).toThrow() 103 + 104 + expect(() => { 105 + expect(queryByTestId('radio')).toHaveValue('') 106 + }).toThrow() 107 + }) 108 + 109 + test('throws when the expected input value does not match', () => { 110 + const {container} = render(`<input data-testid="one" value="foo" />`) 111 + const input = container.firstChild 112 + let errorMessage 113 + try { 114 + expect(input).toHaveValue('something else') 115 + } catch (error) { 116 + errorMessage = error.message 117 + } 118 + 119 + expect(errorMessage).toMatchInlineSnapshot(` 120 + expect(element).toHaveValue(something else) 121 + 122 + Expected the element to have value: 123 + something else 124 + Received: 125 + foo 126 + `) 127 + }) 128 + 129 + test('throws with type information when the expected text input value has loose equality with received value', () => { 130 + const {container} = render(`<input data-testid="one" value="8" />`) 131 + const input = container.firstChild 132 + let errorMessage 133 + try { 134 + expect(input).toHaveValue(8) 135 + } catch (error) { 136 + errorMessage = error.message 137 + } 138 + 139 + expect(errorMessage).toMatchInlineSnapshot(` 140 + expect(element).toHaveValue(8) 141 + 142 + Expected the element to have value: 143 + 8 (number) 144 + Received: 145 + 8 (string) 146 + `) 147 + }) 148 + 149 + test('throws when using not but the expected input value does match', () => { 150 + const {container} = render(`<input data-testid="one" value="foo" />`) 151 + const input = container.firstChild 152 + let errorMessage 153 + 154 + try { 155 + expect(input).not.toHaveValue('foo') 156 + } catch (error) { 157 + errorMessage = error.message 158 + } 159 + expect(errorMessage).toMatchInlineSnapshot(` 160 + expect(element).not.toHaveValue(foo) 161 + 162 + Expected the element not to have value: 163 + foo 164 + Received: 165 + foo 166 + `) 167 + }) 168 + 169 + test('throws when the form has no a value but a value is expected', () => { 170 + const {container} = render(`<input data-testid="one" />`) 171 + const input = container.firstChild 172 + let errorMessage 173 + 174 + try { 175 + expect(input).toHaveValue() 176 + } catch (error) { 177 + errorMessage = error.message 178 + } 179 + expect(errorMessage).toMatchInlineSnapshot(` 180 + expect(element).toHaveValue(expected) 181 + 182 + Expected the element to have value: 183 + (any) 184 + Received: 185 + `) 186 + }) 187 + 188 + test('throws when the form has a value but none is expected', () => { 189 + const {container} = render(`<input data-testid="one" value="foo" />`) 190 + const input = container.firstChild 191 + let errorMessage 192 + 193 + try { 194 + expect(input).not.toHaveValue() 195 + } catch (error) { 196 + errorMessage = error.message 197 + } 198 + expect(errorMessage).toMatchInlineSnapshot(` 199 + expect(element).not.toHaveValue(expected) 200 + 201 + Expected the element not to have value: 202 + (any) 203 + Received: 204 + foo 205 + `) 206 + }) 207 + 208 + test('handles value of aria-valuenow', () => { 209 + const valueToCheck = 70 210 + const {queryByTestId} = render(` 211 + <div role="meter" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${valueToCheck}" data-testid="meter"></div> 212 + <div role="textbox" aria-valuenow="${valueToCheck}" data-testid="textbox"></div> 213 + `) 214 + 215 + expect(queryByTestId('meter')).toHaveValue(valueToCheck) 216 + expect(queryByTestId('meter')).not.toHaveValue(valueToCheck + 1) 217 + 218 + // Role that does not support aria-valuenow 219 + expect(queryByTestId('textbox')).not.toHaveValue(70) 220 + }) 221 + })
+19
test/browser/fixtures/expect-dom/utils.ts
··· 1 + function render(html: string) { 2 + const container = document.createElement('div') 3 + container.innerHTML = html 4 + const queryByTestId = (testId: string) => 5 + container.querySelector(`[data-testid="${testId}"]`) as HTMLElement | SVGElement | null 6 + // asFragment has been stolen from react-testing-library 7 + const asFragment = () => 8 + document.createRange().createContextualFragment(container.innerHTML) 9 + const getInputByTestId = (testId: string) => queryByTestId(testId) as HTMLInputElement 10 + 11 + // Some tests need to look up global ids with document.getElementById() 12 + // so we need to be inside an actual document. 13 + document.body.innerHTML = '' 14 + document.body.appendChild(container) 15 + 16 + return { container, queryByTestId, asFragment, getInputByTestId } 17 + } 18 + 19 + export { render }
+16
test/browser/fixtures/expect-dom/vitest.config.js
··· 1 + import { defineConfig } from 'vitest/config' 2 + import { fileURLToPath } from 'node:url' 3 + import { instances, provider } from '../../settings' 4 + 5 + export default defineConfig({ 6 + cacheDir: fileURLToPath(new URL("./node_modules/.vite", import.meta.url)), 7 + test: { 8 + browser: { 9 + enabled: true, 10 + provider, 11 + instances, 12 + isolate: false, 13 + }, 14 + setupFiles: './setup.ts', 15 + }, 16 + })
+34 -33
packages/browser/src/client/tester/expect-element.ts
··· 1 1 import type { Locator } from '@vitest/browser/context' 2 2 import type { ExpectPollOptions } from 'vitest' 3 - import * as matchers from '@testing-library/jest-dom/matchers' 4 3 import { chai, expect } from 'vitest' 4 + import { matchers } from './expect' 5 5 import { processTimeoutOptions } from './utils' 6 6 7 - export async function setupExpectDom(): Promise<void> { 8 - expect.extend(matchers) 9 - expect.element = <T extends Element | Locator>(elementOrLocator: T, options?: ExpectPollOptions) => { 10 - if (!(elementOrLocator instanceof Element) && !('element' in elementOrLocator)) { 11 - throw new Error(`Invalid element or locator: ${elementOrLocator}. Expected an instance of Element or Locator, received ${typeof elementOrLocator}`) 7 + function element<T extends Element | null | Locator>(elementOrLocator: T, options?: ExpectPollOptions): unknown { 8 + if (elementOrLocator != null && !(elementOrLocator instanceof Element) && !('element' in elementOrLocator)) { 9 + throw new Error(`Invalid element or locator: ${elementOrLocator}. Expected an instance of Element or Locator, received ${typeof elementOrLocator}`) 10 + } 11 + 12 + return expect.poll<Element | null>(function element(this: object) { 13 + if (elementOrLocator instanceof Element || elementOrLocator == null) { 14 + return elementOrLocator 15 + } 16 + chai.util.flag(this, '_poll.element', true) 17 + 18 + const isNot = chai.util.flag(this, 'negate') as boolean 19 + const name = chai.util.flag(this, '_name') as string 20 + // element selector uses prettyDOM under the hood, which is an expensive call 21 + // that should not be called on each failed locator attempt to avoid memory leak: 22 + // https://github.com/vitest-dev/vitest/issues/7139 23 + const isLastPollAttempt = chai.util.flag(this, '_isLastPollAttempt') 24 + // special case for `toBeInTheDocument` matcher 25 + if (isNot && name === 'toBeInTheDocument') { 26 + return elementOrLocator.query() 12 27 } 13 28 14 - return expect.poll<Element | null>(function element(this: object) { 15 - if (elementOrLocator instanceof Element || elementOrLocator == null) { 16 - return elementOrLocator 17 - } 18 - chai.util.flag(this, '_poll.element', true) 29 + if (isLastPollAttempt) { 30 + return elementOrLocator.element() 31 + } 19 32 20 - const isNot = chai.util.flag(this, 'negate') as boolean 21 - const name = chai.util.flag(this, '_name') as string 22 - // element selector uses prettyDOM under the hood, which is an expensive call 23 - // that should not be called on each failed locator attempt to avoid memory leak: 24 - // https://github.com/vitest-dev/vitest/issues/7139 25 - const isLastPollAttempt = chai.util.flag(this, '_isLastPollAttempt') 26 - // special case for `toBeInTheDocument` matcher 27 - if (isNot && name === 'toBeInTheDocument') { 28 - return elementOrLocator.query() 29 - } 33 + const result = elementOrLocator.query() 30 34 31 - if (isLastPollAttempt) { 32 - return elementOrLocator.element() 33 - } 35 + if (!result) { 36 + throw new Error(`Cannot find element with locator: ${JSON.stringify(elementOrLocator)}`) 37 + } 34 38 35 - const result = elementOrLocator.query() 36 - 37 - if (!result) { 38 - throw new Error(`Cannot find element with locator: ${JSON.stringify(elementOrLocator)}`) 39 - } 40 - 41 - return result 42 - }, processTimeoutOptions(options)) 43 - } 39 + return result 40 + }, processTimeoutOptions(options)) 44 41 } 42 + 43 + expect.extend(matchers) 44 + // Vitest typecheck doesn't pick up this assignment for some reason 45 + Object.assign(expect, { element })
-1
packages/browser/src/client/tester/jest-dom.ts
··· 1 - export type { default } from '@testing-library/jest-dom/matchers'
+1 -1
packages/browser/src/client/tester/runner.ts
··· 171 171 const runner = new BrowserRunner({ 172 172 config, 173 173 }) 174 + cachedRunner = runner 174 175 175 176 const [diffOptions] = await Promise.all([ 176 177 loadDiffConfig(config, executor as unknown as VitestExecutor), 177 178 loadSnapshotSerializers(config, executor as unknown as VitestExecutor), 178 179 ]) 179 180 runner.config.diffOptions = diffOptions 180 - cachedRunner = runner 181 181 getWorkerState().onFilterStackTrace = (stack: string) => { 182 182 const stacks = parseStacktrace(stack, { 183 183 getSourceMap(file) {
+8 -3
packages/browser/src/client/tester/tester.ts
··· 1 1 import { channel, client, onCancel } from '@vitest/browser/client' 2 2 import { page, server, userEvent } from '@vitest/browser/context' 3 - import { collectTests, setupCommonEnv, SpyModule, startCoverageInsideWorker, startTests, stopCoverageInsideWorker } from 'vitest/browser' 3 + import { 4 + collectTests, 5 + setupCommonEnv, 6 + SpyModule, 7 + startCoverageInsideWorker, 8 + startTests, 9 + stopCoverageInsideWorker, 10 + } from 'vitest/browser' 4 11 import { executor, getBrowserState, getConfig, getWorkerState } from '../utils' 5 12 import { setupDialogsSpy } from './dialog' 6 - import { setupExpectDom } from './expect-element' 7 13 import { setupConsoleLogSpy } from './logger' 8 14 import { VitestBrowserClientMocker } from './mocker' 9 15 import { createModuleMockerInterceptor } from './msw' ··· 52 58 53 59 setupConsoleLogSpy() 54 60 setupDialogsSpy() 55 - setupExpectDom() 56 61 57 62 const runner = await initiateRunner(state, mocker, config) 58 63
+52
packages/browser/src/client/tester/expect/index.ts
··· 1 + import type { MatchersObject } from '@vitest/expect' 2 + import toBeChecked from './toBeChecked' 3 + import toBeEmptyDOMElement from './toBeEmptyDOMElement' 4 + import { toBeDisabled, toBeEnabled } from './toBeEnabled' 5 + import toBeInTheDocument from './toBeInTheDocument' 6 + import { toBeInvalid, toBeValid } from './toBeInvalid' 7 + import toBePartiallyChecked from './toBePartiallyChecked' 8 + import toBeRequired from './toBeRequired' 9 + import toBeVisible from './toBeVisible' 10 + import toContainElement from './toContainElement' 11 + import toContainHTML from './toContainHTML' 12 + import toHaveAccessibleDescription from './toHaveAccessibleDescription' 13 + import toHaveAccessibleErrorMessage from './toHaveAccessibleErrorMessage' 14 + import toHaveAccessibleName from './toHaveAccessibleName' 15 + import toHaveAttribute from './toHaveAttribute' 16 + import toHaveClass from './toHaveClass' 17 + import toHaveDisplayValue from './toHaveDisplayValue' 18 + import toHaveFocus from './toHaveFocus' 19 + import toHaveFormValues from './toHaveFormValues' 20 + import toHaveRole from './toHaveRole' 21 + import toHaveSelection from './toHaveSelection' 22 + import toHaveStyle from './toHaveStyle' 23 + import toHaveTextContent from './toHaveTextContent' 24 + import toHaveValue from './toHaveValue' 25 + 26 + export const matchers: MatchersObject = { 27 + toBeDisabled, 28 + toBeEnabled, 29 + toBeEmptyDOMElement, 30 + toBeInTheDocument, 31 + toBeInvalid, 32 + toBeRequired, 33 + toBeValid, 34 + toBeVisible, 35 + toContainElement, 36 + toContainHTML, 37 + toHaveAccessibleDescription, 38 + toHaveAccessibleErrorMessage, 39 + toHaveAccessibleName, 40 + toHaveAttribute, 41 + toHaveClass, 42 + toHaveFocus, 43 + toHaveFormValues, 44 + toHaveStyle, 45 + toHaveTextContent, 46 + toHaveValue, 47 + toHaveDisplayValue, 48 + toBeChecked, 49 + toBePartiallyChecked, 50 + toHaveRole, 51 + toHaveSelection, 52 + }
+77
packages/browser/src/client/tester/expect/toBeChecked.ts
··· 1 + /** 2 + * The MIT License (MIT) 3 + * Copyright (c) 2017 Kent C. Dodds 4 + * 5 + * Permission is hereby granted, free of charge, to any person obtaining a copy 6 + * of this software and associated documentation files (the "Software"), to deal 7 + * in the Software without restriction, including without limitation the rights 8 + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + * copies of the Software, and to permit persons to whom the Software is 10 + * furnished to do so, subject to the following conditions: 11 + * 12 + * The above copyright notice and this permission notice shall be included in all 13 + * copies or substantial portions of the Software. 14 + */ 15 + 16 + import type { ExpectationResult, MatcherState } from '@vitest/expect' 17 + import type { Locator } from '../locators' 18 + import { getAriaChecked, getAriaCheckedRoles, getAriaRole } from 'ivya/utils' 19 + import { getElementFromUserInput, isInputElement, toSentence } from './utils' 20 + 21 + const supportedRoles = getAriaCheckedRoles() 22 + 23 + export default function toBeChecked( 24 + this: MatcherState, 25 + actual: Element | Locator, 26 + ): ExpectationResult { 27 + const htmlElement = getElementFromUserInput(actual, toBeChecked, this) 28 + 29 + const isValidInput = () => { 30 + return ( 31 + isInputElement(htmlElement) 32 + && ['checkbox', 'radio'].includes(htmlElement.type) 33 + ) 34 + } 35 + 36 + const isValidAriaElement = () => { 37 + return ( 38 + supportedRoles.includes(getAriaRole(htmlElement) || '') 39 + && ['true', 'false'].includes(htmlElement.getAttribute('aria-checked') || '') 40 + ) 41 + } 42 + 43 + if (!isValidInput() && !isValidAriaElement()) { 44 + return { 45 + pass: false, 46 + message: () => 47 + `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`, 48 + } 49 + } 50 + 51 + const checkedValue = getAriaChecked(htmlElement) 52 + const isChecked = checkedValue === true // don't tolerate "mixed", see toBePartiallyChecked 53 + 54 + return { 55 + pass: isChecked, 56 + message: () => { 57 + const is = isChecked ? 'is' : 'is not' 58 + return [ 59 + this.utils.matcherHint( 60 + `${this.isNot ? '.not' : ''}.toBeChecked`, 61 + 'element', 62 + '', 63 + ), 64 + '', 65 + `Received element ${is} checked:`, 66 + ` ${this.utils.printReceived(htmlElement.cloneNode(false))}`, 67 + ].join('\n') 68 + }, 69 + } 70 + } 71 + 72 + function supportedRolesSentence() { 73 + return toSentence( 74 + supportedRoles.map(role => `role="${role}"`), 75 + { lastWordConnector: ' or ' }, 76 + ) 77 + }
+49
packages/browser/src/client/tester/expect/toBeEmptyDOMElement.ts
··· 1 + /** 2 + * The MIT License (MIT) 3 + * Copyright (c) 2017 Kent C. Dodds 4 + * 5 + * Permission is hereby granted, free of charge, to any person obtaining a copy 6 + * of this software and associated documentation files (the "Software"), to deal 7 + * in the Software without restriction, including without limitation the rights 8 + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + * copies of the Software, and to permit persons to whom the Software is 10 + * furnished to do so, subject to the following conditions: 11 + * 12 + * The above copyright notice and this permission notice shall be included in all 13 + * copies or substantial portions of the Software. 14 + */ 15 + 16 + import type { ExpectationResult, MatcherState } from '@vitest/expect' 17 + import type { Locator } from '../locators' 18 + import { getElementFromUserInput } from './utils' 19 + 20 + export default function toBeEmptyDOMElement( 21 + this: MatcherState, 22 + actual: Element | Locator, 23 + ): ExpectationResult { 24 + const htmlElement = getElementFromUserInput(actual, toBeEmptyDOMElement, this) 25 + 26 + return { 27 + pass: isEmptyElement(htmlElement), 28 + message: () => { 29 + return [ 30 + this.utils.matcherHint( 31 + `${this.isNot ? '.not' : ''}.toBeEmptyDOMElement`, 32 + 'element', 33 + '', 34 + ), 35 + '', 36 + 'Received:', 37 + ` ${this.utils.printReceived(htmlElement.innerHTML)}`, 38 + ].join('\n') 39 + }, 40 + } 41 + } 42 + 43 + /** 44 + * Identifies if an element doesn't contain child nodes (excluding comments) 45 + */ 46 + function isEmptyElement(element: HTMLElement | SVGElement): boolean { 47 + const nonCommentChildNodes = [...element.childNodes].filter(node => node.nodeType !== Node.COMMENT_NODE) 48 + return nonCommentChildNodes.length === 0 49 + }
+75
packages/browser/src/client/tester/expect/toBeEnabled.ts
··· 1 + /** 2 + * The MIT License (MIT) 3 + * Copyright (c) 2017 Kent C. Dodds 4 + * 5 + * Permission is hereby granted, free of charge, to any person obtaining a copy 6 + * of this software and associated documentation files (the "Software"), to deal 7 + * in the Software without restriction, including without limitation the rights 8 + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + * copies of the Software, and to permit persons to whom the Software is 10 + * furnished to do so, subject to the following conditions: 11 + * 12 + * The above copyright notice and this permission notice shall be included in all 13 + * copies or substantial portions of the Software. 14 + */ 15 + 16 + import type { ExpectationResult, MatcherState } from '@vitest/expect' 17 + import type { Locator } from '../locators' 18 + import { getAriaDisabled } from 'ivya/utils' 19 + import { getElementFromUserInput, getTag } from './utils' 20 + 21 + export function toBeDisabled( 22 + this: MatcherState, 23 + actual: Element | Locator, 24 + ): ExpectationResult { 25 + const htmlElement = getElementFromUserInput(actual, toBeDisabled, this) 26 + const isDisabled = isElementDisabled(htmlElement) 27 + return { 28 + pass: isDisabled, 29 + message: () => { 30 + const is = isDisabled ? 'is' : 'is not' 31 + return [ 32 + this.utils.matcherHint( 33 + `${this.isNot ? '.not' : ''}.toBeDisabled`, 34 + 'element', 35 + '', 36 + ), 37 + '', 38 + `Received element ${is} disabled:`, 39 + ` ${this.utils.printReceived(htmlElement.cloneNode(false))}`, 40 + ].join('\n') 41 + }, 42 + } 43 + } 44 + 45 + export function toBeEnabled( 46 + this: MatcherState, 47 + actual: Element | Locator, 48 + ): ExpectationResult { 49 + const htmlElement = getElementFromUserInput(actual, toBeEnabled, this) 50 + const isDisabled = isElementDisabled(htmlElement) 51 + return { 52 + pass: !isDisabled, 53 + message: () => { 54 + const is = !isDisabled ? 'is' : 'is not' 55 + return [ 56 + this.utils.matcherHint( 57 + `${this.isNot ? '.not' : ''}.toBeEnabled`, 58 + 'element', 59 + '', 60 + ), 61 + '', 62 + `Received element ${is} enabled:`, 63 + ` ${this.utils.printReceived(htmlElement.cloneNode(false))}`, 64 + ].join('\n') 65 + }, 66 + } 67 + } 68 + 69 + function isElementDisabled(element: HTMLElement | SVGElement) { 70 + // ivya doesn't support custom elements check 71 + if (getTag(element).includes('-')) { 72 + return element.hasAttribute('disabled') 73 + } 74 + return getAriaDisabled(element) 75 + }
+59
packages/browser/src/client/tester/expect/toBeInTheDocument.ts
··· 1 + /** 2 + * The MIT License (MIT) 3 + * Copyright (c) 2017 Kent C. Dodds 4 + * 5 + * Permission is hereby granted, free of charge, to any person obtaining a copy 6 + * of this software and associated documentation files (the "Software"), to deal 7 + * in the Software without restriction, including without limitation the rights 8 + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + * copies of the Software, and to permit persons to whom the Software is 10 + * furnished to do so, subject to the following conditions: 11 + * 12 + * The above copyright notice and this permission notice shall be included in all 13 + * copies or substantial portions of the Software. 14 + */ 15 + 16 + import type { ExpectationResult, MatcherState } from '@vitest/expect' 17 + import type { Locator } from '../locators' 18 + import { getElementFromUserInput } from './utils' 19 + 20 + export default function toBeInTheDocument( 21 + this: MatcherState, 22 + actual: Element | Locator | null, 23 + ): ExpectationResult { 24 + let htmlElement: null | HTMLElement | SVGElement = null 25 + 26 + if (actual !== null || !this.isNot) { 27 + htmlElement = getElementFromUserInput(actual, toBeInTheDocument, this) 28 + } 29 + 30 + const pass 31 + = htmlElement === null 32 + ? false 33 + : htmlElement.ownerDocument === htmlElement.getRootNode({ composed: true }) 34 + 35 + const errorFound = () => { 36 + return `expected document not to contain element, found ${this.utils.stringify( 37 + htmlElement?.cloneNode(true), 38 + )} instead` 39 + } 40 + const errorNotFound = () => { 41 + return `element could not be found in the document` 42 + } 43 + 44 + return { 45 + pass, 46 + message: () => { 47 + return [ 48 + this.utils.matcherHint( 49 + `${this.isNot ? '.not' : ''}.toBeInTheDocument`, 50 + 'element', 51 + '', 52 + ), 53 + '', 54 + 55 + this.utils.RECEIVED_COLOR(this.isNot ? errorFound() : errorNotFound()), 56 + ].join('\n') 57 + }, 58 + } 59 + }
+93
packages/browser/src/client/tester/expect/toBeInvalid.ts
··· 1 + /** 2 + * The MIT License (MIT) 3 + * Copyright (c) 2017 Kent C. Dodds 4 + * 5 + * Permission is hereby granted, free of charge, to any person obtaining a copy 6 + * of this software and associated documentation files (the "Software"), to deal 7 + * in the Software without restriction, including without limitation the rights 8 + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + * copies of the Software, and to permit persons to whom the Software is 10 + * furnished to do so, subject to the following conditions: 11 + * 12 + * The above copyright notice and this permission notice shall be included in all 13 + * copies or substantial portions of the Software. 14 + */ 15 + 16 + import type { ExpectationResult, MatcherState } from '@vitest/expect' 17 + import type { Locator } from '../locators' 18 + import { getElementFromUserInput, getTag } from './utils' 19 + 20 + const FORM_TAGS = ['FORM', 'INPUT', 'SELECT', 'TEXTAREA'] 21 + 22 + function isElementHavingAriaInvalid(element: HTMLElement | SVGElement) { 23 + return ( 24 + element.hasAttribute('aria-invalid') 25 + && element.getAttribute('aria-invalid') !== 'false' 26 + ) 27 + } 28 + 29 + function isSupportsValidityMethod(element: HTMLElement | SVGElement): element is HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLFormElement { 30 + return FORM_TAGS.includes(getTag(element)) 31 + } 32 + 33 + function isElementInvalid(element: HTMLElement | SVGElement) { 34 + const isHaveAriaInvalid = isElementHavingAriaInvalid(element) 35 + if (isSupportsValidityMethod(element)) { 36 + return isHaveAriaInvalid || !element.checkValidity() 37 + } 38 + else { 39 + return isHaveAriaInvalid 40 + } 41 + } 42 + 43 + export function toBeInvalid( 44 + this: MatcherState, 45 + element: HTMLElement | SVGElement | Locator, 46 + ): ExpectationResult { 47 + const htmlElement = getElementFromUserInput(element, toBeInvalid, this) 48 + 49 + const isInvalid = isElementInvalid(htmlElement) 50 + 51 + return { 52 + pass: isInvalid, 53 + message: () => { 54 + const is = isInvalid ? 'is' : 'is not' 55 + return [ 56 + this.utils.matcherHint( 57 + `${this.isNot ? '.not' : ''}.toBeInvalid`, 58 + 'element', 59 + '', 60 + ), 61 + '', 62 + `Received element ${is} currently invalid:`, 63 + ` ${this.utils.printReceived(htmlElement.cloneNode(false))}`, 64 + ].join('\n') 65 + }, 66 + } 67 + } 68 + 69 + export function toBeValid( 70 + this: MatcherState, 71 + element: HTMLElement | SVGElement | Locator, 72 + ): ExpectationResult { 73 + const htmlElement = getElementFromUserInput(element, toBeInvalid, this) 74 + 75 + const isValid = !isElementInvalid(htmlElement) 76 + 77 + return { 78 + pass: isValid, 79 + message: () => { 80 + const is = isValid ? 'is' : 'is not' 81 + return [ 82 + this.utils.matcherHint( 83 + `${this.isNot ? '.not' : ''}.toBeValid`, 84 + 'element', 85 + '', 86 + ), 87 + '', 88 + `Received element ${is} currently valid:`, 89 + ` ${this.utils.printReceived(htmlElement.cloneNode(false))}`, 90 + ].join('\n') 91 + }, 92 + } 93 + }
+80
packages/browser/src/client/tester/expect/toBePartiallyChecked.ts
··· 1 + /** 2 + * The MIT License (MIT) 3 + * Copyright (c) 2017 Kent C. Dodds 4 + * 5 + * Permission is hereby granted, free of charge, to any person obtaining a copy 6 + * of this software and associated documentation files (the "Software"), to deal 7 + * in the Software without restriction, including without limitation the rights 8 + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + * copies of the Software, and to permit persons to whom the Software is 10 + * furnished to do so, subject to the following conditions: 11 + * 12 + * The above copyright notice and this permission notice shall be included in all 13 + * copies or substantial portions of the Software. 14 + */ 15 + 16 + import type { ExpectationResult, MatcherState } from '@vitest/expect' 17 + import type { Locator } from '../locators' 18 + import { getAriaChecked as ivyaGetAriaChecked } from 'ivya/utils' 19 + import { getElementFromUserInput, isInputElement } from './utils' 20 + 21 + export default function toBePartiallyChecked( 22 + this: MatcherState, 23 + actual: Element | Locator, 24 + ): ExpectationResult { 25 + const htmlElement = getElementFromUserInput(actual, toBePartiallyChecked, this) 26 + 27 + const isValidInput = () => { 28 + return ( 29 + isInputElement(htmlElement) && htmlElement.type === 'checkbox' 30 + ) 31 + } 32 + 33 + const isValidAriaElement = () => { 34 + return htmlElement.getAttribute('role') === 'checkbox' 35 + } 36 + 37 + if (!isValidInput() && !isValidAriaElement()) { 38 + return { 39 + pass: false, 40 + message: () => 41 + 'only inputs with type="checkbox" or elements with role="checkbox" and a valid aria-checked attribute can be used with .toBePartiallyChecked(). Use .toHaveValue() instead', 42 + } 43 + } 44 + 45 + const isPartiallyChecked = isAriaMixed(htmlElement) 46 + 47 + return { 48 + pass: isPartiallyChecked, 49 + message: () => { 50 + const is = isPartiallyChecked ? 'is' : 'is not' 51 + return [ 52 + this.utils.matcherHint( 53 + `${this.isNot ? '.not' : ''}.toBePartiallyChecked`, 54 + 'element', 55 + '', 56 + ), 57 + '', 58 + `Received element ${is} partially checked:`, 59 + ` ${this.utils.printReceived(htmlElement.cloneNode(false))}`, 60 + ].join('\n') 61 + }, 62 + } 63 + } 64 + 65 + function isAriaMixed(element: HTMLElement | SVGElement): boolean { 66 + const isMixed = ivyaGetAriaChecked(element) === 'mixed' 67 + if (!isMixed) { 68 + // playwright only looks at aria-checked if element is not a checkbox/radio 69 + if ( 70 + isInputElement(element) 71 + && ['checkbox', 'radio'].includes((element as HTMLInputElement).type) 72 + ) { 73 + const ariaValue = element.getAttribute('aria-checked') 74 + if (ariaValue === 'mixed') { 75 + return true 76 + } 77 + } 78 + } 79 + return isMixed 80 + }
+96
packages/browser/src/client/tester/expect/toBeRequired.ts
··· 1 + /** 2 + * The MIT License (MIT) 3 + * Copyright (c) 2017 Kent C. Dodds 4 + * 5 + * Permission is hereby granted, free of charge, to any person obtaining a copy 6 + * of this software and associated documentation files (the "Software"), to deal 7 + * in the Software without restriction, including without limitation the rights 8 + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + * copies of the Software, and to permit persons to whom the Software is 10 + * furnished to do so, subject to the following conditions: 11 + * 12 + * The above copyright notice and this permission notice shall be included in all 13 + * copies or substantial portions of the Software. 14 + */ 15 + 16 + import type { ExpectationResult, MatcherState } from '@vitest/expect' 17 + import type { Locator } from '../locators' 18 + import { getElementFromUserInput, getTag } from './utils' 19 + 20 + // form elements that support 'required' 21 + const FORM_TAGS = ['SELECT', 'TEXTAREA'] 22 + 23 + const ARIA_FORM_TAGS = ['INPUT', 'SELECT', 'TEXTAREA'] 24 + 25 + const UNSUPPORTED_INPUT_TYPES = [ 26 + 'color', 27 + 'hidden', 28 + 'range', 29 + 'submit', 30 + 'image', 31 + 'reset', 32 + ] 33 + 34 + const SUPPORTED_ARIA_ROLES = [ 35 + 'checkbox', 36 + 'combobox', 37 + 'gridcell', 38 + 'listbox', 39 + 'radiogroup', 40 + 'spinbutton', 41 + 'textbox', 42 + 'tree', 43 + ] 44 + 45 + function isRequiredOnFormTagsExceptInput(element: HTMLElement | SVGElement) { 46 + return FORM_TAGS.includes(getTag(element)) && element.hasAttribute('required') 47 + } 48 + 49 + function isRequiredOnSupportedInput(element: HTMLElement | SVGElement) { 50 + return ( 51 + getTag(element) === 'INPUT' 52 + && element.hasAttribute('required') 53 + && ((element.hasAttribute('type') 54 + && !UNSUPPORTED_INPUT_TYPES.includes(element.getAttribute('type') || '')) 55 + || !element.hasAttribute('type')) 56 + ) 57 + } 58 + 59 + function isElementRequiredByARIA(element: HTMLElement | SVGElement) { 60 + return ( 61 + element.hasAttribute('aria-required') 62 + && element.getAttribute('aria-required') === 'true' 63 + && (ARIA_FORM_TAGS.includes(getTag(element)) 64 + || (element.hasAttribute('role') 65 + && SUPPORTED_ARIA_ROLES.includes(element.getAttribute('role') || ''))) 66 + ) 67 + } 68 + 69 + export default function toBeRequired( 70 + this: MatcherState, 71 + element: HTMLElement | SVGElement | Locator, 72 + ): ExpectationResult { 73 + const htmlElement = getElementFromUserInput(element, toBeRequired, this) 74 + 75 + const isRequired 76 + = isRequiredOnFormTagsExceptInput(htmlElement) 77 + || isRequiredOnSupportedInput(htmlElement) 78 + || isElementRequiredByARIA(htmlElement) 79 + 80 + return { 81 + pass: isRequired, 82 + message: () => { 83 + const is = isRequired ? 'is' : 'is not' 84 + return [ 85 + this.utils.matcherHint( 86 + `${this.isNot ? '.not' : ''}.toBeRequired`, 87 + 'element', 88 + '', 89 + ), 90 + '', 91 + `Received element ${is} required:`, 92 + ` ${this.utils.printReceived(htmlElement.cloneNode(false))}`, 93 + ].join('\n') 94 + }, 95 + } 96 + }
+81
packages/browser/src/client/tester/expect/toBeVisible.ts
··· 1 + /** 2 + * The MIT License (MIT) 3 + * Copyright (c) 2017 Kent C. Dodds 4 + * 5 + * Permission is hereby granted, free of charge, to any person obtaining a copy 6 + * of this software and associated documentation files (the "Software"), to deal 7 + * in the Software without restriction, including without limitation the rights 8 + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + * copies of the Software, and to permit persons to whom the Software is 10 + * furnished to do so, subject to the following conditions: 11 + * 12 + * The above copyright notice and this permission notice shall be included in all 13 + * copies or substantial portions of the Software. 14 + */ 15 + 16 + import type { ExpectationResult, MatcherState } from '@vitest/expect' 17 + import type { Locator } from '../locators' 18 + import { server } from '@vitest/browser/context' 19 + import { beginAriaCaches, endAriaCaches, isElementVisible as ivyaIsVisible } from 'ivya/utils' 20 + import { getElementFromUserInput } from './utils' 21 + 22 + export default function toBeVisible( 23 + this: MatcherState, 24 + actual: Element | Locator, 25 + ): ExpectationResult { 26 + const htmlElement = getElementFromUserInput(actual, toBeVisible, this) 27 + const isInDocument 28 + = htmlElement.ownerDocument === htmlElement.getRootNode({ composed: true }) 29 + beginAriaCaches() 30 + const isVisible = isInDocument && isElementVisible(htmlElement) 31 + endAriaCaches() 32 + return { 33 + pass: isVisible, 34 + message: () => { 35 + const is = isVisible ? 'is' : 'is not' 36 + return [ 37 + this.utils.matcherHint( 38 + `${this.isNot ? '.not' : ''}.toBeVisible`, 39 + 'element', 40 + '', 41 + ), 42 + '', 43 + `Received element ${is} visible${ 44 + isInDocument ? '' : ' (element is not in the document)' 45 + }:`, 46 + ` ${this.utils.printReceived(htmlElement.cloneNode(false))}`, 47 + ].join('\n') 48 + }, 49 + } 50 + } 51 + 52 + function isElementVisible(element: HTMLElement | SVGElement): boolean { 53 + const isIvyaVisible = ivyaIsVisible(element) 54 + // if it's visible or not, but we are not in webkit, respect the result 55 + if (server.browser !== 'webkit') { 56 + return isIvyaVisible 57 + } 58 + // if we are in webkit and it's not visible, fallback to jest-dom check 59 + // because ivya doesn't use .checkVisibility here 60 + const detailsElement = element.closest('details') 61 + if (!detailsElement || element === detailsElement) { 62 + return isIvyaVisible 63 + } 64 + return isElementVisibleInDetails(element as HTMLElement) 65 + } 66 + 67 + function isElementVisibleInDetails(targetElement: HTMLElement) { 68 + let currentElement: HTMLElement | null = targetElement 69 + 70 + while (currentElement) { 71 + if (currentElement.tagName === 'DETAILS') { 72 + const isSummary = currentElement.querySelector('summary') === targetElement 73 + if (!(currentElement as HTMLDetailsElement).open && !isSummary) { 74 + return false 75 + } 76 + } 77 + currentElement = currentElement.parentElement 78 + } 79 + 80 + return targetElement.offsetParent !== null 81 + }
+50
packages/browser/src/client/tester/expect/toContainElement.ts
··· 1 + /** 2 + * The MIT License (MIT) 3 + * Copyright (c) 2017 Kent C. Dodds 4 + * 5 + * Permission is hereby granted, free of charge, to any person obtaining a copy 6 + * of this software and associated documentation files (the "Software"), to deal 7 + * in the Software without restriction, including without limitation the rights 8 + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + * copies of the Software, and to permit persons to whom the Software is 10 + * furnished to do so, subject to the following conditions: 11 + * 12 + * The above copyright notice and this permission notice shall be included in all 13 + * copies or substantial portions of the Software. 14 + */ 15 + 16 + import type { ExpectationResult, MatcherState } from '@vitest/expect' 17 + import type { Locator } from '../locators' 18 + import { getElementFromUserInput } from './utils' 19 + 20 + export default function toContainElement( 21 + this: MatcherState, 22 + actual: Element | Locator, 23 + expectedElement: Element | Locator | null, 24 + ): ExpectationResult { 25 + const containerElement = getElementFromUserInput(actual, toContainElement, this) 26 + const childElement = expectedElement !== null 27 + ? getElementFromUserInput(expectedElement, toContainElement, this) 28 + : null 29 + 30 + return { 31 + pass: containerElement.contains(childElement), 32 + message: () => { 33 + return [ 34 + this.utils.matcherHint( 35 + `${this.isNot ? '.not' : ''}.toContainElement`, 36 + 'element', 37 + 'element', 38 + ), 39 + '', 40 + 41 + this.utils.RECEIVED_COLOR(`${this.utils.stringify( 42 + containerElement.cloneNode(false), 43 + )} ${ 44 + this.isNot ? 'contains:' : 'does not contain:' 45 + } ${this.utils.stringify(childElement ? childElement.cloneNode(false) : null)} 46 + `), 47 + ].join('\n') 48 + }, 49 + } 50 + }
+53
packages/browser/src/client/tester/expect/toContainHTML.ts
··· 1 + /** 2 + * The MIT License (MIT) 3 + * Copyright (c) 2017 Kent C. Dodds 4 + * 5 + * Permission is hereby granted, free of charge, to any person obtaining a copy 6 + * of this software and associated documentation files (the "Software"), to deal 7 + * in the Software without restriction, including without limitation the rights 8 + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + * copies of the Software, and to permit persons to whom the Software is 10 + * furnished to do so, subject to the following conditions: 11 + * 12 + * The above copyright notice and this permission notice shall be included in all 13 + * copies or substantial portions of the Software. 14 + */ 15 + 16 + import type { ExpectationResult, MatcherState } from '@vitest/expect' 17 + import type { Locator } from '../locators' 18 + import { getElementFromUserInput } from './utils' 19 + 20 + function getNormalizedHtml(container: HTMLElement | SVGElement, htmlText: string) { 21 + const div = container.ownerDocument.createElement('div') 22 + div.innerHTML = htmlText 23 + return div.innerHTML 24 + } 25 + 26 + export default function toContainHTML( 27 + this: MatcherState, 28 + actual: Element | Locator, 29 + htmlText: string, 30 + ): ExpectationResult { 31 + const htmlElement = getElementFromUserInput(actual, toContainHTML, this) 32 + 33 + if (typeof htmlText !== 'string') { 34 + throw new TypeError(`.toContainHTML() expects a string value, got ${htmlText}`) 35 + } 36 + 37 + return { 38 + pass: htmlElement.outerHTML.includes(getNormalizedHtml(htmlElement, htmlText)), 39 + message: () => { 40 + return [ 41 + this.utils.matcherHint( 42 + `${this.isNot ? '.not' : ''}.toContainHTML`, 43 + 'element', 44 + '', 45 + ), 46 + 'Expected:', 47 + ` ${this.utils.EXPECTED_COLOR(htmlText)}`, 48 + 'Received:', 49 + ` ${this.utils.printReceived(htmlElement.cloneNode(true))}`, 50 + ].join('\n') 51 + }, 52 + } 53 + }
+67
packages/browser/src/client/tester/expect/toHaveAccessibleDescription.ts
··· 1 + /** 2 + * The MIT License (MIT) 3 + * Copyright (c) 2017 Kent C. Dodds 4 + * 5 + * Permission is hereby granted, free of charge, to any person obtaining a copy 6 + * of this software and associated documentation files (the "Software"), to deal 7 + * in the Software without restriction, including without limitation the rights 8 + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + * copies of the Software, and to permit persons to whom the Software is 10 + * furnished to do so, subject to the following conditions: 11 + * 12 + * The above copyright notice and this permission notice shall be included in all 13 + * copies or substantial portions of the Software. 14 + */ 15 + 16 + import type { ExpectationResult, MatcherState } from '@vitest/expect' 17 + import type { Locator } from '../locators' 18 + import { getElementAccessibleDescription } from 'ivya/utils' 19 + import { getElementFromUserInput, getMessage } from './utils' 20 + 21 + export default function toHaveAccessibleDescription( 22 + this: MatcherState, 23 + actual: Element | Locator, 24 + expectedAccessibleDescription?: string | RegExp, 25 + ): ExpectationResult { 26 + const htmlElement = getElementFromUserInput(actual, toHaveAccessibleDescription, this) 27 + const actualAccessibleDescription = getElementAccessibleDescription(htmlElement, false) 28 + 29 + const missingExpectedValue = arguments.length === 1 30 + 31 + let pass = false 32 + if (missingExpectedValue) { 33 + // When called without an expected value we only want to validate that the element has an 34 + // accessible description, whatever it may be. 35 + pass = actualAccessibleDescription !== '' 36 + } 37 + else { 38 + pass 39 + = expectedAccessibleDescription instanceof RegExp 40 + ? expectedAccessibleDescription.test(actualAccessibleDescription) 41 + : this.equals( 42 + actualAccessibleDescription, 43 + expectedAccessibleDescription, 44 + this.customTesters, 45 + ) 46 + } 47 + 48 + return { 49 + pass, 50 + 51 + message: () => { 52 + const to = this.isNot ? 'not to' : 'to' 53 + return getMessage( 54 + this, 55 + this.utils.matcherHint( 56 + `${this.isNot ? '.not' : ''}.toHaveAccessibleDescription`, 57 + 'element', 58 + '', 59 + ), 60 + `Expected element ${to} have accessible description`, 61 + expectedAccessibleDescription, 62 + 'Received', 63 + actualAccessibleDescription, 64 + ) 65 + }, 66 + } 67 + }
+78
packages/browser/src/client/tester/expect/toHaveAccessibleErrorMessage.ts
··· 1 + /** 2 + * The MIT License (MIT) 3 + * Copyright (c) 2017 Kent C. Dodds 4 + * 5 + * Permission is hereby granted, free of charge, to any person obtaining a copy 6 + * of this software and associated documentation files (the "Software"), to deal 7 + * in the Software without restriction, including without limitation the rights 8 + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + * copies of the Software, and to permit persons to whom the Software is 10 + * furnished to do so, subject to the following conditions: 11 + * 12 + * The above copyright notice and this permission notice shall be included in all 13 + * copies or substantial portions of the Software. 14 + */ 15 + 16 + import type { ExpectationResult, MatcherState } from '@vitest/expect' 17 + import type { Locator } from '../locators' 18 + import { getElementAccessibleErrorMessage } from 'ivya/utils' 19 + import { getElementFromUserInput, getMessage, redent } from './utils' 20 + 21 + export default function toHaveAccessibleErrorMessage( 22 + this: MatcherState, 23 + actual: Element | Locator, 24 + expectedAccessibleErrorMessage?: string | RegExp, 25 + ): ExpectationResult { 26 + const htmlElement = getElementFromUserInput(actual, toHaveAccessibleErrorMessage, this) 27 + const actualAccessibleErrorMessage = getElementAccessibleErrorMessage(htmlElement) ?? '' 28 + 29 + const missingExpectedValue = arguments.length === 1 30 + 31 + let pass = false 32 + if (missingExpectedValue) { 33 + // When called without an expected value we only want to validate that the element has an 34 + // accessible description, whatever it may be. 35 + pass = actualAccessibleErrorMessage !== '' 36 + } 37 + else { 38 + pass 39 + = expectedAccessibleErrorMessage instanceof RegExp 40 + ? expectedAccessibleErrorMessage.test(actualAccessibleErrorMessage) 41 + : this.equals( 42 + actualAccessibleErrorMessage, 43 + expectedAccessibleErrorMessage, 44 + this.customTesters, 45 + ) 46 + } 47 + 48 + return { 49 + pass, 50 + 51 + message: () => { 52 + const to = this.isNot ? 'not to' : 'to' 53 + if (expectedAccessibleErrorMessage == null) { 54 + return [ 55 + this.utils.matcherHint( 56 + `${this.isNot ? '.not' : ''}.toHaveAccessibleErrorMessage`, 57 + 'element', 58 + '', 59 + ), 60 + `Expected element ${to} have accessible error message, but got${!this.isNot ? ' nothing' : ''}`, 61 + this.isNot ? this.utils.RECEIVED_COLOR(redent(actualAccessibleErrorMessage, 2)) : '', 62 + ].filter(Boolean).join('\n\n') 63 + } 64 + return getMessage( 65 + this, 66 + this.utils.matcherHint( 67 + `${this.isNot ? '.not' : ''}.toHaveAccessibleErrorMessage`, 68 + 'element', 69 + '', 70 + ), 71 + `Expected element ${to} have accessible error message`, 72 + expectedAccessibleErrorMessage, 73 + 'Received', 74 + actualAccessibleErrorMessage, 75 + ) 76 + }, 77 + } 78 + }
+62
packages/browser/src/client/tester/expect/toHaveAccessibleName.ts
··· 1 + /** 2 + * The MIT License (MIT) 3 + * Copyright (c) 2017 Kent C. Dodds 4 + * 5 + * Permission is hereby granted, free of charge, to any person obtaining a copy 6 + * of this software and associated documentation files (the "Software"), to deal 7 + * in the Software without restriction, including without limitation the rights 8 + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + * copies of the Software, and to permit persons to whom the Software is 10 + * furnished to do so, subject to the following conditions: 11 + * 12 + * The above copyright notice and this permission notice shall be included in all 13 + * copies or substantial portions of the Software. 14 + */ 15 + 16 + import type { ExpectationResult, MatcherState } from '@vitest/expect' 17 + import type { Locator } from '../locators' 18 + import { getElementAccessibleName } from 'ivya/utils' 19 + import { getElementFromUserInput, getMessage } from './utils' 20 + 21 + export default function toHaveAccessibleName( 22 + this: MatcherState, 23 + actual: Element | Locator, 24 + expectedAccessibleName?: string | RegExp, 25 + ): ExpectationResult { 26 + const htmlElement = getElementFromUserInput(actual, toHaveAccessibleName, this) 27 + const actualAccessibleName = getElementAccessibleName(htmlElement, false) 28 + const missingExpectedValue = arguments.length === 1 29 + 30 + let pass = false 31 + if (missingExpectedValue) { 32 + // When called without an expected value we only want to validate that the element has an 33 + // accessible name, whatever it may be. 34 + pass = actualAccessibleName !== '' 35 + } 36 + else { 37 + pass 38 + = expectedAccessibleName instanceof RegExp 39 + ? expectedAccessibleName.test(actualAccessibleName) 40 + : this.equals(actualAccessibleName, expectedAccessibleName, this.customTesters) 41 + } 42 + 43 + return { 44 + pass, 45 + 46 + message: () => { 47 + const to = this.isNot ? 'not to' : 'to' 48 + return getMessage( 49 + this, 50 + this.utils.matcherHint( 51 + `${this.isNot ? '.not' : ''}.${toHaveAccessibleName.name}`, 52 + 'element', 53 + '', 54 + ), 55 + `Expected element ${to} have accessible name`, 56 + expectedAccessibleName, 57 + 'Received', 58 + actualAccessibleName, 59 + ) 60 + }, 61 + } 62 + }
+74
packages/browser/src/client/tester/expect/toHaveAttribute.ts
··· 1 + /** 2 + * The MIT License (MIT) 3 + * Copyright (c) 2017 Kent C. Dodds 4 + * 5 + * Permission is hereby granted, free of charge, to any person obtaining a copy 6 + * of this software and associated documentation files (the "Software"), to deal 7 + * in the Software without restriction, including without limitation the rights 8 + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + * copies of the Software, and to permit persons to whom the Software is 10 + * furnished to do so, subject to the following conditions: 11 + * 12 + * The above copyright notice and this permission notice shall be included in all 13 + * copies or substantial portions of the Software. 14 + */ 15 + 16 + import type { ExpectationResult, MatcherState } from '@vitest/expect' 17 + import type { Locator } from '../locators' 18 + import { getElementFromUserInput, getMessage } from './utils' 19 + 20 + export default function toHaveAttribute( 21 + this: MatcherState, 22 + actual: Element | Locator, 23 + attribute: string, 24 + expectedValue?: string, 25 + ): ExpectationResult { 26 + const htmlElement = getElementFromUserInput(actual, toHaveAttribute, this) 27 + const isExpectedValuePresent = expectedValue !== undefined 28 + const hasAttribute = htmlElement.hasAttribute(attribute) 29 + const receivedValue = htmlElement.getAttribute(attribute) 30 + return { 31 + pass: isExpectedValuePresent 32 + ? hasAttribute && this.equals(receivedValue, expectedValue, this.customTesters) 33 + : hasAttribute, 34 + message: () => { 35 + const to = this.isNot ? 'not to' : 'to' 36 + const receivedAttribute = hasAttribute 37 + ? printAttribute(this.utils.stringify, attribute, receivedValue) 38 + : null 39 + const matcher = this.utils.matcherHint( 40 + `${this.isNot ? '.not' : ''}.toHaveAttribute`, 41 + 'element', 42 + this.utils.printExpected(attribute), 43 + { 44 + secondArgument: isExpectedValuePresent 45 + ? this.utils.printExpected(expectedValue) 46 + : undefined, 47 + comment: getAttributeComment( 48 + this.utils.stringify, 49 + attribute, 50 + expectedValue, 51 + ), 52 + }, 53 + ) 54 + return getMessage( 55 + this, 56 + matcher, 57 + `Expected the element ${to} have attribute`, 58 + printAttribute(this.utils.stringify, attribute, expectedValue), 59 + 'Received', 60 + receivedAttribute, 61 + ) 62 + }, 63 + } 64 + } 65 + 66 + function printAttribute(stringify: (obj: unknown) => string, name: string, value: unknown) { 67 + return value === undefined ? name : `${name}=${stringify(value)}` 68 + } 69 + 70 + function getAttributeComment(stringify: (obj: unknown) => string, name: string, value: unknown) { 71 + return value === undefined 72 + ? `element.hasAttribute(${stringify(name)})` 73 + : `element.getAttribute(${stringify(name)}) === ${stringify(value)}` 74 + }
+137
packages/browser/src/client/tester/expect/toHaveClass.ts
··· 1 + /** 2 + * The MIT License (MIT) 3 + * Copyright (c) 2017 Kent C. Dodds 4 + * 5 + * Permission is hereby granted, free of charge, to any person obtaining a copy 6 + * of this software and associated documentation files (the "Software"), to deal 7 + * in the Software without restriction, including without limitation the rights 8 + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + * copies of the Software, and to permit persons to whom the Software is 10 + * furnished to do so, subject to the following conditions: 11 + * 12 + * The above copyright notice and this permission notice shall be included in all 13 + * copies or substantial portions of the Software. 14 + */ 15 + 16 + import type { ExpectationResult, MatcherState } from '@vitest/expect' 17 + import type { Locator } from '../locators' 18 + import { getElementFromUserInput, getMessage } from './utils' 19 + 20 + export default function toHaveClass( 21 + this: MatcherState, 22 + actual: Element | Locator, 23 + ...params: (string | RegExp)[] | [string, options?: { exact: boolean }] 24 + ): ExpectationResult { 25 + const htmlElement = getElementFromUserInput(actual, toHaveClass, this) 26 + const { expectedClassNames, options } = getExpectedClassNamesAndOptions(params) 27 + 28 + const received = splitClassNames(htmlElement.getAttribute('class')) 29 + const expected = expectedClassNames.reduce( 30 + (acc, className) => { 31 + return acc.concat( 32 + typeof className === 'string' || !className 33 + ? splitClassNames(className) 34 + : className, 35 + ) 36 + }, 37 + [] as (string | RegExp)[], 38 + ) 39 + 40 + const hasRegExp = expected.some(className => className instanceof RegExp) 41 + if (options.exact && hasRegExp) { 42 + throw new Error('Exact option does not support RegExp expected class names') 43 + } 44 + 45 + if (options.exact) { 46 + return { 47 + pass: isSubset(expected, received) && expected.length === received.length, 48 + message: () => { 49 + const to = this.isNot ? 'not to' : 'to' 50 + return getMessage( 51 + this, 52 + this.utils.matcherHint( 53 + `${this.isNot ? '.not' : ''}.toHaveClass`, 54 + 'element', 55 + this.utils.printExpected(expected.join(' ')), 56 + ), 57 + `Expected the element ${to} have EXACTLY defined classes`, 58 + expected.join(' '), 59 + 'Received', 60 + received.join(' '), 61 + ) 62 + }, 63 + } 64 + } 65 + 66 + return expected.length > 0 67 + ? { 68 + pass: isSubset(expected, received), 69 + message: () => { 70 + const to = this.isNot ? 'not to' : 'to' 71 + return getMessage( 72 + this, 73 + this.utils.matcherHint( 74 + `${this.isNot ? '.not' : ''}.toHaveClass`, 75 + 'element', 76 + this.utils.printExpected(expected.join(' ')), 77 + ), 78 + `Expected the element ${to} have class`, 79 + expected.join(' '), 80 + 'Received', 81 + received.join(' '), 82 + ) 83 + }, 84 + } 85 + : { 86 + pass: this.isNot ? received.length > 0 : false, 87 + message: () => 88 + this.isNot 89 + ? getMessage( 90 + this, 91 + this.utils.matcherHint('.not.toHaveClass', 'element', ''), 92 + 'Expected the element to have classes', 93 + '(none)', 94 + 'Received', 95 + received.join(' '), 96 + ) 97 + : [ 98 + this.utils.matcherHint(`.toHaveClass`, 'element'), 99 + 'At least one expected class must be provided.', 100 + ].join('\n'), 101 + } 102 + } 103 + 104 + function getExpectedClassNamesAndOptions( 105 + params: (string | RegExp)[] | [string, options?: { exact: boolean }], 106 + ): { 107 + expectedClassNames: (string | RegExp)[] 108 + options: { exact: boolean } 109 + } { 110 + const lastParam = params.pop() 111 + let expectedClassNames, options 112 + 113 + if (typeof lastParam === 'object' && !(lastParam instanceof RegExp)) { 114 + expectedClassNames = params 115 + options = lastParam 116 + } 117 + else { 118 + expectedClassNames = params.concat(lastParam) 119 + options = { exact: false } 120 + } 121 + return { expectedClassNames: expectedClassNames as string[], options } 122 + } 123 + 124 + function splitClassNames(str: string | undefined | null): string[] { 125 + if (!str) { 126 + return [] 127 + } 128 + return str.split(/\s+/).filter(s => s.length > 0) 129 + } 130 + 131 + function isSubset(subset: (string | RegExp)[], superset: string[]) { 132 + return subset.every(strOrRegexp => 133 + typeof strOrRegexp === 'string' 134 + ? superset.includes(strOrRegexp) 135 + : superset.some(className => strOrRegexp.test(className)), 136 + ) 137 + }
+82
packages/browser/src/client/tester/expect/toHaveDisplayValue.ts
··· 1 + /** 2 + * The MIT License (MIT) 3 + * Copyright (c) 2017 Kent C. Dodds 4 + * 5 + * Permission is hereby granted, free of charge, to any person obtaining a copy 6 + * of this software and associated documentation files (the "Software"), to deal 7 + * in the Software without restriction, including without limitation the rights 8 + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + * copies of the Software, and to permit persons to whom the Software is 10 + * furnished to do so, subject to the following conditions: 11 + * 12 + * The above copyright notice and this permission notice shall be included in all 13 + * copies or substantial portions of the Software. 14 + */ 15 + 16 + import type { ExpectationResult, MatcherState } from '@vitest/expect' 17 + import type { Locator } from '../locators' 18 + import { getElementFromUserInput, getMessage, getTag, isInputElement } from './utils' 19 + 20 + export default function toHaveDisplayValue( 21 + this: MatcherState, 22 + actual: Element | Locator, 23 + expectedValue: string | RegExp | Array<string | RegExp>, 24 + ): ExpectationResult { 25 + const htmlElement = getElementFromUserInput(actual, toHaveDisplayValue, this) 26 + const tagName = getTag(htmlElement) 27 + 28 + if (!['SELECT', 'INPUT', 'TEXTAREA'].includes(tagName)) { 29 + throw new Error( 30 + '.toHaveDisplayValue() currently supports only input, textarea or select elements, try with another matcher instead.', 31 + ) 32 + } 33 + 34 + if (isInputElement(htmlElement) && ['radio', 'checkbox'].includes(htmlElement.type)) { 35 + throw new Error( 36 + `.toHaveDisplayValue() currently does not support input[type="${htmlElement.type}"], try with another matcher instead.`, 37 + ) 38 + } 39 + 40 + const values = getValues(tagName, htmlElement) 41 + const expectedValues = getExpectedValues(expectedValue) 42 + const numberOfMatchesWithValues = expectedValues.filter(expected => 43 + values.some(value => 44 + expected instanceof RegExp 45 + ? expected.test(value) 46 + : this.equals(value, String(expected), this.customTesters), 47 + ), 48 + ).length 49 + 50 + const matchedWithAllValues = numberOfMatchesWithValues === values.length 51 + const matchedWithAllExpectedValues 52 + = numberOfMatchesWithValues === expectedValues.length 53 + 54 + return { 55 + pass: matchedWithAllValues && matchedWithAllExpectedValues, 56 + message: () => 57 + getMessage( 58 + this, 59 + this.utils.matcherHint( 60 + `${this.isNot ? '.not' : ''}.toHaveDisplayValue`, 61 + 'element', 62 + '', 63 + ), 64 + `Expected element ${this.isNot ? 'not ' : ''}to have display value`, 65 + expectedValue, 66 + 'Received', 67 + values, 68 + ), 69 + } 70 + } 71 + 72 + function getValues(tagName: string, htmlElement: HTMLElement | SVGElement) { 73 + return tagName === 'SELECT' 74 + ? Array.from(htmlElement as HTMLSelectElement) 75 + .filter(option => (option as HTMLOptionElement).selected) 76 + .map(option => option.textContent || '') 77 + : [(htmlElement as HTMLInputElement).value] 78 + } 79 + 80 + function getExpectedValues(expectedValue: string | RegExp | Array<string | RegExp>): Array<string | RegExp> { 81 + return Array.isArray(expectedValue) ? expectedValue : [expectedValue] 82 + }
+52
packages/browser/src/client/tester/expect/toHaveFocus.ts
··· 1 + /** 2 + * The MIT License (MIT) 3 + * Copyright (c) 2017 Kent C. Dodds 4 + * 5 + * Permission is hereby granted, free of charge, to any person obtaining a copy 6 + * of this software and associated documentation files (the "Software"), to deal 7 + * in the Software without restriction, including without limitation the rights 8 + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + * copies of the Software, and to permit persons to whom the Software is 10 + * furnished to do so, subject to the following conditions: 11 + * 12 + * The above copyright notice and this permission notice shall be included in all 13 + * copies or substantial portions of the Software. 14 + */ 15 + 16 + import type { ExpectationResult, MatcherState } from '@vitest/expect' 17 + import type { Locator } from '../locators' 18 + import { getElementFromUserInput } from './utils' 19 + 20 + export default function toHaveFocus( 21 + this: MatcherState, 22 + actual: Element | Locator, 23 + ): ExpectationResult { 24 + const htmlElement = getElementFromUserInput(actual, toHaveFocus, this) 25 + 26 + return { 27 + pass: htmlElement.ownerDocument.activeElement === htmlElement, 28 + message: () => { 29 + return [ 30 + this.utils.matcherHint( 31 + `${this.isNot ? '.not' : ''}.toHaveFocus`, 32 + 'element', 33 + '', 34 + ), 35 + '', 36 + ...(this.isNot 37 + ? [ 38 + 'Received element is focused:', 39 + ` ${this.utils.printReceived(htmlElement)}`, 40 + ] 41 + : [ 42 + 'Expected element with focus:', 43 + ` ${this.utils.printExpected(htmlElement)}`, 44 + 'Received element with focus:', 45 + ` ${this.utils.printReceived( 46 + htmlElement.ownerDocument.activeElement, 47 + )}`, 48 + ]), 49 + ].join('\n') 50 + }, 51 + } 52 + }
+119
packages/browser/src/client/tester/expect/toHaveFormValues.ts
··· 1 + /** 2 + * The MIT License (MIT) 3 + * Copyright (c) 2017 Kent C. Dodds 4 + * 5 + * Permission is hereby granted, free of charge, to any person obtaining a copy 6 + * of this software and associated documentation files (the "Software"), to deal 7 + * in the Software without restriction, including without limitation the rights 8 + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + * copies of the Software, and to permit persons to whom the Software is 10 + * furnished to do so, subject to the following conditions: 11 + * 12 + * The above copyright notice and this permission notice shall be included in all 13 + * copies or substantial portions of the Software. 14 + */ 15 + 16 + import type { ExpectationResult, MatcherState } from '@vitest/expect' 17 + import type { Locator } from '../locators' 18 + import { cssEscape } from 'ivya/utils' 19 + import { arrayAsSetComparison, getElementFromUserInput, getSingleElementValue, getTag } from './utils' 20 + 21 + export default function toHaveFormValues( 22 + this: MatcherState, 23 + actual: Element | Locator, 24 + expectedValues: Record<string, unknown>, 25 + ): ExpectationResult { 26 + const formElement = getElementFromUserInput(actual, toHaveFormValues, this) 27 + 28 + if (!(formElement instanceof HTMLFieldSetElement) && !(formElement instanceof HTMLFormElement)) { 29 + throw new TypeError(`toHaveFormValues must be called on a form or a fieldset, instead got ${getTag(formElement)}`) 30 + } 31 + 32 + if (!expectedValues || typeof expectedValues !== 'object') { 33 + throw new TypeError( 34 + `toHaveFormValues must be called with an object of expected form values. Got ${expectedValues}`, 35 + ) 36 + } 37 + 38 + const formValues = getAllFormValues(formElement) 39 + return { 40 + pass: Object.entries(expectedValues).every(([name, expectedValue]) => 41 + this.equals(formValues[name], expectedValue, [arrayAsSetComparison, ...this.customTesters]), 42 + ), 43 + message: () => { 44 + const to = this.isNot ? 'not to' : 'to' 45 + const matcher = `${this.isNot ? '.not' : ''}.toHaveFormValues` 46 + 47 + const commonKeyValues: Record<string, unknown> = {} 48 + for (const key in formValues) { 49 + if (!Object.hasOwn(expectedValues, key)) { 50 + continue 51 + } 52 + commonKeyValues[key] = formValues[key] 53 + } 54 + 55 + return [ 56 + this.utils.matcherHint(matcher, 'element', ''), 57 + `Expected the element ${to} have form values`, 58 + this.utils.diff(expectedValues, commonKeyValues), 59 + ].join('\n\n') 60 + }, 61 + } 62 + } 63 + 64 + // Returns the combined value of several elements that have the same name 65 + // e.g. radio buttons or groups of checkboxes 66 + function getMultiElementValue(elements: HTMLInputElement[]) { 67 + const types = [...new Set(elements.map(element => element.type))] 68 + if (types.length !== 1) { 69 + throw new Error( 70 + 'Multiple form elements with the same name must be of the same type', 71 + ) 72 + } 73 + switch (types[0]) { 74 + case 'radio': { 75 + const selected = elements.find(radio => radio.checked) 76 + return selected ? selected.value : undefined 77 + } 78 + case 'checkbox': 79 + return elements 80 + .filter(checkbox => checkbox.checked) 81 + .map(checkbox => checkbox.value) 82 + default: 83 + // NOTE: Not even sure this is a valid use case, but just in case... 84 + return elements.map(element => element.value) 85 + } 86 + } 87 + 88 + function getFormValue(container: HTMLFormElement | HTMLFieldSetElement, name: string) { 89 + const elements = [...container.querySelectorAll(`[name="${cssEscape(name)}"]`)] 90 + /* istanbul ignore if */ 91 + if (elements.length === 0) { 92 + return undefined // shouldn't happen, but just in case 93 + } 94 + switch (elements.length) { 95 + case 1: 96 + return getSingleElementValue(elements[0]) 97 + default: 98 + return getMultiElementValue(elements as HTMLInputElement[]) 99 + } 100 + } 101 + 102 + // Strips the `[]` suffix off a form value name 103 + function getPureName(name: string) { 104 + return /\[\]$/.test(name) ? name.slice(0, -2) : name 105 + } 106 + 107 + function getAllFormValues(container: HTMLFormElement | HTMLFieldSetElement) { 108 + const values: Record<string, unknown> = {} 109 + 110 + for (const element of container.elements) { 111 + if (!('name' in element)) { 112 + continue 113 + } 114 + const name = element.name as string 115 + values[getPureName(name)] = getFormValue(container, name) 116 + } 117 + 118 + return values 119 + }
+48
packages/browser/src/client/tester/expect/toHaveRole.ts
··· 1 + /** 2 + * The MIT License (MIT) 3 + * Copyright (c) 2017 Kent C. Dodds 4 + * 5 + * Permission is hereby granted, free of charge, to any person obtaining a copy 6 + * of this software and associated documentation files (the "Software"), to deal 7 + * in the Software without restriction, including without limitation the rights 8 + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + * copies of the Software, and to permit persons to whom the Software is 10 + * furnished to do so, subject to the following conditions: 11 + * 12 + * The above copyright notice and this permission notice shall be included in all 13 + * copies or substantial portions of the Software. 14 + */ 15 + 16 + import type { ExpectationResult, MatcherState } from '@vitest/expect' 17 + import type { Locator } from '../locators' 18 + import { beginAriaCaches, endAriaCaches, getAriaRole } from 'ivya/utils' 19 + import { getElementFromUserInput, getMessage } from './utils' 20 + 21 + export default function toHaveRole( 22 + this: MatcherState, 23 + actual: Element | Locator, 24 + expectedRole: string, 25 + ): ExpectationResult { 26 + const htmlElement = getElementFromUserInput(actual, toHaveRole, this) 27 + beginAriaCaches() 28 + const actualRole = getAriaRole(htmlElement) 29 + endAriaCaches() 30 + return { 31 + pass: actualRole === expectedRole, 32 + message: () => { 33 + const to = this.isNot ? 'not to' : 'to' 34 + return getMessage( 35 + this, 36 + this.utils.matcherHint( 37 + `${this.isNot ? '.not' : ''}.toHaveRole`, 38 + 'element', 39 + '', 40 + ), 41 + `Expected element ${to} have role`, 42 + expectedRole, 43 + 'Received', 44 + actualRole, 45 + ) 46 + }, 47 + } 48 + }
+134
packages/browser/src/client/tester/expect/toHaveSelection.ts
··· 1 + /** 2 + * The MIT License (MIT) 3 + * Copyright (c) 2017 Kent C. Dodds 4 + * 5 + * Permission is hereby granted, free of charge, to any person obtaining a copy 6 + * of this software and associated documentation files (the "Software"), to deal 7 + * in the Software without restriction, including without limitation the rights 8 + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + * copies of the Software, and to permit persons to whom the Software is 10 + * furnished to do so, subject to the following conditions: 11 + * 12 + * The above copyright notice and this permission notice shall be included in all 13 + * copies or substantial portions of the Software. 14 + */ 15 + 16 + import type { ExpectationResult, MatcherState } from '@vitest/expect' 17 + import type { Locator } from '../locators' 18 + import { arrayAsSetComparison, getElementFromUserInput, getMessage, getTag } from './utils' 19 + 20 + export default function toHaveSelection( 21 + this: MatcherState, 22 + element: HTMLElement | SVGElement | Locator, 23 + expectedSelection: string, 24 + ): ExpectationResult { 25 + const htmlElement = getElementFromUserInput(element, toHaveSelection, this) 26 + 27 + const expectsSelection = expectedSelection !== undefined 28 + 29 + if (expectsSelection && typeof expectedSelection !== 'string') { 30 + throw new Error(`expected selection must be a string or undefined`) 31 + } 32 + 33 + const receivedSelection = getSelection(htmlElement) 34 + 35 + return { 36 + pass: expectsSelection 37 + ? this.equals(receivedSelection, expectedSelection, [arrayAsSetComparison, ...this.customTesters]) 38 + : Boolean(receivedSelection), 39 + message: () => { 40 + const to = this.isNot ? 'not to' : 'to' 41 + const matcher = this.utils.matcherHint( 42 + `${this.isNot ? '.not' : ''}.toHaveSelection`, 43 + 'element', 44 + expectedSelection, 45 + ) 46 + return getMessage( 47 + this, 48 + matcher, 49 + `Expected the element ${to} have selection`, 50 + expectsSelection ? expectedSelection : '(any)', 51 + 'Received', 52 + receivedSelection, 53 + ) 54 + }, 55 + } 56 + } 57 + 58 + function getSelection(element: HTMLElement | SVGElement): string { 59 + const selection = element.ownerDocument.getSelection() 60 + 61 + if (!selection) { 62 + return '' 63 + } 64 + 65 + if (['INPUT', 'TEXTAREA'].includes(getTag(element))) { 66 + const input = element as HTMLInputElement | HTMLTextAreaElement 67 + if (['radio', 'checkbox'].includes(input.type)) { 68 + return '' 69 + } 70 + if (input.selectionStart == null || input.selectionEnd == null) { 71 + return '' 72 + } 73 + return input.value 74 + .toString() 75 + .substring(input.selectionStart, input.selectionEnd) 76 + } 77 + 78 + if (selection.anchorNode === null || selection.focusNode === null) { 79 + // No selection 80 + return '' 81 + } 82 + 83 + const originalRange = selection.getRangeAt(0) 84 + const temporaryRange = element.ownerDocument.createRange() 85 + 86 + if (selection.containsNode(element, false)) { 87 + // Whole element is inside selection 88 + temporaryRange.selectNodeContents(element) 89 + selection.removeAllRanges() 90 + selection.addRange(temporaryRange) 91 + } 92 + else if ( 93 + element.contains(selection.anchorNode) 94 + && element.contains(selection.focusNode) 95 + ) { 96 + // Element contains selection, nothing to do 97 + } 98 + else { 99 + // Element is partially selected 100 + const selectionStartsWithinElement 101 + = element === originalRange.startContainer 102 + || element.contains(originalRange.startContainer) 103 + const selectionEndsWithinElement 104 + = element === originalRange.endContainer 105 + || element.contains(originalRange.endContainer) 106 + selection.removeAllRanges() 107 + 108 + if (selectionStartsWithinElement || selectionEndsWithinElement) { 109 + temporaryRange.selectNodeContents(element) 110 + 111 + if (selectionStartsWithinElement) { 112 + temporaryRange.setStart( 113 + originalRange.startContainer, 114 + originalRange.startOffset, 115 + ) 116 + } 117 + if (selectionEndsWithinElement) { 118 + temporaryRange.setEnd( 119 + originalRange.endContainer, 120 + originalRange.endOffset, 121 + ) 122 + } 123 + 124 + selection.addRange(temporaryRange) 125 + } 126 + } 127 + 128 + const result = selection.toString() 129 + 130 + selection.removeAllRanges() 131 + selection.addRange(originalRange) 132 + 133 + return result 134 + }
+178
packages/browser/src/client/tester/expect/toHaveStyle.ts
··· 1 + import type { ExpectationResult, MatcherState } from '@vitest/expect' 2 + import type { Locator } from '../locators' 3 + import { server } from '@vitest/browser/context' 4 + import { getElementFromUserInput } from './utils' 5 + 6 + const browser = server.config.browser.name 7 + 8 + // these values should keep the `style` attribute instead of computing px to be consistent with jsdom 9 + // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascade/used_value#difference_from_computed_value 10 + const usedValuesProps = new Set([ 11 + 'backgroundPosition', 12 + 'background-position', 13 + 'bottom', 14 + 'left', 15 + 'right', 16 + 'top', 17 + 'height', 18 + 'width', 19 + 'margin-bottom', 20 + 'marginBottom', 21 + 'margin-left', 22 + 'marginLeft', 23 + 'margin-right', 24 + 'marginRight', 25 + 'margin-top', 26 + 'marginTop', 27 + 'min-height', 28 + 'minHeight', 29 + 'min-width', 30 + 'minWidth', 31 + 'padding-bottom', 32 + 'padding-left', 33 + 'padding-right', 34 + 'padding-top', 35 + 'text-indent', 36 + 'paddingBottom', 37 + 'paddingLeft', 38 + 'paddingRight', 39 + 'paddingTop', 40 + 'textIndent', 41 + ]) 42 + 43 + export default function toHaveStyle( 44 + this: MatcherState, 45 + actual: Element | Locator, 46 + css: string | Record<string, unknown>, 47 + ): ExpectationResult { 48 + const htmlElement = getElementFromUserInput(actual, toHaveStyle, this) 49 + const { getComputedStyle } = htmlElement.ownerDocument.defaultView! 50 + 51 + const expected = typeof css === 'object' 52 + ? getStyleFromObjectCSS(css) 53 + : computeCSSStyleDeclaration(css) 54 + const received = getComputedStyle(htmlElement) 55 + const receivedCustomKeys = new Set(Array.from(htmlElement.style)) 56 + 57 + return { 58 + pass: isSubset(expected, htmlElement, received, receivedCustomKeys), 59 + message: () => { 60 + const matcher = `${this.isNot ? '.not' : ''}.toHaveStyle` 61 + const expectedKeys = new Set(Object.keys(expected)) 62 + const receivedObject = Array.from(received) 63 + .filter(prop => expectedKeys.has(prop)) 64 + .reduce( 65 + (obj, prop) => { 66 + const styleSheet = receivedCustomKeys.has(prop) && usedValuesProps.has(prop) 67 + ? htmlElement.style 68 + : received 69 + obj[prop] = styleSheet[prop as 'color'] 70 + return obj 71 + }, 72 + {} as Record<string, unknown>, 73 + ) 74 + const receivedString = printoutObjectStyles(receivedObject) 75 + const diff = receivedString === '' 76 + ? 'Expected styles could not be parsed by the browser. Did you make a typo?' 77 + : this.utils.diff( 78 + printoutObjectStyles(expected), 79 + receivedString, 80 + ) 81 + return [ 82 + this.utils.matcherHint(matcher, 'element', ''), 83 + diff, 84 + ].join('\n\n') 85 + }, 86 + } 87 + } 88 + 89 + function getStyleFromObjectCSS(css: Record<string, unknown>): Record<string, unknown> { 90 + const doc = browser === 'chrome' || browser === 'chromium' 91 + ? document 92 + : document.implementation.createHTMLDocument('') 93 + 94 + const copy = doc.createElement('div') 95 + doc.body.appendChild(copy) 96 + const keys = Object.keys(css) 97 + 98 + keys.forEach((property) => { 99 + copy.style[property as 'color'] = css[property] as string 100 + }) 101 + 102 + const styles: Record<string, unknown> = {} 103 + // to get normalized colors (blue -> rgb(0, 0, 255)) 104 + const computedStyles = window.getComputedStyle(copy) 105 + keys.forEach((property) => { 106 + const styleSheet = usedValuesProps.has(property) ? copy.style : computedStyles 107 + const value = styleSheet[property as 'color'] 108 + // ignore invalid keys 109 + if (value != null) { 110 + styles[property] = value 111 + } 112 + }) 113 + copy.remove() 114 + 115 + return styles 116 + } 117 + 118 + function computeCSSStyleDeclaration(css: string): Record<string, unknown> { 119 + // on chromium for styles to be computed, they need to be inserted into the actual document 120 + // webkit will also not compute _some_ style like transform if it's not in the document 121 + const doc = browser === 'chrome' || browser === 'chromium' || browser === 'webkit' 122 + ? document 123 + : document.implementation.createHTMLDocument('') 124 + 125 + const rootElement = doc.createElement('div') 126 + rootElement.setAttribute('style', css.replace(/\n/g, '')) 127 + doc.body.appendChild(rootElement) 128 + 129 + const computedStyle = window.getComputedStyle(rootElement) 130 + 131 + const styleDeclaration = Array.from(rootElement.style).reduce((acc, prop) => { 132 + acc[prop] = usedValuesProps.has(prop) 133 + ? rootElement.style.getPropertyValue(prop) 134 + : computedStyle.getPropertyValue(prop) 135 + return acc 136 + }, {} as Record<string, unknown>) 137 + rootElement.remove() 138 + return styleDeclaration 139 + } 140 + 141 + function printoutObjectStyles(styles: Record<string, unknown>): string { 142 + return Object.keys(styles) 143 + .sort() 144 + .map(prop => `${prop}: ${styles[prop]};`) 145 + .join('\n') 146 + } 147 + 148 + function isSubset( 149 + styles: Record<string, unknown>, 150 + element: HTMLElement | SVGElement, 151 + computedStyle: CSSStyleDeclaration, 152 + receivedCustomKeys: Set<string>, 153 + ): boolean { 154 + const keys = Object.keys(styles) 155 + if (!keys.length) { 156 + return false 157 + } 158 + return keys.every((prop) => { 159 + const value = styles[prop as 'color'] 160 + const isCustomProperty = prop.startsWith('--') 161 + const spellingVariants = [prop] 162 + if (!isCustomProperty) { 163 + spellingVariants.push(prop.toLowerCase()) 164 + } 165 + 166 + const pass = spellingVariants.some( 167 + (name) => { 168 + const styleSheet = receivedCustomKeys.has(prop) && usedValuesProps.has(prop) 169 + ? element.style 170 + : computedStyle 171 + return styleSheet[name as 'color'] === value 172 + || styleSheet.getPropertyValue(name) === value 173 + }, 174 + ) 175 + return pass 176 + }, 177 + ) 178 + }
+54
packages/browser/src/client/tester/expect/toHaveTextContent.ts
··· 1 + /** 2 + * The MIT License (MIT) 3 + * Copyright (c) 2017 Kent C. Dodds 4 + * 5 + * Permission is hereby granted, free of charge, to any person obtaining a copy 6 + * of this software and associated documentation files (the "Software"), to deal 7 + * in the Software without restriction, including without limitation the rights 8 + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + * copies of the Software, and to permit persons to whom the Software is 10 + * furnished to do so, subject to the following conditions: 11 + * 12 + * The above copyright notice and this permission notice shall be included in all 13 + * copies or substantial portions of the Software. 14 + */ 15 + 16 + import type { ExpectationResult, MatcherState } from '@vitest/expect' 17 + import type { Locator } from '../locators' 18 + import { getMessage, getNodeFromUserInput, matches, normalize } from './utils' 19 + 20 + export default function toHaveTextContent( 21 + this: MatcherState, 22 + actual: Element | Locator, 23 + matcher: string | RegExp, 24 + options: { normalizeWhitespace?: boolean } = { normalizeWhitespace: true }, 25 + ): ExpectationResult { 26 + const node = getNodeFromUserInput(actual, toHaveTextContent, this) 27 + 28 + const textContent = options.normalizeWhitespace 29 + ? normalize(node.textContent || '') 30 + : (node.textContent || '').replace(/\u00A0/g, ' ') // Replace &nbsp; with normal spaces 31 + 32 + const checkingWithEmptyString = textContent !== '' && matcher === '' 33 + 34 + return { 35 + pass: !checkingWithEmptyString && matches(textContent, matcher), 36 + message: () => { 37 + const to = this.isNot ? 'not to' : 'to' 38 + return getMessage( 39 + this, 40 + this.utils.matcherHint( 41 + `${this.isNot ? '.not' : ''}.toHaveTextContent`, 42 + 'element', 43 + '', 44 + ), 45 + checkingWithEmptyString 46 + ? `Checking with empty string will always match, use .toBeEmptyDOMElement() instead` 47 + : `Expected element ${to} have text content`, 48 + matcher, 49 + 'Received', 50 + textContent, 51 + ) 52 + }, 53 + } 54 + }
+68
packages/browser/src/client/tester/expect/toHaveValue.ts
··· 1 + /** 2 + * The MIT License (MIT) 3 + * Copyright (c) 2017 Kent C. Dodds 4 + * 5 + * Permission is hereby granted, free of charge, to any person obtaining a copy 6 + * of this software and associated documentation files (the "Software"), to deal 7 + * in the Software without restriction, including without limitation the rights 8 + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + * copies of the Software, and to permit persons to whom the Software is 10 + * furnished to do so, subject to the following conditions: 11 + * 12 + * The above copyright notice and this permission notice shall be included in all 13 + * copies or substantial portions of the Software. 14 + */ 15 + 16 + import type { ExpectationResult, MatcherState } from '@vitest/expect' 17 + import type { Locator } from '../locators' 18 + import { arrayAsSetComparison, getElementFromUserInput, getMessage, getSingleElementValue, isInputElement } from './utils' 19 + 20 + export default function toHaveValue( 21 + this: MatcherState, 22 + actual: Element | Locator, 23 + expectedValue?: string, 24 + ): ExpectationResult { 25 + const htmlElement = getElementFromUserInput(actual, toHaveValue, this) 26 + 27 + if ( 28 + isInputElement(htmlElement) 29 + && ['checkbox', 'radio'].includes(htmlElement.type) 30 + ) { 31 + throw new Error( 32 + 'input with type=checkbox or type=radio cannot be used with .toHaveValue(). Use .toBeChecked() for type=checkbox or .toHaveFormValues() instead', 33 + ) 34 + } 35 + 36 + const receivedValue = getSingleElementValue(htmlElement) 37 + const expectsValue = expectedValue !== undefined 38 + 39 + let expectedTypedValue = expectedValue 40 + let receivedTypedValue = receivedValue 41 + // eslint-disable-next-line eqeqeq 42 + if (expectedValue == receivedValue && expectedValue !== receivedValue) { 43 + expectedTypedValue = `${expectedValue} (${typeof expectedValue})` 44 + receivedTypedValue = `${receivedValue} (${typeof receivedValue})` 45 + } 46 + 47 + return { 48 + pass: expectsValue 49 + ? this.equals(receivedValue, expectedValue, [arrayAsSetComparison, ...this.customTesters]) 50 + : Boolean(receivedValue), 51 + message: () => { 52 + const to = this.isNot ? 'not to' : 'to' 53 + const matcher = this.utils.matcherHint( 54 + `${this.isNot ? '.not' : ''}.toHaveValue`, 55 + 'element', 56 + expectedValue, 57 + ) 58 + return getMessage( 59 + this, 60 + matcher, 61 + `Expected the element ${to} have value`, 62 + expectsValue ? expectedTypedValue : '(any)', 63 + 'Received', 64 + receivedTypedValue, 65 + ) 66 + }, 67 + } 68 + }
+281
packages/browser/src/client/tester/expect/utils.ts
··· 1 + /** 2 + * The MIT License (MIT) 3 + * Copyright (c) 2017 Kent C. Dodds 4 + * 5 + * Permission is hereby granted, free of charge, to any person obtaining a copy 6 + * of this software and associated documentation files (the "Software"), to deal 7 + * in the Software without restriction, including without limitation the rights 8 + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + * copies of the Software, and to permit persons to whom the Software is 10 + * furnished to do so, subject to the following conditions: 11 + * 12 + * The above copyright notice and this permission notice shall be included in all 13 + * copies or substantial portions of the Software. 14 + */ 15 + 16 + import type { MatcherState } from '@vitest/expect' 17 + import { Locator } from '../locators' 18 + 19 + export function getElementFromUserInput( 20 + elementOrLocator: Element | Locator | null, 21 + // TODO: minifier doesn't keep names, so we need to update this 22 + matcherFn: (...args: any) => any, 23 + context: MatcherState, 24 + ): HTMLElement | SVGElement { 25 + if (elementOrLocator instanceof Locator) { 26 + elementOrLocator = elementOrLocator.element() 27 + } 28 + 29 + if ( 30 + elementOrLocator instanceof HTMLElement 31 + || elementOrLocator instanceof SVGElement 32 + ) { 33 + return elementOrLocator 34 + } 35 + 36 + throw new UserInputElementTypeError( 37 + elementOrLocator, 38 + matcherFn, 39 + context, 40 + ) 41 + } 42 + 43 + export function getNodeFromUserInput( 44 + elementOrLocator: Element | Locator, 45 + matcherFn: (...args: any) => any, 46 + context: MatcherState, 47 + ): Node { 48 + if (elementOrLocator instanceof Locator) { 49 + elementOrLocator = elementOrLocator.element() 50 + } 51 + 52 + if ( 53 + elementOrLocator instanceof Node 54 + ) { 55 + return elementOrLocator 56 + } 57 + 58 + throw new UserInputNodeTypeError( 59 + elementOrLocator, 60 + matcherFn, 61 + context, 62 + ) 63 + } 64 + 65 + export function getMessage( 66 + context: MatcherState, 67 + matcher: string, 68 + expectedLabel: string, 69 + expectedValue: unknown, 70 + receivedLabel: string, 71 + receivedValue: unknown, 72 + ): string { 73 + return [ 74 + `${matcher}\n`, 75 + 76 + `${expectedLabel}:\n${context.utils.EXPECTED_COLOR( 77 + redent(display(context, expectedValue), 2), 78 + )}`, 79 + 80 + `${receivedLabel}:\n${context.utils.RECEIVED_COLOR( 81 + redent(display(context, receivedValue), 2), 82 + )}`, 83 + ].join('\n') 84 + } 85 + 86 + export function redent(string: string, count: number): string { 87 + return indentString(stripIndent(string), count) 88 + } 89 + 90 + function indentString(string: string, count: number) { 91 + const regex = /^(?!\s*$)/gm 92 + 93 + return string.replace(regex, ' '.repeat(count)) 94 + } 95 + 96 + function minIndent(string: string) { 97 + const match = string.match(/^[ \t]*(?=\S)/gm) 98 + 99 + if (!match) { 100 + return 0 101 + } 102 + 103 + return match.reduce((r, a) => Math.min(r, a.length), Infinity) 104 + } 105 + 106 + function stripIndent(string: string) { 107 + const indent = minIndent(string) 108 + 109 + if (indent === 0) { 110 + return string 111 + } 112 + 113 + const regex = new RegExp(`^[ \\t]{${indent}}`, 'gm') 114 + 115 + return string.replace(regex, '') 116 + } 117 + 118 + function display(context: MatcherState, value: unknown) { 119 + return typeof value === 'string' ? value : context.utils.stringify(value) 120 + } 121 + 122 + interface ToSentenceOptions { 123 + wordConnector?: string 124 + lastWordConnector?: string 125 + } 126 + 127 + export function toSentence( 128 + array: string[], 129 + { wordConnector = ', ', lastWordConnector = ' and ' }: ToSentenceOptions = {}, 130 + ): string { 131 + return [array.slice(0, -1).join(wordConnector), array[array.length - 1]].join( 132 + array.length > 1 ? lastWordConnector : '', 133 + ) 134 + } 135 + 136 + class GenericTypeError extends Error { 137 + constructor(expectedString: string, received: unknown, matcherFn: (...args: any) => any, context: MatcherState) { 138 + super() 139 + 140 + if (Error.captureStackTrace) { 141 + Error.captureStackTrace(this, matcherFn) 142 + } 143 + let withType = '' 144 + try { 145 + withType = context.utils.printWithType( 146 + 'Received', 147 + received, 148 + context.utils.printReceived, 149 + ) 150 + } 151 + catch { 152 + // Can throw for Document: 153 + // https://github.com/jsdom/jsdom/issues/2304 154 + } 155 + this.message = [ 156 + context.utils.matcherHint( 157 + `${context.isNot ? '.not' : ''}.${matcherFn.name}`, 158 + 'received', 159 + '', 160 + ), 161 + '', 162 + 163 + `${context.utils.RECEIVED_COLOR( 164 + 'received', 165 + )} value must ${expectedString} or a Locator that returns ${expectedString}.`, 166 + withType, 167 + ].join('\n') 168 + } 169 + } 170 + 171 + class UserInputElementTypeError extends GenericTypeError { 172 + constructor( 173 + element: unknown, 174 + matcherFn: (...args: any) => any, 175 + context: MatcherState, 176 + ) { 177 + super('an HTMLElement or an SVGElement', element, matcherFn, context) 178 + } 179 + } 180 + 181 + class UserInputNodeTypeError extends GenericTypeError { 182 + constructor( 183 + element: unknown, 184 + matcherFn: (...args: any) => any, 185 + context: MatcherState, 186 + ) { 187 + super('a Node', element, matcherFn, context) 188 + } 189 + } 190 + 191 + export function getTag(element: Element): string { 192 + // Named inputs, e.g. <input name=tagName>, will be exposed as fields on the parent <form> 193 + // and override its properties. 194 + if (element instanceof HTMLFormElement) { 195 + return 'FORM' 196 + } 197 + // Elements from the svg namespace do not have uppercase tagName right away. 198 + return element.tagName.toUpperCase() 199 + } 200 + 201 + export function isInputElement(element: HTMLElement | SVGElement): element is HTMLInputElement { 202 + return getTag(element) === 'INPUT' 203 + } 204 + 205 + type SimpleInputValue = string | number | boolean | null 206 + 207 + export function getSingleElementValue( 208 + element: Element | undefined, 209 + ): SimpleInputValue | string[] | undefined { 210 + if (!element) { 211 + return undefined 212 + } 213 + 214 + switch (getTag(element)) { 215 + case 'INPUT': 216 + return getInputValue(element as HTMLInputElement) 217 + case 'SELECT': 218 + return getSelectValue(element as HTMLSelectElement) 219 + default: { 220 + return (element as any).value ?? getAccessibleValue(element) 221 + } 222 + } 223 + } 224 + 225 + function getSelectValue({ multiple, options }: HTMLSelectElement) { 226 + const selectedOptions = [...options].filter(option => option.selected) 227 + 228 + if (multiple) { 229 + return [...selectedOptions].map(opt => opt.value) 230 + } 231 + /* istanbul ignore if */ 232 + if (selectedOptions.length === 0) { 233 + return undefined // Couldn't make this happen, but just in case 234 + } 235 + return selectedOptions[0].value 236 + } 237 + 238 + function getInputValue(inputElement: HTMLInputElement) { 239 + switch (inputElement.type) { 240 + case 'number': 241 + return inputElement.value === '' ? null : Number(inputElement.value) 242 + case 'checkbox': 243 + return inputElement.checked 244 + default: 245 + return inputElement.value 246 + } 247 + } 248 + 249 + const rolesSupportingValues = ['meter', 'progressbar', 'slider', 'spinbutton'] 250 + function getAccessibleValue(element: Element) { 251 + if (!rolesSupportingValues.includes(element.getAttribute('role') || '')) { 252 + return undefined 253 + } 254 + return Number(element.getAttribute('aria-valuenow')) 255 + } 256 + 257 + export function normalize(text: string): string { 258 + return text.replace(/\s+/g, ' ').trim() 259 + } 260 + 261 + export function matches(textToMatch: string, matcher: string | RegExp): boolean { 262 + if (matcher instanceof RegExp) { 263 + return matcher.test(textToMatch) 264 + } 265 + else { 266 + return textToMatch.includes(String(matcher)) 267 + } 268 + } 269 + 270 + export function arrayAsSetComparison(a: unknown, b: unknown): boolean | undefined { 271 + if (Array.isArray(a) && Array.isArray(b)) { 272 + const setB = new Set(b) 273 + for (const item of new Set(a)) { 274 + if (!setB.has(item)) { 275 + return false 276 + } 277 + } 278 + return true 279 + } 280 + return undefined 281 + }
+2 -2
packages/browser/src/client/tester/locators/preview.ts
··· 84 84 return userEvent.upload(this.element(), file) 85 85 } 86 86 87 - selectOptions(options_: string | string[] | HTMLElement | HTMLElement[] | Locator | Locator[]): Promise<void> { 88 - return userEvent.selectOptions(this.element(), options_) 87 + selectOptions(options: string | string[] | HTMLElement | HTMLElement[] | Locator | Locator[]): Promise<void> { 88 + return userEvent.selectOptions(this.element(), options) 89 89 } 90 90 91 91 clear(): Promise<void> {