Browser test automation
Waiting for delayed verification mail in Selenium
Design Selenium waits for delayed verification email with explicit timeouts, polling assertions, and teardown—without relying on an assumed CI mail API.

Selenium should wait on a measurable inbox condition—not a fixed Thread.sleep that hopes the ESP is fast. Poll for message presence (or OTP text) with an explicit timeout, assert once, then tear down the mailbox. Prefer application-level mail fakes for most CI; use browser+inbox waits only for end-to-end paths you own.
This pattern guide belongs under email test automation. Pair with Laravel signup verification tests for the generation layer. Mailby Quick Inbox can serve manual QA receives; the Developer API can support automated waits when you wire it intentionally—do not assume undocumented CI magic.
Selenium context and boundaries
You control the app under test. Email delay comes from queues, ESP latency, or greylisting—not from WebDriver.
Boundaries:
- Authorized testing only.
- Avoid scraping random public temp-mail sites in CI (flaky, retention, ToS).
- Explicit waits > implicit sleeps.
- Selenium docs on waits: explicit waits (rel="nofollow noopener").
Decision guide with counterexample
Working pattern:
- Create inbox (private catcher or API inbox).
- Drive signup UI with that address.
- Poll inbox API/UI until subject matches or timeout (e.g. 60–120s).
- Extract OTP/link; complete UI.
- Delete inbox / revoke credentials.
Counterexample (failure):
Thread.sleep(30000);
driver.findElement(By.id("otp")).sendKeys(hardcoded);
Sometimes passes locally, fails in CI when ESP takes 45s, or passes with a stale OTP from a previous run. Root cause: no assertion on mail arrival; no isolation.
Mechanism: timing and cleanup edge cases
- Too short timeout: false negatives on slow queues.
- Too long timeout: masks outages; burns CI minutes.
- No unique address per test: cross-talk between parallel jobs.
- No teardown: quota exhaustion; leaked PII in shared catchers.
- UI wait only: message arrived but element not interactable—use expected conditions.
Step table
| Test step | Timeout/retry | Assertion | Teardown |
|---|---|---|---|
| Allocate inbox | Fail fast if API down | Address non-null | Schedule delete |
| Submit signup | WebDriverWait for success toast | URL/state changed | — |
| Poll for mail | 90s, every 3s | Subject/from match | — |
| Extract OTP | 5s parse | Matches \d{6} | Clear clipboard |
| Enter OTP | Wait clickable | Dashboard visible | Logout |
| End | — | — | Delete inbox; wipe user |
Worked example (pseudocode)
address = inbox.create()
signup(address)
deadline = now + 90s
while now < deadline:
msgs = inbox.list(address)
if msgs contains subject "Verify":
otp = extract(msgs[0])
break
sleep 3s
else:
fail "verification mail not received"
enterOtp(otp)
inbox.delete(address)
Measurable assertion: mail received and OTP regex and post-login selector. Three assertions beat one sleep.
Limitation: public receive-only UIs without API force brittle DOM scraping. Prefer Mailpit in CI or Mailby Developer when you need HTTP polling (developers).
Alternatives
| Approach | When |
|---|---|
| Mail::fake / unit | Default CI |
| Mailpit + API | Local/staging E2E |
| Mailby Developer API | Shared staging receive |
| Manual Quick Inbox | Exploratory QA only |
Short answers
What causes waiting issues for delayed verification mail in Selenium?
Fixed sleeps, shared inboxes, and asserting UI before mail exists.
What should I do first?
Replace sleep with polled inbox assertion + unique address.
When is a permanent address safer?
Human UAT accounts; not parallel CI.
What evidence changes the recommendation?
If generation tests fail, fix Laravel/mailer before adding Selenium waits.
Sources, test date, limitations
- Pattern dated 2026-09-24.
- Selenium waits documentation.
- Related ops: port 25 deployment runbook when self-hosting catchers.
- Limitation: language bindings differ; adapt idioms.
Choosing timeout budgets with data, not folklore
Measure p50/p95 delivery time on staging for two weeks. Set Selenium timeout to roughly p95 + small buffer (for example p95=40s → wait 70–90s). Revisit quarterly. A constant 300s “just in case” hides outages and burns parallel CI agents.
Log the actual wait duration on success. If p95 drifts upward, file an ESP or queue ticket instead of lengthening sleeps forever.
Parallelism and address uniqueness
Two jobs sharing qa@mailpit will steal each other’s OTPs. Generate per-test addresses:
- Mailpit: random local-part
- Mailby Developer: create inbox per test via API
- Catch-all domain:
otp+{uuid}@tests.example.com
Store the address in the test context object; never read it from a global static.
Distinguishing UI flakiness from mail flakiness
If the inbox API shows the message but sendKeys fails, you have a WebDriver problem (overlays, animations, wrong iframe). If the inbox API never shows the message, stop touching Selenium locators and debug mail. Mixing the two wastes the most engineering time on email E2E.
How this differs from the email-test-automation hub
The hub compares automation approaches. This article is a wait-pattern decision guide with a counterexample sleep, a step table, and teardown rules under Selenium specifically.
Manual QA bridge
Exploratory testers can still use Quick Inbox without wiring CI. Capture screenshots of OTP HTML for design review, then delete the inbox. Label any future pipeline integration as an explicit developers task with secrets in the CI vault—not as a hidden scrape of the public UI.
Failure classification for dashboards
Tag CI failures:
mail_timeoutotp_parseui_after_otpinbox_provision
Alert differently. A spike in mail_timeout is an ESP incident; a spike in ui_after_otp is a frontend regression.
Teardown guarantees with try/finally
Always delete inboxes in finally blocks—even on assertion failure—so dumps for debugging are intentional exports, not accidental retention. Respect /data-retention philosophy: short-lived artifacts by default.
Backoff strategies
Linear 3-second polls are fine for 90-second budgets. For longer ESP SLAs, use exponential backoff capped at 10 seconds to reduce API load. Jitter avoids synchronized stampedes in parallel CI.
Secret handling in screenshots
CI artifacts often upload screenshots on failure. Redact OTP fields before upload or disable screenshots for mail tests. Otherwise your S3 bucket becomes an OTP museum.
Contract between backend and QA
Publish a staging guarantee: “verification mail p95 < 30s.” QA timeouts derive from that contract. When marketing installs a new ESP without telling QA, the contract surfaces the break instead of silent flake growth.
Reference implementation notes (language-agnostic)
Pseudocode earlier omitted error types. Expand:
InboxProvisionError— fail fast, no UI stepsMailTimeoutError— include address, subject filter, waited msOtpFormatError— dump redacted body hash, not raw OTP in logsPostAuthUiError— attach DOM screenshot with OTP digits masked
These types make dashboards useful on Monday morning.
Local developer loop
Developers should run Mailpit + one Selenium test locally before pushing. CI-only email E2E creates slow feedback. Document make test-email-e2e with ports and sample .env.
When to delete the Selenium mail test entirely
If product moves to in-app OTP or magic links consumed via API stubs in test builds, delete the wait. The best flaky test fix is removing an unnecessary network dependency. Keep one staging smoke if executives demand “real mail” demos; do not block every PR on ESP weather.
Collaboration with Laravel unit layers
Gate Selenium mail jobs on green unit/feature mail suites. A broken Mailable should never reach a 90-second wait. Pipeline order saves money.
Flake autopsy template
When a mail wait fails once:
- Was inbox provisioned?
- Did register HTTP 200?
- Did queue process in app logs?
- Did catcher receive any message (wrong subject filter)?
- Did OTP regex fail on unexpected copy?
Paste answers in the flake ticket before restarting CI. Blind re-runs train the team to ignore real outages.
Synthetic mailer in test builds
Feature-flag a “OTP always 000000 in E2E builds” path for most PR checks, and reserve real-mail Selenium for nightly. Many orgs cut flake by 80% with that split while still watching real delivery overnight.
Grid and cloud browser considerations
Cloud browser farms add network hops. Increase mail timeout slightly relative to local runs, but keep inbox polling in the test machine region closest to your catcher API. Crossing oceans twice (browser in NA, catcher in EU, ESP in NA) amplifies delay. Architecture for latency deliberately.
Example WebDriverWait style (Java-like pseudocode)
wait.until(driver -> inbox.list(addr).stream().anyMatch(m -> m.subject.contains("Verify")));
String otp = extract(inbox.latest(addr));
wait.until(ExpectedConditions.elementToBeClickable(otpField)).sendKeys(otp);
The first wait is mail; the second is UI. Splitting them preserves diagnosis. Port the idea to Python WebDriverWait or JS until—same structure.
Nightly report email (ironically)
Send the nightly suite summary to a durable engineering list, not a temporary inbox. The report includes mail_timeout counts. Dogfooding temporary mail for the report itself is a comedy of errors waiting to happen.
Operational SLOs tied to waits
Publish three numbers on the QA dashboard: mail provision success rate, p95 mail wait, and OTP parse success. When p95 crosses the Selenium timeout, page the mail owner—not the frontend owner. Aligning ownership to metrics stops the wrong team from “fixing” locators while SMTP burns.
Also keep a freeze window during ESP migrations: pause real-mail Selenium, rely on fakes, then re-enable with a fresh baseline. Most multi-day flake storms start as an unannounced provider change.
Finally, document the manual fallback: human opens Quick Inbox, completes one signup, attaches screenshots to the release checklist when automation is red but business must ship a non-mail change.
Why fixed sleeps feel productive and are not
A thirty-second sleep “almost always works” on a quiet laptop. CI is not quiet: shared ESP sandboxes, cold queues, and noisy neighbors stretch delivery past the sleep while the test still races ahead. Polling with a timeout converts hope into a measurable miss. When the miss fires, you gain a timestamped failure instead of a wrong OTP typed into a lucky green test.
Budget engineering time for inbox helpers the same way you budget page objects. Email is part of the product surface for signup; treating it as an afterthought is why suites rot.
Reader checklist
- Unique inbox per test
- Poll until subject match or timeout
- Assert OTP format
- Assert post-login UI
- Tear down inbox in
finally - Gate behind unit mail fakes
If any box is unchecked, expect flakes.
Conclusion
Wait for mail as data, not as time. Unique inboxes, explicit timeouts, triple assertions, teardown. Use Quick Inbox for manual QA; automate receive only through APIs you deliberately configure—such as developers—never via silent hope.
Try it on Mailby
Open a receive-only disposable inbox when a short-lived address fits the job — session-bound, with timed purge.
