@@ -4,6 +4,8 @@ import runBrowser from "../helpers/run-browser";
44jest . setTimeout ( 120000 ) ;
55
66const INDICATOR_ID = "webpack-dev-middleware-building-indicator" ;
7+ const INDICATOR_ENTRY = require . resolve ( "../../client-src/indicator.js" ) ;
8+ const INDICATOR_STATE_KEY = "__webpack_dev_middleware_hot_indicator_state__" ;
79
810/**
911 * @param {string } text text rendered into #app
@@ -143,3 +145,140 @@ describe("building indicator (browser)", () => {
143145 expect ( await page . evaluate ( ( ) => globalThis . __badgeSeen ) ) . toBe ( false ) ;
144146 } ) ;
145147} ) ;
148+
149+ describe ( "indicator shared state across bundled copies (browser)" , ( ) => {
150+ let hotApp ;
151+ let browser ;
152+ let page ;
153+
154+ /**
155+ * Two real bundled copies of the indicator module — one per compilation —
156+ * exposed as globals so the tests can drive both from the page.
157+ * @param {string } globalName global to expose the copy under
158+ * @returns {string } app source
159+ */
160+ const exposeIndicator = ( globalName ) =>
161+ `globalThis.${ globalName } = require(${ JSON . stringify ( INDICATOR_ENTRY ) } );` ;
162+
163+ const start = async ( ) => {
164+ hotApp = await createHotApp ( {
165+ apps : [
166+ { name : "a" , code : exposeIndicator ( "indicatorA" ) } ,
167+ { name : "b" , code : exposeIndicator ( "indicatorB" ) } ,
168+ ] ,
169+ } ) ;
170+ ( { page, browser } = await runBrowser ( ) ) ;
171+ } ;
172+
173+ afterEach ( async ( ) => {
174+ // try/finally: a rejected browser.close() must not leak the watcher,
175+ // the server, and the temp dir behind it.
176+ try {
177+ if ( browser ) {
178+ await browser . close ( ) ;
179+ }
180+ } finally {
181+ browser = undefined ;
182+ if ( hotApp ) {
183+ const closing = hotApp ;
184+ hotApp = undefined ;
185+ await closing . close ( ) ;
186+ }
187+ }
188+ } ) ;
189+
190+ it ( "drives a single badge from a second bundled copy" , async ( ) => {
191+ await start ( ) ;
192+ await page . goto ( hotApp . url ) ;
193+
194+ const state = await page . evaluate ( ( id ) => {
195+ globalThis . indicatorA . show ( "Rebuilding…" ) ;
196+ globalThis . indicatorB . show ( "Rebuilding… 42%" , 42 ) ;
197+
198+ const hosts = document . querySelectorAll ( `#${ id } ` ) ;
199+
200+ return {
201+ count : hosts . length ,
202+ text : hosts [ 0 ] ? hosts [ 0 ] . shadowRoot . textContent : "" ,
203+ } ;
204+ } , INDICATOR_ID ) ;
205+
206+ // One badge, adopted (not stacked) by the second copy, showing its text.
207+ expect ( state . count ) . toBe ( 1 ) ;
208+ expect ( state . text ) . toContain ( "42%" ) ;
209+
210+ // The state is shared, so the first copy's hide() removes the badge the
211+ // second copy updated.
212+ await page . evaluate ( ( ) => globalThis . indicatorA . hide ( ) ) ;
213+ expect (
214+ await page . evaluate ( ( id ) => document . getElementById ( id ) , INDICATOR_ID ) ,
215+ ) . toBeNull ( ) ;
216+ } ) ;
217+
218+ it ( "keeps the badge while another copy's source is still building" , async ( ) => {
219+ await start ( ) ;
220+ await page . goto ( hotApp . url ) ;
221+
222+ await page . evaluate ( ( ) => {
223+ globalThis . indicatorA . show ( "Rebuilding a…" , undefined , "a" ) ;
224+ globalThis . indicatorB . show ( "Rebuilding b…" , undefined , "b" ) ;
225+ globalThis . indicatorB . hide ( "b" ) ;
226+ } ) ;
227+ expect (
228+ await page . evaluate (
229+ ( id ) => document . getElementById ( id ) !== null ,
230+ INDICATOR_ID ,
231+ ) ,
232+ ) . toBe ( true ) ;
233+
234+ await page . evaluate ( ( ) => globalThis . indicatorA . hide ( "a" ) ) ;
235+ expect (
236+ await page . evaluate (
237+ ( id ) => document . getElementById ( id ) !== null ,
238+ INDICATOR_ID ,
239+ ) ,
240+ ) . toBe ( false ) ;
241+ } ) ;
242+
243+ it ( "ignores hiding an unknown source and still removes unconditionally" , async ( ) => {
244+ await start ( ) ;
245+ await page . goto ( hotApp . url ) ;
246+
247+ await page . evaluate ( ( ) => {
248+ globalThis . indicatorA . show ( "Rebuilding…" , undefined , "a" ) ;
249+ globalThis . indicatorA . hide ( "unknown" ) ;
250+ } ) ;
251+ expect (
252+ await page . evaluate (
253+ ( id ) => document . getElementById ( id ) !== null ,
254+ INDICATOR_ID ,
255+ ) ,
256+ ) . toBe ( true ) ;
257+
258+ // Without a source the badge is removed even with builds pending.
259+ await page . evaluate ( ( ) => globalThis . indicatorA . hide ( ) ) ;
260+ expect (
261+ await page . evaluate (
262+ ( id ) => document . getElementById ( id ) !== null ,
263+ INDICATOR_ID ,
264+ ) ,
265+ ) . toBe ( false ) ;
266+ } ) ;
267+
268+ it ( "fills state fields missing from an older copy's shape" , async ( ) => {
269+ await start ( ) ;
270+ // An older package version created a leaner shared state before the
271+ // bundles load.
272+ await page . evaluateOnNewDocument ( ( key ) => {
273+ globalThis [ key ] = { host : null } ;
274+ } , INDICATOR_STATE_KEY ) ;
275+ await page . goto ( hotApp . url ) ;
276+
277+ const count = await page . evaluate ( ( id ) => {
278+ globalThis . indicatorA . show ( "Rebuilding…" ) ;
279+ return document . querySelectorAll ( `#${ id } ` ) . length ;
280+ } , INDICATOR_ID ) ;
281+
282+ expect ( count ) . toBe ( 1 ) ;
283+ } ) ;
284+ } ) ;
0 commit comments