Using MailFlat with TestCafe
TestCafe fixtures map cleanly onto inbox lifecycle: beforeEach opens one, afterEach deletes it.
How it fits together
TestCafe test code runs in Node, so the client can be called directly. There is no task bridge to register and no proxy to configure; the fixture hooks reach the API the same way any Node script would. What is worth thinking about is time. TestCafe measures a test against its own execution timeout, while waiting for mail runs on a separate clock inside waitForOtp. When those two clocks disagree, the runner stops the test in the middle of a poll and the failure reads like a delivery problem when it is really a runner setting.
Install
The client has no dependencies of its own and runs on the Node that TestCafe already uses.
npm i -D @mailflat/sdk
The test
beforeEach opens the inbox and afterEach deletes it, so a failed test still cleans up after itself. Opening per test rather than per fixture matters more than it looks: waitForOtp returns the code from the newest message in the box, so a box that survives from one test to the next can hand the second test the first test's code without ever failing.
// tests/signup.js
import { Selector } from "testcafe";
import { MailFlat } from "@mailflat/sdk";
const mf = new MailFlat({ apiKey: process.env.MAILFLAT_API_KEY });
let inbox;
fixture("signup")
.page("https://staging.example.com/signup")
.beforeEach(async () => {
inbox = await mf.create({
prefix: `tc-${Date.now().toString(36)}`,
retentionHours: 2,
});
})
.afterEach(async () => {
await inbox.delete();
});
test("accepts a real verification code", async (t) => {
await t
.typeText("#email", inbox.address)
.click("#submit")
.typeText("#code", await inbox.waitForOtp({ timeout: 60000 }))
.expect(Selector("h1").innerText).contains("Dashboard");
});
Worth knowing
Run TestCafe with --test-execution-timeout high enough to cover the wait, or the runner stops the test mid-poll. A 60 second waitForOtp inside a 30 second execution timeout can never succeed, and the failure comes from the runner rather than from the client, which is what makes it easy to misread.
waitForOtp defaults to a 30 second timeout and polls once a second. Raising the timeout costs nothing while mail is arriving normally, because the call returns the moment the code lands rather than waiting out the clock.
TestCafe's concurrency flag runs tests in parallel. Because the inbox is opened per test rather than per fixture, parallel workers never share an address.
The address is permanent until you delete it. If a suite is easier to reason about with one known address, create it once outside the test run and skip the beforeEach; only the messages inside it expire.
When it goes wrong
| Symptom | Why | What to do |
|---|---|---|
| beforeEach fails with 403 before a browser ever opens | The key can read mail but cannot open a box. Creating and deleting sit behind the inbox:manage scope, and a new key does not get it by default. | Issue a key that includes inbox:manage, or open one inbox by hand and reuse its address. Reuse is a supported pattern here rather than a workaround, because the address does not expire. |
| waitForOtp times out and the error says mail did arrive | Mail landed but no code could be read out of it. That is a parsing miss, not a delivery failure, and the two produce very different fixes. | The error quotes the newest message. Read inbox.latest() and pull the code yourself, and send us the format so it stops needing a workaround. |
| The code that arrives belongs to the previous test | waitForOtp returns the newest message already in the box, so if the box outlived the last test the call succeeds instantly with a stale code and nothing looks broken. | Keep the inbox per test as shown above, or delete the messages between tests. A green test reading last test's code is the failure mode this page exists to prevent. |
| waitForOtp throws EncryptedInboxError | The inbox is end to end encrypted, so the server cannot read the body and cannot extract a code from it either. | Use a non encrypted inbox for automated runs, or read the message and decrypt it in your own code. |
| A suite that passed yesterday now finds an empty inbox | Messages expire on the plan retention window, which is 2 hours on Free. The inbox itself is untouched. | Trigger the mail inside the test instead of relying on one sent earlier. If a test genuinely needs older mail, paid plans hold messages for up to 30 days. |