{"record":{"id":"48e831750b6440cc","repo":"microsoft/playwright","slug":"test-step-can-only-be-called-from-a-test","errorCode":null,"errorMessage":"test.step() can only be called from a test","messagePattern":"test\\.step\\(\\) can only be called from a test","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/playwright/src/common/testType.ts","lineNumber":280,"sourceCode":"    }\n\n    const testInfo = currentTestInfo();\n    if (!testInfo)\n      throw new Error(`test.setTimeout() can only be called from a test`);\n    testInfo.setTimeout(timeout);\n  }\n\n  private _use(location: Location, fixtures: Fixtures) {\n    const suite = this._currentSuite(location, `test.use()`);\n    if (!suite)\n      return;\n    suite._use.push({ fixtures, location });\n  }\n\n  async _step<T>(expectation: 'pass'|'skip', title: string, body: (step: TestStepInfo) => T | Promise<T>, options: {box?: boolean, location?: Location, timeout?: number, params?: Record<string, any>, subtitle?: string } = {}): Promise<T> {\n    const testInfo = currentTestInfo();\n    if (!testInfo)\n      throw new Error(`test.step() can only be called from a test`);\n    await testInfo._onUserStepBegin?.(title);\n    const step = testInfo._addStep({ category: 'test.step', title, subtitle: options.subtitle, location: options.location, box: options.box, params: options.params });\n    return await currentZone().with('stepZone', step).run(async () => {\n      try {\n        let result: Awaited<ReturnType<typeof raceAgainstDeadline<T>>> | undefined = undefined;\n        result = await raceAgainstDeadline(async () => {\n          try {\n            return await step.info._runStepBody(expectation === 'skip', body, step.location);\n          } catch (e) {\n            // If the step timed out, the test fixtures will tear down, which in turn\n            // will abort unfinished actions in the step body. Record such errors here.\n            if (result?.timedOut)\n              testInfo._failWithError(e);\n            throw e;\n          }\n        }, options.timeout ? monotonicTime() + options.timeout : 0);\n        if (result.timedOut)\n          throw new TimeoutError(`Step timeout of ${options.timeout}ms exceeded.`);","sourceCodeStart":262,"sourceCodeEnd":298,"githubUrl":"https://github.com/microsoft/playwright/blob/312030cdcee20c006343e5f8420fe451b9aa6390/packages/playwright/src/common/testType.ts#L262-L298","documentation":"test.step() (and step.skip()) internally call _step, which requires an active test: it reads currentTestInfo() from the runner's async context. When no test is currently executing, the returned testInfo is undefined and the error is thrown.","triggerScenarios":"Calling test.step() outside a running test: at module/collector top level, inside beforeAll/afterAll (worker-scoped hooks have no current test), in a spawned worker/child process, or in a dynamically imported module evaluated outside the test zone.","commonSituations":"Moving helper code that uses test.step into beforeAll; calling test.step from a utility invoked during test collection or from a standalone script that imports the test object; using test.step inside a task/promise that escapes the test's async context after the test finished.","solutions":["Move the test.step() call inside test() or beforeEach/afterEach (test-scoped hooks)","For worker-scoped setup (beforeAll), use plain logging or annotate the test instead, or restructure setup into a beforeEach","If called from a shared helper, pass and check currentTestInfo or accept a fallback path when not inside a test"],"exampleFix":"// before\ntest.beforeAll(async () => {\n  await test.step('setup database', async () => { ... }); // throws\n});\n\n// after\ntest.beforeEach(async () => {\n  await test.step('setup database', async () => { ... });\n});","handlingStrategy":"type-guard","validationCode":"import { test } from '@playwright/test';\n// Only inside test-scoped hooks; before calling from helpers:\nconst insideTest = !!((await import('@playwright/test'))._baseTest as any)?.currentTestInfo?.();","typeGuard":"import { test } from '@playwright/test';\nasync function safeStep(title: string, body: () => Promise<void>) {\n  try {\n    await test.step(title, body);\n  } catch (e) {\n    if (!(e as Error).message.includes('can only be called from a test')) throw e;\n    console.log(`(outside test) ${title}`);\n    await body();\n  }\n}","tryCatchPattern":"try { await test.step('x', fn); }\ncatch (e) { if ((e as Error).message.includes('can only be called from a test')) { /* fall back to direct call */ await fn(); } else throw e; }","preventionTips":["Keep test.step() inside test(), beforeEach, or afterEach only","Never call test.step in beforeAll/afterAll or at collection time","In shared helpers, prefer the try/catch fallback or pass an explicit flag instead of assuming a test context"],"tags":["playwright-test","test-step","lifecycle","async-context"],"backgroundTag":"api-called-outside-expected-context","analyzedSha":"312030cdcee20c006343e5f8420fe451b9aa6390","analyzedAt":"2026-09-07T14:42:43.271Z","contentChangedAt":"2026-09-07T14:42:43.271Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}