Merge pull request #1563 from nasa/open-623

[Timers] Add stop button
This commit is contained in:
Pete Richards
2017-06-19 18:20:25 -07:00
committed by GitHub
13 changed files with 539 additions and 127 deletions

View File

@@ -1,33 +1,36 @@
.l-time-display { .l-time-display {
$transTime: 200ms; $transTime: 200ms;
$controlSize: 14px;
$control1ControlW: $controlSize + $interiorMargin;
$control2ControlW: $control1ControlW * 2;
line-height: 140%; line-height: 140%;
&:hover { &:hover {
.l-btn.control { .l-btn.controls {
opacity: 1; opacity: 1;
} }
} }
&.l-timer { &.l-timer {
.l-value:before,
.control {
font-size: 0.8em;
}
.l-value:before { .l-value:before {
// Direction +/- element // Direction +/- element
font-size: $controlSize;
margin-right: $interiorMarginSm; margin-right: $interiorMarginSm;
} }
.control { .controls {
@include trans-prop-nice((width, opacity), $transTime); @include trans-prop-nice((width, opacity), $transTime);
font-size: $controlSize;
line-height: inherit; line-height: inherit;
margin-right: 0; margin-right: 0;
opacity: 0; opacity: 0;
width: 0; width: 0;
} .flex-elem {
&:hover .control {
margin-right: $interiorMargin; margin-right: $interiorMargin;
}
}
&:hover .controls {
opacity: 1; opacity: 1;
width: 1em; width: $control2ControlW;
} }
} }
@@ -35,4 +38,34 @@
color: pullForward($colorBodyFg, 50%); color: pullForward($colorBodyFg, 50%);
font-weight: 400; font-weight: 400;
} }
// States
&.s-state-stopped,
&.s-state-paused {
.l-value {
opacity: 0.4;
}
}
&.s-state-started {
.l-value {
opacity: 1;
}
}
&.s-state-stopped {
// Hide Stop button, 1controlW
.t-btn-stop {
display: none;
}
&:hover .controls { width: $control1ControlW; }
}
&.s-state-paused {
// Paused, do something visual
.l-value {
&:before { @extend .pulse; }
}
}
} }

View File

@@ -28,6 +28,8 @@ define([
"./src/controllers/RefreshingController", "./src/controllers/RefreshingController",
"./src/actions/StartTimerAction", "./src/actions/StartTimerAction",
"./src/actions/RestartTimerAction", "./src/actions/RestartTimerAction",
"./src/actions/StopTimerAction",
"./src/actions/PauseTimerAction",
"text!./res/templates/clock.html", "text!./res/templates/clock.html",
"text!./res/templates/timer.html", "text!./res/templates/timer.html",
'legacyRegistry' 'legacyRegistry'
@@ -39,6 +41,8 @@ define([
RefreshingController, RefreshingController,
StartTimerAction, StartTimerAction,
RestartTimerAction, RestartTimerAction,
StopTimerAction,
PauseTimerAction,
clockTemplate, clockTemplate,
timerTemplate, timerTemplate,
legacyRegistry legacyRegistry
@@ -139,6 +143,17 @@ define([
"cssClass": "icon-play", "cssClass": "icon-play",
"priority": "preferred" "priority": "preferred"
}, },
{
"key": "timer.pause",
"implementation": PauseTimerAction,
"depends": [
"now"
],
"category": "contextual",
"name": "Pause",
"cssClass": "icon-pause",
"priority": "preferred"
},
{ {
"key": "timer.restart", "key": "timer.restart",
"implementation": RestartTimerAction, "implementation": RestartTimerAction,
@@ -149,6 +164,17 @@ define([
"name": "Restart at 0", "name": "Restart at 0",
"cssClass": "icon-refresh", "cssClass": "icon-refresh",
"priority": "preferred" "priority": "preferred"
},
{
"key": "timer.stop",
"implementation": StopTimerAction,
"depends": [
"now"
],
"category": "contextual",
"name": "Stop",
"cssClass": "icon-box",
"priority": "preferred"
} }
], ],
"types": [ "types": [

View File

@@ -19,11 +19,16 @@
this source code distribution or the Licensing information page available this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<div class="l-time-display l-digital l-timer s-timer" ng-controller="TimerController as timer"> <div class="l-time-display l-digital l-timer s-timer s-state-{{timer.timerState}}" ng-controller="TimerController as timer">
<div class="l-elem-wrapper l-flex-row"> <div class="l-elem-wrapper l-flex-row">
<div class="l-elem-wrapper l-flex-row controls">
<a ng-click="timer.clickStopButton()"
title="Stop"
class="flex-elem s-icon-button t-btn-stop icon-box"></a>
<a ng-click="timer.clickButton()" <a ng-click="timer.clickButton()"
title="{{timer.buttonText()}}" title="{{timer.buttonText()}}"
class="flex-elem control s-icon-button {{timer.buttonCssClass()}}"></a> class="flex-elem s-icon-button t-btn-pauseplay {{timer.buttonCssClass()}}"></a>
</div>
<span class="flex-elem l-value {{timer.signClass()}}"> <span class="flex-elem l-value {{timer.signClass()}}">
<span class="value" <span class="value"
ng-class="{ active:timer.text() }">{{timer.text() || "--:--:--"}} ng-class="{ active:timer.text() }">{{timer.text() || "--:--:--"}}

View File

@@ -25,13 +25,10 @@ define(
function () { function () {
/** /**
* Implements the "Start" and "Restart" action for timers. * Implements the "Pause" action for timers.
* *
* Sets the reference timestamp in a timer to the current * Sets the reference pausedTime in a timer to the current
* time, such that it begins counting up. * time, such that it stops counting up.
*
* Both "Start" and "Restart" share this implementation, but
* control their visibility with different `appliesTo` behavior.
* *
* @implements {Action} * @implements {Action}
* @memberof platform/features/clock * @memberof platform/features/clock
@@ -40,22 +37,35 @@ define(
* time (typically wrapping `Date.now`) * time (typically wrapping `Date.now`)
* @param {ActionContext} context the context for this action * @param {ActionContext} context the context for this action
*/ */
function AbstractStartTimerAction(now, context) { function PauseTimerAction(now, context) {
this.domainObject = context.domainObject; this.domainObject = context.domainObject;
this.now = now; this.now = now;
} }
AbstractStartTimerAction.prototype.perform = function () { PauseTimerAction.appliesTo = function (context) {
var model =
(context.domainObject && context.domainObject.getModel()) ||
{};
// We show this variant for timers which have
// a target time, or is in a playing state.
return model.type === 'timer' &&
model.timerState === 'started';
};
PauseTimerAction.prototype.perform = function () {
var domainObject = this.domainObject, var domainObject = this.domainObject,
now = this.now; now = this.now;
function setTimestamp(model) { function updateModel(model) {
model.timestamp = now(); model.timerState = 'paused';
model.pausedTime = now();
} }
return domainObject.useCapability('mutation', setTimestamp); return domainObject.useCapability('mutation', updateModel);
}; };
return AbstractStartTimerAction; return PauseTimerAction;
} }
); );

View File

@@ -21,8 +21,8 @@
*****************************************************************************/ *****************************************************************************/
define( define(
['./AbstractStartTimerAction'], [],
function (AbstractStartTimerAction) { function () {
/** /**
* Implements the "Restart at 0" action. * Implements the "Restart at 0" action.
@@ -30,7 +30,6 @@ define(
* Behaves the same as (and delegates functionality to) * Behaves the same as (and delegates functionality to)
* the "Start" action. * the "Start" action.
* *
* @extends {platform/features/clock.AbstractTimerAction}
* @implements {Action} * @implements {Action}
* @memberof platform/features/clock * @memberof platform/features/clock
* @constructor * @constructor
@@ -39,24 +38,33 @@ define(
* @param {ActionContext} context the context for this action * @param {ActionContext} context the context for this action
*/ */
function RestartTimerAction(now, context) { function RestartTimerAction(now, context) {
AbstractStartTimerAction.apply(this, [now, context]); this.domainObject = context.domainObject;
this.now = now;
} }
RestartTimerAction.prototype =
Object.create(AbstractStartTimerAction.prototype);
RestartTimerAction.appliesTo = function (context) { RestartTimerAction.appliesTo = function (context) {
var model = var model =
(context.domainObject && context.domainObject.getModel()) || (context.domainObject && context.domainObject.getModel()) ||
{}; {};
// We show this variant for timers which already have // We show this variant for timers which already have a target time.
// a target time.
return model.type === 'timer' && return model.type === 'timer' &&
model.timestamp !== undefined; model.timerState !== 'stopped';
};
RestartTimerAction.prototype.perform = function () {
var domainObject = this.domainObject,
now = this.now;
function updateModel(model) {
model.timestamp = now();
model.timerState = 'started';
model.pausedTime = undefined;
}
return domainObject.useCapability('mutation', updateModel);
}; };
return RestartTimerAction; return RestartTimerAction;
} }
); );

View File

@@ -21,8 +21,8 @@
*****************************************************************************/ *****************************************************************************/
define( define(
['./AbstractStartTimerAction'], [],
function (AbstractStartTimerAction) { function () {
/** /**
* Implements the "Start" action for timers. * Implements the "Start" action for timers.
@@ -30,7 +30,6 @@ define(
* Sets the reference timestamp in a timer to the current * Sets the reference timestamp in a timer to the current
* time, such that it begins counting up. * time, such that it begins counting up.
* *
* @extends {platform/features/clock.AbstractTimerAction}
* @implements {Action} * @implements {Action}
* @memberof platform/features/clock * @memberof platform/features/clock
* @constructor * @constructor
@@ -39,12 +38,10 @@ define(
* @param {ActionContext} context the context for this action * @param {ActionContext} context the context for this action
*/ */
function StartTimerAction(now, context) { function StartTimerAction(now, context) {
AbstractStartTimerAction.apply(this, [now, context]); this.domainObject = context.domainObject;
this.now = now;
} }
StartTimerAction.prototype =
Object.create(AbstractStartTimerAction.prototype);
StartTimerAction.appliesTo = function (context) { StartTimerAction.appliesTo = function (context) {
var model = var model =
(context.domainObject && context.domainObject.getModel()) || (context.domainObject && context.domainObject.getModel()) ||
@@ -53,10 +50,28 @@ define(
// We show this variant for timers which do not yet have // We show this variant for timers which do not yet have
// a target time. // a target time.
return model.type === 'timer' && return model.type === 'timer' &&
model.timestamp === undefined; model.timerState !== 'started';
};
StartTimerAction.prototype.perform = function () {
var domainObject = this.domainObject,
now = this.now;
function updateModel(model) {
//if we are resuming
if (model.pausedTime) {
var timeShift = now() - model.pausedTime;
model.timestamp = model.timestamp + timeShift;
} else {
model.timestamp = now();
}
model.timerState = 'started';
model.pausedTime = undefined;
}
return domainObject.useCapability('mutation', updateModel);
}; };
return StartTimerAction; return StartTimerAction;
} }
); );

View File

@@ -0,0 +1,71 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[],
function () {
/**
* Implements the "Stop" action for timers.
*
* Sets the reference timestamp in a timer undefined,
* such that it is reset and makes no movements.
*
* @implements {Action}
* @memberof platform/features/clock
* @constructor
* @param {Function} now a function which returns the current
* time (typically wrapping `Date.now`)
* @param {ActionContext} context the context for this action
*/
function StopTimerAction(now, context) {
this.domainObject = context.domainObject;
this.now = now;
}
StopTimerAction.appliesTo = function (context) {
var model =
(context.domainObject && context.domainObject.getModel()) ||
{};
// We show this variant for timers which do not yet have
// a target time.
return model.type === 'timer' &&
model.timerState !== 'stopped';
};
StopTimerAction.prototype.perform = function () {
var domainObject = this.domainObject;
function updateModel(model) {
model.timestamp = undefined;
model.timerState = 'stopped';
model.pausedTime = undefined;
}
return domainObject.useCapability('mutation', updateModel);
};
return StopTimerAction;
}
);

View File

@@ -42,6 +42,7 @@ define(
active = true, active = true,
relativeTimestamp, relativeTimestamp,
lastTimestamp, lastTimestamp,
relativeTimerState,
self = this; self = this;
function update() { function update() {
@@ -51,12 +52,9 @@ define(
self.textValue = formatter(timeDelta); self.textValue = formatter(timeDelta);
self.signValue = timeDelta < 0 ? "-" : self.signValue = timeDelta < 0 ? "-" :
timeDelta >= 1000 ? "+" : ""; timeDelta >= 1000 ? "+" : "";
self.signCssClass = timeDelta < 0 ? "icon-minus" :
timeDelta >= 1000 ? "icon-plus" : "";
} else { } else {
self.textValue = ""; self.textValue = "";
self.signValue = ""; self.signValue = "";
self.signCssClass = "";
} }
} }
@@ -68,19 +66,50 @@ define(
relativeTimestamp = timestamp; relativeTimestamp = timestamp;
} }
function updateTimerState(timerState) {
self.timerState = relativeTimerState = timerState;
}
function updateActions(actionCapability, actionKey) {
self.relevantAction = actionCapability &&
actionCapability.getActions(actionKey)[0];
self.stopAction = relativeTimerState !== 'stopped' ?
actionCapability && actionCapability.getActions('timer.stop')[0] : undefined;
}
function isPaused() {
return relativeTimerState === 'paused';
}
function handleLegacyTimer(model) {
if (model.timerState === undefined) {
model.timerState = model.timestamp === undefined ?
'stopped' : 'started';
}
}
function updateObject(domainObject) { function updateObject(domainObject) {
var model = domainObject.getModel(), var model = domainObject.getModel();
timestamp = model.timestamp, handleLegacyTimer(model);
var timestamp = model.timestamp,
formatKey = model.timerFormat, formatKey = model.timerFormat,
timerState = model.timerState,
actionCapability = domainObject.getCapability('action'), actionCapability = domainObject.getCapability('action'),
actionKey = (timestamp === undefined) ? actionKey = (timerState !== 'started') ?
'timer.start' : 'timer.restart'; 'timer.start' : 'timer.pause';
updateFormat(formatKey); updateFormat(formatKey);
updateTimestamp(timestamp); updateTimestamp(timestamp);
updateTimerState(timerState);
updateActions(actionCapability, actionKey);
self.relevantAction = actionCapability && //if paused on startup show last known position
actionCapability.getActions(actionKey)[0]; if (isPaused() && !lastTimestamp) {
lastTimestamp = model.pausedTime;
}
update(); update();
} }
@@ -98,8 +127,16 @@ define(
function tick() { function tick() {
var lastSign = self.signValue, var lastSign = self.signValue,
lastText = self.textValue; lastText = self.textValue;
if (!isPaused()) {
lastTimestamp = now(); lastTimestamp = now();
update(); update();
}
if (relativeTimerState === undefined) {
handleModification();
}
// We're running in an animation frame, not in a digest cycle. // We're running in an animation frame, not in a digest cycle.
// We need to trigger a digest cycle if our displayable data // We need to trigger a digest cycle if our displayable data
// changes. // changes.
@@ -130,8 +167,8 @@ define(
/** /**
* Get the CSS class to display the right icon * Get the CSS class to display the right icon
* for the start/restart button. * for the start/pause button.
* @returns {string} cssClass to display * @returns {string} cssclass to display
*/ */
TimerController.prototype.buttonCssClass = function () { TimerController.prototype.buttonCssClass = function () {
return this.relevantAction ? return this.relevantAction ?
@@ -139,7 +176,7 @@ define(
}; };
/** /**
* Get the text to show for the start/restart button * Get the text to show for the start/pause button
* (e.g. in a tooltip) * (e.g. in a tooltip)
* @returns {string} name of the action * @returns {string} name of the action
*/ */
@@ -150,7 +187,7 @@ define(
/** /**
* Perform the action associated with the start/restart button. * Perform the action associated with the start/pause button.
*/ */
TimerController.prototype.clickButton = function () { TimerController.prototype.clickButton = function () {
if (this.relevantAction) { if (this.relevantAction) {
@@ -159,6 +196,16 @@ define(
} }
}; };
/**
* Perform the action associated with the stop button.
*/
TimerController.prototype.clickStopButton = function () {
if (this.stopAction) {
this.stopAction.perform();
this.updateObject(this.$scope.domainObject);
}
};
/** /**
* Get the sign (+ or -) of the current timer value, as * Get the sign (+ or -) of the current timer value, as
* displayable text. * displayable text.
@@ -168,15 +215,6 @@ define(
return this.signValue; return this.signValue;
}; };
/**
* Get the sign (+ or -) of the current timer value, as
* a CSS class.
* @returns {string} sign of the current timer value
*/
TimerController.prototype.signClass = function () {
return this.signCssClass;
};
/** /**
* Get the text to display for the current timer value. * Get the text to display for the current timer value.
* @returns {string} current timer value * @returns {string} current timer value

View File

@@ -21,13 +21,14 @@
*****************************************************************************/ *****************************************************************************/
define( define(
["../../src/actions/AbstractStartTimerAction"], ["../../src/actions/PauseTimerAction"],
function (AbstractStartTimerAction) { function (PauseTimerAction) {
describe("A timer's start/restart action", function () { describe("A timer's Pause action", function () {
var mockNow, var mockNow,
mockDomainObject, mockDomainObject,
testModel, testModel,
testContext,
action; action;
function asPromise(value) { function asPromise(value) {
@@ -38,11 +39,23 @@ define(
}; };
} }
function testState(type, timerState, timestamp, expected) {
testModel.type = type;
testModel.timerState = timerState;
testModel.timestamp = timestamp;
if (expected) {
expect(PauseTimerAction.appliesTo(testContext)).toBeTruthy();
} else {
expect(PauseTimerAction.appliesTo(testContext)).toBeFalsy();
}
}
beforeEach(function () { beforeEach(function () {
mockNow = jasmine.createSpy('now'); mockNow = jasmine.createSpy('now');
mockDomainObject = jasmine.createSpyObj( mockDomainObject = jasmine.createSpyObj(
'domainObject', 'domainObject',
['getCapability', 'useCapability'] ['getCapability', 'useCapability', 'getModel']
); );
mockDomainObject.useCapability.andCallFake(function (c, v) { mockDomainObject.useCapability.andCallFake(function (c, v) {
@@ -51,24 +64,41 @@ define(
return asPromise(true); return asPromise(true);
} }
}); });
mockDomainObject.getModel.andCallFake(function () {
return testModel;
});
testModel = {}; testModel = {};
testContext = {domainObject: mockDomainObject};
action = new AbstractStartTimerAction(mockNow, { action = new PauseTimerAction(mockNow, testContext);
domainObject: mockDomainObject
});
}); });
it("updates the model with a timestamp", function () { it("updates the model with a timerState", function () {
testModel.timerState = 'started';
action.perform();
expect(testModel.timerState).toEqual('paused');
});
it("updates the model with a pausedTime", function () {
testModel.pausedTime = undefined;
mockNow.andReturn(12000); mockNow.andReturn(12000);
action.perform(); action.perform();
expect(testModel.timestamp).toEqual(12000); expect(testModel.pausedTime).toEqual(12000);
}); });
it("does not truncate milliseconds", function () { it("applies only to timers in a playing state", function () {
mockNow.andReturn(42321); //in a stopped state
action.perform(); testState('timer', 'stopped', undefined, false);
expect(testModel.timestamp).toEqual(42321);
//in a paused state
testState('timer', 'paused', 12000, false);
//in a playing state
testState('timer', 'started', 12000, true);
//not a timer
testState('clock', 'started', 12000, false);
}); });
}); });
} }

View File

@@ -39,6 +39,18 @@ define(
}; };
} }
function testState(type, timerState, timestamp, expected) {
testModel.type = type;
testModel.timerState = timerState;
testModel.timestamp = timestamp;
if (expected) {
expect(RestartTimerAction.appliesTo(testContext)).toBeTruthy();
} else {
expect(RestartTimerAction.appliesTo(testContext)).toBeFalsy();
}
}
beforeEach(function () { beforeEach(function () {
mockNow = jasmine.createSpy('now'); mockNow = jasmine.createSpy('now');
mockDomainObject = jasmine.createSpyObj( mockDomainObject = jasmine.createSpyObj(
@@ -63,23 +75,36 @@ define(
}); });
it("updates the model with a timestamp", function () { it("updates the model with a timestamp", function () {
testModel.pausedTime = 12000;
mockNow.andReturn(12000); mockNow.andReturn(12000);
action.perform(); action.perform();
expect(testModel.timestamp).toEqual(12000); expect(testModel.timestamp).toEqual(12000);
}); });
it("applies only to timers with a target time", function () { it("updates the model with a pausedTime", function () {
testModel.type = 'timer'; testModel.pausedTime = 12000;
testModel.timestamp = 12000; action.perform();
expect(RestartTimerAction.appliesTo(testContext)).toBeTruthy(); expect(testModel.pausedTime).toEqual(undefined);
});
testModel.type = 'timer'; it("updates the model with a timerState", function () {
testModel.timestamp = undefined; testModel.timerState = 'stopped';
expect(RestartTimerAction.appliesTo(testContext)).toBeFalsy(); action.perform();
expect(testModel.timerState).toEqual('started');
});
testModel.type = 'clock'; it("applies only to timers in a non-stopped state", function () {
testModel.timestamp = 12000; //in a stopped state
expect(RestartTimerAction.appliesTo(testContext)).toBeFalsy(); testState('timer', 'stopped', undefined, false);
//in a paused state
testState('timer', 'paused', 12000, true);
//in a playing state
testState('timer', 'started', 12000, true);
//not a timer
testState('clock', 'paused', 12000, false);
}); });
}); });
} }

View File

@@ -39,6 +39,18 @@ define(
}; };
} }
function testState(type, timerState, timestamp, expected) {
testModel.type = type;
testModel.timerState = timerState;
testModel.timestamp = timestamp;
if (expected) {
expect(StartTimerAction.appliesTo(testContext)).toBeTruthy();
} else {
expect(StartTimerAction.appliesTo(testContext)).toBeFalsy();
}
}
beforeEach(function () { beforeEach(function () {
mockNow = jasmine.createSpy('now'); mockNow = jasmine.createSpy('now');
mockDomainObject = jasmine.createSpyObj( mockDomainObject = jasmine.createSpyObj(
@@ -57,7 +69,7 @@ define(
}); });
testModel = {}; testModel = {};
testContext = { domainObject: mockDomainObject }; testContext = {domainObject: mockDomainObject};
action = new StartTimerAction(mockNow, testContext); action = new StartTimerAction(mockNow, testContext);
}); });
@@ -68,18 +80,30 @@ define(
expect(testModel.timestamp).toEqual(12000); expect(testModel.timestamp).toEqual(12000);
}); });
it("applies only to timers without a target time", function () { it("updates the model with a pausedTime", function () {
testModel.type = 'timer'; testModel.pausedTime = 12000;
testModel.timestamp = 12000; action.perform();
expect(StartTimerAction.appliesTo(testContext)).toBeFalsy(); expect(testModel.pausedTime).toEqual(undefined);
});
testModel.type = 'timer'; it("updates the model with a timerState", function () {
testModel.timestamp = undefined; testModel.timerState = undefined;
expect(StartTimerAction.appliesTo(testContext)).toBeTruthy(); action.perform();
expect(testModel.timerState).toEqual('started');
});
testModel.type = 'clock'; it("applies only to timers not in a playing state", function () {
testModel.timestamp = 12000; //in a stopped state
expect(StartTimerAction.appliesTo(testContext)).toBeFalsy(); testState('timer', 'stopped', undefined, true);
//in a paused state
testState('timer', 'paused', 12000, true);
//in a playing state
testState('timer', 'started', 12000, false);
//not a timer
testState('clock', 'paused', 12000, false);
}); });
}); });
} }

View File

@@ -0,0 +1,110 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
["../../src/actions/StopTimerAction"],
function (StopTimerAction) {
describe("A timer's stop action", function () {
var mockNow,
mockDomainObject,
testModel,
testContext,
action;
function asPromise(value) {
return (value || {}).then ? value : {
then: function (callback) {
return asPromise(callback(value));
}
};
}
function testState(type, timerState, timestamp, expected) {
testModel.type = type;
testModel.timerState = timerState;
testModel.timestamp = timestamp;
if (expected) {
expect(StopTimerAction.appliesTo(testContext)).toBeTruthy();
} else {
expect(StopTimerAction.appliesTo(testContext)).toBeFalsy();
}
}
beforeEach(function () {
mockNow = jasmine.createSpy('now');
mockDomainObject = jasmine.createSpyObj(
'domainObject',
['getCapability', 'useCapability', 'getModel']
);
mockDomainObject.useCapability.andCallFake(function (c, v) {
if (c === 'mutation') {
testModel = v(testModel) || testModel;
return asPromise(true);
}
});
mockDomainObject.getModel.andCallFake(function () {
return testModel;
});
testModel = {};
testContext = {domainObject: mockDomainObject};
action = new StopTimerAction(mockNow, testContext);
});
it("updates the model with a timestamp", function () {
mockNow.andReturn(12000);
action.perform();
expect(testModel.timestamp).toEqual(undefined);
});
it("updates the model with a pausedTime", function () {
testModel.pausedTime = 12000;
action.perform();
expect(testModel.pausedTime).toEqual(undefined);
});
it("updates the model with a timerState", function () {
testModel.timerState = 'started';
action.perform();
expect(testModel.timerState).toEqual('stopped');
});
it("applies only to timers in a non-stopped state", function () {
//in a stopped state
testState('timer', 'stopped', undefined, false);
//in a paused state
testState('timer', 'paused', 12000, true);
//in a playing state
testState('timer', 'started', 12000, true);
//not a timer
testState('clock', 'paused', 12000, false);
});
});
}
);

View File

@@ -34,7 +34,8 @@ define(
mockDomainObject, mockDomainObject,
mockActionCapability, mockActionCapability,
mockStart, mockStart,
mockRestart, mockPause,
mockStop,
testModel, testModel,
controller; controller;
@@ -67,8 +68,12 @@ define(
'start', 'start',
['getMetadata', 'perform'] ['getMetadata', 'perform']
); );
mockRestart = jasmine.createSpyObj( mockPause = jasmine.createSpyObj(
'restart', 'paused',
['getMetadata', 'perform']
);
mockStop = jasmine.createSpyObj(
'stopped',
['getMetadata', 'perform'] ['getMetadata', 'perform']
); );
mockNow = jasmine.createSpy('now'); mockNow = jasmine.createSpy('now');
@@ -82,11 +87,14 @@ define(
mockActionCapability.getActions.andCallFake(function (k) { mockActionCapability.getActions.andCallFake(function (k) {
return [{ return [{
'timer.start': mockStart, 'timer.start': mockStart,
'timer.restart': mockRestart 'timer.pause': mockPause,
'timer.stop': mockStop
}[k]]; }[k]];
}); });
mockStart.getMetadata.andReturn({ cssClass: "icon-play", name: "Start" });
mockRestart.getMetadata.andReturn({ cssClass: "icon-refresh", name: "Restart" }); mockStart.getMetadata.andReturn({cssClass: "icon-play", name: "Start"});
mockPause.getMetadata.andReturn({cssClass: "icon-pause", name: "Pause"});
mockStop.getMetadata.andReturn({cssClass: "icon-box", name: "Stop"});
mockScope.domainObject = mockDomainObject; mockScope.domainObject = mockDomainObject;
testModel = {}; testModel = {};
@@ -144,28 +152,37 @@ define(
expect(controller.text()).toEqual("0D 00:00:00"); expect(controller.text()).toEqual("0D 00:00:00");
}); });
it("shows cssClass & name for the applicable start/restart action", function () { it("shows cssClass & name for the applicable start/pause action", function () {
invokeWatch('domainObject', mockDomainObject); invokeWatch('domainObject', mockDomainObject);
expect(controller.buttonCssClass()).toEqual("icon-play"); expect(controller.buttonCssClass()).toEqual("icon-play");
expect(controller.buttonText()).toEqual("Start"); expect(controller.buttonText()).toEqual("Start");
testModel.timestamp = 12321; testModel.timestamp = 12321;
testModel.timerState = 'started';
invokeWatch('model.modified', 1); invokeWatch('model.modified', 1);
expect(controller.buttonCssClass()).toEqual("icon-refresh"); expect(controller.buttonCssClass()).toEqual("icon-pause");
expect(controller.buttonText()).toEqual("Restart"); expect(controller.buttonText()).toEqual("Pause");
}); });
it("performs correct start/restart action on click", function () { it("performs correct start/pause/stop action on click", function () {
//test start
invokeWatch('domainObject', mockDomainObject); invokeWatch('domainObject', mockDomainObject);
expect(mockStart.perform).not.toHaveBeenCalled(); expect(mockStart.perform).not.toHaveBeenCalled();
controller.clickButton(); controller.clickButton();
expect(mockStart.perform).toHaveBeenCalled(); expect(mockStart.perform).toHaveBeenCalled();
//test pause
testModel.timestamp = 12321; testModel.timestamp = 12321;
testModel.timerState = 'started';
invokeWatch('model.modified', 1); invokeWatch('model.modified', 1);
expect(mockRestart.perform).not.toHaveBeenCalled(); expect(mockPause.perform).not.toHaveBeenCalled();
controller.clickButton(); controller.clickButton();
expect(mockRestart.perform).toHaveBeenCalled(); expect(mockPause.perform).toHaveBeenCalled();
//test stop
expect(mockStop.perform).not.toHaveBeenCalled();
controller.clickStopButton();
expect(mockStop.perform).toHaveBeenCalled();
}); });
it("stops requesting animation frames when destroyed", function () { it("stops requesting animation frames when destroyed", function () {