Skip to content
29 changes: 24 additions & 5 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,16 +349,24 @@ await expect(myInput).toHaveAttribute('class', 'form-control')
await expect(myInput).toHaveAttribute('class', expect.stringContaining('control'))
```

### toHaveAttr
Checks if an element has a specific attribute with a value.

Same as `toHaveAttribute`.
##### Usage

```js
const myInput = await $('input')
await expect(myInput).toHaveAttribute('class')
await expect(myInput).toHaveAttribute('class', { wait: 1000 })
```

Checks if an element does not have the specified attribute.

##### Usage

```js
const myInput = await $('input')
await expect(myInput).toHaveAttr('class', 'form-control')
await expect(myInput).toHaveAttr('class', expect.stringContaining('control'))
await expect(myInput).not.toHaveAttribute('class')
await expect(myInput).not.toHaveAttribute('class', { wait: 1000 })
```

### toHaveElementClass
Expand All @@ -376,7 +384,7 @@ await expect(myInput).toHaveElementClass(expect.stringContaining('form'), { mess

### toHaveElementProperty

Checks if an element has a certain property.
Checks if an element has a certain property and value

##### Usage

Expand All @@ -386,6 +394,17 @@ await expect(elem).toHaveElementProperty('height', 23)
await expect(elem).not.toHaveElementProperty('height', 0)
```

Checks if an element has a certain property.

##### Usage

```js
const elem = await $('#elem')
await expect(elem).toHaveElementProperty('height')
// Does not have height property
await expect(elem).not.toHaveElementProperty('height')
```

### toHaveValue

Checks if an input element has a certain value.
Expand Down
2 changes: 1 addition & 1 deletion docs/Examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe('suite', () => {
const myInput = await $('input')

await expect(myInput).toHaveElementClass('form-control', { message: 'Not a form control!', })
await expect(myInput).toHaveAttribute('class', 'form-control') // alias toHaveAttr
await expect(myInput).toHaveAttribute('class', 'form-control')

await expect(myInput).toHaveValue('value', 'user', { containing: true, ignoreCase: true })

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { browser, $, $$ } from '@wdio/globals'
import { expect } from 'expect-webdriverio'

describe('WebdriverIO Custom Matchers', () => {
beforeEach(async () => {
Expand Down Expand Up @@ -149,7 +148,6 @@ describe('WebdriverIO Custom Matchers', () => {
await searchButton.click()

// The search modal input should be focused after clicking

await browser.pause(500) // Wait for modal to open
const searchInput = await $('.DocSearch-Input')
if (await searchInput.isExisting()) {
Expand Down
42 changes: 41 additions & 1 deletion playgrounds/mocha/test/specs/wdio-matchers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,31 @@ describe('WebdriverIO Custom Matchers', () => {
})

describe('Element attribute matchers', () => {
it('should verify element exists', async () => {
const docsLink = await $('a[href="/docs/gettingstarted"]')
await expect(docsLink).toHaveAttribute('href')
})

it('should verify element exists immediately', async () => {
const docsLink = await $('a[href="/docs/gettingstarted"]')
await expect(docsLink).toHaveAttribute('href', { wait: 0 })
})

it('should verify element has attribute', async () => {
const docsLink = await $('a[href="/docs/gettingstarted"]')
await expect(docsLink).toHaveAttribute('href', '/docs/gettingstarted')
})

it('should verify element does not exist', async () => {
const docsLink = await $('a[href="/docs/gettingstarted"]')
await expect(docsLink).not.toHaveAttribute('non-existent-attribute')
})

it('should verify element does not exist immediately', async () => {
const docsLink = await $('a[href="/docs/gettingstarted"]')
await expect(docsLink).not.toHaveAttribute('non-existent-attribute', { wait: 0 })
})

it('should verify attribute contains value', async () => {
const docsLink = await $('a[href="/docs/gettingstarted"]')
await expect(docsLink).toHaveAttribute('href', expect.stringContaining('docs'))
Expand All @@ -106,10 +126,30 @@ describe('WebdriverIO Custom Matchers', () => {
})

describe('Element property matchers', () => {
it('should verify element property', async () => {
it('should verify element property value', async () => {
const searchButton = await $('.DocSearch-Button')
await expect(searchButton).toHaveElementProperty('type', 'button')
})

it('should verify that element property exists', async () => {
const searchButton = await $('.DocSearch-Button')
await expect(searchButton).toHaveElementProperty('type')
})

it('should verify that element property exists immediately', async () => {
const searchButton = await $('.DocSearch-Button')
await expect(searchButton).toHaveElementProperty('type', { wait: 0 })
})

it('should verify that element property does not exist', async () => {
const searchButton = await $('.DocSearch-Button')
await expect(searchButton).not.toHaveElementProperty('doesNNotExist')
})

it('should verify that element property does not exist immediately', async () => {
const searchButton = await $('.DocSearch-Button')
await expect(searchButton).not.toHaveElementProperty('doesNNotExist', { wait: 0 })
})
})

describe('Element value matchers', () => {
Expand Down
124 changes: 87 additions & 37 deletions src/matchers/element/toHaveAttribute.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { AsyncAssertionResult } from 'expect-webdriverio'
import { DEFAULT_OPTIONS } from '../../constants.js'
import type { WdioElementMaybePromise } from '../../types.js'
import {
Expand All @@ -7,77 +8,123 @@ import {
waitUntil,
wrapExpectedWithArray
} from '../../utils.js'
import { isStringOptions } from '../../util/commandOptionsUtils.js'

async function conditionAttr(el: WebdriverIO.Element, attribute: string) {
const attr = await el.getAttribute(attribute)
if (typeof attr !== 'string') {
return { result: false, value: attr }
async function conditionAttributeIsPresent(el: WebdriverIO.Element, attribute: string) {
const attributeValue = await el.getAttribute(attribute)
if (typeof attributeValue !== 'string') {
return { result: false, value: attributeValue }
}
return { result: true, value: attr }
return { result: true, value: attributeValue }

}

async function conditionAttrAndValue(el: WebdriverIO.Element, attribute: string, value: string | RegExp | AsymmetricMatcher<string>, options: ExpectWebdriverIO.StringOptions) {
const attr = await el.getAttribute(attribute)
if (typeof attr !== 'string') {
return { result: false, value: attr }
async function conditionAttributeValueMatchWithExpected(el: WebdriverIO.Element, attribute: string, expectedValue: string | RegExp | AsymmetricMatcher<string>, options: ExpectWebdriverIO.StringOptions) {
const attributeValue = await el.getAttribute(attribute)
if (typeof attributeValue !== 'string') {
return { result: false, value: attributeValue }
}

return compareText(attr, value, options)
return compareText(attributeValue, expectedValue, options)
}

export async function toHaveAttributeAndValue(received: WdioElementMaybePromise, attribute: string, value: string | RegExp | AsymmetricMatcher<string>, options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS) {
const isNot = this.isNot
const { expectation = 'attribute', verb = 'have' } = this
export async function toHaveAttributeAndValue(received: WdioElementMaybePromise, attribute: string, expectedValue: string | RegExp | AsymmetricMatcher<string>, options: ExpectWebdriverIO.StringOptions) {
const { expectation = 'attribute', verb = 'have', isNot } = this

let el = await received?.getElement()
let attr
const pass = await waitUntil(async () => {
const result = await executeCommand.call(this, el, conditionAttrAndValue, options, [attribute, value, options])
el = result.el as WebdriverIO.Element
attr = result.values

return result.success
}, isNot, options)

const expected = wrapExpectedWithArray(el, attr, value)
const pass = await waitUntil(
async () => {
const result = await executeCommand.call(this, el, conditionAttributeValueMatchWithExpected, options, [attribute, expectedValue, options])
el = result.el as WebdriverIO.Element
attr = result.values

return result.success
},
isNot,
{ wait: options.wait, interval: options.interval }
)

const expected = wrapExpectedWithArray(el, attr, expectedValue)
const message = enhanceError(el, expected, attr, this, verb, expectation, attribute, options)

return {
pass,
message: (): string => message
} as ExpectWebdriverIO.AssertionResult
}
}

async function toHaveAttributeFn(received: WdioElementMaybePromise, attribute: string) {
const isNot = this.isNot
const { expectation = 'attribute', verb = 'have' } = this
async function toHaveAttributeFn(received: WdioElementMaybePromise, attribute: string, options: ExpectWebdriverIO.CommandOptions) {
const { expectation = 'attribute', verb = 'have', isNot } = this

let el = await received?.getElement()

const pass = await waitUntil(async () => {
const result = await executeCommand.call(this, el, conditionAttr, {}, [attribute])
el = result.el as WebdriverIO.Element
const pass = await waitUntil(
async () => {
const result = await executeCommand.call(this, el, conditionAttributeIsPresent, options, [attribute])
el = result.el as WebdriverIO.Element

return result.success
}, isNot, {})
return result.success
},
isNot,
{ wait: options.wait, interval: options.interval }
)

const message = enhanceError(el, !isNot, pass, this, verb, expectation, attribute, {})
const message = enhanceError(el, !isNot, pass, this, verb, expectation, attribute, options)

return {
pass,
message: (): string => message
}
}

/**
* @deprecated since 5.7.1 Passing explicit `undefined` as a value is deprecated. Omit the third argument entirely or use `toHaveAttribute(el, attribute, options)`.
*/
export async function toHaveAttribute(
received: WdioElementMaybePromise,
attribute: string,
value?: string | RegExp | AsymmetricMatcher<string>,
value: undefined,
options?: ExpectWebdriverIO.StringOptions
): Promise<AsyncAssertionResult>

/**
* When called with only the attribute name (and optional configuration options).
*/
export async function toHaveAttribute(
received: WdioElementMaybePromise,
attribute: string,
options?: ExpectWebdriverIO.StringOptions
): Promise<AsyncAssertionResult>

/**
* When called with an expected attribute name and value.
*/
export async function toHaveAttribute(
received: WdioElementMaybePromise,
attribute: string,
value: string | RegExp | AsymmetricMatcher<string>,
options?: ExpectWebdriverIO.StringOptions
): Promise<AsyncAssertionResult>

export async function toHaveAttribute(
received: WdioElementMaybePromise,
attribute: string,
valueOrOptions?: string | RegExp | AsymmetricMatcher<string> | ExpectWebdriverIO.StringOptions,
options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS
) {
): Promise<AsyncAssertionResult> {
const matcherName = 'toHaveAttribute'

let value: string | RegExp | AsymmetricMatcher<string> | undefined
if (isStringOptions(valueOrOptions)) {
options = valueOrOptions
value = undefined
} else {
value = valueOrOptions as string | RegExp | AsymmetricMatcher<string>
}

await options.beforeAssertion?.({
matcherName: 'toHaveAttribute',
matcherName,
expectedValue: [attribute, value],
options,
})
Expand All @@ -86,10 +133,10 @@ export async function toHaveAttribute(
// Name and value is passed in e.g. el.toHaveAttribute('attr', 'value', (opts))
? await toHaveAttributeAndValue.call(this, received, attribute, value, options)
// Only name is passed in e.g. el.toHaveAttribute('attr')
: await toHaveAttributeFn.call(this, received, attribute)
: await toHaveAttributeFn.call(this, received, attribute, options)

await options.afterAssertion?.({
matcherName: 'toHaveAttribute',
matcherName,
expectedValue: [attribute, value],
options,
result
Expand All @@ -98,4 +145,7 @@ export async function toHaveAttribute(
return result
}

/**
* @deprecated since 5.7.0 Use `toHaveAttribute`
*/
export const toHaveAttr = toHaveAttribute
Loading
Loading