From bba4dc67fc573b5b48b74e35a584e4452ceb2655 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 12 Nov 2024 12:09:04 -0800 Subject: [PATCH] feat: step timeout option Fixes https://github.com/microsoft/playwright/issues/33475 --- docs/src/test-api/class-test.md | 6 ++++++ packages/playwright/src/common/testType.ts | 14 +++++++++++--- packages/playwright/types/test.d.ts | 2 +- tests/playwright-test/test-step.spec.ts | 17 +++++++++++++++++ utils/generate_types/overrides-test.d.ts | 2 +- 5 files changed, 36 insertions(+), 5 deletions(-) diff --git a/docs/src/test-api/class-test.md b/docs/src/test-api/class-test.md index 7ea05d4c56..77a11c073f 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 f22fd159d8..119095bfd1 100644 --- a/packages/playwright/src/common/testType.ts +++ b/packages/playwright/src/common/testType.ts @@ -21,7 +21,7 @@ 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, ManualPromise, zones } from 'playwright-core/lib/utils'; const testTypeSymbol = Symbol('testType'); @@ -256,19 +256,25 @@ 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: () => 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 () => { + const timeoutPromise = new ManualPromise(); + const timer = options.timeout + ? setTimeout(() => timeoutPromise.reject(new StepTimeoutError(`Step timeout ${options.timeout}ms exceeded.`)), options.timeout) + : undefined; try { - const result = await body(); + const result = await Promise.race([body(), timeoutPromise]); step.complete({}); return result; } catch (error) { step.complete({ error }); throw error; + } finally { + clearTimeout(timer); } }); } @@ -302,6 +308,8 @@ function validateTestDetails(details: TestDetails) { return { annotations, tags }; } +class StepTimeoutError extends Error {} + export const rootTestType = new TestTypeImpl([]); export function mergeTests(...tests: TestType[]) { diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 305ae67caf..b3d66a7f6d 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 1dfdbe0577..bd0c19693d 100644 --- a/tests/playwright-test/test-step.spec.ts +++ b/tests/playwright-test/test-step.spec.ts @@ -386,6 +386,23 @@ 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.passed).toBe(0); + console.log(result.output); + expect(result.output).toContain('Error: Step timeout 100ms exceeded.'); +}); + 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 49a7093dd3..fc0d90a7db 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;