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.
npm install --save-dev cypressRegister 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.
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
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")
})
})npx cypress run --spec cypress/e2e/phone-verification.cy.tsThe 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
| Failure | Meaning and next step |
|---|---|
| Task is not registered | Check which Cypress config loads setupNodeEvents |
| Task times out before SDK | Keep the task timeout above timeoutMs |
SDK reports timeout | Check application's accepted send and allocated number |
SDK reports missing_otp | Fresh SMS received; inspect its expected format |
| Account assertion fails | Code 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.