Browser tests that pass everywhere except Microsoft Edge on Windows CI runners are frustrating because they often look random. A selector works locally, the same step passes in Chromium or Firefox, then Edge on a hosted Windows runner fails with a timeout, a layout mismatch, or a download that never appears. The temptation is to call it flakiness and retry the pipeline. That usually hides the root cause.

The more useful mental model is this: Edge on Windows is not just “another browser” in a generic test farm. It is a browser with its own rendering pipeline, font fallback behavior, download policies, security features, and sensitivity to runner configuration. When tests fail only in Microsoft Edge, the failure is often deterministic but exposed by a platform-specific difference that your other browsers do not share.

This guide focuses on the practical causes behind browser tests fail only in Microsoft Edge scenarios, and on the debugging workflow that helps you separate true product defects from environment issues in Windows CI.

Start by classifying the failure mode

Before changing code, classify what kind of failure you are seeing. That sounds obvious, but the fastest way to waste time is to debug an assertion problem as though it were a browser configuration problem, or vice versa.

Common Edge-only failure patterns include:

  • The page renders, but a button is slightly lower or wider than expected.
  • An element is visible in screenshots locally, but absent or clipped on Windows runners.
  • A file download works in Chrome but is blocked or redirected in Edge.
  • A test that waits for navigation times out only in Edge.
  • A click hits the wrong element because the page shifted by a few pixels.
  • A screenshot diff shows text reflow, different anti-aliasing, or icon substitution.

If the failure only appears in CI, treat the runner as part of the system under test. The browser is not the only variable.

A good first pass is to ask three questions:

  1. Does the test fail only on Windows runners, or only in Edge on Windows runners?
  2. Is the symptom visual, behavioral, timing-related, or download-related?
  3. Does the failure disappear when you slow the test down, change the viewport, or run in headed mode?

The answers point to different root causes.

Confirm the browser, channel, and mode exactly

Edge has multiple channels and modes that can matter in CI, especially when a runner image updates out from under you. Record the exact browser binary and launch flags used by the job.

For Playwright, that means not assuming the channel or executable path is what you expect.

import { test, expect } from '@playwright/test';
test('debug edge launch details', async ({ page, browserName }) => {
  console.log('browser:', browserName);
  await page.goto('https://example.com');
  await expect(page).toHaveTitle(/Example/);
});

If you are using Selenium, verify that the EdgeDriver version matches the installed browser version closely enough to avoid protocol mismatches.

from selenium import webdriver
from selenium.webdriver.edge.options import Options

options = Options() options.add_argument(‘–headless=new’)

driver = webdriver.Edge(options=options) driver.get(‘https://example.com’) print(driver.capabilities) driver.quit()

Key things to capture in logs:

  • Browser version
  • Driver version
  • Headless or headed mode
  • Viewport size and device scale factor
  • OS build of the Windows runner
  • Any custom launch flags

That information often reveals an invisible difference, such as one job using a newer Edge build after image rotation while another remains pinned.

Font fallback is a common source of “random” layout failures

Font rendering is one of the most underestimated causes of Edge browser automation failure. A page can look fine in a local environment with common developer fonts installed, but render differently on a clean Windows runner. That matters when your assertions depend on precise layout, clipped text, element height, or screenshot matching.

Why Edge can expose this more often:

  • It may use a different font fallback path than your local system.
  • Windows runners often have a smaller font set than developer workstations.
  • Headless rendering can change glyph metrics slightly.
  • Subtle font differences can change line wrapping, which shifts buttons and form fields.

Typical symptoms:

  • A text label wraps onto two lines in CI, pushing a button down.
  • A locator based on visible text finds the wrong element because the text is partially clipped.
  • Screenshot diffs show changed kerning, line height, or icon alignment.

How to debug font issues

Inspect the computed font family in the browser, not just the CSS source. A page can request one font stack and receive another due to fallback.

typescript

const font = await page.locator('h1').evaluate(el => getComputedStyle(el).fontFamily);
console.log(font);

Then compare the computed layout dimensions between local and CI.

typescript

const box = await page.locator('button:has-text("Submit")').boundingBox();
console.log(box);

If the size differs, the issue may be font-related rather than an assertion problem.

What to do about it

  • Use web-safe or bundled fonts in test environments when visual stability matters.
  • Avoid pixel-perfect assertions for text-heavy layouts unless the test is explicitly visual regression.
  • Prefer semantic locators over position-based clicks.
  • Allow wrapping tolerance in screenshots and component tests.
  • If your app uses icon fonts, confirm the font files load in CI and are not blocked by CSP or path issues.

If you maintain a design system, consider a test mode with stable test fonts that are bundled as part of the build or image.

Graphics and compositing differences can shift hit targets

Windows CI runners, especially hosted ones, can behave differently around GPU acceleration, compositing, and rasterization. Edge may render some pages in a way that changes how shadows, transforms, animations, or sticky elements appear during automation.

This is especially relevant if your test does any of the following:

  • Clicks an element after an animation
  • Targets a fixed header or overlay menu
  • Takes screenshots of canvas-heavy or SVG-heavy pages
  • Interacts with virtualized lists or scrolling containers

A test that uses a visible point click can fail if the browser computes a different hit target due to a transform or overlay. In Edge, small rendering differences may surface where Chrome happens to pass.

Try these experiments one at a time:

  • Run headed instead of headless.
  • Disable animations in the app during tests.
  • Force a fixed viewport.
  • Capture screenshots before the click.
  • Inspect element coordinates and z-index stacking.

A useful pattern in Playwright is to ask the browser to tell you what is actually under the pointer point.

typescript

const target = page.locator('button:has-text("Save")');
await target.scrollIntoViewIfNeeded();
console.log(await target.boundingBox());
await target.click({ trial: true });

If the trial click fails, it often means another element is intercepting the click or the element is not yet stable.

Practical mitigations

  • Wait for the UI to settle, not just for the element to exist.
  • Prefer toBeVisible() plus toBeEnabled() before clicking.
  • Disable CSS transitions in test runs.
  • Avoid clicks on elements near animated or sticky overlays.
  • Where possible, use keyboard-driven interactions for forms and dialogs.

Download handling is often different in Edge on Windows

Download tests can be especially tricky because browser security behavior, file system permissions, and CI runner configuration all intersect. A workflow that works locally might fail in Edge on a Windows runner because the file download path is not writable, automatic downloads are blocked, or the browser prompts differently.

This is one of the most common sources of “it only fails in Edge” reports when the application exports files, PDFs, CSVs, or reports.

What to verify first

  • The download directory exists and is writable.
  • The test is not depending on a GUI download prompt.
  • The browser context is configured to accept downloads.
  • The file name is predictable, or the test can discover it dynamically.

In Playwright, a robust download test is usually event-based.

typescript

const [download] = await Promise.all([
  page.waitForEvent('download'),
  page.getByRole('button', { name: 'Export CSV' }).click()
]);

const path = await download.path(); console.log(path);

If Edge fails here but other browsers pass, inspect the browser’s policies and the runner’s filesystem permissions.

Windows runner specifics to check

  • Is the job running as a user with access to the temp directory?
  • Is OneDrive, controlled folder access, or antivirus software interfering with downloads?
  • Does the runner image use a path with spaces or restricted permissions?
  • Are you cleaning up the download folder between tests?

For Selenium, pay attention to browser preferences and profile configuration, because the default download behavior can vary significantly across environments.

Treat runner configuration as a first-class cause

A surprising number of Edge-only failures are not browser bugs at all. They are configuration mismatches between local and CI environments. Windows runners can differ in ways that affect test behavior even if the code and browser version are identical.

Things to compare between local and CI:

  • Windows version and build number
  • Display resolution and scaling
  • Time zone and locale
  • Default browser language
  • Installed fonts and language packs
  • Disk space and temp directory paths
  • User profile persistence
  • Security policies and group policy settings

Browser automation on CI is sensitive to environment drift, especially when tests rely on rendering, downloads, or file paths.

Resolution and scaling

Edge can behave differently when the Windows display scaling is 125 percent or 150 percent. On hosted runners, you may not control the display directly, but the virtual display and viewport still matter. A click that lands correctly at 1920 by 1080 may miss at a smaller viewport or different scale.

Make viewport explicit in your tests rather than relying on defaults.

import { defineConfig } from '@playwright/test';

export default defineConfig({ use: { viewport: { width: 1365, height: 768 }, deviceScaleFactor: 1 } });

Locale and time zone

Edge can expose locale-sensitive formatting differences in date pickers, currency fields, and accessibility labels. If a test expects MM/DD/YYYY but the runner renders DD/MM/YYYY, assertions can fail in ways that look like browser drift.

Set these explicitly in your test harness when the application is locale-aware.

Use artifacts to see what the browser actually saw

When Edge fails only in CI, logs alone are rarely enough. You need artifacts that preserve the browser state at the moment of failure.

Good artifacts to collect:

  • Video of the test run
  • Screenshot on failure
  • Trace or event log
  • Browser console output
  • Network log or HAR file
  • Download directory contents

For Playwright, tracing is especially useful because it captures actions, DOM snapshots, network activity, and screenshots.

import { test } from '@playwright/test';

test.beforeEach(async ({ context }) => { await context.tracing.start({ screenshots: true, snapshots: true }); });

test.afterEach(async ({ context }) => { await context.tracing.stop({ path: ‘trace.zip’ }); });

If you use Selenium, capture browser logs and screenshots at the point of failure, then compare the exact page state with a passing browser.

A single screenshot often answers questions like:

  • Was the element actually visible?
  • Did the page scroll unexpectedly?
  • Is a modal overlay covering the target?
  • Did the browser open a permission prompt or download bar?

Make selectors and waits resilient to Edge timing

Not every Edge-only failure is visual. Sometimes the browser is just slower to reach a stable state because the page uses async rendering, animations, or delayed hydration. Edge on Windows runners may expose timing gaps that Chrome masks.

This is where brittle waits fail.

Prefer state-based waiting over arbitrary sleep

A bad pattern:

typescript

await page.waitForTimeout(3000);
await page.getByRole('button', { name: 'Continue' }).click();

A better pattern:

typescript

const button = page.getByRole('button', { name: 'Continue' });
await button.waitFor({ state: 'visible' });
await button.click();

Even better, assert readiness explicitly when the page has known signals.

typescript

await expect(page.locator('[data-test="results-ready"]')).toBeVisible();

If the app has a loading skeleton, network idle may not be a reliable signal because modern apps keep background requests alive.

Use stable locators

Selectors based on layout, text fragments, or ancestor order are more likely to break when Edge changes wrapping or rendering. Prefer roles, labels, and stable test IDs.

  • getByRole() for accessible controls
  • getByLabel() for form fields
  • data-testid for ambiguous components

This is not just a best practice, it is a defense against browser-specific rendering differences.

Check downloads, permissions, and security prompts separately

Edge can surface permission-related behavior differently from Chromium-based expectations depending on policy, profile, or site settings. CI runs often do not have the same persistent profile state that developers have locally.

That means tests involving:

  • File downloads
  • Clipboard access
  • Notifications
  • Popups
  • Authentication flows

can behave differently even when the application is healthy.

If the test depends on a download, store the file in a dedicated temp directory and assert on the file contents rather than on a UI prompt. If it depends on authentication, use a stable test account and a storage state or cookie setup that bypasses interactive UI where possible.

Compare Edge against another browser with the same runner

When you suspect a browser-specific issue, the fastest diagnostic path is often to run the same test on the same runner with another browser and compare the artifacts.

The goal is not to prove that Edge is worse. The goal is to isolate the variable.

Questions to answer:

  • Does the DOM differ, or just the rendered pixels?
  • Does the click fail before the network request fires?
  • Is the element visible but not interactable?
  • Does the download never start, or does it start and fail on disk?

If Chrome passes and Edge fails, do not stop at “browser difference”. Compare the screenshot, the console logs, and the exact timestamps. The first divergence usually points to the root cause.

A practical debugging checklist

Use this when an Edge-only failure lands in your CI queue.

1. Capture environment details

Record browser version, driver version, Windows build, viewport, locale, and headless state.

2. Reproduce with artifacts

Run the job with screenshots, traces, videos, and console logs enabled.

3. Compare rendering

Check whether the issue is due to layout shift, font fallback, clipping, or overlay interception.

4. Check download and permissions paths

Validate file write locations, profile persistence, and browser download policies.

5. Remove timing assumptions

Replace sleep-based waits with state-based waits and explicit readiness checks.

6. Reduce environmental drift

Pin browser versions when practical, standardize the Windows runner image, and document any required fonts or policies.

7. Re-run in a clean profile

If a persistent profile is used locally, test with a fresh profile in CI to eliminate hidden browser state.

Example CI debugging setup for Playwright on Windows

A minimal GitHub Actions job can help you surface Edge-specific issues by collecting artifacts and keeping the test environment explicit.

name: e2e
on: [push, pull_request]

jobs: edge-windows: runs-on: windows-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - run: npx playwright install –with-deps msedge - run: npx playwright test –project=msedge - uses: actions/upload-artifact@v4 if: failure() with: name: playwright-artifacts path: | playwright-report/ test-results/

This does not solve the problem by itself, but it makes the failure repeatable and inspectable.

When to fix the test, when to fix the app, and when to escalate

Not every Edge-only failure belongs in test code.

Fix the test when:

  • The selector is brittle.
  • The wait strategy is naive.
  • The assertion depends on pixel-perfect layout that is not part of the product requirement.
  • The download path or profile setup is not deterministic.

Fix the app when:

  • The UI truly breaks in Edge due to unsupported CSS or JavaScript behavior.
  • The app depends on a browser-specific assumption that is not valid.
  • Accessibility or interaction patterns are inconsistent across browsers.

Escalate infrastructure when:

  • The runner image changed.
  • The browser version changed unexpectedly.
  • Security policies or installed fonts differ from the documented baseline.
  • The same test passes on self-hosted Windows but fails on hosted Windows.

A final way to think about Edge-only flakiness

Most browser tests fail only in Microsoft Edge on Windows CI runners for one of four reasons: text is rendered differently, graphics are composited differently, downloads are handled differently, or the runner is configured differently from the environment you trust. Once you know which bucket the failure belongs in, the debugging path becomes much shorter.

The practical habit to build is simple: collect more evidence before making code changes. A clean artifact set, a clear environment snapshot, and a small number of controlled repro experiments will usually tell you whether you are dealing with a browser bug, a test bug, or a runner problem.

For broader background on the discipline behind these workflows, see software testing, test automation, and continuous integration.