Skip to content

Commit

Permalink
tests(gatsby): Add unit tests for develop state machine (#26051)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ascorbic authored Jul 28, 2020
1 parent 3808011 commit ec10aab
Show file tree
Hide file tree
Showing 2 changed files with 266 additions and 1 deletion.
263 changes: 263 additions & 0 deletions packages/gatsby/src/state-machines/__tests__/develop.ts
Original file line number Diff line number Diff line change
@@ -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<void> => {
throw new Error(`fail`)
}

const rejectService = async (): Promise<void> => Promise.reject(`fail`)

const machine = developMachine.withConfig(
{
actions,
services,
},
{
program: {} as IProgram,
}
)

const tick = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0))

const resetMocks = (mocks: Record<string, jest.Mock>): 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()
})
})
4 changes: 3 additions & 1 deletion packages/gatsby/src/state-machines/develop/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const developConfig: MachineConfig<IBuildContext, any, AnyEventObject> = {
WEBHOOK_RECEIVED: undefined,
},
invoke: {
id: `initialize`,
src: `initialize`,
onDone: {
target: `initializingData`,
Expand All @@ -59,6 +60,7 @@ const developConfig: MachineConfig<IBuildContext, any, AnyEventObject> = {
},
},
invoke: {
id: `initialize-data`,
src: `initializeData`,
data: ({
parentSpan,
Expand Down Expand Up @@ -88,6 +90,7 @@ const developConfig: MachineConfig<IBuildContext, any, AnyEventObject> = {
},
runningPostBootstrap: {
invoke: {
id: `post-bootstrap`,
src: `postBootstrap`,
onDone: `runningQueries`,
},
Expand Down Expand Up @@ -210,7 +213,6 @@ const developConfig: MachineConfig<IBuildContext, any, AnyEventObject> = {
},
onError: {
actions: `panic`,
target: `waiting`,
},
},
},
Expand Down

0 comments on commit ec10aab

Please sign in to comment.