iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more
Automate the mailbox step instead of pausing a Playwright test for a human to copy a code. The reliable pattern is to give your tests an isolated Outlook or Microsoft 365 mailbox, obtain a least-privileged Microsoft Graph access token, poll the inbox for a message that belongs to the current test run, extract its verification code or URL, and finish the flow in the browser. Playwright supports TypeScript directly and can also issue HTTP requests, so one test can control the UI and inspect mail without switching tools.
Delivery time is environment-dependent; Microsoft’s documented APIs do not provide a universal verification-email SLA. Your test therefore needs a bounded polling loop, precise message matching, safe handling of tokens and message content, and a final assertion that the application actually marked the account as verified.
What does the Playwright–Microsoft Graph email-verification workflow look like?
The browser submits the registration form, while Microsoft Graph supplies the message that proves the registration workflow completed. A typical run has five stages:
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallOutdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware match- Prepare an isolated identity. Use a dedicated test mailbox or test user rather than a personal inbox.
- Authenticate to Graph. Register an app in Microsoft identity platform and request only the delegated or application mail permission required to read the test mailbox.
- Start a unique browser run. Generate a run ID and use it in the address, subject expectation, or another value the application sends in the message.
- Poll and parse. Read recent messages, follow pagination, identify the correct message, and extract a URL or one-time code from text or HTML.
- Complete and verify. Navigate or submit the value in Playwright, then assert the application’s verified state. Do not treat receipt of an email alone as success.
Microsoft recommends Microsoft Graph for Outlook mail, calendar, and contacts. Production integrations should use the stable v1.0 Graph endpoints where the required operation is available; beta endpoints can change.
#1 Best Overall
What do I need before writing the test?
Prepare the mailbox, identity, permissions, and test environment before adding browser steps. Separating these concerns makes failures diagnosable.
Mailbox and application prerequisites
- A dedicated Outlook or Microsoft 365 mailbox, or an isolated test identity with a mailbox, that your test run can safely read and clean.
- An app registration in Microsoft identity platform with tenant consent for the required Graph mail permission.
- A secret or certificate stored in your CI secret manager, never in source control or a test report.
- A Playwright project configured for TypeScript. Playwright supports TypeScript out of the box; add a separate
tsc --noEmitcheck so type errors are caught independently of test execution. - A test address strategy. A unique plus-address, generated mailbox, or run-specific recipient prevents an old message from satisfying a new test.
Delegated or application access?
Delegated access acts on behalf of a signed-in user. It is useful when your test harness already has an interactive or refresh-token-based user session, but interactive consent can be awkward in headless CI. Application access runs as the registered app and is generally easier to schedule, provided tenant administrators grant the application mail permission and restrict it to the test mailbox where your organization’s policy supports that arrangement.
Choose the least-privileged permission that permits the read operation you actually perform. Keep the mailbox and app dedicated to testing so a broad permission cannot expose unrelated employee mail.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
How do I obtain a Microsoft Graph access token safely?
Graph mail endpoints are OAuth-protected. Your test must send an access token in the Authorization: Bearer header; a mailbox password or a browser cookie is not a substitute.
Use CI-provided credentials
Have your pipeline acquire a short-lived token with your organization’s approved OAuth flow, then expose the resulting token to the test process through an environment variable or an in-memory fixture. For an app-only flow, the token request normally uses the tenant, client identity, client credential, and the Graph default scope. For delegated access, obtain a token on behalf of the test user through your approved sign-in flow. The exact consent and credential method is a tenant decision, so do not hard-code a secret or assume that a developer’s interactive login will work in CI.
Rank #2
const graphToken = process.env.GRAPH_ACCESS_TOKEN;
if (!graphToken) {
throw new Error('GRAPH_ACCESS_TOKEN is required for mailbox checks');
}
Keep secrets and mail content out of diagnostics
- Mask the token, client secret, authorization headers, verification URLs, and message bodies in CI logs.
- Prefer logging a message ID, subject hash, and received timestamp over the full message.
- Give the test identity only the mailbox access it needs, and rotate credentials on the schedule required by your tenant.
How do I query the inbox with Microsoft Graph?
Use a folder-specific collection when the application delivers to Inbox, or use /me/messages when mail may be moved by a rule. Select only the fields required for matching and parsing. Graph responses can be paged, so a successful first response is not proof that every matching message was returned.
type GraphPage<T> = {
value: T[];
'@odata.nextLink'?: string;
};
type MailMessage = {
id: string;
subject?: string;
from?: { emailAddress?: { address?: string } };
toRecipients?: { emailAddress?: { address?: string } }[];
receivedDateTime?: string;
body?: { contentType?: string; content?: string };
bodyPreview?: string;
internetMessageId?: string;
isRead?: boolean;
};
async function graphGet(url: string, token: string): Promise {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/json',
Prefer: 'outlook.body-content-type="text"'
}
});
if (!response.ok) {
throw new Error(`Graph request failed: ${response.status} ${await response.text()}`);
}
return response.json() as Promise<T>;
}
async function listMessages(token: string, recipient: string, since: Date) {
const query = new URLSearchParams({
'$select': 'id,subject,from,toRecipients,receivedDateTime,body,bodyPreview,internetMessageId,isRead',
'$orderby': 'receivedDateTime desc',
'$top': '25'
});
const first = `https://graph.microsoft.com/v1.0/me/mailFolders/inbox/messages?${query}`;
const messages: MailMessage[] = [];
let next: string | undefined = first;
while (next) {
const page = await graphGet<GraphPage<MailMessage>>(next, token);
messages.push(...page.value);
next = page['@odata.nextLink'];
}
return messages.filter((message) => {
const received = message.receivedDateTime ? new Date(message.receivedDateTime) : undefined;
const recipients = message.toRecipients?.map((r) => r.emailAddress?.address?.toLowerCase());
return !!received && received >= since && recipients?.includes(recipient.toLowerCase());
});
}
The complete @odata.nextLink value must be requested as returned; do not rebuild it and accidentally drop its continuation token. You can add OData filters for a known sender, subject fragment, recipient, or time boundary, but retain a client-side check because mail fields and delivery timing can vary.
How do I wait for the correct verification email?
Poll until a bounded deadline, and match several independent signals. A subject alone is too weak when old messages or parallel test runs exist.
async function waitForMessage(
token: string,
recipient: string,
runId: string,
startedAt: Date,
timeoutMs = 120_000,
intervalMs = 3_000
): Promise<MailMessage> {
const deadline = Date.now() + timeoutMs;
let lastCount = 0;
while (Date.now() < deadline) {
const messages = await listMessages(token, recipient, startedAt);
const match = messages.find((message) => {
const subject = message.subject ?? '';
const body = message.body?.content ?? message.bodyPreview ?? '';
return subject.includes('Verify') && body.includes(runId);
});
if (match) return match;
lastCount = messages.length;
await new Promise((resolve) => setTimeout(resolve, intervalMs));
}
throw new Error(`Verification email not found before timeout; ${lastCount} recent messages inspected`);
}
The timeout and interval above are example test settings, not a Microsoft delivery guarantee. Tune them to your environment and fail with a useful diagnostic. Record only non-sensitive metadata such as the number of messages inspected and the last polling time.
Use a run-specific matching strategy
- Recipient: require the exact address generated for this run.
- Time: ignore messages received before the test started, allowing for small clock differences if your systems are not synchronized.
- Sender and subject: check the expected sender address and a stable subject fragment.
- Body marker: include a run ID or other non-secret marker in the signup request if the application can echo it.
- Message identity: use the current Graph result for this run. A message ID can change after mailbox actions, so do not persist it as a permanent business key.
How do I extract a verification URL or code?
Request the body as text when possible, then parse only the narrow pattern your application generates. If the service sends HTML, treat it as untrusted input and avoid a general-purpose HTML-to-code heuristic.
Extracting a one-time code
function extractCode(content: string): string {
const match = content.match(/b(d{6})b/);
if (!match) throw new Error('No six-digit verification code found');
return match[1];
}
Change the expression to the application’s documented format; the six-digit example is not a claim about every provider. If several numbers appear, first anchor the search to a label such as “verification code” and reject ambiguous matches.
Free tools Windows power users keep installed
One-click scans. No signup required.
Extracting a verification link
function extractVerificationUrl(content: string): string {
const candidates = content.match(/https?://[^s<>"']+/g) ?? [];
const url = candidates.find((value) => value.includes('/verify'));
if (!url) throw new Error('No verification URL found');
return url.replace(/[),.]+$/, '');
}
Allow-list the host or path used by your test environment before navigating. Never follow an arbitrary URL copied from an untrusted message, and redact the URL if an assertion fails.
How do I combine Graph requests with Playwright actions?
Keep the browser flow in Playwright and call Graph from a small helper. Playwright’s APIRequestContext can send HTTP(S) requests for setup and post-condition checks, although a native fetch helper is also suitable.
import { test, expect } from '@playwright/test';
test('verifies a new account from its email', async ({ page, request }) => {
const runId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
const address = `qa+${runId}@example.test`;
const startedAt = new Date();
await page.goto('/signup');
await page.getByLabel('Email').fill(address);
await page.getByLabel('Password').fill(process.env.TEST_PASSWORD ?? '');
await page.getByRole('button', { name: /sign up/i }).click();
await expect(page.getByText(/check your email/i)).toBeVisible();
const message = await waitForMessage(
process.env.GRAPH_ACCESS_TOKEN!,
address,
runId,
startedAt
);
const content = message.body?.content ?? message.bodyPreview ?? '';
const verificationUrl = extractVerificationUrl(content);
await page.goto(verificationUrl);
await expect(page.getByText(/verified|welcome/i)).toBeVisible();
});
The request fixture is available when you need an HTTP setup or status check against your application. If the verification link opens a different origin, configure that origin deliberately and assert the resulting application state rather than relying only on a URL change.
Verify the state, not just the page
After following the link or submitting the code, check a durable signal: a verified badge, an authenticated endpoint response, or a protected page that was inaccessible before verification. This catches cases where the email link is malformed, expired, or accepted by the UI without updating the account.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Repair Windows errors before they cause bigger problems3Fix the driver behind crashes, sound loss and screen glitchesRank #4
What commonly fails, and how do I fix it?
No message is found
- Confirm the token’s tenant, user, and permission, then make a minimal Graph request to the intended mailbox.
- Check whether a mail rule moved the message out of Inbox; use
/me/messagesor query the destination folder. - Increase the bounded timeout for a slow test environment, but do not remove the deadline.
- Verify that the recipient, sender, subject, and run marker are exactly what the application emitted.
The wrong message is selected
Old mail and parallel workers are the usual causes. Generate a unique recipient or run marker, apply a start-time boundary, inspect sender and recipient addresses, and continue through every @odata.nextLink page.
Graph returns 401 or 403
A 401 usually indicates an expired, missing, or audience-mismatched token. A 403 commonly means the app lacks consent for the required mail permission or the tenant’s application-access policy blocks the mailbox. Reissue a token after consent changes and verify the least-privilege permission with the tenant administrator.
Parsing breaks after an email-template change
Keep the parser narrow and test it against representative text and HTML fixtures. Prefer a stable label, data attribute, or URL path owned by your application; fail loudly when the expected value is absent instead of selecting the first number or link in the message.
The test passes locally but fails in CI
Check secret injection, tenant selection, mailbox access, system-clock differences, and network egress. CI may deliver mail more slowly or run workers concurrently, so isolate addresses and avoid depending on a developer’s cached sign-in.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Which implementation choices should I make?
The following trade-offs help standardize a team implementation.
| Decision | Option 1 | Option 2 | Practical guidance |
|---|---|---|---|
| Authentication | Delegated user access | Application access | Use delegated access when an approved user session is already part of the harness; use app-only access for unattended CI when tenant policy permits it. |
| Mailbox scope | Shared test mailbox | Per-run or per-worker mailbox | A shared mailbox is simpler; isolated identities reduce cross-run contamination and make cleanup safer. |
| Message matching | Subject only | Recipient, time, sender, subject, and run marker | Use multiple signals. Subject-only matching is vulnerable to stale mail. |
| Polling | Single read | Bounded repeated reads | Polling handles variable delivery; always set a timeout and report a non-sensitive diagnostic. |
| Body handling | Raw HTML heuristic | Text or narrowly scoped parser | Prefer text and an application-specific pattern; treat HTML as untrusted. |
| Cleanup | Leave messages in place | Delete or archive test messages under policy | Cleanup keeps later runs deterministic, but retain only what your organization’s audit rules require. |
How should I clean up and run this in CI?
Use a fixture or teardown step to remove test accounts and messages when your tenant policy allows it. Cleanup should be best-effort and must not hide the original assertion failure. If message deletion is not permitted, isolate each run with unique recipients and enforce a mailbox retention policy outside the test.
- Inject the token and test password through the CI secret store.
- Run a type check with
tsc --noEmit, then execute Playwright. - Assign each worker a unique recipient or run marker.
- Capture only sanitized metadata in reports; redact message bodies, tokens, and verification URLs.
- On failure, retain the Graph status code and timing information, not the private email content.
With this design, Graph handles mailbox retrieval and Playwright handles the user-visible workflow. The separation keeps authentication, polling, parsing, and browser assertions independently testable while preserving an end-to-end check of the real verification path.
Frequently Asked Questions
Can I read Outlook mail in Playwright without Microsoft Graph?
You can use another mail-testing service or a provider-specific API, but a browser test should not automate an Outlook web session as a substitute for an API permission model. Graph gives the test an explicit OAuth identity, mailbox scope, and message collection that can be audited and restricted.
Should each Playwright worker use a different mailbox?
Separate mailboxes provide the strongest isolation when workers run concurrently or when messages cannot be reliably tagged with a run ID. If mailbox provisioning is expensive, unique recipients plus strict sender, timestamp, and body-marker matching can isolate workers within one test mailbox.
Why does Microsoft Graph return only some messages?
Message collections are paged. Read the returned @odata.nextLink URL until it is absent, and do not reconstruct continuation URLs yourself. Also check whether a mail rule moved the message to a folder other than Inbox.
Is a verification-email timeout a product bug?
Not necessarily. Delivery depends on the application, Microsoft 365 configuration, throttling, and the test environment, and there is no universal SLA in the documented workflow. Report the elapsed time and Graph response status, then distinguish a bounded test timeout from a confirmed application delivery defect.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

