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-%% | ||||
| * 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 | ||||
| * since: v1.20 | ||||
| * langs: | ||||
|  | ||||
| @ -1206,7 +1206,9 @@ export class InjectedScript { | ||||
|     { | ||||
|       // Element state / boolean values.
 | ||||
|       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'); | ||||
|       } else if (expression === 'to.be.unchecked') { | ||||
|         elementState = this.elementState(element, 'unchecked'); | ||||
| @ -1277,7 +1279,7 @@ export class InjectedScript { | ||||
|     { | ||||
|       // Single text value.
 | ||||
|       let received: string | undefined; | ||||
|       if (expression === 'to.have.attribute') { | ||||
|       if (expression === 'to.have.attribute.value') { | ||||
|         const value = element.getAttribute(options.expressionArg); | ||||
|         if (value === null) | ||||
|           return { received: null, matches: false }; | ||||
|  | ||||
| @ -21,7 +21,7 @@ import { expectTypes, callLogText, filteredStackTrace } from '../util'; | ||||
| import { toBeTruthy } from './toBeTruthy'; | ||||
| import { toEqual } from './toEqual'; | ||||
| 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 { TestInfoImpl, type TestStepInternal } from '../worker/testInfo'; | ||||
| import type { ExpectMatcherContext } from './expect'; | ||||
| @ -177,13 +177,25 @@ export function toHaveAttribute( | ||||
|   this: ExpectMatcherContext, | ||||
|   locator: LocatorEx, | ||||
|   name: string, | ||||
|   expected: string | RegExp, | ||||
|   expected: string | RegExp | undefined | { 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) => { | ||||
|     const expectedText = toExpectedTextValues([expected]); | ||||
|     return await locator._expect('to.have.attribute', { expressionArg: name, expectedText, isNot, timeout }); | ||||
|   }, expected, options); | ||||
|     const expectedText = toExpectedTextValues([expected as (string | RegExp)]); | ||||
|     return await locator._expect('to.have.attribute.value', { expressionArg: name, expectedText, isNot, timeout }); | ||||
|   }, expected as (string | RegExp), options); | ||||
| } | ||||
| 
 | ||||
| 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; | ||||
|   }): 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 | ||||
|    * relaxed regular expression. | ||||
|  | ||||
| @ -262,6 +262,22 @@ test.describe('toHaveAttribute', () => { | ||||
|       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', () => { | ||||
|  | ||||
| @ -859,3 +859,23 @@ test('should chain expect matchers and expose matcher utils', async ({ runInline | ||||
|   expect(result.failed).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