UNPKG

utquidem

Version:

The meta-framework suite designed from scratch for frontend-focused modern web development.

336 lines (277 loc) 10.9 kB
import { HMR_SOCK_PATH } from '@modern-js/utils'; // use require to prevent tsconfig linting client code const { __addQuery__, updateStyle, removeStyle } = require('../src/client'); // const MockedWebSocket = jest.mocked(WebSocket); describe('plugin-unbundled client test', () => { let OriginalWebSocket: any; let originalLocation: any; let mockOverlayRegistered = false; class MockElement extends HTMLElement { constructor() { super(); this.className = 'my-custom-mock-element'; } } const prepareErrorOverlay = () => { jest.spyOn(customElements, 'get').mockImplementation(() => MockElement); if (!mockOverlayRegistered) { jest .mocked(customElements.get) .mockImplementationOnce((elementName: string) => { customElements.define(elementName, MockElement); mockOverlayRegistered = true; return MockElement; }); } }; beforeAll(() => { OriginalWebSocket = global.WebSocket; originalLocation = window.location; global.WebSocket = jest.fn(() => ({})) as any; Object.defineProperty(window, 'location', { configurable: true, value: { ...originalLocation, reload: jest.fn() }, }); jest.spyOn(console, 'info').mockImplementation(); jest.spyOn(console, 'warn').mockImplementation(); jest.spyOn(console, 'error').mockImplementation(); }); afterAll(() => { global.WebSocket = OriginalWebSocket; window.location = originalLocation; }); beforeEach(() => { jest.clearAllMocks(); }); afterEach(() => { jest.useRealTimers(); // clean up JSDOM after each test document.head.innerHTML = ''; document.body.innerHTML = ''; }); it('add query test', () => { const url = 'https://example.com/api'; const firstQuery = __addQuery__(url, 'a=1'); expect(firstQuery).toEqual(`${url}?a=1`); const secondQuery = __addQuery__(firstQuery, 'b=2'); expect(secondQuery).toEqual(`${firstQuery}&b=2`); }); it('test update/remove style', () => { const testStyleCode = 'test style'; const appendChildSpy = jest.spyOn(document.head, 'appendChild'); const removeChildSpy = jest.spyOn(document.head, 'removeChild'); const styleId = 225; updateStyle(styleId, testStyleCode); // should insert style into document expect(document.head.appendChild).toHaveBeenCalledTimes(1); const [appendedElement] = appendChildSpy.mock.calls[0] as [ HTMLStyleElement, ]; expect(appendedElement.nodeName).toEqual('STYLE'); expect(appendedElement.textContent).toEqual(testStyleCode); jest.clearAllMocks(); // remove style non existing style removeStyle(styleId - 1); expect(removeChildSpy).not.toHaveBeenCalled(); removeStyle(styleId); expect(removeChildSpy).toHaveBeenCalledTimes(1); const [removedChild] = removeChildSpy.mock.calls[0]; expect(removedChild).toBe(appendedElement); }); it('test websocket initialization', () => { // must use isolate modules to re-import client code for test // this is due to client initialization on module load jest.isolateModules(() => { require('../src/client'); require('../src/constants'); expect(jest.mocked(WebSocket).mock.instances).toHaveLength(1); // check correct websocket address expect(jest.mocked(WebSocket).mock.calls[0][0]).toEqual( `${location.origin.replace(/http/, 'ws')}${HMR_SOCK_PATH}`, ); // get websocket instance const createdWebSocket = jest.mocked(WebSocket).mock.results[0] .value as WebSocket; expect(createdWebSocket).toBeTruthy(); // check websocket initialized correctly expect(createdWebSocket.onmessage).toBeTruthy(); expect(createdWebSocket.onerror).toBeTruthy(); expect(createdWebSocket.onclose).toBeTruthy(); }); }); it('test websocket reconnect', () => { jest.useFakeTimers(); jest.isolateModules(() => { require('../src/client'); // get websocket instance const createdWebSocket = jest.mocked(WebSocket).mock.results[0] .value as WebSocket; jest.mocked(WebSocket).mockClear(); const someLongTimeDuration = 6000000; // 100 minutes jest.advanceTimersByTime(someLongTimeDuration); expect(jest.mocked(WebSocket)).not.toHaveBeenCalled(); // initialization is already tested by previous test createdWebSocket.onclose!(new CloseEvent('test close')); // do not immediatly reconnect, wait a while expect(jest.mocked(WebSocket)).not.toHaveBeenCalled(); jest.advanceTimersByTime(someLongTimeDuration); expect(jest.mocked(WebSocket)).toHaveBeenCalled(); }); }); it('test websocket connection error', () => { jest.isolateModules(() => { require('../src/client'); // get websocket instance const createdWebSocket = jest.mocked(WebSocket).mock.results[0] .value as WebSocket; // initialization is already tested by previous test createdWebSocket.onerror!(new Event('test error')); }); }); // // // TODO: refactor websocket command type to enum it('test websocket "connected" message', () => { jest.isolateModules(() => { require('../src/client'); // get websocket instance const createdWebSocket = jest.mocked(WebSocket).mock.results[0] .value as WebSocket; // initialization is already tested by previous test const messageObj = { type: 'connected' }; createdWebSocket.onmessage!( new MessageEvent('test', { data: JSON.stringify(messageObj) }), ); }); }); it('test websocket "reload" message', () => { jest.isolateModules(() => { require('../src/client'); expect(window.location.reload).not.toHaveBeenCalled(); // get websocket instance const createdWebSocket = jest.mocked(WebSocket).mock.results[0] .value as WebSocket; // initialization is already tested by previous test const messageObj = { type: 'reload' }; createdWebSocket.onmessage!( new MessageEvent('test', { data: JSON.stringify(messageObj) }), ); expect(window.location.reload).toHaveBeenCalledTimes(1); }); }); it('test websocket "error" message', () => { jest.isolateModules(() => { prepareErrorOverlay(); require('../src/client'); // get websocket instance const createdWebSocket = jest.mocked(WebSocket).mock.results[0] .value as WebSocket; let insertedElements = document.querySelectorAll( '.my-custom-mock-element', ); expect(insertedElements).toHaveLength(0); // initialization is already tested by previous test const messageObj = { type: 'error', message: 'this is a test error' }; createdWebSocket.onmessage!( new MessageEvent('test', { data: JSON.stringify(messageObj) }), ); insertedElements = document.querySelectorAll('.my-custom-mock-element'); expect(insertedElements).toHaveLength(1); createdWebSocket.onmessage!( new MessageEvent('test', { data: JSON.stringify(messageObj) }), ); insertedElements = document.querySelectorAll('.my-custom-mock-element'); expect(insertedElements).toHaveLength(1); }); }); it('test websocket first "update" on error message', () => { jest.isolateModules(() => { prepareErrorOverlay(); require('../src/client'); // get websocket instance const createdWebSocket = jest.mocked(WebSocket).mock.results[0] .value as WebSocket; // initialization is already tested by previous test const messageObj: any = { type: 'error', message: 'this is a test error', }; createdWebSocket.onmessage!( new MessageEvent('test', { data: JSON.stringify(messageObj) }), ); // if there is already a error in first update // which meanings some resource load failed // so should reload page to ensure app run correctly expect(window.location.reload).not.toHaveBeenCalled(); messageObj.type = 'update'; createdWebSocket.onmessage!( new MessageEvent('test', { data: JSON.stringify(messageObj) }), ); expect(window.location.reload).toHaveBeenCalled(); // set up update message messageObj.hmrTimestamp = 123456; messageObj.changes = [{ id: 1, accepted: true }]; }); }); it('test "createHotContext"', () => { jest.isolateModules(() => { const { createHotContext } = require('../src/client'); const hotmodule = createHotContext('myModule'); expect(typeof hotmodule.accept).toBe('function'); expect(typeof hotmodule.decline).toBe('function'); expect(typeof hotmodule.dispose).toBe('function'); expect(typeof hotmodule.prune).toBe('function'); expect(typeof hotmodule.invalidate).toBe('function'); const pruneFn = jest.fn(); const disposeFn = jest.fn(); const acceptFn = jest.fn(); hotmodule.dispose(disposeFn); hotmodule.prune(pruneFn); hotmodule.accept(acceptFn); const hotmodule2 = createHotContext('myModule2'); expect(hotmodule2).not.toBe(hotmodule); expect(disposeFn).not.toHaveBeenCalled(); const hotmoduleCopy = createHotContext('myModule'); expect(hotmoduleCopy).toBe(hotmodule); expect(disposeFn).toHaveBeenCalledTimes(1); disposeFn.mockClear(); createHotContext('myModule'); expect(disposeFn).not.toHaveBeenCalled(); }); }); it('test websocket "prune" message', () => { jest.isolateModules(() => { prepareErrorOverlay(); const { createHotContext } = require('../src/client'); const hotmodule = createHotContext('myModule'); const pruneFn = jest.fn(); hotmodule.prune(pruneFn); // get websocket instance const createdWebSocket = jest.mocked(WebSocket).mock.results[0] .value as WebSocket; // initialization is already tested by previous test const messageObj: any = { type: 'prune', changes: ['myModule'], }; createdWebSocket.onmessage!( new MessageEvent('test', { data: JSON.stringify(messageObj) }), ); expect(pruneFn).toHaveBeenCalledTimes(1); }); }); it('test websocket "update" hot module', () => { // TODO: solve ESM support issue to test dynamic import }); it('test websocket "close" message', () => { jest.isolateModules(() => { require('../src/client'); // get websocket instance const createdWebSocket = jest.mocked(WebSocket).mock.results[0] .value as WebSocket; // initialization is already tested by previous test const messageObj = { type: 'close' }; createdWebSocket.onmessage!( new MessageEvent('test', { data: JSON.stringify(messageObj) }), ); }); }); });