any
types. Playwright will not judge you on your TypeScript skills and will run your tests.
More importantly, though, TypeScript is the better choice when planning to maintain a test suite in the long term because its benefits outweigh the added complexity. Auto-completion alone will make your test creation more manageable, and even though nobody likes to deal with type errors, when your test suite becomes a software project on its own, TypeScript warnings will help you discover problems in your code early.
A few days ago, I found another TypeScript feature that allows me to structure complex end-to-end tests with test steps while writing less code. Doesn’t this sound exciting?
Let me show you how you can replace repetitive test.step
calls with a single @step
decorator. Let’s go!
The first problem: Playwright reports can be challenging to scan and understand.
By default, all your test actions and assertions will be a long wall of instructions shown in UI mode or your test reports. That’s not a big deal for twenty instructions, but if you’re testing a complex UI flow, your test instruction count will quickly hit a hundred actions. Looking at a report with that many instructions isn’t great.

The second problem: wrapping all methods in a test step is annoying.
Here’s an examplePlaywrightPage
POM to test the search on the official Playwright docs.
search
in this case) isn’t a big deal, but wrapping every public POM method will quickly feel like unnecessary busy work.
The solution: automagically wrap your POM methods with TypeScript decorators.
Playwright doesn’t include magic tricks to avoid this repetition, but we can use TypeScript tooling to make things easier. A TypeScript benefit I haven’t mentioned yet is that TypeScript is a compiler that transforms your.ts
files into JavaScript.
In practice this means, that you can use modern JavaScript features in TypeScript and transform them to JavaScript code supported in older browsers or runtimes.
One of these modern JavaScript features is decorators. JavaScript Decorators aren’t supported anywhere but work if you use TypeScript.
JavaScript Decorators — a proposal that’s been in the making forever.
The JavaScript decorator spec proposal saw the light of day eight years ago and reached ECMAScript proposal stage three. Proposals on stage three are considered “ready to implement”.Decorators are functions called on classes, class elements, or other JavaScript syntax forms during definition.This definition is somewhat cryptic. Let me explain the feature in my own words.
With decorators, you can access, replace, or wrap class methods (like a method in your POM class) with other functions with a very condensed and developer-friendly syntax.Wrapping class methods is what we need to avoid all these
test.step
instructions. Let’s find out how to define a decorator!
How to replace test.step
instructions with TypeScript decorators
Let’s look at our POM class again.
search
method, we want to remove the test.step
from within the function body and somehow wrap the Playwright instructions with a test step. This situation is a perfect use case for decorators.
First, we must apply a new decorator.
@step
line before the class method definition. Now TypeScript will complain…

step
function.
@step
syntax, it will try to call a step
function. This function can be available in the current scope, or you can import it from somewhere in your codebase.
When found, the step
decorator function will be called with a function reference to our original method (target
) and must return another function (replacementMethod
). This returned function will replace the decorated method. Thanks to some low-level JavaScript, you can then call the original method (target
) with the passed-in arguments (args
).
When you now run your tests and call a decorated class method, nothing has changed yet, but we’re ready to apply some compilation magic. Let’s extend the decorator!
target.call
in a test.step
. Then, you only need to find a way to define a step name; ideally, it would relate to your original method.
You might have noticed it already; the decorator function will be called not only with the function reference but also with context
. The replacement methods will also run in the same this context as the original method. With these two things, we can combine the POM class name (this.constructor.name
) and the method name (context.name
) to define a relatable step name.
If we rerun our test, the @step
decorator will do its magic and automatically wrap POM methods in a nice Playwright test step showing us the POM class and method. Beautiful!

test.step
calls with @step
decorators!
How to pass custom step names to a decorator
To pass a custom step name to your decorator, you must change how you decorate the class methods. Instead of “just applying” a decorator with@step
you can also execute your decorators with @step()
.
This change allows you to hand in arguments like a possible step name.
stepName
) to define a new step name in your replacement method. If stepName
isn’t defined, the code above falls back to the class name / method name combination.
You can now name your test steps with a single line!
Unfortunately, this approach has one downside. Once you make your step decorator function executable, you must execute it everywhere. @step
must become @step()
.
()
is a very reasonable trade-off for the ability of setting custom step names, though.