Skip to content

fix: keep reloading after a cancelled "Leave site?" dialog - #5744

Merged
alexander-akait merged 4 commits into
mainfrom
fix/beforeunload-cancel-blocks-reload
Sep 19, 2026
Merged

alexander-akait merged 4 commits into
mainfrom
fix/beforeunload-cancel-blocks-reload

Conversation

@alexander-akait

@alexander-akait alexander-akait commented Sep 19, 2026

Copy link
Copy Markdown
Member

Summary

Fixes #5571. beforeunload sets isUnloading and nothing ever clears it, but that event only means the page may be leaving — any listener can cancel it. When the user clicks "Cancel" on the dialog, the page stays with the flag set, so reloadApp returns early for the rest of its life and every hot update and live reload is silently dropped until a manual refresh. Form guards and editor-integration tools install such a listener routinely.

No event announces a cancelled unload. Measured in Chrome:

events
cancelled beforeunload, blur, focus — then nothing, page alive
confirmed beforeunload, blur, focus, pagehide, visibilitychange:hidden, unload

So pagehide is what separates them, but it only arrives once the next document has loaded — with the response delayed 1s and 4s, the beforeunloadpagehide gap measured 1023ms and 4024ms. Its absence therefore can't be told from a slow navigation at any fixed moment, which is the wall #5574 hit.

The suppression is now a grace period rather than permanent, with pagehide making it permanent once the page really is going. The length is an explicit trade, documented at the constant: too short and a slow navigation can still be interrupted by a reload, which is what #841 added this for; too long and the cancelled dialog keeps dropping updates. pageshow clears it too, for the same bug reached the other way — navigate away, hit Back, and the page restored from the back/forward cache is the same script with the flag left set.

What kind of change does this PR introduce?

fix

Did you add tests for your changes?

Yes — five cases under test/client/index.test.js, covering a reload with no unload in progress, suppression while the page may be leaving, the cancelled unload lapsing, pagehide keeping it suppressed, and the back/forward-cache restore. The two that assert the fix fail on main and the two asserting preserved behaviour still pass. Also verified end to end in Chrome against a real server, cancelling the dialog and then editing a file: on main the client logs App updated. Recompiling... and stops there; with this change it goes on to App updated. Reloading... and the page reloads.

Does this PR introduce a breaking change?

No.

If relevant, what needs to be documented once your changes are merged or what have you already documented?

n/a

Use of AI

Claude Code investigated the report, measured the event sequences and timings in Chrome that the fix turns on, wrote the change and the tests, and ran the before/after verification above.


Generated by Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Live reload now continues working correctly after cancelling a “Leave site?” dialog.
    • Reload suppression remains accurate during page navigation and restoration from the browser back/forward cache.
    • Reload behavior now recovers correctly after the unload grace period expires.
  • Tests

    • Added coverage for reload behavior during pending unload prompts, page transitions, grace-period expiry, and page restoration.
    • Improved overlay checks when displaying successive errors and confirming recovery after fixes.

`beforeunload` set `isUnloading` and nothing ever cleared it. The event only
means the page *may* be leaving, though: any listener can cancel it, and when
the user then clicks "Cancel" the page stays with the flag set, so `reloadApp`
returns early for the rest of its life and every hot update and live reload is
silently dropped until a manual refresh. Form guards and editor-integration
tools set such a listener routinely.

There is no event that announces a cancelled unload. Measured in Chrome, one
fires `beforeunload`, `blur` and `focus` — and a confirmed unload fires those
three too, then `pagehide`. So `pagehide` is what separates them, but it only
arrives once the next document has loaded: with the response delayed 1s and 4s,
`beforeunload`-to-`pagehide` measured 1023ms and 4024ms. Its absence therefore
cannot be told from a slow navigation at any fixed moment.

The suppression is now a grace period instead of permanent, with `pagehide`
making it permanent once the page really is going. The length trades the two
failures against each other — too short and a slow navigation can still be
interrupted by a reload, which is what #544 added this for; too long and the
cancelled dialog keeps dropping updates.

`pageshow` clears it as well, for the same bug reached the other way: navigate
away, come back, and the page restored from the back/forward cache is the same
script with the flag left set.

Verified end to end in Chrome against the real server, cancelling the dialog
and then editing a file: on `main` the client logs "App updated. Recompiling..."
and stops there; with this change it goes on to "App updated. Reloading..." and
the page reloads.

Fixes #5571

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
@changeset-bot

changeset-bot Bot commented Sep 19, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4990db8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
webpack-dev-server Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Sep 19, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 30eb393e-81ea-4a11-8acd-8b1442f9add4

📥 Commits

Reviewing files that changed from the base of the PR and between 513b9eb and 4990db8.

📒 Files selected for processing (1)
  • test/e2e/overlay.test.js

Included review availability: Your plan provides up to 8 included reviews per hour; 5 remain after this review.


Walkthrough

The client now clears its unload grace timer and resets isUnloading after 2000 ms. pagehide marks the page as unloading, and pageshow clears that state. New tests cover reload behavior before, during, and after these events. Overlay tests now poll for document reloads and changed overlay HTML. A changeset declares a patch release for webpack-dev-server.

Priority: ➖ Normal

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The changes to test/e2e/overlay.test.js alter overlay-test timing and assertions. They wait for a live-reloaded document and compare successive overlay contents. They do not test cancelled `beforeun… Remove the test/e2e/overlay.test.js changes from this pull request, or move them to a separate pull request.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: restoring reload behavior after a cancelled "Leave site?" dialog.
Linked Issues check ✅ Passed Issue #5571 requires recovery after a cancelled beforeunload dialog and support for later HMR or live reload updates. client-src/index.js adds a 2000 ms grace-period reset for status.isUnloading
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 3 files.
Full details: Out of Scope Changes check

Explanation

The changes to test/e2e/overlay.test.js alter overlay-test timing and assertions. They wait for a live-reloaded document and compare successive overlay contents. They do not test cancelled beforeunload dialogs, isUnloading, or recovery of HMR or live reload after cancellation. The available diff therefore shows unrelated test maintenance in this pull request.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Warning

Some tools did not complete. Review the errors below.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

test/e2e/overlay.test.js

ESLint failed to execute (timeout).


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

alexander-akait and others added 2 commits September 19, 2026 12:46
`overlay.test.js:447` timed out for its full two minutes on macOS/Node 26,
waiting for `#webpack-dev-server-client-overlay` to become hidden after a
second, different build error is written.

That hidden state is not one the test can rely on seeing. The client dismisses
the overlay when the `invalid` message announces the rebuild and shows it again
from the errors that rebuild produces, so it exists only for the length of the
build in between — and only if `invalid` arrives separately at all. Waiting for
it is a race, and losing it costs the whole timeout rather than reporting
anything useful.

It is also not what the test is about. Its name says "then show other error",
so what matters is the second error reaching the overlay, whether or not the
overlay flickered getting there. It now waits for the overlay's content to
change instead, which holds however the client gets there.

The snapshots are untouched, so the same page and overlay HTML is still
asserted. Point the second write at the same content as the first, so no new
error is produced, and the test fails — it is the wait that changed, not what
is being checked.

The file's six other `hidden: true` waits are left alone: those follow an
Escape keypress or a write of the good fixture, where hidden is where the
overlay stays.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
macOS/Node 24 failed with "Execution context was destroyed, most likely
because of a navigation" at `overlay.test.js:486`, 15s in rather than on a
timeout.

Restoring the good fixture produces a build that succeeds, so the client does
two things in sequence: it dismisses the overlay when `invalid` announces the
rebuild, then live reloads the page when the build lands. The test read the
page on the first of those — `waitForSelector(hidden)` resolves on the dismiss
— which leaves the read racing the reload that follows, and losing it destroys
the execution context underneath `page.evaluate`.

Both tests that restore the fixture now read the page until it settles, with
the same retry covering the reload, instead of reading once the moment the
overlay goes. Same shape as the previous commit: the wait was keyed on a
transient step rather than the state being asserted.

Snapshots are untouched again, so the same page HTML is asserted, and the
overlay suite passes three runs in a row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 9f2537b7-87be-4e75-9412-bf3b5da55d3f

📥 Commits

Reviewing files that changed from the base of the PR and between f6a0706 and 513b9eb.

📒 Files selected for processing (1)
  • test/e2e/overlay.test.js

Included review availability: Your plan provides up to 8 included reviews per hour; 5 remain after this review.

Comment thread test/e2e/overlay.test.js
`CodeRabbit` pointed out that the previous commit's wait can settle before the
live reload, so the page it snapshots is the document from before the fix.
Measured, and it is right: marking the document and reading it the moment the
overlay goes always caught the pre-reload one.

The HTML happens to be identical either way here, so nothing was flaky or
wrong, but the test claims to check the page after the fix and was checking the
page before it — and reading on the dismiss is also what left the evaluate
racing the reload in the first place.

Both tests now mark the document before restoring the fixture and wait for one
that no longer carries the mark, so the capture is of the reloaded page.
Point that wait at a value no document will have and both fail, where before
the same edit left them passing.

Snapshots are untouched, and the suite passes three runs in a row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
@alexander-akait
alexander-akait merged commit 1dd13c6 into main Sep 19, 2026
14 checks passed
@alexander-akait
alexander-akait deleted the fix/beforeunload-cancel-blocks-reload branch September 19, 2026 14:02
@codecov

codecov Bot commented Sep 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.65%. Comparing base (d09c426) to head (4990db8).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5744      +/-   ##
==========================================
+ Coverage   90.29%   90.65%   +0.36%     
==========================================
  Files          13       13              
  Lines        6241     6271      +30     
==========================================
+ Hits         5635     5685      +50     
+ Misses        606      586      -20     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

isUnloading flag stuck as true after user cancels "Leave site?" dialog, blocking HMR/Live Reload

1 participant