E2E reliability is decided not in the browser but in the data. The most frequent source of flakiness is not a capricious locator but a shared user, order or record that several tests read and change at once. While data is shared and mutable, no care in the scenario will save it: tests will affect each other. So the professional approach starts with the question of where data comes from and where it goes after the test, and only then with clicks.
The way to prepare data is chosen by cost and reliability. Creating a user or an order through an internal test API is almost always faster and more stable than clicking their creation through in the UI. A direct write to the database is acceptable for your own, controlled database - but only if it preserves invariants and schema constraints, otherwise the test works with an impossible state. Creation via the UI is justified only when the creation itself is a tested user path.
The key to safe parallelism is data uniqueness. If a test creates a user with a fixed address, two parallel runs will collide on it. Uniqueness is ensured by weaving the worker's identifier and a random suffix into the data: an email like e2e-<workerIndex>-<uuid>@example.test is guaranteed not to overlap with a neighboring worker or a past run. Each test works with its own user, not a shared one.
It helps to see once how such a unique dataset is created for a specific test. Below is a scenario that, through the testInfo available in the test, takes the worker index, adds a random uuid and creates its own user via the API. You return to this technique whenever a test needs fresh data no one else will touch.
A separate discipline is cleanup. Data created by a test must be removable, otherwise the test database swells and starts to affect runs. Three mechanisms work: a namespace by worker or test id, so data does not get confused between them; a TTL that automatically cleans up the old; and explicit deletion after the test. Together they make a parallel run safe - each cleans up after itself without touching neighbors.
The fixture approach joins creation and cleanup into one lifecycle. Data is created before the test body and deleted after it in one place, so the test gets it ready and does not worry about cleanup manually. This is more reliable than create and delete scattered across scenarios: if the test failed in the middle, cleanup still runs, and the next run starts from a clean state, not from the debris of the previous one.
An important principle is the minimality and reproducibility of the seed. A factory should create exactly the data the scenario needs, not the full graph of related entities just in case: the excess slows things down and makes the test opaque. And the seed should be predictable: the same scenario on the same input must give the same result, otherwise reproducing a failure and fixing it will be impossible.
The typical data failures are the main cause of flakiness under parallelism. A shared admin, user or order that several tests change at once - green one by one and red together. Fixed data without a namespace, colliding between workers. And the absence of cleanup, because of which the database accumulates junk and changes run behavior. Give each test unique minimal data and clean it up - and parallelism becomes safe.
test('edits the profile', async ({ page }, testInfo) => {
// Uniqueness: worker index + random uuid - collides with no one
const email = `e2e-${testInfo.workerIndex}-${crypto.randomUUID()}@example.test`
const user = await usersApi.create({ email })
// the test works with its own user
})