// This is by no means a full test file for loader.js so feel free to add more tests. import mock from "xhr-mock" import DevLoader from "../dev-loader" import emitter from "../emitter" jest.mock(`../emitter`) jest.mock(`../socketIo`, () => { return { default: jest.fn(), getPageData: jest.fn().mockResolvedValue(), } }) describe(`Dev loader`, () => { describe(`loadPageDataJson`, () => { let originalBasePath let originalPathPrefix let xhrCount /** * @param {string} path * @param {number} status * @param {string|Object?} responseText * @param {boolean?} json */ const mockPageData = (path, status, responseText = ``, json = false) => { mock.get(`/page-data${path}/page-data.json`, (req, res) => { xhrCount++ if (json) { res.header(`content-type`, `application/json`) } return res .status(status) .body( typeof responseText === `string` ? responseText : JSON.stringify(responseText) ) }) } const defaultPayload = { path: `/mypage/`, } // replace the real XHR object with the mock XHR object before each test beforeEach(() => { originalBasePath = global.__BASE_PATH__ originalPathPrefix = global.__PATH_PREFIX__ global.__BASE_PATH__ = `` global.__PATH_PREFIX__ = `` xhrCount = 0 mock.setup() }) // put the real XHR object back and clear the mocks after each test afterEach(() => { global.__BASE_PATH__ = originalBasePath global.__PATH_PREFIX__ = originalPathPrefix mock.teardown() }) it(`should return a pageData json on success`, async () => { const devLoader = new DevLoader(null, []) mockPageData(`/mypage`, 200, defaultPayload, true) const expectation = { status: `success`, pagePath: `/mypage`, payload: defaultPayload, } expect(await devLoader.loadPageDataJson(`/mypage/`)).toEqual(expectation) expect(devLoader.pageDataDb.get(`/mypage`)).toEqual(expectation) expect(xhrCount).toBe(1) }) it(`should return a pageData json on success without contentType`, async () => { const devLoader = new DevLoader(null, []) mockPageData(`/mypage`, 200, defaultPayload) const expectation = { status: `success`, pagePath: `/mypage`, payload: defaultPayload, } expect(await devLoader.loadPageDataJson(`/mypage/`)).toEqual(expectation) expect(devLoader.pageDataDb.get(`/mypage`)).toEqual(expectation) expect(xhrCount).toBe(1) }) it(`should return a pageData json with an empty compilation hash (gatsby develop)`, async () => { const devLoader = new DevLoader(null, []) const payload = { ...defaultPayload, webpackCompilationHash: `` } mockPageData(`/mypage`, 200, payload) const expectation = { status: `success`, pagePath: `/mypage`, payload, } expect(await devLoader.loadPageDataJson(`/mypage/`)).toEqual(expectation) expect(devLoader.pageDataDb.get(`/mypage`)).toEqual(expectation) expect(xhrCount).toBe(1) }) it(`should load a 404 page when page-path file is not a gatsby json`, async () => { const devLoader = new DevLoader(null, []) const payload = { ...defaultPayload, path: `/404.html/` } mockPageData(`/unknown-page`, 200, { random: `string` }, true) mockPageData(`/404.html`, 200, payload, true) const expectation = { status: `success`, pagePath: `/404.html`, notFound: true, payload, } expect(await devLoader.loadPageDataJson(`/unknown-page/`)).toEqual( expectation ) expect(devLoader.pageDataDb.get(`/unknown-page`)).toEqual(expectation) expect(xhrCount).toBe(2) }) it(`should load a 404 page when page-path file is not a json`, async () => { const devLoader = new DevLoader(null, []) const payload = { ...defaultPayload, path: `/404.html/` } mockPageData(`/unknown-page`, 200) mockPageData(`/404.html`, 200, payload, true) const expectation = { status: `success`, pagePath: `/404.html`, notFound: true, payload, } expect(await devLoader.loadPageDataJson(`/unknown-page/`)).toEqual( expectation ) expect(devLoader.pageDataDb.get(`/unknown-page`)).toEqual(expectation) expect(xhrCount).toBe(2) }) it(`should load a 404 page when path returns a 404`, async () => { const devLoader = new DevLoader(null, []) const payload = { ...defaultPayload, path: `/404.html/` } mockPageData(`/unknown-page`, 200) mockPageData(`/404.html`, 200, payload, true) const expectation = { status: `success`, pagePath: `/404.html`, notFound: true, payload, } expect(await devLoader.loadPageDataJson(`/unknown-page/`)).toEqual( expectation ) expect(devLoader.pageDataDb.get(`/unknown-page`)).toEqual(expectation) expect(xhrCount).toBe(2) }) it(`should return the dev-404-page when no 404 page can be found`, async () => { const devLoader = new DevLoader(null, []) const payload = { ...defaultPayload, path: `/dev-404-page/` } mockPageData(`/unknown-page`, 404) mockPageData(`/404.html`, 404) mockPageData(`/dev-404-page`, 200, payload, true) const expectation = { status: `success`, pagePath: `/dev-404-page`, notFound: true, payload, } expect(await devLoader.loadPageDataJson(`/unknown-page/`)).toEqual( expectation ) expect(devLoader.pageDataDb.get(`/unknown-page`)).toEqual({ notFound: true, pagePath: `/404.html`, status: `failure`, }) expect(xhrCount).toBe(3) }) it(`should return an error when status is 500`, async () => { const devLoader = new DevLoader(null, []) mockPageData(`/error-page`, 500) const expectation = { status: `error`, pagePath: `/error-page`, } expect(await devLoader.loadPageDataJson(`/error-page/`)).toEqual( expectation ) expect(devLoader.pageDataDb.get(`/error-page`)).toEqual(expectation) expect(xhrCount).toBe(1) }) it(`should retry 3 times before returning an error`, async () => { const devLoader = new DevLoader(null, []) mockPageData(`/blocked-page`, 0) const expectation = { status: `error`, retries: 3, pagePath: `/blocked-page`, } expect(await devLoader.loadPageDataJson(`/blocked-page/`)).toEqual( expectation ) expect(devLoader.pageDataDb.get(`/blocked-page`)).toEqual(expectation) expect(xhrCount).toBe(4) }) it(`should recover if we get 1 failure`, async () => { const devLoader = new DevLoader(null, []) const payload = { path: `/blocked-page/`, } let xhrCount = 0 mock.get(`/page-data/blocked-page/page-data.json`, (req, res) => { if (xhrCount++ === 0) { return res.status(0).body(``) } else { res.header(`content-type`, `application/json`) return res.status(200).body(JSON.stringify(payload)) } }) const expectation = { status: `success`, retries: 1, pagePath: `/blocked-page`, payload, } expect(await devLoader.loadPageDataJson(`/blocked-page/`)).toEqual( expectation ) expect(devLoader.pageDataDb.get(`/blocked-page`)).toEqual(expectation) expect(xhrCount).toBe(2) }) it(`shouldn't load pageData multiple times`, async () => { const devLoader = new DevLoader(null, []) mockPageData(`/mypage`, 200, defaultPayload, true) const expectation = await devLoader.loadPageDataJson(`/mypage/`) expect(await devLoader.loadPageDataJson(`/mypage/`)).toBe(expectation) expect(xhrCount).toBe(1) }) }) describe(`loadPage`, () => { const createSyncRequires = components => { return { components, } } let originalPathPrefix beforeEach(() => { originalPathPrefix = global.__PATH_PREFIX__ global.__PATH_PREFIX__ = `` mock.setup() mock.get(`/page-data/app-data.json`, (req, res) => res .status(200) .header(`content-type`, `application/json`) .body( JSON.stringify({ webpackCompilationHash: `123`, }) ) ) emitter.emit.mockReset() }) afterEach(() => { global.__PATH_PREFIX__ = originalPathPrefix mock.teardown() }) it(`should be successful when component can be loaded`, async () => { const syncRequires = createSyncRequires({ chunk: `instance`, }) const devLoader = new DevLoader(syncRequires, []) const pageData = { path: `/mypage/`, componentChunkName: `chunk`, result: { pageContext: `something something`, }, } devLoader.loadPageDataJson = jest.fn(() => Promise.resolve({ payload: pageData, status: `success`, }) ) const expectation = await devLoader.loadPage(`/mypage/`) expect(expectation).toMatchSnapshot() expect(Object.keys(expectation)).toEqual([`component`, `json`, `page`]) expect(devLoader.pageDb.get(`/mypage`)).toEqual( expect.objectContaining({ payload: expectation, status: `success`, }) ) expect(emitter.emit).toHaveBeenCalledTimes(1) expect(emitter.emit).toHaveBeenCalledWith(`onPostLoadPageResources`, { page: expectation, pageResources: expectation, }) }) it(`should set not found on finalResult`, async () => { const syncRequires = createSyncRequires({ chunk: `instance`, }) const devLoader = new DevLoader(syncRequires, []) const pageData = { path: `/mypage/`, componentChunkName: `chunk`, } devLoader.loadPageDataJson = jest.fn(() => Promise.resolve({ payload: pageData, status: `success`, notFound: true, }) ) await devLoader.loadPage(`/mypage/`) const expectation = devLoader.pageDb.get(`/mypage`) expect(expectation).toHaveProperty(`notFound`, true) expect(emitter.emit).toHaveBeenCalledTimes(1) expect(emitter.emit).toHaveBeenCalledWith(`onPostLoadPageResources`, { page: expectation.payload, pageResources: expectation.payload, }) }) it(`should return an error when component cannot be loaded`, async () => { const syncRequires = createSyncRequires({ chunk: false, }) const devLoader = new DevLoader(syncRequires, []) const pageData = { path: `/mypage/`, componentChunkName: `chunk`, } devLoader.loadPageDataJson = jest.fn(() => Promise.resolve({ payload: pageData, status: `success`, }) ) await devLoader.loadPage(`/mypage/`) const expectation = devLoader.pageDb.get(`/mypage`) expect(expectation).toHaveProperty(`status`, `error`) expect(emitter.emit).toHaveBeenCalledTimes(0) }) it(`should return an error pageData contains an error`, async () => { const syncRequires = createSyncRequires({ chunk: `instance`, }) const devLoader = new DevLoader(syncRequires, []) const pageData = { path: `/mypage/`, componentChunkName: `chunk`, } devLoader.loadPageDataJson = jest.fn(() => Promise.resolve({ payload: pageData, status: `error`, }) ) expect(await devLoader.loadPage(`/mypage/`)).toEqual({ status: `error` }) expect(devLoader.pageDb.size).toBe(0) expect(emitter.emit).toHaveBeenCalledTimes(0) }) it(`should throw an error when 404 cannot be fetched`, async () => { const devLoader = new DevLoader(null, []) devLoader.loadPageDataJson = jest.fn(() => Promise.resolve({ status: `failure`, }) ) try { await devLoader.loadPage(`/404.html/`) } catch (err) { expect(err.message).toEqual( expect.stringContaining(`404 page could not be found`) ) } expect(devLoader.pageDb.size).toBe(0) expect(emitter.emit).toHaveBeenCalledTimes(0) }) it(`should cache the result of loadPage`, async () => { const syncRequires = createSyncRequires({ chunk: `instance`, }) const devLoader = new DevLoader(syncRequires, []) devLoader.loadPageDataJson = jest.fn(() => Promise.resolve({ payload: { componentChunkName: `chunk`, }, status: `success`, }) ) const expectation = await devLoader.loadPage(`/mypage/`) expect(await devLoader.loadPage(`/mypage/`)).toBe(expectation) expect(devLoader.loadPageDataJson).toHaveBeenCalledTimes(1) }) }) describe(`loadPageSync`, () => { it(`returns page resources when already fetched`, () => { const devLoader = new DevLoader(null, []) devLoader.pageDb.set(`/mypage`, { payload: true }) expect(devLoader.loadPageSync(`/mypage/`)).toBe(true) }) it(`returns page resources when already fetched`, () => { const devLoader = new DevLoader(null, []) expect(devLoader.loadPageSync(`/mypage/`)).toBeUndefined() }) }) describe(`prefetch`, () => { const flushPromises = () => new Promise(resolve => setImmediate(resolve)) it(`shouldn't prefetch when shouldPrefetch is false`, () => { const devLoader = new DevLoader(null, []) devLoader.shouldPrefetch = jest.fn(() => false) devLoader.doPrefetch = jest.fn() devLoader.apiRunner = jest.fn() expect(devLoader.prefetch(`/mypath/`)).toBe(false) expect(devLoader.shouldPrefetch).toHaveBeenCalledWith(`/mypath/`) expect(devLoader.apiRunner).not.toHaveBeenCalled() expect(devLoader.doPrefetch).not.toHaveBeenCalled() }) it(`should trigger custom prefetch logic when core is disabled`, () => { const devLoader = new DevLoader(null, []) devLoader.shouldPrefetch = jest.fn(() => true) devLoader.doPrefetch = jest.fn() devLoader.apiRunner = jest.fn() devLoader.prefetchDisabled = true expect(devLoader.prefetch(`/mypath/`)).toBe(false) expect(devLoader.shouldPrefetch).toHaveBeenCalledWith(`/mypath/`) expect(devLoader.apiRunner).toHaveBeenCalledWith(`onPrefetchPathname`, { pathname: `/mypath/`, }) expect(devLoader.doPrefetch).not.toHaveBeenCalled() }) it(`should prefetch when not yet triggered`, async () => { jest.useFakeTimers() const devLoader = new DevLoader(null, []) devLoader.shouldPrefetch = jest.fn(() => true) devLoader.apiRunner = jest.fn() devLoader.doPrefetch = jest.fn(() => Promise.resolve({})) expect(devLoader.prefetch(`/mypath/`)).toBe(true) // wait for doPrefetchPromise await flushPromises() expect(devLoader.apiRunner).toHaveBeenCalledWith(`onPrefetchPathname`, { pathname: `/mypath/`, }) expect(devLoader.apiRunner).toHaveBeenNthCalledWith( 2, `onPostPrefetchPathname`, { pathname: `/mypath/`, } ) }) it(`should only run apis once`, async () => { const devLoader = new DevLoader(null, []) devLoader.shouldPrefetch = jest.fn(() => true) devLoader.apiRunner = jest.fn() devLoader.doPrefetch = jest.fn(() => Promise.resolve({})) expect(devLoader.prefetch(`/mypath/`)).toBe(true) expect(devLoader.prefetch(`/mypath/`)).toBe(true) // wait for doPrefetchPromise await flushPromises() expect(devLoader.apiRunner).toHaveBeenCalledTimes(2) expect(devLoader.apiRunner).toHaveBeenNthCalledWith( 1, `onPrefetchPathname`, expect.anything() ) expect(devLoader.apiRunner).toHaveBeenNthCalledWith( 2, `onPostPrefetchPathname`, expect.anything() ) }) }) })