From 5203c780aebf306243316b2bf5a58a3356505cf1 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 13 Nov 2024 11:17:54 -0800 Subject: [PATCH] feat: step timeout option (#33560) --- docs/src/test-api/class-test.md | 6 +++ packages/playwright/src/common/testType.ts | 11 +++-- packages/playwright/types/test.d.ts | 2 +- tests/playwright-test/test-step.spec.ts | 52 ++++++++++++++++++++++ utils/generate_types/overrides-test.d.ts | 2 +- 5 files changed, 67 insertions(+), 6 deletions(-) diff --git a/docs/src/test-api/class-test.md b/docs/src/test-api/class-test.md index 7ea05d4c5655d..77a11c073f3f2 100644 --- a/docs/src/test-api/class-test.md +++ b/docs/src/test-api/class-test.md @@ -1767,6 +1767,12 @@ Whether to box the step in the report. Defaults to `false`. When the step is box Specifies a custom location for the step to be shown in test reports and trace viewer. By default, location of the [`method: Test.step`] call is shown. +### option: Test.step.timeout +* since: v1.50 +- `timeout` <[float]> + +Maximum time in milliseconds for the step to finish. Defaults to `0` (no timeout). + ## method: Test.use * since: v1.10 diff --git a/packages/playwright/src/common/testType.ts b/packages/playwright/src/common/testType.ts index f22fd159d86da..d3c2f1c23a95d 100644 --- a/packages/playwright/src/common/testType.ts +++ b/packages/playwright/src/common/testType.ts @@ -21,7 +21,8 @@ import { wrapFunctionWithLocation } from '../transform/transform'; import type { FixturesWithLocation } from './config'; import type { Fixtures, TestType, TestDetails } from '../../types/test'; import type { Location } from '../../types/testReporter'; -import { getPackageManagerExecCommand, zones } from 'playwright-core/lib/utils'; +import { getPackageManagerExecCommand, monotonicTime, raceAgainstDeadline, zones } from 'playwright-core/lib/utils'; +import { errors } from 'playwright-core'; const testTypeSymbol = Symbol('testType'); @@ -256,16 +257,18 @@ export class TestTypeImpl { suite._use.push({ fixtures, location }); } - async _step(title: string, body: () => Promise, options: {box?: boolean, location?: Location } = {}): Promise { + async _step(title: string, body: () => T | Promise, options: {box?: boolean, location?: Location, timeout?: number } = {}): Promise { const testInfo = currentTestInfo(); if (!testInfo) throw new Error(`test.step() can only be called from a test`); const step = testInfo._addStep({ category: 'test.step', title, location: options.location, box: options.box }); return await zones.run('stepZone', step, async () => { try { - const result = await body(); + const result = await raceAgainstDeadline(async () => body(), options.timeout ? monotonicTime() + options.timeout : 0); + if (result.timedOut) + throw new errors.TimeoutError(`Step timeout ${options.timeout}ms exceeded.`); step.complete({}); - return result; + return result.result; } catch (error) { step.complete({ error }); throw error; diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 305ae67caff4c..b3d66a7f6d716 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -5536,7 +5536,7 @@ export interface TestType(title: string, body: () => T | Promise, options?: { box?: boolean, location?: Location }): Promise; + step(title: string, body: () => T | Promise, options?: { box?: boolean, location?: Location, timeout?: number }): Promise; /** * `expect` function can be used to create test assertions. Read more about [test assertions](https://playwright.dev/docs/test-assertions). * diff --git a/tests/playwright-test/test-step.spec.ts b/tests/playwright-test/test-step.spec.ts index 1dfdbe0577bba..f0e3099540af9 100644 --- a/tests/playwright-test/test-step.spec.ts +++ b/tests/playwright-test/test-step.spec.ts @@ -386,6 +386,58 @@ test('should not pass arguments and return value from step', async ({ runInlineT expect(result.output).toContain('v2 = 20'); }); +test('step timeout option', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('step with timeout', async () => { + await test.step('my step', async () => { + await new Promise(() => {}); + }, { timeout: 100 }); + }); + ` + }, { reporter: '', workers: 1 }); + expect(result.exitCode).toBe(1); + expect(result.failed).toBe(1); + expect(result.output).toContain('Error: Step timeout 100ms exceeded.'); +}); + +test('step timeout longer than test timeout', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + import { defineConfig } from '@playwright/test'; + export default defineConfig({ timeout: 900 }); + `, + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('step with timeout', async () => { + await test.step('my step', async () => { + await new Promise(() => {}); + }, { timeout: 5000 }); + }); + ` + }, { reporter: '', workers: 1 }); + expect(result.exitCode).toBe(1); + expect(result.failed).toBe(1); + expect(result.output).toContain('Test timeout of 900ms exceeded.'); +}); + +test('step timeout is errors.TimeoutError', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + import { test, expect, errors } from '@playwright/test'; + test('step timeout error type', async () => { + const e = await test.step('my step', async () => { + await new Promise(() => {}); + }, { timeout: 100 }).catch(e => e); + expect(e).toBeInstanceOf(errors.TimeoutError); + }); + ` + }, { reporter: '', workers: 1 }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); +}); + test('should mark step as failed when soft expect fails', async ({ runInlineTest }) => { const result = await runInlineTest({ 'reporter.ts': stepIndentReporter, diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 49a7093dd35bb..fc0d90a7db563 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -162,7 +162,7 @@ export interface TestType Promise | any): void; afterAll(title: string, inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | any): void; use(fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs>): void; - step(title: string, body: () => T | Promise, options?: { box?: boolean, location?: Location }): Promise; + step(title: string, body: () => T | Promise, options?: { box?: boolean, location?: Location, timeout?: number }): Promise; expect: Expect<{}>; extend(fixtures: Fixtures): TestType; info(): TestInfo;