feat: add drive multi-selection in store (#1736)

We lay the foundation for multi-selecting drives by implementing it into
the `store` and relevant modules interacting with the `store`.

Change-Type: patch
Changelog-Entry: Add drive multi-selection to the store.
This commit is contained in:
Benedict Aas 2018-02-23 17:45:49 +00:00 committed by GitHub
parent ee93013220
commit 207c2ef5b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 482 additions and 114 deletions

View File

@ -118,6 +118,7 @@ module.exports = function (
previouslySelected: selectionState.isCurrentDrive(drive.device) previouslySelected: selectionState.isCurrentDrive(drive.device)
}) })
selectionState.deselectOtherDrives(drive.device)
selectionState.toggleDrive(drive.device) selectionState.toggleDrive(drive.device)
} }
}) })
@ -132,7 +133,7 @@ module.exports = function (
* DriveSelectorController.closeModal(); * DriveSelectorController.closeModal();
*/ */
this.closeModal = () => { this.closeModal = () => {
const selectedDrive = selectionState.getDrive() const selectedDrive = selectionState.getCurrentDrive()
// Sanity check to cover the case where a drive is selected, // Sanity check to cover the case where a drive is selected,
// the drive is then unplugged from the computer and the modal // the drive is then unplugged from the computer and the modal

View File

@ -38,7 +38,7 @@
</div> </div>
<span class="list-group-item-section tick tick--success" <span class="list-group-item-section tick tick--success"
ng-show="modal.constraints.isDriveValid(drive, modal.state.getImage())" ng-show="modal.constraints.isDriveValid(drive, modal.state.getImage())"
ng-disabled="!modal.state.isCurrentDrive(drive.device)"></span> ng-disabled="!modal.state.isDriveSelected(drive.device)"></span>
</li> </li>
<li class="list-group-item" <li class="list-group-item"
ng-show="!modal.drives.hasAvailableDrives()"> ng-show="!modal.drives.hasAvailableDrives()">

View File

@ -51,7 +51,7 @@ module.exports = function ($state) {
if (!options.preserveImage) { if (!options.preserveImage) {
selectionState.deselectImage() selectionState.deselectImage()
} }
selectionState.deselectDrive() selectionState.deselectAllDrives()
analytics.logEvent('Restart', options) analytics.logEvent('Restart', options)
$state.go('main') $state.go('main')
} }

View File

@ -66,11 +66,11 @@
'text-disabled': main.shouldDriveStepBeDisabled() 'text-disabled': main.shouldDriveStepBeDisabled()
}"> }">
<span class="drive-step step-name" <span class="drive-step step-name"
uib-tooltip="{{ main.selection.getDrive().description }} ({{ main.selection.getDrive().displayName }})"> uib-tooltip="{{ main.selection.getCurrentDrive().description }} ({{ main.selection.getCurrentDrive().displayName }})">
<!-- middleEllipses errors on undefined, therefore fallback to empty string --> <!-- middleEllipses errors on undefined, therefore fallback to empty string -->
{{ (main.selection.getDrive().description || "") | middleEllipses:11 }} {{ (main.selection.getCurrentDrive().description || "") | middleEllipses:11 }}
</span> </span>
<span class="step-drive step-size">{{ main.selection.getDrive().size | closestUnit }}</span> <span class="step-drive step-size">{{ main.selection.getCurrentDrive().size | closestUnit }}</span>
<span class="step-drive step-warning glyphicon glyphicon-exclamation-sign" <span class="step-drive step-warning glyphicon glyphicon-exclamation-sign"
uib-tooltip="{{ main.constraints.getDriveImageCompatibilityStatuses(main.selection.getDrive(), main.selection.getImage())[0].message }}" uib-tooltip="{{ main.constraints.getDriveImageCompatibilityStatuses(main.selection.getDrive(), main.selection.getImage())[0].message }}"
ng-show="main.constraints.hasDriveImageCompatibilityStatus(main.selection.getDrive(), main.selection.getImage())"></span> ng-show="main.constraints.hasDriveImageCompatibilityStatus(main.selection.getDrive(), main.selection.getImage())"></span>
@ -97,7 +97,7 @@
percentage="main.state.getFlashState().percentage" percentage="main.state.getFlashState().percentage"
striped="{{ main.state.getFlashState().type == 'check' }}" striped="{{ main.state.getFlashState().type == 'check' }}"
ng-attr-active="{{ main.state.isFlashing() }}" 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()"> ng-disabled="main.shouldFlashStepBeDisabled() || main.state.getLastFlashErrorCode()">
<span ng-bind="flash.getProgressButtonLabel()"></span> <span ng-bind="flash.getProgressButtonLabel()"></span>
</progress-button> </progress-button>

View File

@ -48,13 +48,41 @@ exports.selectDrive = (driveDevice) => {
* selectionState.toggleDrive('/dev/disk2'); * selectionState.toggleDrive('/dev/disk2');
*/ */
exports.toggleDrive = (driveDevice) => { exports.toggleDrive = (driveDevice) => {
if (exports.isCurrentDrive(driveDevice)) { if (exports.isDriveSelected(driveDevice)) {
exports.deselectDrive() exports.deselectDrive(driveDevice)
} else { } else {
exports.selectDrive(driveDevice) 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 * @summary Select an image
* @function * @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 * @function
* @public * @public
* *
* @returns {Object} drive * @returns {Object} drive
* *
* @example * @example
* const drive = selectionState.getDrive(); * const drive = selectionState.getCurrentDrive();
* console.log(drive)
* > { device: '/dev/disk1', name: 'Flash drive', ... }
*/ */
exports.getDrive = () => { exports.getCurrentDrive = () => {
return _.find(availableDrives.getDrives(), { const device = _.head(exports.getSelectedDevices())
device: store.getState().getIn([ 'selection', 'drive' ]) return _.find(availableDrives.getDrives(), { device })
})
} }
/** /**
@ -252,7 +299,7 @@ exports.getImageRecommendedDriveSize = () => {
* } * }
*/ */
exports.hasDrive = () => { 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 * @function
* @public * @public
* *
* @param {String} driveDevice - drive device identifier
*
* @example * @example
* selectionState.deselectDrive(); * selectionState.deselectDrive('/dev/sdc');
*
* @example
* selectionState.deselectDrive('\\\\.\\PHYSICALDRIVE3');
*/ */
exports.deselectDrive = () => { exports.deselectDrive = (driveDevice) => {
store.dispatch({ 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 * @summary Clear selections
* @function * @function
@ -306,13 +371,10 @@ exports.deselectImage = () => {
* *
* @example * @example
* selectionState.clear(); * selectionState.clear();
*
* @example
* selectionState.clear({ preserveImage: true });
*/ */
exports.clear = () => { exports.clear = () => {
exports.deselectImage() exports.deselectImage()
exports.deselectDrive() exports.deselectAllDrives()
} }
/** /**
@ -333,5 +395,29 @@ exports.isCurrentDrive = (driveDevice) => {
return false 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)
} }

View File

@ -82,7 +82,9 @@ const selectImageNoNilFields = [
*/ */
const DEFAULT_STATE = Immutable.fromJS({ const DEFAULT_STATE = Immutable.fromJS({
availableDrives: [], availableDrives: [],
selection: {}, selection: {
devices: new Immutable.OrderedSet()
},
isFlashing: false, isFlashing: false,
flashResults: {}, flashResults: {},
flashState: { 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 * @function
* @private * @public
* *
* @param {Object} state - application state * @param {Object} state - state object
* @param {String} device - drive device * @returns {Object} new state
* @returns {(Object|Undefined)} drive
* *
* @example * @example
* const drive = findDrive(state, '/dev/disk2'); * const drives = getAvailableDrives(state)
* _.find(drives, { device: '/dev/sda' })
*/ */
const findDrive = (state, device) => { const getAvailableDrives = (state) => {
/* eslint-disable lodash/prefer-lodash-method */ // eslint-disable-next-line lodash/prefer-lodash-method
return state.get('availableDrives').toJS()
return state.get('availableDrives').find((drive) => {
return drive.get('device') === device
})
/* eslint-enable lodash/prefer-lodash-method */
} }
/** /**
@ -160,6 +157,8 @@ const findDrive = (state, device) => {
const storeReducer = (state = DEFAULT_STATE, action) => { const storeReducer = (state = DEFAULT_STATE, action) => {
switch (action.type) { switch (action.type) {
case ACTIONS.SET_AVAILABLE_DRIVES: { case ACTIONS.SET_AVAILABLE_DRIVES: {
// Type: action.data : Array<DriveObject>
if (!action.data) { if (!action.data) {
throw errors.createError({ throw errors.createError({
title: 'Missing drives' title: 'Missing drives'
@ -167,20 +166,20 @@ const storeReducer = (state = DEFAULT_STATE, action) => {
} }
// Convert object instances to plain objects // 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({ 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 AUTOSELECT_DRIVE_COUNT = 1
const numberOfDrives = action.data.length const numberOfDrives = drives.length
if (numberOfDrives === AUTOSELECT_DRIVE_COUNT) { 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 // Even if there's no image selected, we need to call several
// drive/image related checks, and `{}` works fine with them // drive/image related checks, and `{}` works fine with them
@ -198,27 +197,42 @@ const storeReducer = (state = DEFAULT_STATE, action) => {
!constraints.isSystemDrive(drive) !constraints.isSystemDrive(drive)
])) { ])) {
// Auto-select this drive
return storeReducer(newState, { return storeReducer(newState, {
type: ACTIONS.SELECT_DRIVE, type: ACTIONS.SELECT_DRIVE,
data: drive.device data: drive.device
}) })
} }
}
const selectedDevice = newState.getIn([ 'selection', 'drive' ]) // Deselect this drive in case it still is selected
if (selectedDevice && !_.find(action.data, {
device: selectedDevice
})) {
return storeReducer(newState, { 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: { case ACTIONS.SET_FLASH_STATE: {
// Type: action.data : FlashStateObject
if (!state.get('isFlashing')) { if (!state.get('isFlashing')) {
throw errors.createError({ throw errors.createError({
title: 'Can\'t set the flashing state when not flashing' 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: { case ACTIONS.UNSET_FLASHING_FLAG: {
// Type: action.data : FlashResultsObject
if (!action.data) { if (!action.data) {
throw errors.createError({ throw errors.createError({
title: 'Missing results' title: 'Missing results'
@ -305,40 +321,46 @@ const storeReducer = (state = DEFAULT_STATE, action) => {
} }
case ACTIONS.SELECT_DRIVE: { case ACTIONS.SELECT_DRIVE: {
if (!action.data) { // Type: action.data : String
const device = action.data
if (!device) {
throw errors.createError({ throw errors.createError({
title: 'Missing drive' title: 'Missing drive'
}) })
} }
if (!_.isString(action.data)) { if (!_.isString(device)) {
throw errors.createError({ 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) { if (!selectedDrive) {
throw errors.createError({ 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({ throw errors.createError({
title: 'The drive is write-protected' title: 'The drive is write-protected'
}) })
} }
const image = state.getIn([ 'selection', 'image' ]) const image = state.getIn([ 'selection', 'image' ])
if (image && !constraints.isDriveLargeEnough(selectedDrive.toJS(), image.toJS())) { if (image && !constraints.isDriveLargeEnough(selectedDrive, image.toJS())) {
throw errors.createError({ throw errors.createError({
title: 'The drive is not large enough' 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 // TODO(jhermsmeier): Consolidate these assertions
@ -346,6 +368,8 @@ const storeReducer = (state = DEFAULT_STATE, action) => {
// place where all the image extension / format handling // place where all the image extension / format handling
// takes place, to avoid having to check 2+ locations with different logic // takes place, to avoid having to check 2+ locations with different logic
case ACTIONS.SELECT_IMAGE: { case ACTIONS.SELECT_IMAGE: {
// Type: action.data : ImageObject
verifyNoNilFields(action.data, selectImageNoNilFields, 'image') verifyNoNilFields(action.data, selectImageNoNilFields, 'image')
if (!_.isString(action.data.path)) { 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(() => { // Remove image-incompatible drives from selection with `constraints.isDriveValid`
if (selectedDrive && !_.every([ return _.reduce(selectedDevices.toJS(), (accState, device) => {
constraints.isDriveValid(selectedDrive.toJS(), action.data), const drive = _.find(getAvailableDrives(state), { device })
constraints.isDriveSizeRecommended(selectedDrive.toJS(), action.data) if (!constraints.isDriveValid(drive, action.data) || !constraints.isDriveSizeRecommended(drive, action.data)) {
])) { return storeReducer(accState, {
return storeReducer(state, { type: ACTIONS.DESELECT_DRIVE,
type: ACTIONS.DESELECT_DRIVE data: device
}) })
} }
return state return accState
}).setIn([ 'selection', 'image' ], Immutable.fromJS(action.data)) }, state).setIn([ 'selection', 'image' ], Immutable.fromJS(action.data))
} }
case ACTIONS.DESELECT_DRIVE: { 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: { case ACTIONS.DESELECT_IMAGE: {
@ -457,6 +498,8 @@ const storeReducer = (state = DEFAULT_STATE, action) => {
} }
case ACTIONS.SET_SETTINGS: { case ACTIONS.SET_SETTINGS: {
// Type: action.data : SettingsObject
if (!action.data) { if (!action.data) {
throw errors.createError({ throw errors.createError({
title: 'Missing settings' title: 'Missing settings'

View File

@ -143,8 +143,7 @@ describe('Model: availableDrives', function () {
describe('given no selected image and no selected drive', function () { describe('given no selected image and no selected drive', function () {
beforeEach(function () { beforeEach(function () {
selectionState.deselectDrive() selectionState.clear()
selectionState.deselectImage()
}) })
it('should auto-select a single valid available drive', function () { 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.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' this.imagePath = '/mnt/bar/foo.img'
} }
selectionState.deselectDrive() selectionState.clear()
selectionState.selectImage({ selectionState.selectImage({
path: this.imagePath, path: this.imagePath,
extension: 'img', 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', device: '/dev/sdb',
name: 'Foo', name: 'Foo',
size: 2000000000, size: 2000000000,
@ -420,7 +419,7 @@ describe('Model: availableDrives', function () {
}) })
afterEach(function () { afterEach(function () {
selectionState.deselectDrive() selectionState.clear()
}) })
it('should be deleted if its not contained in the available drives anymore', function () { it('should be deleted if its not contained in the available drives anymore', function () {

View File

@ -28,8 +28,8 @@ describe('Model: selectionState', function () {
selectionState.clear() selectionState.clear()
}) })
it('getDrive() should return undefined', function () { it('getCurrentDrive() should return undefined', function () {
const drive = selectionState.getDrive() const drive = selectionState.getCurrentDrive()
m.chai.expect(drive).to.be.undefined m.chai.expect(drive).to.be.undefined
}) })
@ -96,9 +96,9 @@ describe('Model: selectionState', function () {
selectionState.selectDrive('/dev/disk2') selectionState.selectDrive('/dev/disk2')
}) })
describe('.getDrive()', function () { describe('.getCurrentDrive()', function () {
it('should return the drive', function () { it('should return the drive', function () {
const drive = selectionState.getDrive() const drive = selectionState.getCurrentDrive()
m.chai.expect(drive).to.deep.equal({ m.chai.expect(drive).to.deep.equal({
device: '/dev/disk2', device: '/dev/disk2',
name: 'USB Drive', name: 'USB Drive',
@ -115,11 +115,13 @@ describe('Model: selectionState', function () {
}) })
}) })
describe('.setDrive()', function () { describe('.selectDrive()', function () {
it('should override the drive', function () { it('should queue the drive', function () {
selectionState.selectDrive('/dev/disk5') selectionState.selectDrive('/dev/disk5')
const drive = selectionState.getDrive() const drives = selectionState.getSelectedDevices()
m.chai.expect(drive).to.deep.equal({ const lastDriveDevice = _.last(drives)
const lastDrive = _.find(availableDrives.getDrives(), { device: lastDriveDevice })
m.chai.expect(lastDrive).to.deep.equal({
device: '/dev/disk5', device: '/dev/disk5',
name: 'USB Drive', name: 'USB Drive',
size: 999999999, size: 999999999,
@ -128,17 +130,117 @@ describe('Model: selectionState', function () {
}) })
}) })
describe('.removeDrive()', function () { describe('.deselectDrive()', function () {
it('should clear the drive', function () { it('should clear drives', function () {
selectionState.deselectDrive() const firstDrive = selectionState.getCurrentDrive()
const drive = selectionState.getDrive() selectionState.deselectDrive(firstDrive.device)
const secondDrive = selectionState.getCurrentDrive()
selectionState.deselectDrive(secondDrive.device)
const drive = selectionState.getCurrentDrive()
m.chai.expect(drive).to.be.undefined 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('given no drive', function () {
describe('.setDrive()', function () { describe('.selectDrive()', function () {
it('should be able to set a drive', function () { it('should be able to set a drive', function () {
availableDrives.setDrives([ availableDrives.setDrives([
{ {
@ -150,7 +252,7 @@ describe('Model: selectionState', function () {
]) ])
selectionState.selectDrive('/dev/disk5') selectionState.selectDrive('/dev/disk5')
const drive = selectionState.getDrive() const drive = selectionState.getCurrentDrive()
m.chai.expect(drive).to.deep.equal({ m.chai.expect(drive).to.deep.equal({
device: '/dev/disk5', device: '/dev/disk5',
name: 'USB Drive', 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([ availableDrives.setDrives([
{ {
device: '/dev/disk1', device: '/dev/disk1',
@ -219,7 +321,7 @@ describe('Model: selectionState', function () {
selectionState.selectImage(this.image) selectionState.selectImage(this.image)
}) })
describe('.setDrive()', function () { describe('.selectDrive()', function () {
it('should throw if drive is not large enough', function () { it('should throw if drive is not large enough', function () {
availableDrives.setDrives([ availableDrives.setDrives([
{ {
@ -298,7 +400,7 @@ describe('Model: selectionState', function () {
}) })
}) })
describe('.setImage()', function () { describe('.selectImage()', function () {
it('should override the image', function () { it('should override the image', function () {
selectionState.selectImage({ selectionState.selectImage({
path: 'bar.img', path: 'bar.img',
@ -319,7 +421,7 @@ describe('Model: selectionState', function () {
}) })
}) })
describe('.removeImage()', function () { describe('.deselectImage()', function () {
it('should clear the image', function () { it('should clear the image', function () {
selectionState.deselectImage() selectionState.deselectImage()
@ -332,7 +434,9 @@ describe('Model: selectionState', function () {
}) })
describe('given no image', function () { describe('given no image', function () {
describe('.setImage()', function () { describe('.selectImage()', function () {
afterEach(selectionState.clear)
it('should be able to set an image', function () { it('should be able to set an image', function () {
selectionState.selectImage({ selectionState.selectImage({
path: 'foo.img', path: 'foo.img',
@ -755,7 +859,7 @@ describe('Model: selectionState', function () {
{ {
device: '/dev/disk1', device: '/dev/disk1',
name: 'USB Drive', name: 'USB Drive',
size: 999999999, size: 123456789,
isReadOnly: false isReadOnly: false
} }
]) ])
@ -767,10 +871,10 @@ describe('Model: selectionState', function () {
path: 'foo.img', path: 'foo.img',
extension: 'img', extension: 'img',
size: { size: {
original: 9999999999, original: 1234567890,
final: { final: {
estimation: false, estimation: false,
value: 9999999999 value: 1234567890
} }
} }
}) })
@ -890,20 +994,126 @@ describe('Model: selectionState', function () {
m.chai.expect(selectionState.hasImage()).to.be.false m.chai.expect(selectionState.hasImage()).to.be.false
}) })
}) })
describe('.deselectImage()', function () { describe('.deselectImage()', function () {
it('should not clear any drives', function () { beforeEach(function () {
selectionState.deselectImage() 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 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 () { it('should not clear the image', function () {
selectionState.deselectDrive()
m.chai.expect(selectionState.hasImage()).to.be.true 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('.isCurrentDrive()', function () {
describe('given a selected drive', function () { describe('given a selected drive', function () {
beforeEach(function () { beforeEach(function () {
@ -924,6 +1134,11 @@ describe('Model: selectionState', function () {
selectionState.selectDrive('/dev/sdb') selectionState.selectDrive('/dev/sdb')
}) })
afterEach(function () {
selectionState.clear()
availableDrives.setDrives([])
})
it('should return false if an undefined value is passed', function () { it('should return false if an undefined value is passed', function () {
m.chai.expect(selectionState.isCurrentDrive()).to.be.false m.chai.expect(selectionState.isCurrentDrive()).to.be.false
}) })
@ -939,7 +1154,7 @@ describe('Model: selectionState', function () {
describe('given no selected drive', function () { describe('given no selected drive', function () {
beforeEach(function () { beforeEach(function () {
selectionState.deselectDrive() selectionState.clear()
}) })
it('should return false if an undefined value is passed', function () { 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 () { describe('given a selected drive', function () {
beforeEach(function () { beforeEach(function () {
this.drive = { this.drive = {
@ -971,7 +1186,7 @@ describe('Model: selectionState', function () {
this.drive, this.drive,
{ {
device: '/dev/disk2', device: '/dev/disk2',
name: 'USB Drive', name: 'USB Drive 2',
size: 999999999, size: 999999999,
isReadOnly: false isReadOnly: false
} }
@ -980,13 +1195,18 @@ describe('Model: selectionState', function () {
selectionState.selectDrive(this.drive.device) selectionState.selectDrive(this.drive.device)
}) })
afterEach(function () {
selectionState.clear()
availableDrives.setDrives([])
})
it('should be able to remove the drive', function () { it('should be able to remove the drive', function () {
m.chai.expect(selectionState.hasDrive()).to.be.true m.chai.expect(selectionState.hasDrive()).to.be.true
selectionState.toggleDrive(this.drive.device) selectionState.toggleDrive(this.drive.device)
m.chai.expect(selectionState.hasDrive()).to.be.false 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 = { const drive = {
device: '/dev/disk2', device: '/dev/disk2',
name: 'USB Drive', name: 'USB Drive',
@ -994,29 +1214,48 @@ describe('Model: selectionState', function () {
isReadOnly: false 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) selectionState.toggleDrive(drive.device)
m.chai.expect(selectionState.getDrive()).to.deep.equal(drive) m.chai.expect(selectionState.getCurrentDrive()).to.deep.equal(this.drive)
m.chai.expect(selectionState.getDrive()).to.not.deep.equal(this.drive) m.chai.expect(selectionState.getCurrentDrive()).to.not.deep.equal(drive)
}) })
}) })
describe('given no selected drive', function () { describe('given no selected drive', function () {
beforeEach(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 () { it('should set the drive', function () {
const drive = { const drive = {
device: '/dev/disk2', device: '/dev/disk2',
name: 'USB Drive', name: 'USB Drive 2',
size: 999999999, size: 999999999,
isReadOnly: false isReadOnly: false
} }
m.chai.expect(selectionState.hasDrive()).to.be.false m.chai.expect(selectionState.hasDrive()).to.be.false
selectionState.toggleDrive(drive.device) selectionState.toggleDrive(drive.device)
m.chai.expect(selectionState.getDrive()).to.deep.equal(drive) m.chai.expect(selectionState.getCurrentDrive()).to.deep.equal(drive)
}) })
}) })
}) })