chore: introduce expect.configure (#22533)
This commit is contained in:
		
							parent
							
								
									76a2afc836
								
							
						
					
					
						commit
						a1007bbe2c
					
				|  | @ -121,7 +121,17 @@ The same works with soft assertions: | |||
| expect.soft(value, 'my soft assertion').toBe(56); | ||||
| ``` | ||||
| 
 | ||||
| ## Polling | ||||
| ## expect.configurte | ||||
| 
 | ||||
| You can create your own pre-configured `expect` instance to have its own | ||||
| defaults such as `timeout`, `soft` and `poll`. | ||||
| 
 | ||||
| ```js | ||||
| const slowExpect = expect.configure({ timeout: 10000 }); | ||||
| await slowExpect(locator).toHaveText('Submit); | ||||
| ``` | ||||
| 
 | ||||
| ## expect.poll | ||||
| 
 | ||||
| You can convert any synchronous `expect` to an asynchronous polling one using `expect.poll`. | ||||
| 
 | ||||
|  | @ -152,7 +162,7 @@ await expect.poll(async () => { | |||
| }).toBe(200); | ||||
| ``` | ||||
| 
 | ||||
| ## Retrying | ||||
| ## expect.toPass | ||||
| 
 | ||||
| You can retry blocks of code until they are passing successfully. | ||||
| 
 | ||||
|  |  | |||
|  | @ -34,10 +34,18 @@ export function currentlyLoadingFileSuite() { | |||
|   return currentFileSuite; | ||||
| } | ||||
| 
 | ||||
| let currentExpectConfigureTimeout: number | undefined; | ||||
| 
 | ||||
| export function setCurrentExpectConfigureTimeout(timeout: number | undefined) { | ||||
|   currentExpectConfigureTimeout = timeout; | ||||
| } | ||||
| 
 | ||||
| export function currentExpectTimeout(options: { timeout?: number }) { | ||||
|   const testInfo = currentTestInfo(); | ||||
|   if (options.timeout !== undefined) | ||||
|     return options.timeout; | ||||
|   if (currentExpectConfigureTimeout !== undefined) | ||||
|     return currentExpectConfigureTimeout; | ||||
|   let defaultExpectTimeout = testInfo?._projectInternal?.expect?.timeout; | ||||
|   if (typeof defaultExpectTimeout === 'undefined') | ||||
|     defaultExpectTimeout = 5000; | ||||
|  |  | |||
|  | @ -18,6 +18,7 @@ import { | |||
|   captureRawStack, | ||||
|   createAfterActionTraceEventForExpect, | ||||
|   createBeforeActionTraceEventForExpect, | ||||
|   isString, | ||||
|   pollAgainstTimeout } from 'playwright-core/lib/utils'; | ||||
| import type { ExpectZone } from 'playwright-core/lib/utils'; | ||||
| import { | ||||
|  | @ -48,7 +49,7 @@ import { | |||
| } from './matchers'; | ||||
| import { toMatchSnapshot, toHaveScreenshot, toHaveScreenshotStepTitle } from './toMatchSnapshot'; | ||||
| import type { Expect } from '../../types/test'; | ||||
| import { currentTestInfo, currentExpectTimeout } from '../common/globals'; | ||||
| import { currentTestInfo, currentExpectTimeout, setCurrentExpectConfigureTimeout } from '../common/globals'; | ||||
| import { filteredStackTrace, serializeError, stringifyStackFrames, trimLongString } from '../util'; | ||||
| import { | ||||
|   expect as expectLibrary, | ||||
|  | @ -106,29 +107,59 @@ export const printReceivedStringContainExpectedResult = ( | |||
| 
 | ||||
| // #endregion
 | ||||
| 
 | ||||
| type ExpectMessageOrOptions = undefined | string | { message?: string, timeout?: number, intervals?: number[] }; | ||||
| type ExpectMessage = string | { message?: string }; | ||||
| 
 | ||||
| function createExpect(actual: unknown, messageOrOptions: ExpectMessageOrOptions, isSoft: boolean, isPoll: boolean, generator?: Generator): any { | ||||
|   return new Proxy(expectLibrary(actual), new ExpectMetaInfoProxyHandler(messageOrOptions, isSoft, isPoll, generator)); | ||||
| function createMatchers(actual: unknown, info: ExpectMetaInfo): any { | ||||
|   return new Proxy(expectLibrary(actual), new ExpectMetaInfoProxyHandler(info)); | ||||
| } | ||||
| 
 | ||||
| export const expect: Expect = new Proxy(expectLibrary, { | ||||
|   apply: function(target: any, thisArg: any, argumentsList: [actual: unknown, messageOrOptions: ExpectMessageOrOptions]) { | ||||
| function createExpect(info: ExpectMetaInfo) { | ||||
|   const expect: Expect = new Proxy(expectLibrary, { | ||||
|     apply: function(target: any, thisArg: any, argumentsList: [unknown, ExpectMessage?]) { | ||||
|       const [actual, messageOrOptions] = argumentsList; | ||||
|     return createExpect(actual, messageOrOptions, false /* isSoft */, false /* isPoll */); | ||||
|       const message = isString(messageOrOptions) ? messageOrOptions : messageOrOptions?.message || info.message; | ||||
|       const newInfo = { ...info, message }; | ||||
|       if (newInfo.isPoll) { | ||||
|         if (typeof actual !== 'function') | ||||
|           throw new Error('`expect.poll()` accepts only function as a first argument'); | ||||
|         newInfo.generator = actual as any; | ||||
|       } | ||||
|       return createMatchers(actual, newInfo); | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
| expect.soft = (actual: unknown, messageOrOptions: ExpectMessageOrOptions) => { | ||||
|   return createExpect(actual, messageOrOptions, true /* isSoft */, false /* isPoll */); | ||||
|   expect.soft = (actual: unknown, messageOrOptions?: ExpectMessage) => { | ||||
|     return expect.configure({ soft: true })(actual, messageOrOptions) as any; | ||||
|   }; | ||||
| 
 | ||||
| expect.poll = (actual: unknown, messageOrOptions: ExpectMessageOrOptions) => { | ||||
|   if (typeof actual !== 'function') | ||||
|     throw new Error('`expect.poll()` accepts only function as a first argument'); | ||||
|   return createExpect(actual, messageOrOptions, false /* isSoft */, true /* isPoll */, actual as any); | ||||
|   expect.poll = (actual: unknown, messageOrOptions?: ExpectMessage & { timeout?: number, intervals?: number[] }) => { | ||||
|     const poll = isString(messageOrOptions) ? {} : messageOrOptions || {}; | ||||
|     return expect.configure({ poll })(actual, messageOrOptions) as any; | ||||
|   }; | ||||
| 
 | ||||
|   expect.configure = (configuration: { message?: string, timeout?: number, soft?: boolean, poll?: boolean | { timeout?: number, intervals?: number[] } }) => { | ||||
|     const newInfo = { ...info }; | ||||
|     if ('message' in configuration) | ||||
|       newInfo.message = configuration.message; | ||||
|     if ('timeout' in configuration) | ||||
|       newInfo.timeout = configuration.timeout; | ||||
|     if ('soft' in configuration) | ||||
|       newInfo.isSoft = configuration.soft; | ||||
|     if ('poll' in configuration) { | ||||
|       newInfo.isPoll = !!configuration.poll; | ||||
|       if (typeof configuration.poll === 'object') { | ||||
|         newInfo.pollTimeout = configuration.poll.timeout; | ||||
|         newInfo.pollIntervals = configuration.poll.intervals; | ||||
|       } | ||||
|     } | ||||
|     return createExpect(newInfo); | ||||
|   }; | ||||
| 
 | ||||
|   return expect; | ||||
| } | ||||
| 
 | ||||
| export const expect: Expect = createExpect({}); | ||||
| 
 | ||||
| expectLibrary.setState({ expand: false }); | ||||
| 
 | ||||
| const customAsyncMatchers = { | ||||
|  | @ -168,10 +199,10 @@ type Generator = () => any; | |||
| 
 | ||||
| type ExpectMetaInfo = { | ||||
|   message?: string; | ||||
|   isNot: boolean; | ||||
|   isSoft: boolean; | ||||
|   isPoll: boolean; | ||||
|   nameTokens: string[]; | ||||
|   isNot?: boolean; | ||||
|   isSoft?: boolean; | ||||
|   isPoll?: boolean; | ||||
|   timeout?: number; | ||||
|   pollTimeout?: number; | ||||
|   pollIntervals?: number[]; | ||||
|   generator?: Generator; | ||||
|  | @ -180,15 +211,8 @@ type ExpectMetaInfo = { | |||
| class ExpectMetaInfoProxyHandler implements ProxyHandler<any> { | ||||
|   private _info: ExpectMetaInfo; | ||||
| 
 | ||||
|   constructor(messageOrOptions: ExpectMessageOrOptions, isSoft: boolean, isPoll: boolean, generator?: Generator) { | ||||
|     this._info = { isSoft, isPoll, generator, isNot: false, nameTokens: [] }; | ||||
|     if (typeof messageOrOptions === 'string') { | ||||
|       this._info.message = messageOrOptions; | ||||
|     } else { | ||||
|       this._info.message = messageOrOptions?.message; | ||||
|       this._info.pollTimeout = messageOrOptions?.timeout; | ||||
|       this._info.pollIntervals = messageOrOptions?.intervals; | ||||
|     } | ||||
|   constructor(info: ExpectMetaInfo) { | ||||
|     this._info = { ...info }; | ||||
|   } | ||||
| 
 | ||||
|   get(target: Object, matcherName: string | symbol, receiver: any): any { | ||||
|  | @ -205,7 +229,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> { | |||
|     if (this._info.isPoll) { | ||||
|       if ((customAsyncMatchers as any)[matcherName] || matcherName === 'resolves' || matcherName === 'rejects') | ||||
|         throw new Error(`\`expect.poll()\` does not support "${matcherName}" matcher.`); | ||||
|       matcher = (...args: any[]) => pollMatcher(matcherName, this._info.isNot, this._info.pollIntervals, currentExpectTimeout({ timeout: this._info.pollTimeout }), this._info.generator!, ...args); | ||||
|       matcher = (...args: any[]) => pollMatcher(matcherName, !!this._info.isNot, this._info.pollIntervals, currentExpectTimeout({ timeout: this._info.pollTimeout }), this._info.generator!, ...args); | ||||
|     } | ||||
|     return (...args: any[]) => { | ||||
|       const testInfo = currentTestInfo(); | ||||
|  | @ -278,6 +302,8 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> { | |||
|           try { | ||||
|             const expectZone: ExpectZone = { title: defaultTitle, wallTime }; | ||||
|             await zones.run<ExpectZone, any>('expectZone', expectZone, async () => { | ||||
|               // We assume that the matcher will read the current expect timeout the first thing.
 | ||||
|               setCurrentExpectConfigureTimeout(this._info.timeout); | ||||
|               await matcher.call(target, ...args); | ||||
|             }); | ||||
|             finalizer(); | ||||
|  |  | |||
|  | @ -4654,6 +4654,12 @@ export type Expect = { | |||
|      not: BaseMatchers<Promise<void>, T>; | ||||
|   }; | ||||
|   extend(matchers: any): void; | ||||
|   configure: (configuration: { | ||||
|     message?: string, | ||||
|     timeout?: number, | ||||
|     soft?: boolean, | ||||
|     poll?: boolean | { timeout?: number, intervals?: number[] }, | ||||
|   }) => Expect; | ||||
|   getState(): { | ||||
|     expand?: boolean; | ||||
|     isNot: boolean; | ||||
|  |  | |||
|  | @ -0,0 +1,160 @@ | |||
| /** | ||||
|  * Copyright Microsoft Corporation. All rights reserved. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| import { test, expect } from './playwright-test-fixtures'; | ||||
| 
 | ||||
| test('should configure timeout', async ({ runInlineTest }) => { | ||||
|   const result = await runInlineTest({ | ||||
|     'a.spec.ts': ` | ||||
|       import { test, expect } from '@playwright/test'; | ||||
|       const fastExpect = expect.configure({ timeout: 1 }); | ||||
|       test('pass', async ({ page }) => { | ||||
|         const time = performance.now(); | ||||
|         try { | ||||
|           await fastExpect(page.locator('li')).toBeVisible(); | ||||
|         } catch (e) { | ||||
|           expect(performance.now() - time).toBeLessThan(5000); | ||||
|         } | ||||
|       }); | ||||
|     ` | ||||
|   }); | ||||
|   expect(result.exitCode).toBe(0); | ||||
|   expect(result.passed).toBe(1); | ||||
| }); | ||||
| 
 | ||||
| test('should configure message', async ({ runInlineTest }) => { | ||||
|   const result = await runInlineTest({ | ||||
|     'expect-test.spec.ts': ` | ||||
|       import { test, expect } from '@playwright/test'; | ||||
|       const namedExpect = expect.configure({ message: 'x-foo must be visible' }); | ||||
|       test('custom expect message', async ({page}) => { | ||||
|         await namedExpect(page.locator('x-foo')).toBeVisible({timeout: 1}); | ||||
|       }); | ||||
|     ` | ||||
|   }); | ||||
|   expect(result.exitCode).toBe(1); | ||||
|   expect(result.passed).toBe(0); | ||||
|   expect(result.output).toContain([ | ||||
|     `    Error: x-foo must be visible`, | ||||
|     ``, | ||||
|     `    Call log:`, | ||||
|   ].join('\n')); | ||||
| }); | ||||
| 
 | ||||
| test('should prefer local message', async ({ runInlineTest }) => { | ||||
|   const result = await runInlineTest({ | ||||
|     'expect-test.spec.ts': ` | ||||
|       import { test, expect } from '@playwright/test'; | ||||
|       const namedExpect = expect.configure({ message: 'x-foo must be visible' }); | ||||
|       test('custom expect message', async ({page}) => { | ||||
|         await namedExpect(page.locator('x-foo'), { message: 'overridden' }).toBeVisible({timeout: 1}); | ||||
|       }); | ||||
|     ` | ||||
|   }); | ||||
|   expect(result.exitCode).toBe(1); | ||||
|   expect(result.passed).toBe(0); | ||||
|   expect(result.output).toContain([ | ||||
|     `    Error: overridden`, | ||||
|     ``, | ||||
|     `    Call log:`, | ||||
|   ].join('\n')); | ||||
| }); | ||||
| 
 | ||||
| test('should configure soft', async ({ runInlineTest }) => { | ||||
|   const result = await runInlineTest({ | ||||
|     'a.spec.ts': ` | ||||
|       import { test, expect } from '@playwright/test'; | ||||
|       const softExpect = expect.configure({ soft: true }); | ||||
|       test('should work', () => { | ||||
|         softExpect(1+1).toBe(3); | ||||
|         console.log('woof-woof'); | ||||
|       }); | ||||
|     ` | ||||
|   }); | ||||
|   expect(result.exitCode).toBe(1); | ||||
|   expect(result.output).toContain('woof-woof'); | ||||
| }); | ||||
| 
 | ||||
| test('should configure poll', async ({ runInlineTest }) => { | ||||
|   const result = await runInlineTest({ | ||||
|     'a.spec.ts': ` | ||||
|       import { test, expect } from '@playwright/test'; | ||||
|       const pollingExpect = expect.configure({ poll: { timeout: 1000, intervals: [0, 10000] } }); | ||||
|       test('should fail', async () => { | ||||
|         let probes = 0; | ||||
|         const startTime = Date.now(); | ||||
|         await pollingExpect(() => ++probes).toBe(3).catch(() => {}); | ||||
|         // Probe at 0 and epsilon.
 | ||||
|         expect(probes).toBe(2); | ||||
|         expect(Date.now() - startTime).toBeLessThan(5000); | ||||
|       }); | ||||
|     ` | ||||
|   }); | ||||
|   expect(result.exitCode).toBe(0); | ||||
| }); | ||||
| 
 | ||||
| test('should chain configure', async ({ runInlineTest }) => { | ||||
|   const result = await runInlineTest({ | ||||
|     'expect-test.spec.ts': ` | ||||
|       import { test, expect } from '@playwright/test'; | ||||
|       const slowExpect = expect.configure({ timeout: 1 }); | ||||
|       const slowAndSoftExpect = slowExpect.configure({ soft: true }); | ||||
|       test('custom expect message', async ({page}) => { | ||||
|         await slowAndSoftExpect(page.locator('x-foo')).toBeVisible({timeout: 1}); | ||||
|         console.log('%% woof-woof'); | ||||
|       }); | ||||
|     ` | ||||
|   }); | ||||
|   expect(result.exitCode).toBe(1); | ||||
|   expect(result.passed).toBe(0); | ||||
|   expect(result.outputLines).toEqual(['woof-woof']); | ||||
| }); | ||||
| 
 | ||||
| test('should cancel effect', async ({ runInlineTest }) => { | ||||
|   const result = await runInlineTest({ | ||||
|     'a.spec.ts': ` | ||||
|       import { test, expect } from '@playwright/test'; | ||||
|       const softExpect = expect.configure({ soft: true }); | ||||
|       const normalExpect = expect.configure({ soft: false }); | ||||
|       test('should work', () => { | ||||
|         normalExpect(1+1).toBe(3); | ||||
|         console.log('%% woof-woof'); | ||||
|       }); | ||||
|     ` | ||||
|   }); | ||||
|   expect(result.exitCode).toBe(1); | ||||
|   expect(result.outputLines).toEqual([]); | ||||
| }); | ||||
| 
 | ||||
| test('should configure soft poll', async ({ runInlineTest }) => { | ||||
|   const result = await runInlineTest({ | ||||
|     'a.spec.ts': ` | ||||
|       import { test, expect } from '@playwright/test'; | ||||
|       const pollingExpect = expect.configure({ soft: true, poll: { timeout: 1000, intervals: [0, 10000] } }); | ||||
|       test('should fail', async () => { | ||||
|         let probes = 0; | ||||
|         const startTime = Date.now(); | ||||
|         await pollingExpect(() => ++probes).toBe(3); | ||||
|         // Probe at 0 and epsilon.
 | ||||
|         expect(probes).toBe(2); | ||||
|         expect(Date.now() - startTime).toBeLessThan(5000); | ||||
|         console.log('%% woof-woof'); | ||||
|       }); | ||||
|     ` | ||||
|   }); | ||||
|   expect(result.exitCode).toBe(1); | ||||
|   expect(result.outputLines).toEqual(['woof-woof']); | ||||
| }); | ||||
|  | @ -36,12 +36,12 @@ test('soft expects should work', async ({ runInlineTest }) => { | |||
|       import { test, expect } from '@playwright/test'; | ||||
|       test('should work', () => { | ||||
|         test.expect.soft(1+1).toBe(3); | ||||
|         console.log('woof-woof'); | ||||
|         console.log('%% woof-woof'); | ||||
|       }); | ||||
|     ` | ||||
|   }); | ||||
|   expect(result.exitCode).toBe(1); | ||||
|   expect(result.output).toContain('woof-woof'); | ||||
|   expect(result.outputLines).toEqual(['woof-woof']); | ||||
| }); | ||||
| 
 | ||||
| test('should report a mixture of soft and non-soft errors', async ({ runInlineTest }) => { | ||||
|  |  | |||
|  | @ -342,6 +342,12 @@ export type Expect = { | |||
|      not: BaseMatchers<Promise<void>, T>; | ||||
|   }; | ||||
|   extend(matchers: any): void; | ||||
|   configure: (configuration: { | ||||
|     message?: string, | ||||
|     timeout?: number, | ||||
|     soft?: boolean, | ||||
|     poll?: boolean | { timeout?: number, intervals?: number[] }, | ||||
|   }) => Expect; | ||||
|   getState(): { | ||||
|     expand?: boolean; | ||||
|     isNot: boolean; | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue