Merge pull request #3988 from overleaf/ae-use-fetch-mock

Add useFetchMock hook for use in Storybook

GitOrigin-RevId: 4eb1c5edf2f94dc6ad51358e109e29c9f62d2058
This commit is contained in:
Alf Eaton 2021-05-11 15:25:22 +01:00 committed by Copybot
parent 29264061d8
commit f8cb1638d1
8 changed files with 132 additions and 100 deletions

View file

@ -1,23 +1,24 @@
import React from 'react' import React from 'react'
import BinaryFile from '../js/features/binary-file/components/binary-file' import BinaryFile from '../js/features/binary-file/components/binary-file'
import fetchMock from 'fetch-mock' import useFetchMock from './hooks/use-fetch-mock'
window.project_id = 'proj123' window.project_id = 'proj123'
fetchMock.restore()
fetchMock.head('express:/project/:project_id/file/:file_id', {
status: 201,
headers: { 'Content-Length': 10000 },
})
fetchMock.get('express:/project/:project_id/file/:file_id', 'Text file content')
fetchMock.post('express:/project/:project_id/linked_file/:file_id/refresh', { const setupFetchMock = fetchMock => {
status: 204, fetchMock
}) .head('express:/project/:project_id/file/:file_id', {
status: 201,
fetchMock.post('express:/project/:project_id/references/indexAll', { headers: { 'Content-Length': 10000 },
status: 204, })
}) .get('express:/project/:project_id/file/:file_id', 'Text file content')
.post('express:/project/:project_id/linked_file/:file_id/refresh', {
status: 204,
})
.post('express:/project/:project_id/references/indexAll', {
status: 204,
})
}
window.project_id = '1234' window.project_id = '1234'
@ -173,10 +174,14 @@ export default {
storeReferencesKeys: () => {}, storeReferencesKeys: () => {},
}, },
decorators: [ decorators: [
BinaryFile => ( Story => {
useFetchMock(setupFetchMock)
return <Story />
},
Story => (
<> <>
<style>{'html, body { height: 100%; }'}</style> <style>{'html, body { height: 100%; }'}</style>
<BinaryFile /> <Story />
</> </>
), ),
], ],

View file

@ -1,11 +1,11 @@
import React from 'react' import React from 'react'
import fetchMock from 'fetch-mock'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import { ContextRoot } from '../js/shared/context/root-context' import { ContextRoot } from '../js/shared/context/root-context'
import ChatPane from '../js/features/chat/components/chat-pane' import ChatPane from '../js/features/chat/components/chat-pane'
import { stubMathJax } from '../../test/frontend/features/chat/components/stubs' import { stubMathJax } from '../../test/frontend/features/chat/components/stubs'
import { setupContext } from './fixtures/context' import { setupContext } from './fixtures/context'
import useFetchMock from './hooks/use-fetch-mock'
const ONE_MINUTE = 60 * 1000 const ONE_MINUTE = 60 * 1000
@ -43,31 +43,30 @@ function generateMessages(count) {
stubMathJax() stubMathJax()
setupContext() setupContext()
export const Conversation = args => <ChatPane {...args} /> export const Conversation = args => {
Conversation.parameters = { useFetchMock(fetchMock => {
setupMocks: () => { fetchMock.get(/messages/, generateMessages(35)).post(/messages/, {})
fetchMock.restore() })
fetchMock.get(/messages/, generateMessages(35))
fetchMock.post(/messages/, {}) return <ChatPane {...args} />
},
} }
export const NoMessages = args => <ChatPane {...args} /> export const NoMessages = args => {
NoMessages.parameters = { useFetchMock(fetchMock => {
setupMocks: () => {
fetchMock.restore()
fetchMock.get(/messages/, []) fetchMock.get(/messages/, [])
}, })
return <ChatPane {...args} />
} }
export const Loading = args => <ChatPane {...args} /> export const Loading = args => {
Loading.parameters = { useFetchMock(fetchMock => {
setupMocks: () => {
fetchMock.restore()
fetchMock.get(/messages/, generateMessages(6), { fetchMock.get(/messages/, generateMessages(6), {
delay: 1000 * 10, delay: 1000 * 10,
}) })
}, })
return <ChatPane {...args} />
} }
export default { export default {
@ -80,10 +79,6 @@ export default {
resetUnreadMessages: () => {}, resetUnreadMessages: () => {},
}, },
decorators: [ decorators: [
(Story, { parameters: { setupMocks } }) => {
if (setupMocks) setupMocks()
return <Story />
},
Story => ( Story => (
<> <>
<style>{'html, body, .chat { height: 100%; width: 100%; }'}</style> <style>{'html, body, .chat { height: 100%; width: 100%; }'}</style>

View file

@ -1,27 +1,29 @@
import React from 'react' import React from 'react'
import fetchMock from 'fetch-mock'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import CloneProjectModal from '../js/features/clone-project-modal/components/clone-project-modal' import CloneProjectModal from '../js/features/clone-project-modal/components/clone-project-modal'
import useFetchMock from './hooks/use-fetch-mock'
export const Interactive = ({ export const Interactive = ({
mockResponse = 200, mockResponse = 200,
mockResponseDelay = 500, mockResponseDelay = 500,
...args ...args
}) => { }) => {
fetchMock.restore().post( useFetchMock(fetchMock => {
'express:/project/:projectId/clone', fetchMock.post(
() => { 'express:/project/:projectId/clone',
switch (mockResponse) { () => {
case 400: switch (mockResponse) {
return { status: 400, body: 'The project name is not valid' } case 400:
return { status: 400, body: 'The project name is not valid' }
default: default:
return mockResponse return mockResponse
} }
}, },
{ delay: mockResponseDelay } { delay: mockResponseDelay }
) )
})
return <CloneProjectModal {...args} /> return <CloneProjectModal {...args} />
} }

View file

@ -1,11 +1,11 @@
import React from 'react' import React from 'react'
import fetchMock from 'fetch-mock'
import MockedSocket from 'socket.io-mock' import MockedSocket from 'socket.io-mock'
import { rootFolderBase } from './fixtures/file-tree-base' import { rootFolderBase } from './fixtures/file-tree-base'
import { rootFolderLimit } from './fixtures/file-tree-limit' import { rootFolderLimit } from './fixtures/file-tree-limit'
import FileTreeRoot from '../js/features/file-tree/components/file-tree-root' import FileTreeRoot from '../js/features/file-tree/components/file-tree-root'
import FileTreeError from '../js/features/file-tree/components/file-tree-error' import FileTreeError from '../js/features/file-tree/components/file-tree-error'
import useFetchMock from './hooks/use-fetch-mock'
const MOCK_DELAY = 2000 const MOCK_DELAY = 2000
@ -13,9 +13,8 @@ window._ide = {
socket: new MockedSocket(), socket: new MockedSocket(),
} }
function defaultSetupMocks() { function defaultSetupMocks(fetchMock) {
fetchMock fetchMock
.restore()
.post( .post(
/\/project\/\w+\/(file|doc|folder)\/\w+\/rename/, /\/project\/\w+\/(file|doc|folder)\/\w+\/rename/,
(path, req) => { (path, req) => {
@ -78,8 +77,11 @@ function defaultSetupMocks() {
}) })
} }
export const FullTree = args => <FileTreeRoot {...args} /> export const FullTree = args => {
FullTree.parameters = { setupMocks: defaultSetupMocks } useFetchMock(defaultSetupMocks)
return <FileTreeRoot {...args} />
}
export const ReadOnly = args => <FileTreeRoot {...args} /> export const ReadOnly = args => <FileTreeRoot {...args} />
ReadOnly.args = { hasWritePermissions: false } ReadOnly.args = { hasWritePermissions: false }
@ -87,28 +89,34 @@ ReadOnly.args = { hasWritePermissions: false }
export const Disconnected = args => <FileTreeRoot {...args} /> export const Disconnected = args => <FileTreeRoot {...args} />
Disconnected.args = { isConnected: false } Disconnected.args = { isConnected: false }
export const NetworkErrors = args => <FileTreeRoot {...args} /> export const NetworkErrors = args => {
NetworkErrors.parameters = { useFetchMock(fetchMock => {
setupMocks: () => {
fetchMock fetchMock
.restore()
.post(/\/project\/\w+\/folder/, 500, { .post(/\/project\/\w+\/folder/, 500, {
delay: MOCK_DELAY, delay: MOCK_DELAY,
}) })
.post(/\/project\/\w+\/(file|doc|folder)\/\w+\/rename/, 500, { .post(/\/project\/\w+\/(file|doc|folder)\/\w+\/rename/, 500, {
delay: MOCK_DELAY, delay: MOCK_DELAY,
}) })
.post(/\/project\/\w+\/(file|doc|folder)\/\w+\/move/, 500, {
delay: MOCK_DELAY,
})
.delete(/\/project\/\w+\/(file|doc|folder)\/\w+/, 500, { .delete(/\/project\/\w+\/(file|doc|folder)\/\w+/, 500, {
delay: MOCK_DELAY, delay: MOCK_DELAY,
}) })
}, })
return <FileTreeRoot {...args} />
} }
export const FallbackError = args => <FileTreeError {...args} /> export const FallbackError = args => <FileTreeError {...args} />
export const FilesLimit = args => <FileTreeRoot {...args} /> export const FilesLimit = args => {
useFetchMock(defaultSetupMocks)
return <FileTreeRoot {...args} />
}
FilesLimit.args = { rootFolder: rootFolderLimit } FilesLimit.args = { rootFolder: rootFolderLimit }
FilesLimit.parameters = { setupMocks: defaultSetupMocks }
export default { export default {
title: 'File Tree', title: 'File Tree',
@ -136,10 +144,6 @@ export default {
onSelect: { action: 'onSelect' }, onSelect: { action: 'onSelect' },
}, },
decorators: [ decorators: [
(Story, { parameters: { setupMocks } }) => {
if (setupMocks) setupMocks()
return <Story />
},
Story => ( Story => (
<> <>
<style>{'html, body, .file-tree { height: 100%; width: 100%; }'}</style> <style>{'html, body, .file-tree { height: 100%; width: 100%; }'}</style>

View file

@ -2,6 +2,7 @@ import React from 'react'
import { ContextRoot } from '../js/shared/context/root-context' import { ContextRoot } from '../js/shared/context/root-context'
import { setupContext } from './fixtures/context' import { setupContext } from './fixtures/context'
import importOverleafModules from '../macros/import-overleaf-module.macro' import importOverleafModules from '../macros/import-overleaf-module.macro'
import useFetchMock from './hooks/use-fetch-mock'
const [ const [
{ {
@ -21,7 +22,11 @@ CollaboratorModal.args = {
type: 'collaborator', type: 'collaborator',
} }
export const TeaserModal = args => <GitBridgeModal {...args} /> export const TeaserModal = args => {
useFetchMock(fetchMock => fetchMock.post('express:/event/:key', 202))
return <GitBridgeModal {...args} />
}
TeaserModal.args = { TeaserModal.args = {
type: 'teaser', type: 'teaser',
} }
@ -34,7 +39,6 @@ export default {
}, },
argTypes: { argTypes: {
handleHide: { action: 'handleHide' }, handleHide: { action: 'handleHide' },
startFreeTrial: { action: 'startFreeTrial' },
}, },
decorators: [ decorators: [
Story => ( Story => (

View file

@ -0,0 +1,21 @@
import { useEffect } from 'react'
import fetchMock from 'fetch-mock'
/**
* Run callback to mock fetch routes, call restore() when unmounted
*/
export default function useFetchMock(callback) {
useEffect(() => {
return () => {
fetchMock.restore()
}
}, [])
// Running fetchMock.restore() here as well,
// in case there was an error before the component was unmounted.
fetchMock.restore()
// The callback has to be run here, rather than in useEffect,
// so it's run before the component is rendered.
callback(fetchMock)
}

View file

@ -1,6 +1,6 @@
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import fetchMock from 'fetch-mock'
import ShareProjectModal from '../js/features/share-project-modal/components/share-project-modal' import ShareProjectModal from '../js/features/share-project-modal/components/share-project-modal'
import useFetchMock from './hooks/use-fetch-mock'
const contacts = [ const contacts = [
// user with edited name // user with edited name
@ -44,11 +44,10 @@ const contacts = [
}, },
] ]
const setupFetchMock = () => { const setupFetchMock = fetchMock => {
const delay = 1000 const delay = 1000
fetchMock fetchMock
.restore()
// list contacts // list contacts
.get('express:/user/contacts', { contacts }, { delay }) .get('express:/user/contacts', { contacts }, { delay })
// change privacy setting // change privacy setting
@ -86,7 +85,7 @@ const ideWithProject = project => {
} }
export const LinkSharingOff = args => { export const LinkSharingOff = args => {
setupFetchMock() useFetchMock(setupFetchMock)
const project = { const project = {
...args.project, ...args.project,
@ -97,7 +96,7 @@ export const LinkSharingOff = args => {
} }
export const LinkSharingOn = args => { export const LinkSharingOn = args => {
setupFetchMock() useFetchMock(setupFetchMock)
const project = { const project = {
...args.project, ...args.project,
@ -108,7 +107,7 @@ export const LinkSharingOn = args => {
} }
export const LinkSharingLoading = args => { export const LinkSharingLoading = args => {
setupFetchMock() useFetchMock(setupFetchMock)
const project = { const project = {
...args.project, ...args.project,
@ -167,7 +166,7 @@ export const RestrictedTokenMember = args => {
} }
export const LegacyLinkSharingReadAndWrite = args => { export const LegacyLinkSharingReadAndWrite = args => {
setupFetchMock() useFetchMock(setupFetchMock)
const project = { const project = {
...args.project, ...args.project,
@ -178,7 +177,7 @@ export const LegacyLinkSharingReadAndWrite = args => {
} }
export const LegacyLinkSharingReadOnly = args => { export const LegacyLinkSharingReadOnly = args => {
setupFetchMock() useFetchMock(setupFetchMock)
const project = { const project = {
...args.project, ...args.project,
@ -189,7 +188,7 @@ export const LegacyLinkSharingReadOnly = args => {
} }
export const LimitedCollaborators = args => { export const LimitedCollaborators = args => {
setupFetchMock() useFetchMock(setupFetchMock)
const project = { const project = {
...args.project, ...args.project,

View file

@ -1,37 +1,39 @@
import React from 'react' import React from 'react'
import fetchMock from 'fetch-mock'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import WordCountModal from '../js/features/word-count-modal/components/word-count-modal' import WordCountModal from '../js/features/word-count-modal/components/word-count-modal'
import useFetchMock from './hooks/use-fetch-mock'
export const Interactive = ({ export const Interactive = ({
mockResponse = 200, mockResponse = 200,
mockResponseDelay = 500, mockResponseDelay = 500,
...args ...args
}) => { }) => {
fetchMock.restore().get( useFetchMock(fetchMock => {
'express:/project/:projectId/wordcount', fetchMock.get(
() => { 'express:/project/:projectId/wordcount',
switch (mockResponse) { () => {
case 400: switch (mockResponse) {
return { status: 400, body: 'The project id is not valid' } case 400:
return { status: 400, body: 'The project id is not valid' }
case 200: case 200:
return { return {
texcount: { texcount: {
headers: 4, headers: 4,
mathDisplay: 40, mathDisplay: 40,
mathInline: 400, mathInline: 400,
textWords: 4000, textWords: 4000,
}, },
} }
default: default:
return mockResponse return mockResponse
} }
}, },
{ delay: mockResponseDelay } { delay: mockResponseDelay }
) )
})
return <WordCountModal {...args} /> return <WordCountModal {...args} />
} }