Skip to content
Learn

One number per parallel test

Most flaky SMS tests read an old code or another worker's code. Mark the inbox before you send, and give each parallel worker its own number.

Phonekit team Updated

Mark the inbox before you send

Reading the latest message can return yesterday's code. Take a mark just before your app sends the text, then wait for the first message after it.

Mark, send, waitTypeScript
const number = phonekit.number(testPhoneNumber)
const after = await number.mark()
await page.getByRole("button", { name: "Send code" }).click()
const receipt = await number.waitForMessage({ after, timeoutMs: 120_000 })
if (!receipt.message.otp) throw new Error("Message arrived without a code")
await page.getByLabel("Verification code").fill(receipt.message.otp)
await page.getByRole("button", { name: "Verify" }).click()
await expect(page.getByRole("heading", { name: "Your account" })).toBeVisible()

The Playwright guide adds the imports, key and setup. mark() doesn't send or clear anything. It records where the inbox is now.

Give each parallel worker its own number

A mark only says when a text arrived, not which test asked for it. Two workers sharing a number can read each other's codes:

StepWorker AWorker B
MarkMarks the inboxMarks the same point
SendAsks for A's codeAsks for B's code
ReadMay get B's codeMay get B's code

Give each worker that runs at the same time its own number, or run those tests one after another. Count overlapping branches and reruns when you size the pool.

The account needs the same care. A signup test can't register the same identity forever, so reset it, clean it up, or test sign-in with an existing account.

Budget the whole test

Set the wait's timeoutMs inside the runner's overall timeout, with room for page setup and the final check. The deadline covers polling and retries, so the test fails with a clear receiving error instead of hanging.

A resend is a separate action in your app. Only add one if the test covers resend, since it can create competing codes and use up your sender's rate limit.

Report the stage that failed

What you seeCheck first
Key or number unavailableKey scope, CI secret and assigned number
Your app rejects the sendApp response and sender settings
Nothing arrives before the deadlineSender delivery status, number, environment
Text arrives with no code foundMessage template and extracted code
Code entered but rejectedAccount state, expiry, competing requests
Verified but the assertion failsPage readiness and expected final state

Log the stage, timing, request IDs and outcome. Leave message bodies and codes out of logs. Anyone with access to the number can open the inbox when they need the text itself.

Questions

Should we retry the whole test?

Find the failing stage first. A retry sends a second code while the first may still be arriving. If you keep retries, reset the account and take a new mark for each attempt.

How do we skip a text that isn't ours?

Inspect the receipt from waitForMessage. If it belongs to something else, wait again from receipt.cursor. A wait can't tell which request sent a text, so separate numbers are the simplest way to keep parallel work apart.

How long should the deadline be?

Base it on how long your texts usually take, then leave room for page setup and the final check. There is no universal SMS delivery time. Report a timeout as a receiving failure, not a rejected code.

Get a number.

Tell us what you're testing. We'll set up your workspace.

Get a number