[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.

feat(browser): add `length` property to locators, `toHaveLength` now accepts locators (#8512)

authored by

Vladimir and committed by
GitHub
(Aug 31, 2025, 11:38 AM +0200) 2308cbf1 a8c7f731

+79 -8
+6
packages/browser/context.d.ts
··· 476 476 readonly selector: string 477 477 478 478 /** 479 + * The number of elements that this locator is matching. 480 + * @see {@link https://vitest.dev/guide/browser/locators#length} 481 + */ 482 + readonly length: number 483 + 484 + /** 479 485 * Click on an element. You can use the options to set the cursor position. 480 486 * @see {@link https://vitest.dev/guide/browser/interactivity-api#userevent-click} 481 487 */
+19
docs/guide/browser/locators.md
··· 956 956 ``` 957 957 ::: 958 958 959 + ### length 960 + 961 + This getter returns a number of elements that this locator is matching. It is equivalent to calling `locator.elements().length`. 962 + 963 + Consider the following DOM structure: 964 + 965 + ```html 966 + <button>Click Me!</button> 967 + <button>Don't click me!</button> 968 + ``` 969 + 970 + This property will always succeed: 971 + 972 + ```ts 973 + page.getByRole('button').length // ✅ 2 974 + page.getByRole('button', { title: 'Click Me!' }).length // ✅ 1 975 + page.getByRole('alert').length // ✅ 0 976 + ``` 977 + 959 978 ## Custom Locators <Version>3.2.0</Version> <Badge type="danger">advanced</Badge> {#custom-locators} 960 979 961 980 You can extend built-in locators API by defining an object of locator factories. These methods will exist as methods on the `page` object and any created locator.
+2
test/core/test/exports.test.ts
··· 62 62 "collectTests": "function", 63 63 "format": "function", 64 64 "getSafeTimers": "function", 65 + "getType": "function", 65 66 "inspect": "function", 66 67 "loadDiffConfig": "function", 67 68 "loadSnapshotSerializers": "function", ··· 221 222 "collectTests": "function", 222 223 "format": "function", 223 224 "getSafeTimers": "function", 225 + "getType": "function", 224 226 "inspect": "function", 225 227 "loadDiffConfig": "function", 226 228 "loadSnapshotSerializers": "function",
+1
packages/vitest/src/public/browser.ts
··· 14 14 export { 15 15 format, 16 16 getSafeTimers, 17 + getType, 17 18 inspect, 18 19 stringify, 19 20 } from '@vitest/utils'
+19
test/browser/fixtures/expect-dom/toHaveLength.test.ts
··· 1 + import { describe, expect, test } from 'vitest'; 2 + import { render } from './utils'; 3 + import { page } from '@vitest/browser/context'; 4 + 5 + describe('.toHaveLength', () => { 6 + test('accepts locator', async () => { 7 + render(` 8 + <button></button> 9 + <button></button> 10 + <button></button> 11 + `) 12 + 13 + await expect.element(page.getByRole('button')).toHaveLength(3) 14 + await expect.element(page.getByRole('button')).not.toHaveLength(0) 15 + 16 + expect(page.getByRole('button')).toHaveLength(3) 17 + expect(page.getByRole('button')).not.toHaveLength(0) 18 + }) 19 + })
+17 -8
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 3 import { chai, expect } from 'vitest' 4 + import { getType } from 'vitest/internal/browser' 4 5 import { matchers } from './expect' 5 6 import { processTimeoutOptions } from './utils' 6 7 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}`) 8 + const kLocator = Symbol.for('$$vitest:locator') 9 + 10 + function element<T extends HTMLElement | SVGElement | null | Locator>(elementOrLocator: T, options?: ExpectPollOptions): unknown { 11 + if (elementOrLocator != null && !(elementOrLocator instanceof HTMLElement) && !(elementOrLocator instanceof SVGElement) && !(kLocator in elementOrLocator)) { 12 + throw new Error(`Invalid element or locator: ${elementOrLocator}. Expected an instance of HTMLElement, SVGElement or Locator, received ${getType(elementOrLocator)}`) 10 13 } 11 14 12 - return expect.poll<Element | null>(function element(this: object) { 15 + return expect.poll<HTMLElement | SVGElement | null>(function element(this: object) { 13 16 if (elementOrLocator instanceof Element || elementOrLocator == null) { 14 17 return elementOrLocator 15 18 } ··· 17 20 18 21 const isNot = chai.util.flag(this, 'negate') as boolean 19 22 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 23 // special case for `toBeInTheDocument` matcher 25 24 if (isNot && name === 'toBeInTheDocument') { 26 25 return elementOrLocator.query() 27 26 } 27 + if (name === 'toHaveLength') { 28 + // we know that `toHaveLength` requires multiple elements, 29 + // but types generally expect a single one 30 + return elementOrLocator.elements() as unknown as HTMLElement 31 + } 32 + 33 + // element selector uses prettyDOM under the hood, which is an expensive call 34 + // that should not be called on each failed locator attempt to avoid memory leak: 35 + // https://github.com/vitest-dev/vitest/issues/7139 36 + const isLastPollAttempt = chai.util.flag(this, '_isLastPollAttempt') 28 37 29 38 if (isLastPollAttempt) { 30 39 return elementOrLocator.element()
+15
packages/browser/src/client/tester/locators/index.ts
··· 43 43 testIdAttribute: server.config.browser.locators.testIdAttribute, 44 44 }) 45 45 46 + const kLocator = Symbol.for('$$vitest:locator') 47 + 46 48 export abstract class Locator { 47 49 public abstract selector: string 48 50 49 51 private _parsedSelector: ParsedSelector | undefined 50 52 protected _container?: Element | undefined 51 53 protected _pwSelector?: string | undefined 54 + 55 + constructor() { 56 + Object.defineProperty(this, kLocator, { 57 + enumerable: false, 58 + writable: false, 59 + configurable: false, 60 + value: kLocator, 61 + }) 62 + } 52 63 53 64 public click(options: UserEventClickOptions = {}): Promise<void> { 54 65 return this.triggerCommand<void>('__vitest_click', this.selector, options) ··· 220 231 public elements(): (HTMLElement | SVGElement)[] { 221 232 const parsedSelector = this._parsedSelector || (this._parsedSelector = selectorEngine.parseSelector(this._pwSelector || this.selector)) 222 233 return selectorEngine.querySelectorAll(parsedSelector, document.documentElement) as (HTMLElement | SVGElement)[] 234 + } 235 + 236 + public get length(): number { 237 + return this.elements().length 223 238 } 224 239 225 240 public all(): Locator[] {