Using MailFlat with WebdriverIO
Hooks in wdio.conf.js open and close the inbox; the spec just reads the address off the global.
How it fits together
WebdriverIO's config file already owns the test lifecycle, so the inbox belongs there too: beforeTest opens one, afterTest deletes it. The hook you pick decides more than it appears to. beforeTest gives every it block its own box, while beforeSuite shares one across the file, and a shared box changes what waitForOtp means: it returns the newest code sitting in the box, which after the first test is last test's code.
The hooks
WebdriverIO awaits hooks that return a promise, so the async functions below finish before the first command runs. Each spec file runs in its own worker process, so a global set here is visible to that file and to nothing else.
// wdio.conf.js
const { MailFlat } = require("@mailflat/sdk");
const mf = new MailFlat({ apiKey: process.env.MAILFLAT_API_KEY });
exports.config = {
async beforeTest() {
global.inbox = await mf.create({
prefix: `wdio-${Date.now().toString(36)}`,
retentionHours: 2,
});
},
async afterTest() {
await global.inbox?.delete();
},
};
The spec
The address is available as soon as the hook resolves, so the spec reads it directly. waitForOtp is awaited inline: it returns the code, so it can sit straight inside setValue.
// test/specs/signup.e2e.js
describe("signup", () => {
it("accepts a real verification code", async () => {
await browser.url("/signup");
await $("#email").setValue(global.inbox.address);
await $("#submit").click();
await $("#code").setValue(await global.inbox.waitForOtp({ timeout: 60000 }));
await expect(browser).toHaveUrl(expect.stringContaining("/dashboard"));
});
});
Worth knowing
WebdriverIO's own waitUntil is for the browser. Use waitForOtp for the mailbox. It polls the API, not the DOM.
Do not wrap waitForOtp inside browser.waitUntil. Both of them poll, so you get one loop driving another, two timeouts that can disagree, and a failure reported by whichever fires first rather than by the one that knows what went wrong.
waitForOtp defaults to a 30 second timeout and a one second poll interval. It returns as soon as the code lands, so a longer timeout only costs anything on the runs that were going to fail anyway.
connectionRetryTimeout in wdio.conf.js governs the WebDriver session, not the mail API. Raising it does nothing for a mailbox that is still empty.
When it goes wrong
| Symptom | Why | What to do |
|---|---|---|
| beforeTest fails with 403 and no browser starts | Opening and deleting a box need the inbox:manage scope, which a key does not carry by default; read access is not enough. | Reissue the key with inbox:manage, or open one inbox up front and put its address in the environment. The address is permanent, so a fixed address is a normal setup rather than a hack. |
| The second test in a file passes instantly with the wrong code | The inbox was opened in beforeSuite instead of beforeTest, so the first test's message is still the newest one and waitForOtp returns its code without waiting. | Move creation to beforeTest as shown above. If a shared box is deliberate, delete the message at the end of each test so the next wait has nothing stale to find. |
| afterTest throws while cleaning up and masks the real failure | The hook ran before the inbox was assigned, usually because beforeTest itself failed. | The optional call above already guards this. Keep it, and read the first error in the report rather than the last one. |
| waitForOtp times out but the error quotes a message | The mail arrived and no code could be extracted from it. Delivery is fine; extraction is not. | Read inbox.latest() and parse the code in your own step, then report the format so it can be handled directly. |
| A run against yesterday's mail finds nothing | Messages expire on the plan retention window, 2 hours on Free and up to 30 days on paid plans. The inbox stays. | Send the mail inside the run. Retention is a message setting, not an address setting. |