diff --git a/lib/gui/app/components/drive-selector/controllers/drive-selector.js b/lib/gui/app/components/drive-selector/controllers/drive-selector.js index 8b1141ec..f61acc5d 100644 --- a/lib/gui/app/components/drive-selector/controllers/drive-selector.js +++ b/lib/gui/app/components/drive-selector/controllers/drive-selector.js @@ -118,6 +118,7 @@ module.exports = function ( previouslySelected: selectionState.isCurrentDrive(drive.device) }) + selectionState.deselectOtherDrives(drive.device) selectionState.toggleDrive(drive.device) } }) @@ -132,7 +133,7 @@ module.exports = function ( * DriveSelectorController.closeModal(); */ this.closeModal = () => { - const selectedDrive = selectionState.getDrive() + const selectedDrive = selectionState.getCurrentDrive() // Sanity check to cover the case where a drive is selected, // the drive is then unplugged from the computer and the modal diff --git a/lib/gui/app/components/drive-selector/templates/drive-selector-modal.tpl.html b/lib/gui/app/components/drive-selector/templates/drive-selector-modal.tpl.html index 2fca7178..084f5a23 100644 --- a/lib/gui/app/components/drive-selector/templates/drive-selector-modal.tpl.html +++ b/lib/gui/app/components/drive-selector/templates/drive-selector-modal.tpl.html @@ -38,7 +38,7 @@ + ng-disabled="!modal.state.isDriveSelected(drive.device)">
  • diff --git a/lib/gui/app/pages/finish/controllers/finish.js b/lib/gui/app/pages/finish/controllers/finish.js index 7e7e5db5..a95fcb90 100644 --- a/lib/gui/app/pages/finish/controllers/finish.js +++ b/lib/gui/app/pages/finish/controllers/finish.js @@ -51,7 +51,7 @@ module.exports = function ($state) { if (!options.preserveImage) { selectionState.deselectImage() } - selectionState.deselectDrive() + selectionState.deselectAllDrives() analytics.logEvent('Restart', options) $state.go('main') } diff --git a/lib/gui/app/pages/main/templates/main.tpl.html b/lib/gui/app/pages/main/templates/main.tpl.html index 99cc5474..dc6ba64b 100644 --- a/lib/gui/app/pages/main/templates/main.tpl.html +++ b/lib/gui/app/pages/main/templates/main.tpl.html @@ -66,11 +66,11 @@ 'text-disabled': main.shouldDriveStepBeDisabled() }"> + uib-tooltip="{{ main.selection.getCurrentDrive().description }} ({{ main.selection.getCurrentDrive().displayName }})"> - {{ (main.selection.getDrive().description || "") | middleEllipses:11 }} + {{ (main.selection.getCurrentDrive().description || "") | middleEllipses:11 }} - {{ main.selection.getDrive().size | closestUnit }} + {{ main.selection.getCurrentDrive().size | closestUnit }} @@ -97,7 +97,7 @@ percentage="main.state.getFlashState().percentage" striped="{{ main.state.getFlashState().type == 'check' }}" ng-attr-active="{{ main.state.isFlashing() }}" - ng-click="flash.flashImageToDrive(main.selection.getImage(), main.selection.getDrive())" + ng-click="flash.flashImageToDrive(main.selection.getImage(), main.selection.getCurrentDrive())" ng-disabled="main.shouldFlashStepBeDisabled() || main.state.getLastFlashErrorCode()"> diff --git a/lib/shared/models/selection-state.js b/lib/shared/models/selection-state.js index 6df9d126..f6d3c65c 100644 --- a/lib/shared/models/selection-state.js +++ b/lib/shared/models/selection-state.js @@ -48,13 +48,41 @@ exports.selectDrive = (driveDevice) => { * selectionState.toggleDrive('/dev/disk2'); */ exports.toggleDrive = (driveDevice) => { - if (exports.isCurrentDrive(driveDevice)) { - exports.deselectDrive() + if (exports.isDriveSelected(driveDevice)) { + exports.deselectDrive(driveDevice) } else { exports.selectDrive(driveDevice) } } +/** + * @summary Deselect all other drives and keep the current drive's status + * @function + * @public + * @deprecated + * + * @description + * This is a temporary function during the transition to multi-writes, + * remove this and its uses when multi-selection should become user-facing. + * + * @param {String} driveDevice - drive device identifier + * + * @example + * console.log(selectionState.getSelectedDevices()) + * > [ '/dev/disk1', '/dev/disk2', '/dev/disk3' ] + * selectionState.deselectOtherDrives('/dev/disk2') + * console.log(selectionState.getSelectedDevices()) + * > [ '/dev/disk2' ] + */ +exports.deselectOtherDrives = (driveDevice) => { + if (exports.isDriveSelected(driveDevice)) { + const otherDevices = _.reject(exports.getSelectedDevices(), _.partial(_.isEqual, driveDevice)) + _.each(otherDevices, exports.deselectDrive) + } else { + exports.deselectAllDrives() + } +} + /** * @summary Select an image * @function @@ -82,19 +110,38 @@ exports.selectImage = (image) => { } /** - * @summary Get drive + * @summary Get all selected drives' devices + * @function + * @public + * + * @returns {String[]} selected drives' devices + * + * @example + * for (driveDevice of selectionState.getSelectedDevices()) { + * console.log(driveDevice) + * } + * > '/dev/disk1' + * > '/dev/disk2' + */ +exports.getSelectedDevices = () => { + return store.getState().getIn([ 'selection', 'devices' ]).toJS() +} + +/** + * @summary Get the head of the list of selected drives * @function * @public * * @returns {Object} drive * * @example - * const drive = selectionState.getDrive(); + * const drive = selectionState.getCurrentDrive(); + * console.log(drive) + * > { device: '/dev/disk1', name: 'Flash drive', ... } */ -exports.getDrive = () => { - return _.find(availableDrives.getDrives(), { - device: store.getState().getIn([ 'selection', 'drive' ]) - }) +exports.getCurrentDrive = () => { + const device = _.head(exports.getSelectedDevices()) + return _.find(availableDrives.getDrives(), { device }) } /** @@ -252,7 +299,7 @@ exports.getImageRecommendedDriveSize = () => { * } */ exports.hasDrive = () => { - return Boolean(exports.getDrive()) + return Boolean(exports.getSelectedDevices().length) } /** @@ -272,16 +319,22 @@ exports.hasImage = () => { } /** - * @summary Remove drive + * @summary Remove drive from selection * @function * @public * + * @param {String} driveDevice - drive device identifier + * * @example - * selectionState.deselectDrive(); + * selectionState.deselectDrive('/dev/sdc'); + * + * @example + * selectionState.deselectDrive('\\\\.\\PHYSICALDRIVE3'); */ -exports.deselectDrive = () => { +exports.deselectDrive = (driveDevice) => { store.dispatch({ - type: store.Actions.DESELECT_DRIVE + type: store.Actions.DESELECT_DRIVE, + data: driveDevice }) } @@ -299,6 +352,18 @@ exports.deselectImage = () => { }) } +/** + * @summary Unselect all drives + * @function + * @public + * + * @example + * selectionState.deselectAllDrives() + */ +exports.deselectAllDrives = () => { + _.each(exports.getSelectedDevices(), exports.deselectDrive) +} + /** * @summary Clear selections * @function @@ -306,13 +371,10 @@ exports.deselectImage = () => { * * @example * selectionState.clear(); - * - * @example - * selectionState.clear({ preserveImage: true }); */ exports.clear = () => { exports.deselectImage() - exports.deselectDrive() + exports.deselectAllDrives() } /** @@ -333,5 +395,29 @@ exports.isCurrentDrive = (driveDevice) => { return false } - return driveDevice === _.get(exports.getDrive(), [ 'device' ]) + return driveDevice === _.get(exports.getCurrentDrive(), [ 'device' ]) +} + +/** + * @summary Check whether a given device is selected. + * @function + * @public + * + * @param {String} driveDevice - drive device identifier + * @returns {Boolean} + * + * @example + * const isSelected = selectionState.isDriveSelected('/dev/sdb') + * + * if (isSelected) { + * selectionState.deselectDrive(driveDevice) + * } + */ +exports.isDriveSelected = (driveDevice) => { + if (!driveDevice) { + return false + } + + const selectedDriveDevices = exports.getSelectedDevices() + return _.includes(selectedDriveDevices, driveDevice) } diff --git a/lib/shared/store.js b/lib/shared/store.js index fdc5265f..34738313 100644 --- a/lib/shared/store.js +++ b/lib/shared/store.js @@ -82,7 +82,9 @@ const selectImageNoNilFields = [ */ const DEFAULT_STATE = Immutable.fromJS({ availableDrives: [], - selection: {}, + selection: { + devices: new Immutable.OrderedSet() + }, isFlashing: false, flashResults: {}, flashState: { @@ -122,25 +124,20 @@ const ACTIONS = _.fromPairs(_.map([ })) /** - * @summary Find a drive from the list of available drives + * @summary Get available drives from the state * @function - * @private + * @public * - * @param {Object} state - application state - * @param {String} device - drive device - * @returns {(Object|Undefined)} drive + * @param {Object} state - state object + * @returns {Object} new state * * @example - * const drive = findDrive(state, '/dev/disk2'); + * const drives = getAvailableDrives(state) + * _.find(drives, { device: '/dev/sda' }) */ -const findDrive = (state, device) => { - /* eslint-disable lodash/prefer-lodash-method */ - - return state.get('availableDrives').find((drive) => { - return drive.get('device') === device - }) - - /* eslint-enable lodash/prefer-lodash-method */ +const getAvailableDrives = (state) => { + // eslint-disable-next-line lodash/prefer-lodash-method + return state.get('availableDrives').toJS() } /** @@ -160,6 +157,8 @@ const findDrive = (state, device) => { const storeReducer = (state = DEFAULT_STATE, action) => { switch (action.type) { case ACTIONS.SET_AVAILABLE_DRIVES: { + // Type: action.data : Array + if (!action.data) { throw errors.createError({ title: 'Missing drives' @@ -167,20 +166,20 @@ const storeReducer = (state = DEFAULT_STATE, action) => { } // Convert object instances to plain objects - action.data = JSON.parse(JSON.stringify(action.data)) + const drives = JSON.parse(JSON.stringify(action.data)) - if (!_.isArray(action.data) || !_.every(action.data, _.isPlainObject)) { + if (!_.isArray(drives) || !_.every(drives, _.isPlainObject)) { throw errors.createError({ - title: `Invalid drives: ${action.data}` + title: `Invalid drives: ${drives}` }) } - const newState = state.set('availableDrives', Immutable.fromJS(action.data)) + const newState = state.set('availableDrives', Immutable.fromJS(drives)) const AUTOSELECT_DRIVE_COUNT = 1 - const numberOfDrives = action.data.length + const numberOfDrives = drives.length if (numberOfDrives === AUTOSELECT_DRIVE_COUNT) { - const drive = _.first(action.data) + const [ drive ] = drives // Even if there's no image selected, we need to call several // drive/image related checks, and `{}` works fine with them @@ -198,27 +197,42 @@ const storeReducer = (state = DEFAULT_STATE, action) => { !constraints.isSystemDrive(drive) ])) { + // Auto-select this drive return storeReducer(newState, { type: ACTIONS.SELECT_DRIVE, data: drive.device }) } - } - const selectedDevice = newState.getIn([ 'selection', 'drive' ]) - - if (selectedDevice && !_.find(action.data, { - device: selectedDevice - })) { + // Deselect this drive in case it still is selected return storeReducer(newState, { - type: ACTIONS.DESELECT_DRIVE + type: ACTIONS.DESELECT_DRIVE, + data: drive.device }) } - return newState + const selectedDevices = newState.getIn([ 'selection', 'devices' ]).toJS() + + // Remove selected drives that are stale, i.e. missing from availableDrives + return _.reduce(selectedDevices, (accState, device) => { + // Check whether the drive still exists in availableDrives + if (device && !_.find(drives, { + device + })) { + // Deselect this drive gone from availableDrives + return storeReducer(accState, { + type: ACTIONS.DESELECT_DRIVE, + data: device + }) + } + + return accState + }, newState) } case ACTIONS.SET_FLASH_STATE: { + // Type: action.data : FlashStateObject + if (!state.get('isFlashing')) { throw errors.createError({ title: 'Can\'t set the flashing state when not flashing' @@ -264,6 +278,8 @@ const storeReducer = (state = DEFAULT_STATE, action) => { } case ACTIONS.UNSET_FLASHING_FLAG: { + // Type: action.data : FlashResultsObject + if (!action.data) { throw errors.createError({ title: 'Missing results' @@ -305,40 +321,46 @@ const storeReducer = (state = DEFAULT_STATE, action) => { } case ACTIONS.SELECT_DRIVE: { - if (!action.data) { + // Type: action.data : String + + const device = action.data + + if (!device) { throw errors.createError({ title: 'Missing drive' }) } - if (!_.isString(action.data)) { + if (!_.isString(device)) { throw errors.createError({ - title: `Invalid drive: ${action.data}` + title: `Invalid drive: ${device}` }) } - const selectedDrive = findDrive(state, action.data) + const selectedDrive = _.find(getAvailableDrives(state), { device }) if (!selectedDrive) { throw errors.createError({ - title: `The drive is not available: ${action.data}` + title: `The drive is not available: ${device}` }) } - if (selectedDrive.get('isReadOnly')) { + if (selectedDrive.isReadOnly) { throw errors.createError({ title: 'The drive is write-protected' }) } const image = state.getIn([ 'selection', 'image' ]) - if (image && !constraints.isDriveLargeEnough(selectedDrive.toJS(), image.toJS())) { + if (image && !constraints.isDriveLargeEnough(selectedDrive, image.toJS())) { throw errors.createError({ title: 'The drive is not large enough' }) } - return state.setIn([ 'selection', 'drive' ], Immutable.fromJS(action.data)) + const selectedDevices = state.getIn([ 'selection', 'devices' ]) + + return state.setIn([ 'selection', 'devices' ], selectedDevices.add(device)) } // TODO(jhermsmeier): Consolidate these assertions @@ -346,6 +368,8 @@ const storeReducer = (state = DEFAULT_STATE, action) => { // place where all the image extension / format handling // takes place, to avoid having to check 2+ locations with different logic case ACTIONS.SELECT_IMAGE: { + // Type: action.data : ImageObject + verifyNoNilFields(action.data, selectImageNoNilFields, 'image') if (!_.isString(action.data.path)) { @@ -432,24 +456,41 @@ const storeReducer = (state = DEFAULT_STATE, action) => { }) } - const selectedDrive = findDrive(state, state.getIn([ 'selection', 'drive' ])) + const selectedDevices = state.getIn([ 'selection', 'devices' ]) - return _.attempt(() => { - if (selectedDrive && !_.every([ - constraints.isDriveValid(selectedDrive.toJS(), action.data), - constraints.isDriveSizeRecommended(selectedDrive.toJS(), action.data) - ])) { - return storeReducer(state, { - type: ACTIONS.DESELECT_DRIVE + // Remove image-incompatible drives from selection with `constraints.isDriveValid` + return _.reduce(selectedDevices.toJS(), (accState, device) => { + const drive = _.find(getAvailableDrives(state), { device }) + if (!constraints.isDriveValid(drive, action.data) || !constraints.isDriveSizeRecommended(drive, action.data)) { + return storeReducer(accState, { + type: ACTIONS.DESELECT_DRIVE, + data: device }) } - return state - }).setIn([ 'selection', 'image' ], Immutable.fromJS(action.data)) + return accState + }, state).setIn([ 'selection', 'image' ], Immutable.fromJS(action.data)) } case ACTIONS.DESELECT_DRIVE: { - return state.deleteIn([ 'selection', 'drive' ]) + // Type: action.data : String + + if (!action.data) { + throw errors.createError({ + title: 'Missing drive' + }) + } + + if (!_.isString(action.data)) { + throw errors.createError({ + title: `Invalid drive: ${action.data}` + }) + } + + const selectedDevices = state.getIn([ 'selection', 'devices' ]) + + // Remove drive from set in state + return state.setIn([ 'selection', 'devices' ], selectedDevices.delete(action.data)) } case ACTIONS.DESELECT_IMAGE: { @@ -457,6 +498,8 @@ const storeReducer = (state = DEFAULT_STATE, action) => { } case ACTIONS.SET_SETTINGS: { + // Type: action.data : SettingsObject + if (!action.data) { throw errors.createError({ title: 'Missing settings' diff --git a/tests/shared/models/available-drives.spec.js b/tests/shared/models/available-drives.spec.js index f0d9a2c4..347af410 100644 --- a/tests/shared/models/available-drives.spec.js +++ b/tests/shared/models/available-drives.spec.js @@ -143,8 +143,7 @@ describe('Model: availableDrives', function () { describe('given no selected image and no selected drive', function () { beforeEach(function () { - selectionState.deselectDrive() - selectionState.deselectImage() + selectionState.clear() }) it('should auto-select a single valid available drive', function () { @@ -164,7 +163,7 @@ describe('Model: availableDrives', function () { ]) m.chai.expect(selectionState.hasDrive()).to.be.true - m.chai.expect(selectionState.getDrive().device).to.equal('/dev/sdb') + m.chai.expect(selectionState.getCurrentDrive().device).to.equal('/dev/sdb') }) }) @@ -176,7 +175,7 @@ describe('Model: availableDrives', function () { this.imagePath = '/mnt/bar/foo.img' } - selectionState.deselectDrive() + selectionState.clear() selectionState.selectImage({ path: this.imagePath, extension: 'img', @@ -240,7 +239,7 @@ describe('Model: availableDrives', function () { } ]) - m.chai.expect(selectionState.getDrive()).to.deep.equal({ + m.chai.expect(selectionState.getCurrentDrive()).to.deep.equal({ device: '/dev/sdb', name: 'Foo', size: 2000000000, @@ -420,7 +419,7 @@ describe('Model: availableDrives', function () { }) afterEach(function () { - selectionState.deselectDrive() + selectionState.clear() }) it('should be deleted if its not contained in the available drives anymore', function () { diff --git a/tests/shared/models/selection-state.spec.js b/tests/shared/models/selection-state.spec.js index a6faecf9..56b7cc95 100644 --- a/tests/shared/models/selection-state.spec.js +++ b/tests/shared/models/selection-state.spec.js @@ -28,8 +28,8 @@ describe('Model: selectionState', function () { selectionState.clear() }) - it('getDrive() should return undefined', function () { - const drive = selectionState.getDrive() + it('getCurrentDrive() should return undefined', function () { + const drive = selectionState.getCurrentDrive() m.chai.expect(drive).to.be.undefined }) @@ -96,9 +96,9 @@ describe('Model: selectionState', function () { selectionState.selectDrive('/dev/disk2') }) - describe('.getDrive()', function () { + describe('.getCurrentDrive()', function () { it('should return the drive', function () { - const drive = selectionState.getDrive() + const drive = selectionState.getCurrentDrive() m.chai.expect(drive).to.deep.equal({ device: '/dev/disk2', name: 'USB Drive', @@ -115,11 +115,13 @@ describe('Model: selectionState', function () { }) }) - describe('.setDrive()', function () { - it('should override the drive', function () { + describe('.selectDrive()', function () { + it('should queue the drive', function () { selectionState.selectDrive('/dev/disk5') - const drive = selectionState.getDrive() - m.chai.expect(drive).to.deep.equal({ + const drives = selectionState.getSelectedDevices() + const lastDriveDevice = _.last(drives) + const lastDrive = _.find(availableDrives.getDrives(), { device: lastDriveDevice }) + m.chai.expect(lastDrive).to.deep.equal({ device: '/dev/disk5', name: 'USB Drive', size: 999999999, @@ -128,17 +130,117 @@ describe('Model: selectionState', function () { }) }) - describe('.removeDrive()', function () { - it('should clear the drive', function () { - selectionState.deselectDrive() - const drive = selectionState.getDrive() + describe('.deselectDrive()', function () { + it('should clear drives', function () { + const firstDrive = selectionState.getCurrentDrive() + selectionState.deselectDrive(firstDrive.device) + const secondDrive = selectionState.getCurrentDrive() + selectionState.deselectDrive(secondDrive.device) + const drive = selectionState.getCurrentDrive() m.chai.expect(drive).to.be.undefined }) }) }) + describe('given several drives', function () { + beforeEach(function () { + this.drives = [ + { + device: '/dev/sdb', + description: 'DataTraveler 2.0', + size: 999999999, + mountpoint: '/media/UNTITLED', + name: '/dev/sdb', + system: false, + isReadOnly: false + }, + { + device: '/dev/disk2', + name: 'USB Drive 2', + size: 999999999, + isReadOnly: false + }, + { + device: '/dev/disk3', + name: 'USB Drive 3', + size: 999999999, + isReadOnly: false + } + ] + + availableDrives.setDrives(this.drives) + + selectionState.selectDrive(this.drives[0].device) + selectionState.selectDrive(this.drives[1].device) + }) + + afterEach(function () { + selectionState.clear() + availableDrives.setDrives([]) + }) + + it('should be able to add more drives', function () { + selectionState.selectDrive(this.drives[2].device) + m.chai.expect(selectionState.getSelectedDevices()).to.deep.equal(_.map(this.drives, 'device')) + }) + + it('should be able to remove drives', function () { + selectionState.deselectDrive(this.drives[1].device) + m.chai.expect(selectionState.getSelectedDevices()).to.deep.equal([ this.drives[0].device ]) + }) + + it('current drive should be affected by add order', function () { + m.chai.expect(selectionState.getCurrentDrive()).to.deep.equal(this.drives[0]) + selectionState.toggleDrive(this.drives[0].device) + selectionState.toggleDrive(this.drives[0].device) + m.chai.expect(selectionState.getCurrentDrive()).to.deep.equal(this.drives[1]) + }) + + it('should keep system drives selected', function () { + const systemDrive = { + device: '/dev/disk0', + name: 'USB Drive 0', + size: 999999999, + isReadOnly: false, + system: true + } + + const newDrives = [ ..._.initial(this.drives), systemDrive ] + availableDrives.setDrives(newDrives) + + selectionState.selectDrive(systemDrive.device) + availableDrives.setDrives(newDrives) + m.chai.expect(selectionState.getSelectedDevices()).to.deep.equal(_.map(newDrives, 'device')) + }) + + it('should be able to remove a drive', function () { + m.chai.expect(selectionState.getSelectedDevices().length).to.equal(2) + selectionState.toggleDrive(this.drives[0].device) + m.chai.expect(selectionState.getSelectedDevices()).to.deep.equal([ this.drives[1].device ]) + }) + + describe('.deselectAllDrives()', function () { + it('should remove all drives', function () { + selectionState.deselectAllDrives() + m.chai.expect(selectionState.getSelectedDevices()).to.deep.equal([]) + }) + }) + + describe('.deselectOtherDrives()', function () { + it('should deselect other drives', function () { + selectionState.deselectOtherDrives(this.drives[0].device) + m.chai.expect(selectionState.getSelectedDevices()).to.not.include.members([ this.drives[1].device ]) + }) + + it('should not remove the specified drive', function () { + selectionState.deselectOtherDrives(this.drives[0].device) + m.chai.expect(selectionState.getSelectedDevices()).to.deep.equal([ this.drives[0].device ]) + }) + }) + }) + describe('given no drive', function () { - describe('.setDrive()', function () { + describe('.selectDrive()', function () { it('should be able to set a drive', function () { availableDrives.setDrives([ { @@ -150,7 +252,7 @@ describe('Model: selectionState', function () { ]) selectionState.selectDrive('/dev/disk5') - const drive = selectionState.getDrive() + const drive = selectionState.getCurrentDrive() m.chai.expect(drive).to.deep.equal({ device: '/dev/disk5', name: 'USB Drive', @@ -159,7 +261,7 @@ describe('Model: selectionState', function () { }) }) - it('should throw if drive is write protected', function () { + it('should throw if drive is read-only', function () { availableDrives.setDrives([ { device: '/dev/disk1', @@ -219,7 +321,7 @@ describe('Model: selectionState', function () { selectionState.selectImage(this.image) }) - describe('.setDrive()', function () { + describe('.selectDrive()', function () { it('should throw if drive is not large enough', function () { availableDrives.setDrives([ { @@ -298,7 +400,7 @@ describe('Model: selectionState', function () { }) }) - describe('.setImage()', function () { + describe('.selectImage()', function () { it('should override the image', function () { selectionState.selectImage({ path: 'bar.img', @@ -319,7 +421,7 @@ describe('Model: selectionState', function () { }) }) - describe('.removeImage()', function () { + describe('.deselectImage()', function () { it('should clear the image', function () { selectionState.deselectImage() @@ -332,7 +434,9 @@ describe('Model: selectionState', function () { }) describe('given no image', function () { - describe('.setImage()', function () { + describe('.selectImage()', function () { + afterEach(selectionState.clear) + it('should be able to set an image', function () { selectionState.selectImage({ path: 'foo.img', @@ -755,7 +859,7 @@ describe('Model: selectionState', function () { { device: '/dev/disk1', name: 'USB Drive', - size: 999999999, + size: 123456789, isReadOnly: false } ]) @@ -767,10 +871,10 @@ describe('Model: selectionState', function () { path: 'foo.img', extension: 'img', size: { - original: 9999999999, + original: 1234567890, final: { estimation: false, - value: 9999999999 + value: 1234567890 } } }) @@ -890,20 +994,126 @@ describe('Model: selectionState', function () { m.chai.expect(selectionState.hasImage()).to.be.false }) }) + describe('.deselectImage()', function () { - it('should not clear any drives', function () { + beforeEach(function () { selectionState.deselectImage() + }) + + it('getCurrentDrive() should return the selected drive object', function () { + const drive = selectionState.getCurrentDrive() + m.chai.expect(drive).to.deep.equal({ + device: '/dev/disk1', + isReadOnly: false, + name: 'USB Drive', + size: 999999999 + }) + }) + + it('getImagePath() should return undefined', function () { + const imagePath = selectionState.getImagePath() + m.chai.expect(imagePath).to.be.undefined + }) + + it('getImageSize() should return undefined', function () { + const imageSize = selectionState.getImageSize() + m.chai.expect(imageSize).to.be.undefined + }) + + it('should not clear any drives', function () { m.chai.expect(selectionState.hasDrive()).to.be.true }) + + it('hasImage() should return false', function () { + const hasImage = selectionState.hasImage() + m.chai.expect(hasImage).to.be.false + }) }) - describe('.deselectDrive()', function () { + + describe('.deselectAllDrives()', function () { + beforeEach(function () { + selectionState.deselectAllDrives() + }) + + it('getCurrentDrive() should return undefined', function () { + const drive = selectionState.getCurrentDrive() + m.chai.expect(drive).to.be.undefined + }) + + it('getImagePath() should return the image path', function () { + const imagePath = selectionState.getImagePath() + m.chai.expect(imagePath).to.equal('foo.img') + }) + + it('getImageSize() should return the image size', function () { + const imageSize = selectionState.getImageSize() + m.chai.expect(imageSize).to.equal(999999999) + }) + + it('hasDrive() should return false', function () { + const hasDrive = selectionState.hasDrive() + m.chai.expect(hasDrive).to.be.false + }) + it('should not clear the image', function () { - selectionState.deselectDrive() m.chai.expect(selectionState.hasImage()).to.be.true }) }) }) + describe('given several drives', function () { + beforeEach(function () { + availableDrives.setDrives([ + { + device: '/dev/disk1', + name: 'USB Drive 1', + size: 999999999, + isReadOnly: false + }, + { + device: '/dev/disk2', + name: 'USB Drive 2', + size: 999999999, + isReadOnly: false + }, + { + device: '/dev/disk3', + name: 'USB Drive 3', + size: 999999999, + isReadOnly: false + } + ]) + + selectionState.selectDrive('/dev/disk1') + selectionState.selectDrive('/dev/disk2') + selectionState.selectDrive('/dev/disk3') + + selectionState.selectImage({ + path: 'foo.img', + extension: 'img', + size: { + original: 999999999, + final: { + estimation: false, + value: 999999999 + } + } + }) + }) + + describe('.clear()', function () { + it('should clear all selections', function () { + m.chai.expect(selectionState.hasDrive()).to.be.true + m.chai.expect(selectionState.hasImage()).to.be.true + + selectionState.clear() + + m.chai.expect(selectionState.hasDrive()).to.be.false + m.chai.expect(selectionState.hasImage()).to.be.false + }) + }) + }) + describe('.isCurrentDrive()', function () { describe('given a selected drive', function () { beforeEach(function () { @@ -924,6 +1134,11 @@ describe('Model: selectionState', function () { selectionState.selectDrive('/dev/sdb') }) + afterEach(function () { + selectionState.clear() + availableDrives.setDrives([]) + }) + it('should return false if an undefined value is passed', function () { m.chai.expect(selectionState.isCurrentDrive()).to.be.false }) @@ -939,7 +1154,7 @@ describe('Model: selectionState', function () { describe('given no selected drive', function () { beforeEach(function () { - selectionState.deselectDrive() + selectionState.clear() }) it('should return false if an undefined value is passed', function () { @@ -952,7 +1167,7 @@ describe('Model: selectionState', function () { }) }) - describe('.toggleSetDrive()', function () { + describe('.toggleDrive()', function () { describe('given a selected drive', function () { beforeEach(function () { this.drive = { @@ -971,7 +1186,7 @@ describe('Model: selectionState', function () { this.drive, { device: '/dev/disk2', - name: 'USB Drive', + name: 'USB Drive 2', size: 999999999, isReadOnly: false } @@ -980,13 +1195,18 @@ describe('Model: selectionState', function () { selectionState.selectDrive(this.drive.device) }) + afterEach(function () { + selectionState.clear() + availableDrives.setDrives([]) + }) + it('should be able to remove the drive', function () { m.chai.expect(selectionState.hasDrive()).to.be.true selectionState.toggleDrive(this.drive.device) m.chai.expect(selectionState.hasDrive()).to.be.false }) - it('should be able to replace the drive', function () { + it('should not replace a different drive', function () { const drive = { device: '/dev/disk2', name: 'USB Drive', @@ -994,29 +1214,48 @@ describe('Model: selectionState', function () { isReadOnly: false } - m.chai.expect(selectionState.getDrive()).to.deep.equal(this.drive) + m.chai.expect(selectionState.getCurrentDrive()).to.deep.equal(this.drive) selectionState.toggleDrive(drive.device) - m.chai.expect(selectionState.getDrive()).to.deep.equal(drive) - m.chai.expect(selectionState.getDrive()).to.not.deep.equal(this.drive) + m.chai.expect(selectionState.getCurrentDrive()).to.deep.equal(this.drive) + m.chai.expect(selectionState.getCurrentDrive()).to.not.deep.equal(drive) }) }) describe('given no selected drive', function () { beforeEach(function () { - selectionState.deselectDrive() + selectionState.clear() + + availableDrives.setDrives([ + { + device: '/dev/disk2', + name: 'USB Drive 2', + size: 999999999, + isReadOnly: false + }, + { + device: '/dev/disk3', + name: 'USB Drive 3', + size: 999999999, + isReadOnly: false + } + ]) + }) + + afterEach(function () { + availableDrives.setDrives([]) }) it('should set the drive', function () { const drive = { device: '/dev/disk2', - name: 'USB Drive', + name: 'USB Drive 2', size: 999999999, isReadOnly: false } m.chai.expect(selectionState.hasDrive()).to.be.false selectionState.toggleDrive(drive.device) - m.chai.expect(selectionState.getDrive()).to.deep.equal(drive) + m.chai.expect(selectionState.getCurrentDrive()).to.deep.equal(drive) }) }) })