diff --git a/src/hmr/hotModuleReplacement.js b/src/hmr/hotModuleReplacement.js index 6d6611b1..802c0a12 100644 --- a/src/hmr/hotModuleReplacement.js +++ b/src/hmr/hotModuleReplacement.js @@ -120,7 +120,11 @@ function updateCss(el, url) { newEl.href = `${url}?${Date.now()}`; - el.parentNode.appendChild(newEl); + if (el.nextSibling) { + el.parentNode.insertBefore(newEl, el.nextSibling); + } else { + el.parentNode.appendChild(newEl); + } } function getReloadUrl(href, src) { @@ -160,6 +164,7 @@ function reloadStyle(src) { if (url) { updateCss(el, url); + loaded = true; } }); @@ -182,18 +187,8 @@ function reloadAll() { function isUrlRequest(url) { // An URL is not an request if - // 1. It's an absolute url - if (/^[a-z][a-z0-9+.-]*:/i.test(url)) { - return false; - } - - // 2. It's a protocol-relative - if (/^\/\//.test(url)) { - return false; - } - - // 3. Its a `#` link - if (/^#/.test(url)) { + // It is not http or https + if (!/^https?:/i.test(url)) { return false; } diff --git a/test/HMR.test.js b/test/HMR.test.js new file mode 100644 index 00000000..be56e01d --- /dev/null +++ b/test/HMR.test.js @@ -0,0 +1,284 @@ +/* eslint-env browser */ +/* eslint-disable no-console */ + +import hotModuleReplacement from '../src/hmr/hotModuleReplacement'; + +function getLoadEvent() { + const event = document.createEvent('Event'); + + event.initEvent('load', false, false); + + return event; +} + +function getErrorEvent() { + const event = document.createEvent('Event'); + + event.initEvent('error', false, false); + + return event; +} + +describe('HMR', () => { + let consoleMock = null; + + beforeEach(() => { + consoleMock = jest.spyOn(console, 'log').mockImplementation(() => () => {}); + + jest.spyOn(Date, 'now').mockImplementation(() => 1479427200000); + + document.head.innerHTML = ''; + document.body.innerHTML = ''; + }); + + afterEach(() => { + consoleMock.mockClear(); + }); + + it('should works', (done) => { + const update = hotModuleReplacement('./src/style.css', {}); + + update(); + + setTimeout(() => { + expect(console.log.mock.calls[0][0]).toMatchSnapshot(); + + const links = Array.prototype.slice.call( + document.querySelectorAll('link') + ); + + expect(links[0].visited).toBe(true); + expect(document.head.innerHTML).toMatchSnapshot(); + + links[1].dispatchEvent(getLoadEvent()); + + expect(links[1].isLoaded).toBe(true); + + done(); + }, 100); + }); + + it('should works with multiple updates', (done) => { + const update = hotModuleReplacement('./src/style.css', {}); + + update(); + + setTimeout(() => { + expect(console.log.mock.calls[0][0]).toMatchSnapshot(); + + const links = Array.prototype.slice.call( + document.querySelectorAll('link') + ); + + expect(links[0].visited).toBe(true); + expect(document.head.innerHTML).toMatchSnapshot(); + + links[1].dispatchEvent(getLoadEvent()); + + expect(links[1].isLoaded).toBe(true); + + jest.spyOn(Date, 'now').mockImplementation(() => 1479427200001); + + const update2 = hotModuleReplacement('./src/style.css', {}); + + update2(); + + setTimeout(() => { + const links2 = Array.prototype.slice.call( + document.querySelectorAll('link') + ); + + expect(links2[0].visited).toBe(true); + expect(links2[0].isLoaded).toBe(true); + expect(document.head.innerHTML).toMatchSnapshot(); + + links2[1].dispatchEvent(getLoadEvent()); + + expect(links2[1].isLoaded).toBe(true); + + done(); + }, 100); + }, 100); + }); + + it('should reloads with locals', (done) => { + const update = hotModuleReplacement('./src/style.css', { + locals: { foo: 'bar' }, + }); + + update(); + + setTimeout(() => { + expect(console.log.mock.calls[0][0]).toMatchSnapshot(); + + const links = Array.prototype.slice.call( + document.querySelectorAll('link') + ); + + expect(links[0].visited).toBe(true); + expect(document.head.innerHTML).toMatchSnapshot(); + + links[1].dispatchEvent(getLoadEvent()); + + expect(links[1].isLoaded).toBe(true); + + done(); + }, 100); + }); + + it('should reloads with reloadAll option', (done) => { + const update = hotModuleReplacement('./src/style.css', { + reloadAll: true, + }); + + update(); + + setTimeout(() => { + expect(console.log.mock.calls[0][0]).toMatchSnapshot(); + + const links = Array.prototype.slice.call( + document.querySelectorAll('link') + ); + + expect(links[0].visited).toBe(true); + expect(document.head.innerHTML).toMatchSnapshot(); + + links[1].dispatchEvent(getLoadEvent()); + + expect(links[1].isLoaded).toBe(true); + + done(); + }, 100); + }); + + it('should reloads with non http/https link href', (done) => { + document.head.innerHTML = + ''; + + const update = hotModuleReplacement('./src/style.css', {}); + + update(); + + setTimeout(() => { + expect(console.log.mock.calls[0][0]).toMatchSnapshot(); + + const links = Array.prototype.slice.call( + document.querySelectorAll('link') + ); + + expect(links[0].visited).toBe(true); + expect(document.head.innerHTML).toMatchSnapshot(); + + links[1].dispatchEvent(getLoadEvent()); + + expect(links[1].isLoaded).toBe(true); + expect(links[2].visited).toBeUndefined(); + + done(); + }, 100); + }); + + it('should reloads with # link href', (done) => { + document.head.innerHTML = + ''; + + const update = hotModuleReplacement('./src/style.css', {}); + + update(); + + setTimeout(() => { + expect(console.log.mock.calls[0][0]).toMatchSnapshot(); + + const links = Array.prototype.slice.call( + document.querySelectorAll('link') + ); + + expect(links[0].visited).toBe(true); + expect(document.head.innerHTML).toMatchSnapshot(); + + links[1].dispatchEvent(getLoadEvent()); + + expect(links[1].isLoaded).toBe(true); + expect(links[2].visited).toBeUndefined(); + + done(); + }, 100); + }); + + it('should reloads with link without href', (done) => { + document.head.innerHTML = + ''; + + const update = hotModuleReplacement('./src/style.css', {}); + + update(); + + setTimeout(() => { + expect(console.log.mock.calls[0][0]).toMatchSnapshot(); + + const links = Array.prototype.slice.call( + document.querySelectorAll('link') + ); + + expect(links[0].visited).toBe(true); + expect(document.head.innerHTML).toMatchSnapshot(); + + links[1].dispatchEvent(getLoadEvent()); + + expect(links[1].isLoaded).toBe(true); + expect(links[2].visited).toBeUndefined(); + + done(); + }, 100); + }); + + it('should reloads with absolute remove url', (done) => { + document.head.innerHTML = + ''; + + const update = hotModuleReplacement('./src/style.css', {}); + + update(); + + setTimeout(() => { + expect(console.log.mock.calls[0][0]).toMatchSnapshot(); + + const links = Array.prototype.slice.call( + document.querySelectorAll('link') + ); + + expect(links[0].visited).toBe(true); + expect(document.head.innerHTML).toMatchSnapshot(); + + links[1].dispatchEvent(getLoadEvent()); + + expect(links[1].isLoaded).toBe(true); + expect(links[2].visited).toBeUndefined(); + + done(); + }, 100); + }); + + it('should handle error event', (done) => { + const update = hotModuleReplacement('./src/style.css', {}); + + update(); + + setTimeout(() => { + expect(console.log.mock.calls[0][0]).toMatchSnapshot(); + + const links = Array.prototype.slice.call( + document.querySelectorAll('link') + ); + + expect(links[0].visited).toBe(true); + expect(document.head.innerHTML).toMatchSnapshot(); + + links[1].dispatchEvent(getErrorEvent()); + + expect(links[1].isLoaded).toBe(true); + + done(); + }, 100); + }); +}); diff --git a/test/__snapshots__/HMR.test.js.snap b/test/__snapshots__/HMR.test.js.snap new file mode 100644 index 00000000..269260d0 --- /dev/null +++ b/test/__snapshots__/HMR.test.js.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`HMR should handle error event 1`] = `"[HMR] css reload %s"`; + +exports[`HMR should handle error event 2`] = `""`; + +exports[`HMR should reloads with # link href 1`] = `"[HMR] css reload %s"`; + +exports[`HMR should reloads with # link href 2`] = `""`; + +exports[`HMR should reloads with absolute remove url 1`] = `"[HMR] css reload %s"`; + +exports[`HMR should reloads with absolute remove url 2`] = `""`; + +exports[`HMR should reloads with link without href 1`] = `"[HMR] css reload %s"`; + +exports[`HMR should reloads with link without href 2`] = `""`; + +exports[`HMR should reloads with locals 1`] = `"[HMR] Detected local css modules. Reload all css"`; + +exports[`HMR should reloads with locals 2`] = `""`; + +exports[`HMR should reloads with non http/https link href 1`] = `"[HMR] css reload %s"`; + +exports[`HMR should reloads with non http/https link href 2`] = `""`; + +exports[`HMR should reloads with reloadAll option 1`] = `"[HMR] Reload all css"`; + +exports[`HMR should reloads with reloadAll option 2`] = `""`; + +exports[`HMR should works 1`] = `"[HMR] css reload %s"`; + +exports[`HMR should works 2`] = `""`; + +exports[`HMR should works with multiple updates 1`] = `"[HMR] css reload %s"`; + +exports[`HMR should works with multiple updates 2`] = `""`; + +exports[`HMR should works with multiple updates 3`] = `""`;