Skip to content

Testing recipe

Test SMS verification with Cypress

Use Cypress to drive the application and a task in its server process to receive the SMS. The Phonekit key stays in setupNodeEvents; the browser receives only the code needed for the verification form.

Keep receipt in the Cypress server process

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.

Add Cypress to your test project. Set PHONEKIT_API_KEY, TEST_PHONE_NUMBER, and TEST_BASE_URL in the runner environment. Add a matching test account and reset its application state. The example uses data-cy attributes on the sign-in fields/buttons and the account heading; replace them with your application's selectors.

Terminal — install CypressShell
npm install --save-dev cypress

Register two small tasks. sms:mark returns the current cursor. sms:wait receives that cursor and returns the fresh code. Keep the key out of config.env: only the number is added to the test configuration.

cypress.config.tsTypeScript
import { defineConfig } from "cypress"
import { Phonekit, type PhonekitCursor } from "@phonekit/sdk"
 
export default defineConfig({
  video: false,
  screenshotOnRunFailure: false,
  e2e: {
    ...(process.env["TEST_BASE_URL"]
      ? { baseUrl: process.env["TEST_BASE_URL"] }
      : {}),
    setupNodeEvents(on, config) {
      const apiKey = process.env["PHONEKIT_API_KEY"]
      const phoneNumber = process.env["TEST_PHONE_NUMBER"]
      if (!apiKey || !phoneNumber) {
        throw new Error("Set PHONEKIT_API_KEY and TEST_PHONE_NUMBER")
      }
      const number = new Phonekit({ apiKey }).number(phoneNumber)
      // Only the test number enters browser configuration.
      config.env["testPhoneNumber"] = phoneNumber
      on("task", {
        "sms:mark": () => number.mark(),
        "sms:wait": ({ after }: { after: PhonekitCursor }) =>
          number.waitForOtp({ after, timeoutMs: 120_000 }),
      })
      return config
    },
  },
})

Run the browser and receipt steps in one chain

cypress/e2e/phone-verification.cy.tsTypeScript
import type { PhonekitCursor } from "@phonekit/sdk"
 
describe("real SMS sign-in", () => {
  it("opens the account after verification", () => {
    cy.visit("/signin")
    cy.env(["testPhoneNumber"]).then(({ testPhoneNumber }) => {
      if (typeof testPhoneNumber !== "string")
        throw new Error("Set TEST_PHONE_NUMBER")
      cy.get('[data-cy="phone-number"]').type(testPhoneNumber)
    })
    cy.task<PhonekitCursor>("sms:mark").then((after) => {
      cy.get('[data-cy="send-code"]').click()
      cy.task<string>(
        "sms:wait",
        { after },
        { timeout: 150_000, log: false }
      ).then((code) => {
        cy.get('[data-cy="verification-code"]').type(code, { log: false })
      })
    })
    cy.get('[data-cy="verify-code"]').click()
    cy.location("pathname").should("eq", "/account")
    cy.get('[data-cy="account-heading"]').should("be.visible")
  })
})
Terminal — run the Cypress specShell
npx cypress run --spec cypress/e2e/phone-verification.cy.ts

The task deadline is 150 seconds, longer than the SDK's 120-second receive deadline. Cypress waits for the returned task promise before continuing the command chain. See Cypress task behavior. The current example uses cy.env(), rather than the removed Cypress.env() interface.

Handle failures at the boundary

FailureMeaning and next step
Task is not registeredCheck which Cypress config loads setupNodeEvents
Task times out before SDKKeep the task timeout above timeoutMs
SDK reports timeoutCheck application's accepted send and allocated number
SDK reports missing_otpFresh SMS received; inspect its expected format
Account assertion failsCode acceptance and application's session/navigation

Scroll horizontally to see all columns.

Avoid automatic Cypress retries for this path until reset and resend behavior are explicit. log: false hides command entries; it does not redact the form or make a screenshot safe. The config disables video and failure screenshots for this fixture.

Common questions

Why not import Phonekit directly into the Cypress spec?

The spec executes in a browser context. Run the SDK in setupNodeEvents tasks so the API key remains in the server process. The spec imports only the cursor type, which disappears from compiled JavaScript.

Why does the task need its own timeout?

Cypress enforces a task deadline independently of the SDK’s receive deadline. If Cypress stops first, it reports a task timeout before the SDK can classify the receipt result. Set the task deadline higher and leave time for application verification.

Does log: false hide the verification code everywhere?

It removes the task/type entry from the Cypress command log. The code still enters the application form, so screen captures and third-party reporters may record it. Review capture settings and artifact access separately.

Can I use Cypress retries with the same number?

Use an explicit reset-and-resend policy first. Each attempt needs a fresh cursor and the sender’s permitted resend interval. Parallel specs also need separate numbers or resource serialization; a new cursor does not reserve a number.