Skip to content

Commit 1f392a6

Browse files
bjohansebasclaude
andcommitted
test: adopt a hybrid unit + browser e2e model, shard CI, and snapshot the console
The hot client, overlay, and building indicator are now exercised end to end in headless Chrome (puppeteer 22, the last CJS release jest can require) against a real webpack watcher: updates applied without reload, the full-reload fallback, reconnect with catch-up sync after a server restart, disconnect() semantics, overlay lifecycle (Escape on the host page, HTML-in-error-message escaping, warnings, pagination), the building badge, multi-compiler ?name= filtering with per-bundle overlay slots, and the custom publish/subscribe API. Console behavior is snapshotted from the real browser console: the full info-level update cycle, logging=none/warn/error gates, and the per-bundle dedup where a sibling's clean rebuild does not re-log another bundle's unchanged warning while its own rebuild does. Each multi-compiler compilation gets its own context dir so editing one entry cannot invalidate the sibling's watcher and shuffle event order. The jsdom suites stay, slimmed to what a browser cannot exercise deterministically: process-update internals, shared state between bundled copies, option parsing, and protocol edges. The review of the new tests also surfaced a real client bug, now fixed: an EventSource error event queued behind disconnect() re-armed the reconnect timer on the orphaned wrapper — close() is now final. e2e runs serially through the new test:e2e script (four concurrent Chrome instances plus watchers starved the rest of the suite), and CI shards every test job 4x with jest --shard, uploading per-shard coverage that a final job aggregates for codecov, mirroring webpack-dev-server's setup. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 4fafede commit 1f392a6

23 files changed

Lines changed: 2418 additions & 542 deletions

.github/workflows/nodejs.yml

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,19 +56,20 @@ jobs:
5656
run: if [ -n "$(git status types --porcelain)" ]; then echo "Missing types. Update types by running 'npm run build:types'"; exit 1; else echo "All types are valid"; fi
5757

5858
test:
59-
name: Test - ${{ matrix.os }} - Node v${{ matrix.node-version }}, Webpack ${{ matrix.webpack-version }}
59+
name: Test - ${{ matrix.os }} - Node v${{ matrix.node-version }}, Webpack ${{ matrix.webpack-version }} (${{ matrix.shard }})
6060

6161
strategy:
6262
fail-fast: false
6363
matrix:
6464
os: [ubuntu-latest, windows-latest, macos-latest]
6565
node-version: [20.x, 22.x, 24.x, 25.x]
6666
webpack-version: [latest]
67+
shard: ["1/4", "2/4", "3/4", "4/4"]
6768

6869
runs-on: ${{ matrix.os }}
6970

7071
concurrency:
71-
group: test-${{ matrix.os }}-v${{ matrix.node-version }}-${{ matrix.webpack-version }}-${{ github.ref }}
72+
group: test-${{ matrix.os }}-v${{ matrix.node-version }}-${{ matrix.webpack-version }}-${{ matrix.shard }}-${{ github.ref }}
7273
cancel-in-progress: true
7374

7475
steps:
@@ -84,9 +85,33 @@ jobs:
8485
run: npm ci
8586

8687
- name: Run tests for webpack version ${{ matrix.webpack-version }}
87-
run: npm run test:coverage -- --ci
88+
run: npm run test:coverage -- --ci --shard=${{ matrix.shard }}
89+
90+
- name: Run browser e2e tests
91+
run: npm run test:e2e -- --ci --shard=${{ matrix.shard }}
92+
93+
- name: Upload coverage artifact
94+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
95+
with:
96+
name: coverage-${{ matrix.os }}-${{ matrix.node-version }}-${{ strategy.job-index }}
97+
path: coverage/lcov.info
98+
99+
upload-coverage:
100+
name: Upload coverage to codecov
101+
needs: test
102+
runs-on: ubuntu-latest
103+
104+
steps:
105+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
106+
107+
- name: Download coverage artifacts
108+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
109+
with:
110+
pattern: coverage-*
111+
path: coverage
88112

89113
- name: Submit coverage data to codecov
90114
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
91115
with:
92116
token: ${{ secrets.CODECOV_TOKEN }}
117+
directory: coverage

client-src/index.js

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ function createEventSourceWrapper() {
145145
let timer;
146146
/** @type {ReturnType<typeof setTimeout>} */
147147
let reconnectTimer;
148+
let closed = false;
148149

149150
const handleOnline = () => {
150151
log.info("connected");
@@ -162,18 +163,30 @@ function createEventSourceWrapper() {
162163
};
163164

164165
/**
165-
* Close the connection and stop the activity timer without scheduling a
166-
* reconnection. A reconnection that is already pending is cancelled too, so
167-
* closing during the reconnect window really is final.
166+
* Tear the current connection down without deciding whether it is final.
168167
*/
169-
const close = () => {
168+
const stop = () => {
170169
clearInterval(timer);
171170
clearTimeout(reconnectTimer);
172171
source.close();
173172
};
174173

174+
/**
175+
* Close for good: no reconnection is scheduled, a pending one is cancelled,
176+
* and error events already queued behind the close (the EventSource fires
177+
* one when its connection dies) can no longer resurrect the wrapper.
178+
*/
179+
const close = () => {
180+
closed = true;
181+
stop();
182+
};
183+
175184
const handleDisconnect = () => {
176-
close();
185+
if (closed) {
186+
return;
187+
}
188+
189+
stop();
177190
reconnectTimer = setTimeout(init, /** @type {number} */ (options.timeout));
178191
};
179192

0 commit comments

Comments
 (0)