mirror of
				https://github.com/microsoft/playwright.git
				synced 2025-06-26 21:40:17 +00:00 
			
		
		
		
	feat: toHaveAttribute without value (#27418)
This time not doing it in other languages due to unjustified generator complexity. Fixes #27341
This commit is contained in:
		
							parent
							
								
									5295d468ad
								
							
						
					
					
						commit
						ac48a47d33
					
				| @ -1151,6 +1151,29 @@ Expected attribute value. | |||||||
| ### option: LocatorAssertions.toHaveAttribute.timeout = %%-csharp-java-python-assertions-timeout-%% | ### option: LocatorAssertions.toHaveAttribute.timeout = %%-csharp-java-python-assertions-timeout-%% | ||||||
| * since: v1.18 | * since: v1.18 | ||||||
| 
 | 
 | ||||||
|  | ## async method: LocatorAssertions.toHaveAttribute#2 | ||||||
|  | * since: v1.40 | ||||||
|  | * langs: js | ||||||
|  | 
 | ||||||
|  | Ensures the [Locator] points to an element with given attribute. The method will assert attribute | ||||||
|  | presence. | ||||||
|  | 
 | ||||||
|  | ```js | ||||||
|  | const locator = page.locator('input'); | ||||||
|  | // Assert attribute existence. | ||||||
|  | await expect(locator).toHaveAttribute('disabled'); | ||||||
|  | await expect(locator).not.toHaveAttribute('open'); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### param: LocatorAssertions.toHaveAttribute#2.name | ||||||
|  | * since: v1.40 | ||||||
|  | - `name` <[string]> | ||||||
|  | 
 | ||||||
|  | Attribute name. | ||||||
|  | 
 | ||||||
|  | ### option: LocatorAssertions.toHaveAttribute#2.timeout = %%-js-assertions-timeout-%% | ||||||
|  | * since: v1.40 | ||||||
|  | 
 | ||||||
| ## async method: LocatorAssertions.toHaveClass | ## async method: LocatorAssertions.toHaveClass | ||||||
| * since: v1.20 | * since: v1.20 | ||||||
| * langs: | * langs: | ||||||
|  | |||||||
| @ -1206,7 +1206,9 @@ export class InjectedScript { | |||||||
|     { |     { | ||||||
|       // Element state / boolean values.
 |       // Element state / boolean values.
 | ||||||
|       let elementState: boolean | 'error:notconnected' | 'error:notcheckbox' | undefined; |       let elementState: boolean | 'error:notconnected' | 'error:notcheckbox' | undefined; | ||||||
|       if (expression === 'to.be.checked') { |       if (expression === 'to.have.attribute') { | ||||||
|  |         elementState = element.hasAttribute(options.expressionArg); | ||||||
|  |       } else if (expression === 'to.be.checked') { | ||||||
|         elementState = this.elementState(element, 'checked'); |         elementState = this.elementState(element, 'checked'); | ||||||
|       } else if (expression === 'to.be.unchecked') { |       } else if (expression === 'to.be.unchecked') { | ||||||
|         elementState = this.elementState(element, 'unchecked'); |         elementState = this.elementState(element, 'unchecked'); | ||||||
| @ -1277,7 +1279,7 @@ export class InjectedScript { | |||||||
|     { |     { | ||||||
|       // Single text value.
 |       // Single text value.
 | ||||||
|       let received: string | undefined; |       let received: string | undefined; | ||||||
|       if (expression === 'to.have.attribute') { |       if (expression === 'to.have.attribute.value') { | ||||||
|         const value = element.getAttribute(options.expressionArg); |         const value = element.getAttribute(options.expressionArg); | ||||||
|         if (value === null) |         if (value === null) | ||||||
|           return { received: null, matches: false }; |           return { received: null, matches: false }; | ||||||
|  | |||||||
| @ -21,7 +21,7 @@ import { expectTypes, callLogText, filteredStackTrace } from '../util'; | |||||||
| import { toBeTruthy } from './toBeTruthy'; | import { toBeTruthy } from './toBeTruthy'; | ||||||
| import { toEqual } from './toEqual'; | import { toEqual } from './toEqual'; | ||||||
| import { toExpectedTextValues, toMatchText } from './toMatchText'; | import { toExpectedTextValues, toMatchText } from './toMatchText'; | ||||||
| import { captureRawStack, constructURLBasedOnBaseURL, isTextualMimeType, pollAgainstDeadline } from 'playwright-core/lib/utils'; | import { captureRawStack, constructURLBasedOnBaseURL, isRegExp, isTextualMimeType, pollAgainstDeadline } from 'playwright-core/lib/utils'; | ||||||
| import { currentTestInfo } from '../common/globals'; | import { currentTestInfo } from '../common/globals'; | ||||||
| import { TestInfoImpl, type TestStepInternal } from '../worker/testInfo'; | import { TestInfoImpl, type TestStepInternal } from '../worker/testInfo'; | ||||||
| import type { ExpectMatcherContext } from './expect'; | import type { ExpectMatcherContext } from './expect'; | ||||||
| @ -177,13 +177,25 @@ export function toHaveAttribute( | |||||||
|   this: ExpectMatcherContext, |   this: ExpectMatcherContext, | ||||||
|   locator: LocatorEx, |   locator: LocatorEx, | ||||||
|   name: string, |   name: string, | ||||||
|   expected: string | RegExp, |   expected: string | RegExp | undefined | { timeout?: number }, | ||||||
|   options?: { timeout?: number }, |   options?: { timeout?: number }, | ||||||
| ) { | ) { | ||||||
|  |   if (!options) { | ||||||
|  |     // Update params for the case toHaveAttribute(name, options);
 | ||||||
|  |     if (typeof expected === 'object' && !isRegExp(expected)) { | ||||||
|  |       options = expected; | ||||||
|  |       expected = undefined; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   if (expected === undefined) { | ||||||
|  |     return toBeTruthy.call(this, 'toHaveAttribute', locator, 'Locator', 'have attribute', 'not have attribute', '', async (isNot, timeout) => { | ||||||
|  |       return await locator._expect('to.have.attribute', { expressionArg: name, isNot, timeout }); | ||||||
|  |     }, options); | ||||||
|  |   } | ||||||
|   return toMatchText.call(this, 'toHaveAttribute', locator, 'Locator', async (isNot, timeout) => { |   return toMatchText.call(this, 'toHaveAttribute', locator, 'Locator', async (isNot, timeout) => { | ||||||
|     const expectedText = toExpectedTextValues([expected]); |     const expectedText = toExpectedTextValues([expected as (string | RegExp)]); | ||||||
|     return await locator._expect('to.have.attribute', { expressionArg: name, expectedText, isNot, timeout }); |     return await locator._expect('to.have.attribute.value', { expressionArg: name, expectedText, isNot, timeout }); | ||||||
|   }, expected, options); |   }, expected as (string | RegExp), options); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function toHaveClass( | export function toHaveClass( | ||||||
|  | |||||||
							
								
								
									
										20
									
								
								packages/playwright/types/test.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								packages/playwright/types/test.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -5583,6 +5583,26 @@ interface LocatorAssertions { | |||||||
|     timeout?: number; |     timeout?: number; | ||||||
|   }): Promise<void>; |   }): Promise<void>; | ||||||
| 
 | 
 | ||||||
|  |   /** | ||||||
|  |    * Ensures the {@link Locator} points to an element with given attribute. The method will assert attribute presence. | ||||||
|  |    * | ||||||
|  |    * ```js
 | ||||||
|  |    * const locator = page.locator('input'); | ||||||
|  |    * // Assert attribute existence.
 | ||||||
|  |    * await expect(locator).toHaveAttribute('disabled'); | ||||||
|  |    * await expect(locator).not.toHaveAttribute('open'); | ||||||
|  |    * ``` | ||||||
|  |    * | ||||||
|  |    * @param name Attribute name. | ||||||
|  |    * @param options | ||||||
|  |    */ | ||||||
|  |   toHaveAttribute(name: string, options?: { | ||||||
|  |     /** | ||||||
|  |      * Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`. | ||||||
|  |      */ | ||||||
|  |     timeout?: number; | ||||||
|  |   }): Promise<void>; | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Ensures the {@link Locator} points to an element with given CSS classes. This needs to be a full match or using a |    * Ensures the {@link Locator} points to an element with given CSS classes. This needs to be a full match or using a | ||||||
|    * relaxed regular expression. |    * relaxed regular expression. | ||||||
|  | |||||||
| @ -262,6 +262,22 @@ test.describe('toHaveAttribute', () => { | |||||||
|       expect(error.message).toContain('expect.not.toHaveAttribute with timeout 1000ms'); |       expect(error.message).toContain('expect.not.toHaveAttribute with timeout 1000ms'); | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
|  | 
 | ||||||
|  |   test('should match attribute without value', async ({ page }) => { | ||||||
|  |     await page.setContent('<div checked id=node>Text content</div>'); | ||||||
|  |     const locator = page.locator('#node'); | ||||||
|  |     await expect(locator).toHaveAttribute('id'); | ||||||
|  |     await expect(locator).toHaveAttribute('checked'); | ||||||
|  |     await expect(locator).not.toHaveAttribute('open'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   test('should support boolean attribute with options', async ({ page }) => { | ||||||
|  |     await page.setContent('<div checked id=node>Text content</div>'); | ||||||
|  |     const locator = page.locator('#node'); | ||||||
|  |     await expect(locator).toHaveAttribute('id', { timeout: 5000 }); | ||||||
|  |     await expect(locator).toHaveAttribute('checked', { timeout: 5000 }); | ||||||
|  |     await expect(locator).not.toHaveAttribute('open', { timeout: 5000 }); | ||||||
|  |   }); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test.describe('toHaveCSS', () => { | test.describe('toHaveCSS', () => { | ||||||
|  | |||||||
| @ -859,3 +859,23 @@ test('should chain expect matchers and expose matcher utils', async ({ runInline | |||||||
|   expect(result.failed).toBe(1); |   expect(result.failed).toBe(1); | ||||||
|   expect(result.exitCode).toBe(1); |   expect(result.exitCode).toBe(1); | ||||||
| }); | }); | ||||||
|  | 
 | ||||||
|  | test('should suppport toHaveAttribute without optional value', async ({ runTSC }) => { | ||||||
|  |   const result = await runTSC({ | ||||||
|  |     'a.spec.ts': ` | ||||||
|  |     import { test, expect as baseExpect } from '@playwright/test'; | ||||||
|  |     test('custom matchers', async ({ page }) => { | ||||||
|  |       const locator = page.locator('#node'); | ||||||
|  |       await test.expect(locator).toHaveAttribute('name', 'value'); | ||||||
|  |       await test.expect(locator).toHaveAttribute('name', 'value', { timeout: 10 }); | ||||||
|  |       await test.expect(locator).toHaveAttribute('disabled'); | ||||||
|  |       await test.expect(locator).toHaveAttribute('disabled', { timeout: 10 }); | ||||||
|  |       // @ts-expect-error
 | ||||||
|  |       await test.expect(locator).toHaveAttribute('disabled', { foo: 1 }); | ||||||
|  |       // @ts-expect-error
 | ||||||
|  |       await test.expect(locator).toHaveAttribute('name', 'value', 'opt'); | ||||||
|  |     }); | ||||||
|  |     ` | ||||||
|  |   }); | ||||||
|  |   expect(result.exitCode).toBe(0); | ||||||
|  | }); | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Yury Semikhatsky
						Yury Semikhatsky