From ec10aab56f379367ba587abb0905ec454fda679b Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Tue, 28 Jul 2020 11:23:10 +0100 Subject: [PATCH] tests(gatsby): Add unit tests for develop state machine (#26051) * feat(gatsby): Add top-level error handling to state machine * Add initial tests * Add tests for top-level machine * Test error handling * Add post-bootstrap to tests --- .../src/state-machines/__tests__/develop.ts | 263 ++++++++++++++++++ .../src/state-machines/develop/index.ts | 4 +- 2 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 packages/gatsby/src/state-machines/__tests__/develop.ts diff --git a/packages/gatsby/src/state-machines/__tests__/develop.ts b/packages/gatsby/src/state-machines/__tests__/develop.ts new file mode 100644 index 0000000000000..fd842ae79ecc5 --- /dev/null +++ b/packages/gatsby/src/state-machines/__tests__/develop.ts @@ -0,0 +1,263 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { developMachine } from "../develop" +import { interpret } from "xstate" +import { IProgram } from "../../commands/types" + +const actions = { + assignStoreAndWorkerPool: jest.fn(), + assignServiceResult: jest.fn(), + callApi: jest.fn(), + finishParentSpan: jest.fn(), + saveDbState: jest.fn(), + logError: jest.fn(), + panic: jest.fn(), +} + +const services = { + initialize: jest.fn(), + initializeData: jest.fn(), + reloadData: jest.fn(), + runQueries: jest.fn(), + startWebpackServer: jest.fn(), + recompile: jest.fn(), + waitForMutations: jest.fn(), + recreatePages: jest.fn(), +} + +const throwService = async (): Promise => { + throw new Error(`fail`) +} + +const rejectService = async (): Promise => Promise.reject(`fail`) + +const machine = developMachine.withConfig( + { + actions, + services, + }, + { + program: {} as IProgram, + } +) + +const tick = (): Promise => new Promise(resolve => setTimeout(resolve, 0)) + +const resetMocks = (mocks: Record): void => + Object.values(mocks).forEach(mock => mock.mockReset()) + +const resetAllMocks = (): void => { + resetMocks(services) + resetMocks(actions) +} + +describe(`the top-level develop state machine`, () => { + beforeEach(() => { + resetAllMocks() + }) + + it(`initialises`, async () => { + const service = interpret(machine) + service.start() + expect(service.state.value).toBe(`initializing`) + }) + + it(`runs node mutation during initialising data state`, () => { + const payload = { foo: 1 } + const service = interpret(machine) + + service.start() + service.send(`done.invoke.initialize`) + expect(service.state.value).toBe(`initializingData`) + service.send(`ADD_NODE_MUTATION`, payload) + expect(actions.callApi).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ type: `ADD_NODE_MUTATION`, ...payload }), + expect.anything() + ) + expect(service.state.context.nodesMutatedDuringQueryRun).toBeTruthy() + }) + + it(`marks source file as dirty during node sourcing`, () => { + const service = interpret(machine) + + service.start() + expect(service.state.value).toBe(`initializing`) + service.send(`done.invoke.initialize`) + expect(service.state.value).toBe(`initializingData`) + expect(service.state.context.sourceFilesDirty).toBeFalsy() + service.send(`SOURCE_FILE_CHANGED`) + expect(service.state.context.sourceFilesDirty).toBeTruthy() + }) + + // This is current behaviour, but it will be queued in future + it(`handles a webhook during node sourcing`, () => { + const webhookBody = { foo: 1 } + const service = interpret(machine) + service.start() + expect(service.state.value).toBe(`initializing`) + service.send(`done.invoke.initialize`) + expect(service.state.value).toBe(`initializingData`) + expect(service.state.context.webhookBody).toBeUndefined() + service.send(`WEBHOOK_RECEIVED`, { payload: { webhookBody } }) + expect(service.state.context.webhookBody).toEqual(webhookBody) + expect(services.reloadData).toHaveBeenCalled() + }) + + it(`queues a node mutation during query running`, () => { + const payload = { foo: 1 } + + const service = interpret(machine) + service.start() + service.send(`done.invoke.initialize`) + service.send(`done.invoke.initialize-data`) + service.send(`done.invoke.post-bootstrap`) + expect(service.state.context.nodeMutationBatch).toBeUndefined() + service.send(`ADD_NODE_MUTATION`, { payload }) + expect(service.state.context.nodeMutationBatch).toEqual( + expect.arrayContaining([payload]) + ) + }) + + it(`starts webpack if there is no compiler`, () => { + const service = interpret(machine) + service.start() + service.send(`done.invoke.initialize`) + service.send(`done.invoke.initialize-data`) + service.send(`done.invoke.post-bootstrap`) + expect(service.state.context.compiler).toBeUndefined() + services.startWebpackServer.mockReset() + service.send(`done.invoke.run-queries`) + expect(services.startWebpackServer).toHaveBeenCalled() + }) + + it(`recompiles if source files have changed`, () => { + const service = interpret(machine) + service.start() + service.send(`done.invoke.initialize`) + service.send(`SOURCE_FILE_CHANGED`) + service.send(`done.invoke.initialize-data`) + service.send(`done.invoke.post-bootstrap`) + // So we don't start webpack instead + service.state.context.compiler = {} as any + services.recompile.mockReset() + service.send(`done.invoke.run-queries`) + expect(services.startWebpackServer).not.toHaveBeenCalled() + expect(services.recompile).toHaveBeenCalled() + }) + + it(`skips compilation if source files are unchanged`, () => { + const service = interpret(machine) + service.start() + service.send(`done.invoke.initialize`) + service.send(`done.invoke.initialize-data`) + service.send(`done.invoke.post-bootstrap`) + service.state.context.compiler = {} as any + services.recompile.mockReset() + service.send(`done.invoke.run-queries`) + expect(services.startWebpackServer).not.toHaveBeenCalled() + expect(services.recompile).not.toHaveBeenCalled() + }) + + it(`recreates pages when waiting is complete`, () => { + const service = interpret(machine) + service.start() + service.send(`done.invoke.initialize`) + service.send(`done.invoke.initialize-data`) + service.send(`done.invoke.post-bootstrap`) + service.state.context.compiler = {} as any + service.send(`done.invoke.run-queries`) + service.send(`done.invoke.waiting`) + + expect(services.recreatePages).toHaveBeenCalled() + }) + + it(`extracts queries when waiting requests it`, () => { + const service = interpret(machine) + service.start() + service.send(`done.invoke.initialize`) + service.send(`done.invoke.initialize-data`) + service.send(`done.invoke.post-bootstrap`) + service.state.context.compiler = {} as any + service.send(`done.invoke.run-queries`) + service.send(`EXTRACT_QUERIES_NOW`) + expect(services.runQueries).toHaveBeenCalled() + }) + + it(`panics on error during initialisation`, async () => { + const service = interpret(machine) + services.initialize.mockImplementationOnce(throwService) + service.start() + await tick() + expect(actions.panic).toHaveBeenCalled() + }) + + it(`panics on rejection during initialisation`, async () => { + const service = interpret(machine) + services.initialize.mockImplementationOnce(rejectService) + service.start() + await tick() + expect(actions.panic).toHaveBeenCalled() + }) + + it(`logs errors during sourcing and transitions to waiting`, async () => { + const service = interpret(machine) + services.initializeData.mockImplementationOnce(throwService) + service.start() + service.send(`done.invoke.initialize`) + await tick() + expect(actions.logError).toHaveBeenCalled() + expect(service.state.value).toEqual(`waiting`) + }) + + it(`logs errors during query running and transitions to waiting`, async () => { + const service = interpret(machine) + services.runQueries.mockImplementationOnce(throwService) + service.start() + service.send(`done.invoke.initialize`) + service.send(`done.invoke.initialize-data`) + service.send(`done.invoke.post-bootstrap`) + await tick() + expect(actions.logError).toHaveBeenCalled() + expect(service.state.value).toEqual(`waiting`) + }) + + it(`panics on errors when launching webpack`, async () => { + const service = interpret(machine) + services.startWebpackServer.mockImplementationOnce(throwService) + service.start() + service.send(`done.invoke.initialize`) + service.send(`done.invoke.initialize-data`) + service.send(`done.invoke.post-bootstrap`) + service.send(`done.invoke.run-queries`) + await tick() + expect(actions.panic).toHaveBeenCalled() + }) + + it(`logs errors during compilation and transitions to waiting`, async () => { + const service = interpret(machine) + services.recompile.mockImplementationOnce(throwService) + service.start() + service.send(`done.invoke.initialize`) + service.send(`done.invoke.initialize-data`) + service.send(`done.invoke.post-bootstrap`) + service.state.context.compiler = {} as any + service.state.context.sourceFilesDirty = true + service.send(`done.invoke.run-queries`) + await tick() + expect(actions.logError).toHaveBeenCalled() + expect(service.state.value).toEqual(`waiting`) + }) + + it(`panics on errors while waiting`, async () => { + const service = interpret(machine) + services.waitForMutations.mockImplementationOnce(throwService) + service.start() + service.send(`done.invoke.initialize`) + service.send(`done.invoke.initialize-data`) + service.send(`done.invoke.post-bootstrap`) + service.state.context.compiler = {} as any + service.send(`done.invoke.run-queries`) + await tick() + expect(actions.panic).toHaveBeenCalled() + }) +}) diff --git a/packages/gatsby/src/state-machines/develop/index.ts b/packages/gatsby/src/state-machines/develop/index.ts index b5e8371d52c4d..6c04a4b12c323 100644 --- a/packages/gatsby/src/state-machines/develop/index.ts +++ b/packages/gatsby/src/state-machines/develop/index.ts @@ -40,6 +40,7 @@ const developConfig: MachineConfig = { WEBHOOK_RECEIVED: undefined, }, invoke: { + id: `initialize`, src: `initialize`, onDone: { target: `initializingData`, @@ -59,6 +60,7 @@ const developConfig: MachineConfig = { }, }, invoke: { + id: `initialize-data`, src: `initializeData`, data: ({ parentSpan, @@ -88,6 +90,7 @@ const developConfig: MachineConfig = { }, runningPostBootstrap: { invoke: { + id: `post-bootstrap`, src: `postBootstrap`, onDone: `runningQueries`, }, @@ -210,7 +213,6 @@ const developConfig: MachineConfig = { }, onError: { actions: `panic`, - target: `waiting`, }, }, },