July 20, 2026
What to Log When Browser Tests Fail Only in CI After a Dependency Lockfile Change
A practical debugging guide for browser tests that fail only in CI after a dependency lockfile change, with logging tactics to separate framework drift, browser drift, and app regressions.
When a browser test fails only in CI after a dependency lockfile change, the temptation is to blame the lockfile and move on. That is usually too shallow. A lockfile update can change your JavaScript runtime graph, browser driver behavior, transitive polyfills, build output, or the exact timing of startup work. In other words, it can expose drift that was already there.
The practical problem is not just making the test pass again. It is figuring out whether you are dealing with framework drift, browser drift, or an app regression. Those are different failures, and they need different logs.
If the failure appears only in CI, the question is rarely, “What changed in the test?” The better question is, “What changed in the environment that the test assumes?”
This article is a lab notebook style guide for teams that want reproducible browser failures, not heroic guesswork. It focuses on what to log, where to log it, and how to use the evidence to narrow the blast radius after a dependency lockfile change.
Why lockfile changes can surface browser failures
A lockfile is supposed to make installs reproducible, but reproducible does not mean stable forever. It freezes a particular dependency graph at a point in time. If that graph changes, several things can shift at once:
- test framework behavior, for example Playwright, Cypress, Selenium bindings, or assertion libraries
- browser automation driver versions, or the browser binaries themselves
- transitive packages that influence serialization, network mocking, module resolution, or date and locale handling
- build output, especially when the app is bundled differently after a dependency update
- CI startup timing, because installing a different set of packages can change cache hits, artifact size, or execution order
The keyword to keep in mind is CI dependency drift. Even if the application source is unchanged, the environment around the test may not be.
Browser tests are especially sensitive because they sit at the boundary between application code, browser behavior, and the machine running the job. That means a small dependency change can move the failure from one layer to another.
First classify the failure before you collect everything
You do not want to dump every possible artifact into every failed job. Log volume grows fast, and so does confusion. Start by classifying the failure into one of three buckets.
1. Framework drift
This is when the test runner, assertion library, selector engine, or browser automation library changes behavior. Symptoms often include:
- a locator that used to resolve now times out
- a wait that used to be sufficient no longer is
- a previously accepted strictness rule now fails
- a helper or fixture changes execution order
- the test starts failing earlier than before, before the browser even reaches the app
Framework drift often appears after a lockfile change because one transitive dependency was upgraded, even if the direct test library version stayed the same.
2. Browser drift
This is when the browser binary, driver, or rendering behavior differs between local and CI, or between CI runs. Symptoms often include:
- screenshot diff noise
- font, layout, or animation timing differences
- console warnings or errors from browser APIs
- headless-only behavior
- feature detection differences, especially around permissions, clipboard, downloads, or media APIs
If the lockfile change altered how the browser was provisioned in CI, this bucket deserves attention.
3. App regression
This is when the application itself changed in a way that breaks the test, and the lockfile change merely exposed the issue. Symptoms often include:
- requests returning different payloads
- hydration or client-side rendering issues
- form submission behavior changing
- race conditions that were always present but only now show up due to timing
- state or cache invalidation problems that surface in CI data
The test failure is real, but the root cause is in the app or in the test assumption, not in the package graph.
The minimum log set that actually helps
When the failure only appears in CI, the most useful logs are the ones that let you compare one run to another without guessing. You want enough context to answer four questions:
- What exact code and dependency graph ran?
- What browser and environment executed the test?
- What happened immediately before the failure?
- What differed from a passing run?
Log the dependency identity, not just the lockfile diff
A lockfile diff by itself is not enough. Record the resolved dependency identity from the CI job:
- package manager name and version
- lockfile hash
- install command and flags
- Node.js version, if relevant
- the top-level test package versions
- browser automation package versions
- browser binary version or image tag
Example CI step:
- name: Capture dependency and runtime identity
run: |
node -v
npm -v
sha256sum package-lock.json || true
npx playwright --version || true
npx playwright install --dry-run || true
For pnpm, yarn, or other package managers, capture equivalent version and lockfile details. The point is not the exact command. The point is to make it obvious whether a run is genuinely identical.
Log the browser identity
Browser tests can fail because the browser changed, not because your app changed. Log:
- browser name
- browser version
- headless or headed mode
- OS image or container base image
- screen size or viewport
- locale and timezone
- GPU or software rendering hints, if available
In Playwright, a small diagnostic helper can print runtime context before the test proceeds:
import { test } from '@playwright/test';
test.beforeEach(async ({ browser, page }, testInfo) => { console.log(JSON.stringify({ title: testInfo.title, browserName: browser.version ? ‘playwright-browser’ : ‘unknown’, viewport: page.viewportSize(), baseURL: testInfo.project.use.baseURL, workerIndex: testInfo.workerIndex })); });
The exact fields will vary by runner, but the goal is the same, capture the execution identity in a way you can compare later.
Log the app build identity
A dependency update can change the bundle without changing app source. Record:
- git commit SHA
- build number or artifact version
- environment name
- bundle hash or artifact digest, if your pipeline produces one
- feature flag snapshot, if your app depends on flags
If your CI job pulls a prebuilt artifact, make sure the artifact identity is in the test logs. A surprisingly common failure mode is “same commit, different artifact.”
Log the test context around the failure, not just the stack trace
A browser stack trace usually tells you where the symptom landed, not why. Add logs for:
- current URL
- page title
- selector or locator that timed out
- network request and response status for the relevant endpoint
- console errors and warnings
- uncaught page errors
- visible DOM state for the component under test
For Playwright, page diagnostics can be concise:
page.on('console', msg => console.log(`console:${msg.type()}:${msg.text()}`));
page.on('pageerror', err => console.log(`pageerror:${err.message}`));
page.on('response', res => {
if (!res.ok()) console.log(`response:${res.status()}:${res.url()}`);
});
This is not about logging everything forever. It is about making the failed CI run explain itself.
What to compare between a passing local run and failing CI run
Once you have logs, compare them in layers.
Compare environment first
Before reading test details, check whether the environments are actually similar:
- same operating system family
- same container image
- same Node runtime
- same package manager behavior
- same browser version
- same viewport and locale
- same secrets and feature flag inputs
If these differ, you do not have a clean local-versus-CI comparison. You have an environment mismatch.
Compare the test timing profile
Many browser failures after dependency drift are timing problems in disguise. Watch for:
- longer cold starts
- slower selectors resolving
- navigation finishing before hydration
- animation or transition delays
- API mocks resolving in a different order
Log timestamps around key steps, not just total duration. That way you can see if the failure is a race, not a functional break.
Compare network behavior
If the app talks to APIs during the test, log request URLs, statuses, and any mocked responses. A lockfile change can alter request serialization, date parsing, or headers through a dependency update. It can also change test doubles or interceptors.
A common failure mode looks like this:
- local test uses a mocked date or cached response
- CI test resolves the same endpoint a few milliseconds later
- the endpoint returns slightly different data or ordering
- a selector, snapshot, or assertion fails
That is not random. It is an unobserved dependency.
Logging by failure signature
Different browser failures benefit from different logs.
If the failure is a timeout
Log:
- the locator expression or CSS selector
- the number of matched elements, if your framework exposes it
- DOM snapshot of the relevant region
- all navigation and network events leading up to the timeout
- whether the page was still loading, idle, or transitioning
Timeouts are often selector problems, but they can also be data problems. If the UI changed because a transitive dependency changed rendering or hydration order, the locator may still be correct but too early.
If the failure is a snapshot or screenshot diff
Log:
- browser version
- font availability
- viewport size
- device scale factor
- animation or reduced motion settings
- whether the test runs headed or headless
Screenshot differences after a lockfile change are often browser drift, but not always. A layout shift caused by a changed CSS processing dependency is an app regression in disguise.
If the failure is a console or page error
Log:
- full console stack if available
- source map availability
- page errors by URL and line
- network failures that occurred beforehand
A browser error that mentions an undefined symbol, a malformed import, or a failed dynamic chunk load often points to build output drift. A runtime warning about an API or permission usually points to browser drift or test environment configuration.
If the failure is an assertion on DOM content
Log:
- the exact text the test expected
- the actual text returned
- whether the text came from server rendering or client-side hydration
- the relevant request payload and response payload
Text assertion failures are often data-dependent. If a lockfile change upgraded a date library, localization package, or serializer, the app might produce a slightly different string even though the functional behavior is unchanged.
A practical logging checklist for CI jobs
Here is a compact checklist that catches most reproducibility problems without over-instrumenting every suite.
Always log
- commit SHA
- lockfile hash
- package manager version
- Node version
- browser version
- container or VM image tag
- test runner version
- viewport and locale
- CI job ID and attempt number
Log on failure only
- console errors and warnings
- page errors
- network requests and failures near the test window
- current URL and page title
- DOM snapshot of the failing component
- screenshot and trace artifact, if supported
- build artifact digest
- feature flag values or test environment toggles
Log conditionally for flaky or high-value tests
- performance timings for major steps
- request/response bodies for key API calls, with secrets redacted
- browser storage state, if statefulness is relevant
- service worker state, if your app uses one
The best failure logs are boring in the good way. They let you compare one run to another without reconstructing the world from scratch.
How to separate framework drift from browser drift
This separation is where most teams save time.
Signs of framework drift
- failures begin immediately after dependency install or test startup
- tests that rely on locators, retries, or auto-waiting change behavior
- API mocking or intercept logic stops matching requests
- assertions on promises or async helpers start timing out
- multiple browser types fail the same way in CI
Actionable logs:
- test runner version
- browser automation library version
- matcher or assertion package version
- trace of waits, retries, and locator resolution
- any changed helper or shared fixture output
Signs of browser drift
- only one browser channel fails
- only headless mode fails
- screenshot or layout mismatches appear
- browser console warnings mention APIs or permissions
- failures correlate with image or driver updates
Actionable logs:
- browser version
- browser channel, if applicable
- OS and container base image
- window size and rendering mode
- screenshot diffs and DOM structure around the affected element
A useful tactic is to pin the browser separately from the framework for one debugging pass. If the test starts passing again under the old browser binary while keeping the new lockfile, that is a strong browser-drift signal.
How to separate app regressions from test assumptions
A lockfile change can expose an application bug, but it can also expose a brittle test.
Ask two questions:
- Did the app behavior actually change?
- Was the test relying on a detail that was never guaranteed?
Log app behavior in terms of contracts
Instead of logging only “button not found,” log the state transition you expected:
- which data should have loaded
- which API response should have driven the UI
- which route should have been active
- which element should have been visible or enabled
If the app contract is still satisfied but the test fails, the test likely assumed too much about timing or markup.
Watch for hidden coupling
Lockfile changes often reveal coupling that was already present:
- a test depends on a CSS class that changed during a build tool upgrade
- a test assumes request ordering that was never guaranteed
- a test depends on a specific date string formatting detail
- a test assumes local storage state that differs between CI shards
These are not framework bugs. They are test design debt.
A short Playwright example for targeted diagnostics
A simple fixture can capture the useful bits without drowning your logs.
import { test, expect } from '@playwright/test';
test('checkout button stays enabled', async ({ page }, testInfo) => {
page.on('console', msg => console.log(`[console] ${msg.type()}: ${msg.text()}`));
page.on('pageerror', err => console.log(`[pageerror] ${err.message}`));
await page.goto(‘/cart’);
console.log([url] ${page.url()});
console.log([title] ${await page.title()});
const button = page.getByRole(‘button’, { name: ‘Checkout’ }); await expect(button).toBeEnabled(); });
This is intentionally modest. In practice, the most useful debugging code is the code you can leave in place until the next incident without turning the suite into a fire hose.
CI configuration details worth logging once and standardizing
Some data changes so often that teams forget to log it. That is where the unpleasant surprises hide.
Container and image details
If your CI uses containers, log the image name and digest. Tags drift, digests do not. A browser binary packed into a new base image can behave differently even when your app code is untouched.
Timezone and locale
Date formatting, sorting, and relative-time text often fail only in CI because the runner uses a different timezone or locale. Log these explicitly, especially if your app or tests compare text output.
Parallelization and sharding
A failure that appears only in CI can be caused by concurrency rather than dependency drift. Log:
- number of workers
- shard index
- test order, if randomized
- shared state setup and teardown
If a lockfile change made tests slower, shard timing can shift enough to expose a race that was always present.
Caches
Cache hits and misses matter. If CI restored a stale browser cache or package cache, the lockfile update might not have been the real cause. Log cache keys and whether they were restored, rebuilt, or partially invalidated.
A debugging sequence that saves time
When browser tests fail only in CI after a dependency lockfile change, use this sequence:
- Capture environment identity, package manager, browser version, image, and build artifact.
- Re-run the failing job with failure-only diagnostics enabled.
- Compare timing, network, console, and DOM snapshots with a known good run.
- Pin one variable at a time, first browser version, then framework version, then image.
- Decide whether the root cause is framework drift, browser drift, or an app regression.
- Only after that, decide whether to update the lockfile, the test, or the app.
That order matters. If you change three variables at once, you lose the trail.
What not to log
More logging is not always better. Avoid logs that create noise without improving comparison:
- every DOM node on every step
- entire network payloads for unrelated requests
- repeated screenshots of unchanged states
- huge stack traces without a nearby timestamp or context
- secrets, tokens, or raw personal data
If a log is expensive to collect, noisy to read, and hard to compare, it probably belongs behind a conditional failure hook rather than in the happy path.
The practical takeaway
The phrase browser tests fail after lockfile change should trigger a structured investigation, not a reinstall-and-pray routine. A lockfile update can uncover framework drift, browser drift, or an app regression, and the only reliable way to tell them apart is to log the execution identity, the browser identity, the app build identity, and the immediate context around the failure.
For teams building a durable test practice, the goal is reproducibility first, then diagnosis, then cleanup. Good logs make that sequence possible. Bad logs turn every CI failure into archaeology.
If you standardize the minimum log set, capture the environment accurately, and compare failures by layer, you can usually tell whether the lockfile change introduced the bug or merely exposed it.