Skip to content

Commit

Permalink
fix(protocol-designer): fix lastModified when exporting (#7024)
Browse files Browse the repository at this point in the history
Closes #6636
  • Loading branch information
IanLondon committed Nov 23, 2020
1 parent e945d0f commit 50096cd
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 55 deletions.
22 changes: 5 additions & 17 deletions protocol-designer/src/components/FileSidebar/FileSidebar.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// @flow
import * as React from 'react'
import cx from 'classnames'
import { saveAs } from 'file-saver'
import {
PrimaryButton,
AlertModal,
Expand Down Expand Up @@ -31,23 +30,13 @@ type Props = {|
createNewFile?: () => mixed,
canDownload: boolean,
onDownload: () => mixed,
downloadData: {
fileData: PDProtocolFile,
fileName: string,
},
fileData: ?PDProtocolFile,
pipettesOnDeck: $PropertyType<InitialDeckSetup, 'pipettes'>,
modulesOnDeck: $PropertyType<InitialDeckSetup, 'modules'>,
savedStepForms: SavedStepFormState,
schemaVersion: number,
|}

const saveFile = (downloadData: $PropertyType<Props, 'downloadData'>) => {
const blob = new Blob([JSON.stringify(downloadData.fileData)], {
type: 'application/json',
})
saveAs(blob, downloadData.fileName)
}

type WarningContent = {|
content: React.Node,
heading: string,
Expand Down Expand Up @@ -168,7 +157,7 @@ export const v5WarningContent: React.Node = (
export function FileSidebar(props: Props): React.Node {
const {
canDownload,
downloadData,
fileData,
loadFile,
createNewFile,
onDownload,
Expand All @@ -186,7 +175,7 @@ export function FileSidebar(props: Props): React.Node {

const cancelModal = () => setShowExportWarningModal(false)

const noCommands = downloadData && downloadData.fileData.commands.length === 0
const noCommands = fileData ? fileData.commands.length === 0 : true
const pipettesWithoutStep = getUnusedEntities(
pipettesOnDeck,
savedStepForms,
Expand Down Expand Up @@ -231,7 +220,7 @@ export function FileSidebar(props: Props): React.Node {
handleCancel: () => setShowBlockingHint(false),
handleContinue: () => {
setShowBlockingHint(false)
saveFile(downloadData)
onDownload()
},
})

Expand All @@ -258,7 +247,7 @@ export function FileSidebar(props: Props): React.Node {
setShowExportWarningModal(false)
setShowBlockingHint(true)
} else {
saveFile(downloadData)
onDownload()
setShowExportWarningModal(false)
}
},
Expand Down Expand Up @@ -290,7 +279,6 @@ export function FileSidebar(props: Props): React.Node {
resetScrollElements()
setShowBlockingHint(true)
} else {
saveFile(downloadData)
onDownload()
}
}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// @flow
import * as React from 'react'
import { shallow, mount } from 'enzyme'
import fileSaver from 'file-saver'
import { PrimaryButton, AlertModal, OutlineButton } from '@opentrons/components'
import { MAGNETIC_MODULE_TYPE } from '@opentrons/shared-data'
import {
Expand All @@ -13,7 +12,6 @@ import { FileSidebar, v4WarningContent, v5WarningContent } from '../FileSidebar'
import { useBlockingHint } from '../../Hints/useBlockingHint'
import type { HintArgs } from '../../Hints/useBlockingHint'

jest.mock('file-saver')
jest.mock('../../Hints/useBlockingHint')

const mockUseBlockingHint: JestMockFn<[HintArgs], ?React.Node> = useBlockingHint
Expand All @@ -23,24 +21,19 @@ describe('FileSidebar', () => {
const pipetteRightId = 'pipetteRightId'
let props, commands, modulesOnDeck, pipettesOnDeck, savedStepForms
beforeEach(() => {
fileSaver.saveAs = jest.fn()

props = {
loadFile: jest.fn(),
createNewFile: jest.fn(),
canDownload: true,
onDownload: jest.fn(),
downloadData: {
fileData: {
labware: {},
labwareDefinitions: {},
metadata: {},
pipettes: {},
robot: { model: 'OT-2 Standard' },
schemaVersion: 3,
commands: [],
},
fileName: 'protocol.json',
fileData: {
labware: {},
labwareDefinitions: {},
metadata: {},
pipettes: {},
robot: { model: 'OT-2 Standard' },
schemaVersion: 3,
commands: [],
},
pipettesOnDeck: {},
modulesOnDeck: {},
Expand Down Expand Up @@ -119,17 +112,12 @@ describe('FileSidebar', () => {
})

it('export button exports protocol when no errors', () => {
props.downloadData.fileData.commands = commands
const blob = new Blob([JSON.stringify(props.downloadData.fileData)], {
type: 'application/json',
})

props.fileData.commands = commands
const wrapper = shallow(<FileSidebar {...props} />)
const downloadButton = wrapper.find(PrimaryButton).at(0)
downloadButton.simulate('click')

expect(props.onDownload).toHaveBeenCalled()
expect(fileSaver.saveAs).toHaveBeenCalledWith(blob, 'protocol.json')
})

it('warning modal is shown when export is clicked with no command', () => {
Expand All @@ -140,10 +128,17 @@ describe('FileSidebar', () => {

expect(alertModal).toHaveLength(1)
expect(alertModal.prop('heading')).toEqual('Your protocol has no steps')

const continueButton = alertModal
.dive()
.find(OutlineButton)
.at(1)
continueButton.simulate('click')
expect(props.onDownload).toHaveBeenCalled()
})

it('warning modal is shown when export is clicked with unused pipette', () => {
props.downloadData.fileData.commands = commands
props.fileData.commands = commands
props.pipettesOnDeck = pipettesOnDeck
props.savedStepForms = savedStepForms

Expand All @@ -161,12 +156,19 @@ describe('FileSidebar', () => {
expect(alertModal.html()).not.toContain(
pipettesOnDeck.pipetteLeftId.spec.displayName
)

const continueButton = alertModal
.dive()
.find(OutlineButton)
.at(1)
continueButton.simulate('click')
expect(props.onDownload).toHaveBeenCalled()
})

it('warning modal is shown when export is clicked with unused module', () => {
props.modulesOnDeck = modulesOnDeck
props.savedStepForms = savedStepForms
props.downloadData.fileData.commands = commands
props.fileData.commands = commands

const wrapper = shallow(<FileSidebar {...props} />)
const downloadButton = wrapper.find(PrimaryButton).at(0)
Expand All @@ -176,13 +178,20 @@ describe('FileSidebar', () => {
expect(alertModal).toHaveLength(1)
expect(alertModal.prop('heading')).toEqual('Unused module')
expect(alertModal.html()).toContain('Magnetic module')

const continueButton = alertModal
.dive()
.find(OutlineButton)
.at(1)
continueButton.simulate('click')
expect(props.onDownload).toHaveBeenCalled()
})

it('warning modal is shown when export is clicked with unused module and pipette', () => {
props.modulesOnDeck = modulesOnDeck
props.pipettesOnDeck = pipettesOnDeck
props.savedStepForms = savedStepForms
props.downloadData.fileData.commands = commands
props.fileData.commands = commands

const wrapper = shallow(<FileSidebar {...props} />)
const downloadButton = wrapper.find(PrimaryButton).at(0)
Expand All @@ -199,10 +208,17 @@ describe('FileSidebar', () => {
expect(alertModal.html()).not.toContain(
pipettesOnDeck.pipetteLeftId.spec.displayName
)

const continueButton = alertModal
.dive()
.find(OutlineButton)
.at(1)
continueButton.simulate('click')
expect(props.onDownload).toHaveBeenCalled()
})

it('blocking hint is shown when protocol is v4', () => {
props.downloadData.fileData.commands = commands
props.fileData.commands = commands
props.pipettesOnDeck = {
pipetteLeftId: {
name: 'string',
Expand Down Expand Up @@ -247,7 +263,7 @@ describe('FileSidebar', () => {
})

it('blocking hint is shown when protocol is v5', () => {
props.downloadData.fileData.commands = commands
props.fileData.commands = commands
props.savedStepForms = savedStepForms

const MockHintComponent = () => {
Expand Down
13 changes: 4 additions & 9 deletions protocol-designer/src/components/FileSidebar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type Props = React.ElementProps<typeof FileSidebarComponent>

type SP = {|
canDownload: boolean,
downloadData: $PropertyType<Props, 'downloadData'>,
fileData: $PropertyType<Props, 'fileData'>,
_canCreateNew: ?boolean,
_hasUnsavedChanges: ?boolean,
pipettesOnDeck: $PropertyType<InitialDeckSetup, 'pipettes'>,
Expand All @@ -40,18 +40,13 @@ export const FileSidebar: React.AbstractComponent<{||}> = connect<
)(FileSidebarComponent)

function mapStateToProps(state: BaseState): SP {
const protocolName =
fileDataSelectors.getFileMetadata(state).protocolName || 'untitled'
const fileData = fileDataSelectors.createFile(state)
const canDownload = selectors.getCurrentPage(state) !== 'file-splash'
const initialDeckSetup = stepFormSelectors.getInitialDeckSetup(state)

return {
canDownload,
downloadData: {
fileData,
fileName: protocolName + '.json',
},
fileData,
pipettesOnDeck: initialDeckSetup.pipettes,
modulesOnDeck: initialDeckSetup.modules,
savedStepForms: stepFormSelectors.getSavedStepForms(state),
Expand All @@ -70,7 +65,7 @@ function mergeProps(
_canCreateNew,
_hasUnsavedChanges,
canDownload,
downloadData,
fileData,
pipettesOnDeck,
modulesOnDeck,
savedStepForms,
Expand All @@ -91,7 +86,7 @@ function mergeProps(
? () => dispatch(actions.toggleNewProtocolModal(true))
: undefined,
onDownload: () => dispatch(loadFileActions.saveProtocolFile()),
downloadData,
fileData,
pipettesOnDeck,
modulesOnDeck,
savedStepForms,
Expand Down
1 change: 1 addition & 0 deletions protocol-designer/src/file-data/reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ function newProtocolMetadata(
...defaultFields,
protocolName: action.payload.name || '',
created: Date.now(),
lastModified: null,
}
}

Expand Down
60 changes: 60 additions & 0 deletions protocol-designer/src/load-file/__tests__/actions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// @flow
import { createFile } from '../../file-data/selectors/fileCreator'
import { getFileMetadata } from '../../file-data/selectors/fileFields'
import { saveProtocolFile } from '../actions'
import { saveFile as saveFileUtil } from '../utils'
jest.mock('../../file-data/selectors/fileCreator')
jest.mock('../../file-data/selectors/fileFields')
jest.mock('../utils')

const createFileSelectorMock: JestMockFn<any, any> = createFile
const getFileMetadataMock: JestMockFn<any, any> = getFileMetadata
const saveFileUtilMock: JestMockFn<[any, string], any> = saveFileUtil

afterEach(() => {
jest.resetAllMocks()
})

describe('saveProtocolFile thunk', () => {
it('should dispatch SAVE_PROTOCOL_FILE and then call saveFile util', () => {
const fakeState = {}
const mockFileData = {}
let actionWasDispatched = false

createFileSelectorMock.mockImplementation(state => {
expect(state).toBe(fakeState)
expect(actionWasDispatched).toBe(true)
return mockFileData
})

getFileMetadataMock.mockImplementation(state => {
expect(state).toBe(fakeState)
expect(actionWasDispatched).toBe(true)
return { protocolName: 'fooFileName' }
})

saveFileUtilMock.mockImplementation((fileData, fileName) => {
expect(fileName).toEqual('fooFileName.json')
expect(fileData).toBe(mockFileData)
})

const dispatch: () => any = jest.fn().mockImplementation(action => {
expect(action).toEqual({ type: 'SAVE_PROTOCOL_FILE' })
actionWasDispatched = true
})

const getState: () => any = jest.fn().mockImplementation(() => {
// once we call getState, the thunk should already have dispatched the action
expect(actionWasDispatched).toBe(true)
return fakeState
})

saveProtocolFile()(dispatch, getState)

expect(dispatch).toHaveBeenCalled()
expect(createFileSelectorMock).toHaveBeenCalled()
expect(getFileMetadataMock).toHaveBeenCalled()
expect(getState).toHaveBeenCalled()
expect(saveFileUtilMock).toHaveBeenCalled()
})
})
22 changes: 19 additions & 3 deletions protocol-designer/src/load-file/actions.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// @flow
import { migration } from './migration'
import { selectors as fileDataSelectors } from '../file-data'
import { saveFile } from './utils'
import type { PDProtocolFile } from '../file-types'
import type { GetState, ThunkAction, ThunkDispatch } from '../types'
import type {
Expand All @@ -8,6 +10,7 @@ import type {
LoadFileAction,
NewProtocolFields,
} from './types'

export type FileUploadMessageAction = {|
type: 'FILE_UPLOAD_MESSAGE',
payload: FileUploadMessage,
Expand Down Expand Up @@ -90,6 +93,19 @@ export const createNewProtocol = (
})

export type SaveProtocolFileAction = {| type: 'SAVE_PROTOCOL_FILE' |}
export const saveProtocolFile = (): SaveProtocolFileAction => ({
type: 'SAVE_PROTOCOL_FILE',
})
export const saveProtocolFile: () => ThunkAction<SaveProtocolFileAction> = () => (
dispatch,
getState
) => {
// dispatching this should update the state, eg lastModified timestamp
dispatch({ type: 'SAVE_PROTOCOL_FILE' })

const state = getState()
const fileData = fileDataSelectors.createFile(state)

const protocolName =
fileDataSelectors.getFileMetadata(state).protocolName || 'untitled'
const fileName = `${protocolName}.json`

saveFile(fileData, fileName)
}

0 comments on commit 50096cd

Please sign in to comment.