microsoft/playwright · error · Error
test.step() can only be called from a test
Error message
test.step() can only be called from a test
What it means
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.
Source
Thrown at packages/playwright/src/common/testType.ts:280
}
const testInfo = currentTestInfo();
if (!testInfo)
throw new Error(`test.setTimeout() can only be called from a test`);
testInfo.setTimeout(timeout);
}
private _use(location: Location, fixtures: Fixtures) {
const suite = this._currentSuite(location, `test.use()`);
if (!suite)
return;
suite._use.push({ fixtures, location });
}
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> {
const testInfo = currentTestInfo();
if (!testInfo)
throw new Error(`test.step() can only be called from a test`);
await testInfo._onUserStepBegin?.(title);
const step = testInfo._addStep({ category: 'test.step', title, subtitle: options.subtitle, location: options.location, box: options.box, params: options.params });
return await currentZone().with('stepZone', step).run(async () => {
try {
let result: Awaited<ReturnType<typeof raceAgainstDeadline<T>>> | undefined = undefined;
result = await raceAgainstDeadline(async () => {
try {
return await step.info._runStepBody(expectation === 'skip', body, step.location);
} catch (e) {
// If the step timed out, the test fixtures will tear down, which in turn
// will abort unfinished actions in the step body. Record such errors here.
if (result?.timedOut)
testInfo._failWithError(e);
throw e;
}
}, options.timeout ? monotonicTime() + options.timeout : 0);
if (result.timedOut)
throw new TimeoutError(`Step timeout of ${options.timeout}ms exceeded.`);View on GitHub (pinned to 312030cdce)
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
Example fix
// before
test.beforeAll(async () => {
await test.step('setup database', async () => { ... }); // throws
});
// after
test.beforeEach(async () => {
await test.step('setup database', async () => { ... });
}); Defensive patterns
Strategy: type-guard
Validate before calling
import { test } from '@playwright/test';
// Only inside test-scoped hooks; before calling from helpers:
const insideTest = !!((await import('@playwright/test'))._baseTest as any)?.currentTestInfo?.(); Type guard
import { test } from '@playwright/test';
async function safeStep(title: string, body: () => Promise<void>) {
try {
await test.step(title, body);
} catch (e) {
if (!(e as Error).message.includes('can only be called from a test')) throw e;
console.log(`(outside test) ${title}`);
await body();
}
} Try / catch
try { await test.step('x', fn); }
catch (e) { if ((e as Error).message.includes('can only be called from a test')) { /* fall back to direct call */ await fn(); } else throw e; } Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- The object has been collected to prevent unbounded heap grow
- Cannot find object to "${method}": ${guid}
- Unknown new child: ${params.guid}
- Cannot find parent object ${parentGuid} to create ${guid}
- Cannot set input files to detached element
AI-assisted analysis of microsoft/playwright@312030cdce (2026-09-07).
Data as JSON: /api/errors/48e831750b6440cc.
Report an issue: GitHub.