mirror of
				https://github.com/microsoft/playwright.git
				synced 2025-06-26 21:40:17 +00:00 
			
		
		
		
	fix(expect): respect custom message in expect.poll (#32603)
Fixes #32582.
This commit is contained in:
		
							parent
							
								
									c24ad36f86
								
							
						
					
					
						commit
						268357238a
					
				@ -121,10 +121,10 @@ function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Re
 | 
				
			|||||||
      const [actual, messageOrOptions] = argumentsList;
 | 
					      const [actual, messageOrOptions] = argumentsList;
 | 
				
			||||||
      const message = isString(messageOrOptions) ? messageOrOptions : messageOrOptions?.message || info.message;
 | 
					      const message = isString(messageOrOptions) ? messageOrOptions : messageOrOptions?.message || info.message;
 | 
				
			||||||
      const newInfo = { ...info, message };
 | 
					      const newInfo = { ...info, message };
 | 
				
			||||||
      if (newInfo.isPoll) {
 | 
					      if (newInfo.poll) {
 | 
				
			||||||
        if (typeof actual !== 'function')
 | 
					        if (typeof actual !== 'function')
 | 
				
			||||||
          throw new Error('`expect.poll()` accepts only function as a first argument');
 | 
					          throw new Error('`expect.poll()` accepts only function as a first argument');
 | 
				
			||||||
        newInfo.generator = actual as any;
 | 
					        newInfo.poll.generator = actual as any;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return createMatchers(actual, newInfo, prefix);
 | 
					      return createMatchers(actual, newInfo, prefix);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -189,10 +189,10 @@ function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Re
 | 
				
			|||||||
    if ('soft' in configuration)
 | 
					    if ('soft' in configuration)
 | 
				
			||||||
      newInfo.isSoft = configuration.soft;
 | 
					      newInfo.isSoft = configuration.soft;
 | 
				
			||||||
    if ('_poll' in configuration) {
 | 
					    if ('_poll' in configuration) {
 | 
				
			||||||
      newInfo.isPoll = !!configuration._poll;
 | 
					      newInfo.poll = configuration._poll ? { ...info.poll, generator: () => {} } : undefined;
 | 
				
			||||||
      if (typeof configuration._poll === 'object') {
 | 
					      if (typeof configuration._poll === 'object') {
 | 
				
			||||||
        newInfo.pollTimeout = configuration._poll.timeout;
 | 
					        newInfo.poll!.timeout = configuration._poll.timeout ?? newInfo.poll!.timeout;
 | 
				
			||||||
        newInfo.pollIntervals = configuration._poll.intervals;
 | 
					        newInfo.poll!.intervals = configuration._poll.intervals ?? newInfo.poll!.intervals;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return createExpect(newInfo, prefix, customMatchers);
 | 
					    return createExpect(newInfo, prefix, customMatchers);
 | 
				
			||||||
@ -249,11 +249,12 @@ type ExpectMetaInfo = {
 | 
				
			|||||||
  message?: string;
 | 
					  message?: string;
 | 
				
			||||||
  isNot?: boolean;
 | 
					  isNot?: boolean;
 | 
				
			||||||
  isSoft?: boolean;
 | 
					  isSoft?: boolean;
 | 
				
			||||||
  isPoll?: boolean;
 | 
					  poll?: {
 | 
				
			||||||
 | 
					    timeout?: number;
 | 
				
			||||||
 | 
					    intervals?: number[];
 | 
				
			||||||
 | 
					    generator: Generator;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
  timeout?: number;
 | 
					  timeout?: number;
 | 
				
			||||||
  pollTimeout?: number;
 | 
					 | 
				
			||||||
  pollIntervals?: number[];
 | 
					 | 
				
			||||||
  generator?: Generator;
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
 | 
					class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
 | 
				
			||||||
@ -287,10 +288,10 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
 | 
				
			|||||||
        this._info.isNot = !this._info.isNot;
 | 
					        this._info.isNot = !this._info.isNot;
 | 
				
			||||||
      return new Proxy(matcher, this);
 | 
					      return new Proxy(matcher, this);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (this._info.isPoll) {
 | 
					    if (this._info.poll) {
 | 
				
			||||||
      if ((customAsyncMatchers as any)[matcherName] || matcherName === 'resolves' || matcherName === 'rejects')
 | 
					      if ((customAsyncMatchers as any)[matcherName] || matcherName === 'resolves' || matcherName === 'rejects')
 | 
				
			||||||
        throw new Error(`\`expect.poll()\` does not support "${matcherName}" matcher.`);
 | 
					        throw new Error(`\`expect.poll()\` does not support "${matcherName}" matcher.`);
 | 
				
			||||||
      matcher = (...args: any[]) => pollMatcher(resolvedMatcherName, !!this._info.isNot, this._info.pollIntervals, this._info.pollTimeout ?? currentExpectTimeout(), this._info.generator!, ...args);
 | 
					      matcher = (...args: any[]) => pollMatcher(resolvedMatcherName, this._info, this._prefix, ...args);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return (...args: any[]) => {
 | 
					    return (...args: any[]) => {
 | 
				
			||||||
      const testInfo = currentTestInfo();
 | 
					      const testInfo = currentTestInfo();
 | 
				
			||||||
@ -302,7 +303,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
 | 
				
			|||||||
      const customMessage = this._info.message || '';
 | 
					      const customMessage = this._info.message || '';
 | 
				
			||||||
      const argsSuffix = computeArgsSuffix(matcherName, args);
 | 
					      const argsSuffix = computeArgsSuffix(matcherName, args);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const defaultTitle = `expect${this._info.isPoll ? '.poll' : ''}${this._info.isSoft ? '.soft' : ''}${this._info.isNot ? '.not' : ''}.${matcherName}${argsSuffix}`;
 | 
					      const defaultTitle = `expect${this._info.poll ? '.poll' : ''}${this._info.isSoft ? '.soft' : ''}${this._info.isNot ? '.not' : ''}.${matcherName}${argsSuffix}`;
 | 
				
			||||||
      const title = customMessage || defaultTitle;
 | 
					      const title = customMessage || defaultTitle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // This looks like it is unnecessary, but it isn't - we need to filter
 | 
					      // This looks like it is unnecessary, but it isn't - we need to filter
 | 
				
			||||||
@ -336,7 +337,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
 | 
				
			|||||||
        const callback = () => matcher.call(target, ...args);
 | 
					        const callback = () => matcher.call(target, ...args);
 | 
				
			||||||
        // toPass and poll matchers can contain other steps, expects and API calls,
 | 
					        // toPass and poll matchers can contain other steps, expects and API calls,
 | 
				
			||||||
        // so they behave like a retriable step.
 | 
					        // so they behave like a retriable step.
 | 
				
			||||||
        const result = (matcherName === 'toPass' || this._info.isPoll) ?
 | 
					        const result = (matcherName === 'toPass' || this._info.poll) ?
 | 
				
			||||||
          zones.run('stepZone', step, callback) :
 | 
					          zones.run('stepZone', step, callback) :
 | 
				
			||||||
          zones.run<ExpectZone, any>('expectZone', { title, stepId: step.stepId }, callback);
 | 
					          zones.run<ExpectZone, any>('expectZone', { title, stepId: step.stepId }, callback);
 | 
				
			||||||
        if (result instanceof Promise)
 | 
					        if (result instanceof Promise)
 | 
				
			||||||
@ -350,25 +351,32 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function pollMatcher(qualifiedMatcherName: any, isNot: boolean, pollIntervals: number[] | undefined, timeout: number, generator: () => any, ...args: any[]) {
 | 
					async function pollMatcher(qualifiedMatcherName: string, info: ExpectMetaInfo, prefix: string[], ...args: any[]) {
 | 
				
			||||||
  const testInfo = currentTestInfo();
 | 
					  const testInfo = currentTestInfo();
 | 
				
			||||||
 | 
					  const poll = info.poll!;
 | 
				
			||||||
 | 
					  const timeout = poll.timeout ?? currentExpectTimeout();
 | 
				
			||||||
  const { deadline, timeoutMessage } = testInfo ? testInfo._deadlineForMatcher(timeout) : TestInfoImpl._defaultDeadlineForMatcher(timeout);
 | 
					  const { deadline, timeoutMessage } = testInfo ? testInfo._deadlineForMatcher(timeout) : TestInfoImpl._defaultDeadlineForMatcher(timeout);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const result = await pollAgainstDeadline<Error|undefined>(async () => {
 | 
					  const result = await pollAgainstDeadline<Error|undefined>(async () => {
 | 
				
			||||||
    if (testInfo && currentTestInfo() !== testInfo)
 | 
					    if (testInfo && currentTestInfo() !== testInfo)
 | 
				
			||||||
      return { continuePolling: false, result: undefined };
 | 
					      return { continuePolling: false, result: undefined };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const value = await generator();
 | 
					    const innerInfo: ExpectMetaInfo = {
 | 
				
			||||||
    let expectInstance = expectLibrary(value) as any;
 | 
					      ...info,
 | 
				
			||||||
    if (isNot)
 | 
					      isSoft: false, // soft is outside of poll, not inside
 | 
				
			||||||
      expectInstance = expectInstance.not;
 | 
					      poll: undefined,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    const value = await poll.generator();
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      expectInstance[qualifiedMatcherName].call(expectInstance, ...args);
 | 
					      let matchers = createMatchers(value, innerInfo, prefix);
 | 
				
			||||||
 | 
					      if (info.isNot)
 | 
				
			||||||
 | 
					        matchers = matchers.not;
 | 
				
			||||||
 | 
					      matchers[qualifiedMatcherName](...args);
 | 
				
			||||||
      return { continuePolling: false, result: undefined };
 | 
					      return { continuePolling: false, result: undefined };
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      return { continuePolling: true, result: error };
 | 
					      return { continuePolling: true, result: error };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }, deadline, pollIntervals ?? [100, 250, 500, 1000]);
 | 
					  }, deadline, poll.intervals ?? [100, 250, 500, 1000]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (result.timedOut) {
 | 
					  if (result.timedOut) {
 | 
				
			||||||
    const message = result.result ? [
 | 
					    const message = result.result ? [
 | 
				
			||||||
 | 
				
			|||||||
@ -263,3 +263,19 @@ test('should propagate string exception from async arrow function', { annotation
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  expect(result.output).toContain('some error');
 | 
					  expect(result.output).toContain('some error');
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('should show custom message', {
 | 
				
			||||||
 | 
					  annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32582' }
 | 
				
			||||||
 | 
					}, async ({ runInlineTest }) => {
 | 
				
			||||||
 | 
					  const result = await runInlineTest({
 | 
				
			||||||
 | 
					    'a.spec.ts': `
 | 
				
			||||||
 | 
					      import { test, expect } from '@playwright/test';
 | 
				
			||||||
 | 
					      test('should fail', async () => {
 | 
				
			||||||
 | 
					        await expect.poll(() => 1, { message: 'custom message', timeout: 500 }).toBe(2);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    `,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  expect(result.output).toContain('Error: custom message');
 | 
				
			||||||
 | 
					  expect(result.output).toContain('Expected: 2');
 | 
				
			||||||
 | 
					  expect(result.output).toContain('Received: 1');
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -987,9 +987,12 @@ expect    |expect.poll.toHaveLength @ a.test.ts:14
 | 
				
			|||||||
pw:api    |  page.goto(about:blank) @ a.test.ts:7
 | 
					pw:api    |  page.goto(about:blank) @ a.test.ts:7
 | 
				
			||||||
test.step |  inner step attempt: 0 @ a.test.ts:8
 | 
					test.step |  inner step attempt: 0 @ a.test.ts:8
 | 
				
			||||||
expect    |    expect.toBe @ a.test.ts:10
 | 
					expect    |    expect.toBe @ a.test.ts:10
 | 
				
			||||||
 | 
					expect    |  expect.toHaveLength @ a.test.ts:6
 | 
				
			||||||
 | 
					expect    |  ↪ error: Error: expect(received).toHaveLength(expected)
 | 
				
			||||||
pw:api    |  page.goto(about:blank) @ a.test.ts:7
 | 
					pw:api    |  page.goto(about:blank) @ a.test.ts:7
 | 
				
			||||||
test.step |  inner step attempt: 1 @ a.test.ts:8
 | 
					test.step |  inner step attempt: 1 @ a.test.ts:8
 | 
				
			||||||
expect    |    expect.toBe @ a.test.ts:10
 | 
					expect    |    expect.toBe @ a.test.ts:10
 | 
				
			||||||
 | 
					expect    |  expect.toHaveLength @ a.test.ts:6
 | 
				
			||||||
hook      |After Hooks
 | 
					hook      |After Hooks
 | 
				
			||||||
fixture   |  fixture: page
 | 
					fixture   |  fixture: page
 | 
				
			||||||
fixture   |  fixture: context
 | 
					fixture   |  fixture: context
 | 
				
			||||||
@ -1036,9 +1039,12 @@ expect    |expect.poll.toBe @ a.test.ts:13
 | 
				
			|||||||
expect    |  expect.toHaveText @ a.test.ts:7
 | 
					expect    |  expect.toHaveText @ a.test.ts:7
 | 
				
			||||||
test.step |  iteration 1 @ a.test.ts:9
 | 
					test.step |  iteration 1 @ a.test.ts:9
 | 
				
			||||||
expect    |    expect.toBeVisible @ a.test.ts:10
 | 
					expect    |    expect.toBeVisible @ a.test.ts:10
 | 
				
			||||||
 | 
					expect    |  expect.toBe @ a.test.ts:6
 | 
				
			||||||
 | 
					expect    |  ↪ error: Error: expect(received).toBe(expected) // Object.is equality
 | 
				
			||||||
expect    |  expect.toHaveText @ a.test.ts:7
 | 
					expect    |  expect.toHaveText @ a.test.ts:7
 | 
				
			||||||
test.step |  iteration 2 @ a.test.ts:9
 | 
					test.step |  iteration 2 @ a.test.ts:9
 | 
				
			||||||
expect    |    expect.toBeVisible @ a.test.ts:10
 | 
					expect    |    expect.toBeVisible @ a.test.ts:10
 | 
				
			||||||
 | 
					expect    |  expect.toBe @ a.test.ts:6
 | 
				
			||||||
hook      |After Hooks
 | 
					hook      |After Hooks
 | 
				
			||||||
fixture   |  fixture: page
 | 
					fixture   |  fixture: page
 | 
				
			||||||
fixture   |  fixture: context
 | 
					fixture   |  fixture: context
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user