Skip to content

Testing recipe

Test real SMS verification with Playwright

Keep your existing browser flow and add a company test number. Playwright requests the SMS in your application; the Phonekit SDK receives its code in the test runner; the browser submits it and checks that sign-in succeeded.

Prepare one repeatable sign-in identity

Install the server-side JS/TS SDK in your test project. Install it with npm install @phonekit/sdk. Keep its key in the test runner environment.

Set PHONEKIT_API_KEY to a read-only key with explicit access to your chosen number, TEST_PHONE_NUMBER to that number in E.164 format, and TEST_BASE_URL to your application's test deployment. Use an existing test account whose phone number matches the fixture. Reset its application state before running this sign-in test.

The example expects a /signin page with accessible labels for Phone number and Verification code, buttons named Send code and Verify, and a /account page headed Your account. Change those selectors and the success assertion to match your application. They describe the application under test, not Phonekit's own sign-in.

Terminal — add Playwright to the test projectShell
npm install --save-dev @playwright/test
npx playwright install chromium

Set the test and artifact policy

Use one worker for a number shared by this suite. Keep the test deadline longer than the SMS wait so the browser still has time to submit and assert success. This configuration disables artifacts that could capture the entered code; choose an appropriate redaction/access policy if your suite needs them.

playwright.config.tsTypeScript
import { defineConfig } from "@playwright/test"
 
export default defineConfig({
  testDir: "./tests",
  timeout: 180_000,
  retries: 0,
  workers: 1,
  use: {
    baseURL: process.env["TEST_BASE_URL"],
    trace: "off",
    screenshot: "off",
    video: "off",
  },
})

Receive a fresh code and finish the browser flow

Capture the cursor immediately before clicking Send code. The cursor belongs to this attempt and is passed explicitly to the wait. The test finishes with a URL and account-heading assertion, so receiving an SMS alone cannot make it pass.

tests/phone-verification.spec.tsTypeScript
import { test, expect } from "@playwright/test"
import { Phonekit } from "@phonekit/sdk"
 
function required(name: string): string {
  const value = process.env[name]
  if (!value) throw new Error(`Set ${name}`)
  return value
}
 
test("signs in with the received SMS code", async ({ page }) => {
  const phoneNumber = required("TEST_PHONE_NUMBER")
  const phonekit = new Phonekit({
    apiKey: required("PHONEKIT_API_KEY"),
    ...(process.env["PHONEKIT_BASE_URL"]
      ? { baseUrl: process.env["PHONEKIT_BASE_URL"] }
      : {}),
  })
  const number = phonekit.number(phoneNumber)
 
  await page.goto("/signin")
  await page.getByLabel("Phone number", { exact: true }).fill(phoneNumber)
  const after = await number.mark()
  await page.getByRole("button", { name: "Send code", exact: true }).click()
  const code = await number.waitForOtp({ after, timeoutMs: 120_000 })
  await page.getByLabel("Verification code", { exact: true }).fill(code)
  await page.getByRole("button", { name: "Verify", exact: true }).click()
 
  await expect(page).toHaveURL(/\/account(?:[/?#]|$)/)
  await expect(
    page.getByRole("heading", { name: "Your account", exact: true })
  ).toBeVisible()
})
Terminal — run the acceptance testShell
npx playwright test tests/phone-verification.spec.ts --workers=1

Diagnose the stage that failed

Failed stageCheck next
Opening or submitting the formApplication deployment, selector, and reset account
mark() reports unavailable accessSelected number and the key's scope
waitForOtp() times outDid the application's sender accept the request for this number?
waitForOtp() reports missing_otpA fresh SMS arrived; inspect its format through the shared inbox or an intentional waitForMessage() handler
Verify or final assertion failsSender's code validity, resend behavior, session creation, and the success selector

Scroll horizontally to see all columns.

Record the failed stage and elapsed time. Keep message bodies, codes and API keys out of the report. A trace may expose a code after it is filled even if the SDK never logs it.

For the browser runner foundation, see Playwright's CI guide. For fixture isolation and controlled resends, use reliable SMS tests.

Common questions

Can parallel Playwright workers share a number?

Give each concurrent verification workflow a different number, or serialize workflows sharing one. A cursor separates earlier messages from later ones; it does not identify the test that requested an SMS.

Should I retry this test automatically?

Start with retries disabled for the real-delivery path. A retry can send another code before the original arrives or reuse application state. Diagnose the failed stage, then make any retry reset the account state, capture a new cursor, and respect the sender’s resend interval.

Can I use my current SMS provider?

Yes. Your application continues sending through its configured provider. Phonekit receives the message addressed to the company test number. Verify that provider and application accept the chosen number before relying on the automated path.

What if I need to inspect the sender or a verification link?

Use waitForMessage() to receive the message and its next cursor, then handle the sender, detected links or other expected fields explicitly. waitForOtp() reads the first fresh message’s detected code and does not skip unrelated messages.

Does the final assertion prove more than code receipt?

Yes. The URL and account-heading checks exercise the application’s response to submitting the code. For stronger coverage, assert a protected account action or the relevant authenticated state too.