Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
95068552
Commit
95068552
authored
Oct 28, 2016
by
Bryce Johnson
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Prepare timetracking frontend for backend.
parent
3b5f3c61
Changes
12
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
871 additions
and
9 deletions
+871
-9
app/assets/javascripts/application.js
app/assets/javascripts/application.js
+1
-0
app/assets/javascripts/directives/tooltip_title.js.es6
app/assets/javascripts/directives/tooltip_title.js.es6
+23
-0
app/assets/javascripts/issuable_time_tracker.js.es6
app/assets/javascripts/issuable_time_tracker.js.es6
+256
-0
app/assets/javascripts/smart_interval.js.es6
app/assets/javascripts/smart_interval.js.es6
+139
-0
app/assets/javascripts/subbable_resource.js.es6
app/assets/javascripts/subbable_resource.js.es6
+53
-0
app/assets/javascripts/weight_select.js
app/assets/javascripts/weight_select.js
+16
-7
app/assets/stylesheets/pages/issuable.scss
app/assets/stylesheets/pages/issuable.scss
+55
-0
app/views/shared/icons/_icon_stopwatch.svg
app/views/shared/icons/_icon_stopwatch.svg
+1
-0
app/views/shared/issuable/_sidebar.html.haml
app/views/shared/issuable/_sidebar.html.haml
+6
-2
spec/javascripts/issuable_time_tracker_spec.js.es6
spec/javascripts/issuable_time_tracker_spec.js.es6
+71
-0
spec/javascripts/smart_interval_spec.js.es6
spec/javascripts/smart_interval_spec.js.es6
+185
-0
spec/javascripts/subbable_resource_spec.js.es6
spec/javascripts/subbable_resource_spec.js.es6
+65
-0
No files found.
app/assets/javascripts/application.js
View file @
95068552
...
...
@@ -49,6 +49,7 @@
/*= require_directory ./blob */
/*= require_directory ./templates */
/*= require_directory ./commit */
/*= require_directory ./directives */
/*= require_directory ./extensions */
/*= require_directory ./lib/utils */
/*= require_directory ./u2f */
...
...
app/assets/javascripts/directives/tooltip_title.js.es6
0 → 100644
View file @
95068552
//= require vue
((global) => {
/**
* This directive ensures the text used to populate a Bootstrap tooltip is
* updated dynamically. The tooltip's `title` is not stored or accessed
* elsewhere, making it reasonably safe to write to as needed.
*/
Vue.directive('tooltip-title', {
update(el, binding) {
const titleInitAttr = 'title';
const titleStoreAttr = 'data-original-title';
const updatedValue = binding.value || el.getAttribute(titleInitAttr);
el.setAttribute(titleInitAttr, updatedValue);
el.setAttribute(titleStoreAttr, updatedValue);
},
});
})(window.gl || (window.gl = {}));
app/assets/javascripts/issuable_time_tracker.js.es6
0 → 100644
View file @
95068552
//= vue
//= smart_interval
//= subbable_resource
((global) => {
$(() => {
const mockData = gl.generateTimeTrackingMockData('estimate-and-spend');
/* This Vue instance represents what will become the parent instance for the
* sidebar. It will be responsible for managing `issuable` state and propagating
* changes to sidebar components.
*/
new Vue({
el: '#issuable-time-tracker',
data: {
time_estimated: mockData.time_estimated,
time_spent: mockData.time_spent,
},
computed: {
fetchIssuable() {
return gl.IssuableResource.get.bind(gl.IssuableResource, { type: 'GET', url: gl.IssuableResource.endpoint });
}
},
methods: {
initPolling() {
new gl. TODO:SmartInterval({
callback: this.fetchIssuable,
startingInterval: 1000,
maxInterval: 10000,
incrementByFactorOf: 2,
lazyStart: false,
});
},
updateState(data) {
data = global.generateTimeTrackingMockData('estimate-and-spend');
this.time_estimated = data.time_estimated;
this.time_spent = data.time_spent;
},
},
created() {
$(document).on('ajax:success', '.gfm-form', (e) => {
// TODO: check if slash command was included.
this.fetchIssuable();
});
},
mounted() {
gl.IssuableResource.subscribe(data => this.updateState(data));
this.initPolling();
}
});
});
Vue.component('issuable-time-tracker', {
props: ['time_estimated', 'time_spent'],
template: `
<div class='time-tracking-component-wrap'>
<div class='sidebar-collapsed-icon'>
<div class='time-tracking-collapsed-summary'>
<div class='compare' v-if='showComparison'>
<span>{{ abbreviateTime(spentPretty) }} / {{ abbreviateTime(estimatedPretty) }}</span>
</div>
<div class='estimate-only' v-if='showEstimateOnly'>
<span class='bold'>-- / {{ abbreviateTime(estimatedPretty) }}</span>
</div>
<div class='spend-only' v-if='showSpentOnly'>
<span class='bold'>{{ abbreviateTime(spentPretty) }} / --</span>
</div>
<div class='no-tracking' v-if='showNoTimeTracking'>
<span class='no-value'>None</span>
</div>
</div>
</div>
<div class='title hide-collapsed'>
Time tracking
<div class='help-button pull-right' v-if='showHelp' v-on:click='toggleHelpState(true)'>
</div>
</div>
<div class='time-tracking-content hide-collapsed'>
<div class='time-tracking-pane-compare' v-if='showComparison'>
<div class='compare-meter' data-toggle='tooltip' data-placement='top' v-tooltip-title='remainingTooltipPretty' >
<div class='meter-container'>
<div :style='{ width: diffPercent }' class='meter-fill'></div>
</div>
<div class='compare-display-container'>
<div class='compare-display pull-left'>
<span class='compare-label'>Spent</span>
<span class='compare-value spent'>{{ spentPretty }}</span>
</div>
<div class='compare-display estimated pull-right'>
<span class='compare-label'>Est</span>
<span class='compare-value'>{{ estimatedPretty }}</span>
</div>
</div>
</div>
</div>
<div class='time-tracking-estimate-only' v-if='showEstimateOnly'>
<span class='bold'>Estimated:</span>
{{ estimatedPretty }}
</div>
<div class='time-tracking-spend-only' v-if='showSpentOnly'>
<span class='bold'>Spent:</span>
{{ spentPretty }}
</div>
<div class='time-tracking-no-tracking' v-if='showNoTimeTracking'>
<span class='no-value'>No estimate or time spent</span>
</div>
<div class='time-tracking-help-state' v-if='showHelp'>
<div class='close-help-button pull-right' v-on:click='toggleHelpState(false)'>
</div>
<div class='time-tracking-info'>
<h4>Track time with slash commands</h4>
<p>Slash commands can be used in the issues description and comment boxes.</p>
<p>
<code>/estimate</code>
will update the estimated time with the latest command.
</p>
<p>
<code>/spend</code>
will update the sum of the time spent.
</p>
</div>
</div>
</div>
</div>
`,
data: function() {
return {
displayHelp: false,
loading: false,
}
},
computed: {
showComparison() {
return !!this.time_estimated && !!this.time_spent;
},
showEstimateOnly() {
return !!this.time_estimated && !this.time_spent;
},
showSpentOnly() {
return !!this.time_spent && !this.time_estimated;
},
showNoTimeTracking() {
return !this.time_estimated && !this.time_spent;
},
showHelp() {
return !!this.displayHelp;
},
estimatedPretty() {
return this.stringifyTime(this.time_estimated);
},
spentPretty() {
return this.stringifyTime(this.time_spent);
},
remainingPretty() {
return this.stringifyTime(this.parsedDiff);
},
remainingTooltipPretty() {
const prefix = this.diffMinutes < 0 ? 'Over by' : 'Time remaining:';
return `${prefix} ${this.remainingPretty}`;
},
parsedDiff () {
const MAX_DAYS = 5, MAX_HOURS = 8, MAX_MINUTES = 60;
const timePeriodConstraints = [
[ 'weeks', MAX_HOURS * MAX_DAYS ],
[ 'days', MAX_MINUTES * MAX_HOURS ],
[ 'hours', MAX_MINUTES ],
[ 'minutes', 1 ]
];
const parsedDiff = {};
let unorderedMinutes = Math.abs(this.diffMinutes);
timePeriodConstraints.forEach((period, idx, collection) => {
const periodName = period[0];
const minutesPerPeriod = period[1];
const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod);
unorderedMinutes -= (periodCount * minutesPerPeriod);
parsedDiff[periodName] = periodCount;
});
return parsedDiff;
},
diffMinutes () {
const time_estimated = this.time_estimated;
const time_spent = this.time_spent;
return time_estimated.totalMinutes - time_spent.totalMinutes;
},
diffPercent() {
const estimate = this.estimate;
return Math.floor((this.time_spent.totalMinutes / this.time_estimated.totalMinutes * 100)) + '%';
},
diffStatus() {
return this.time_estimated.totalMinutes >= this.time_spent.totalMinutes ? 'within_estimate' : 'over_estimate';
}
},
methods: {
abbreviateTime(value) {
return value.split(' ')[0];
},
toggleHelpState(show) {
this.displayHelp = show;
},
stringifyTime(obj) {
return _.reduce(obj, (memo, val, key) => {
return (key !== 'totalMinutes' && val !== 0) ? (memo + `${val}${key.charAt(0)} `) : memo;
}, '').trim();
},
},
});
/***** Mock Data ******/
global.generateTimeTrackingMockData = generateMockStates;
function generateMockStates(state) {
const configurations = {
'estimate-only': {
time_estimated: generateTimeObj(),
time_spent: null
},
'spent-only': {
time_estimated: null,
time_spent: generateTimeObj()
},
'estimate-and-spend': {
time_estimated: generateTimeObj(),
time_spent: generateTimeObj()
},
'nothing': {
time_estimated: null,
time_spent: null
}
};
return configurations[state];
}
function generateTimeObj(
weeks = getRandomInt(0, 12),
days = getRandomInt(0, 7),
hours = getRandomInt(0, 8),
minutes = getRandomInt(0, 60),
totalMinutes = getRandomInt(0, 25 * 7 * 8 * 60)) {
return {
weeks, days, hours, minutes, totalMinutes
};
}
function getRandomInt(min, max) {
const justReturnZero = Math.random > .5;
return justReturnZero ? 0 : Math.floor(Math.random() * (max - min + 1)) + min;
}
}) (window.gl || (window.gl = {}));
app/assets/javascripts/smart_interval.js.es6
0 → 100644
View file @
95068552
/*
* Instances of SmartInterval extend the functionality of `setInterval`, make it configurable
* and controllable by a public API.
*
* */
(() => {
class SmartInterval {
/**
* @param { function } callback Function to be called on each iteration (required)
* @param { milliseconds } startingInterval `currentInterval` is set to this initially
* @param { milliseconds } maxInterval `currentInterval` will be incremented to this
* @param { integer } incrementByFactorOf `currentInterval` is incremented by this factor
* @param { boolean } lazyStart Configure if timer is initialized on instantiation or lazily
*/
constructor({ callback, startingInterval, maxInterval, incrementByFactorOf, lazyStart = false }) {
this.cfg = {
callback,
startingInterval,
maxInterval,
incrementByFactorOf,
lazyStart,
};
this.state = {
intervalId: null,
currentInterval: startingInterval,
pageVisibility: 'visible',
};
this.initInterval();
}
/* public */
start() {
const cfg = this.cfg;
const state = this.state;
state.intervalId = window.setInterval(() => {
cfg.callback();
if (this.getCurrentInterval() === cfg.maxInterval) {
return;
}
this.incrementInterval();
this.resume();
}, this.getCurrentInterval());
}
// cancel the existing timer, setting the currentInterval back to startingInterval
cancel() {
this.setCurrentInterval(this.cfg.startingInterval);
this.stopTimer();
}
// cancel the existing timer, without resetting the currentInterval
pause() {
this.stopTimer();
}
// start a timer, using the existing interval
resume() {
this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped
this.start();
}
destroy() {
this.cancel();
$(document).off('visibilitychange').off('page:before-unload');
}
/* private */
initInterval() {
const cfg = this.cfg;
if (!cfg.lazyStart) {
this.start();
}
this.initVisibilityChangeHandling();
this.initPageUnloadHandling();
}
initVisibilityChangeHandling() {
const cfg = this.cfg;
// cancel interval when tab no longer shown (prevents cached pages from polling)
$(document)
.off('visibilitychange').on('visibilitychange', (e) => {
this.state.pageVisibility = e.target.visibilityState;
this.handleVisibilityChange();
});
}
initPageUnloadHandling() {
const cfg = this.cfg;
// prevent interval continuing after page change, when kept in cache by Turbolinks
$(document).on('page:before-unload', () => this.cancel());
}
handleVisibilityChange() {
const state = this.state;
const intervalAction = state.pageVisibility === 'hidden' ? this.pause : this.resume;
intervalAction.apply(this);
}
getCurrentInterval() {
return this.state.currentInterval;
}
setCurrentInterval(newInterval) {
this.state.currentInterval = newInterval;
}
incrementInterval() {
const cfg = this.cfg;
const currentInterval = this.getCurrentInterval();
let nextInterval = currentInterval * cfg.incrementByFactorOf;
if (nextInterval > cfg.maxInterval) {
nextInterval = cfg.maxInterval;
}
this.setCurrentInterval(nextInterval);
}
stopTimer() {
const state = this.state;
state.intervalId = window.clearInterval(state.intervalId);
}
}
gl.SmartInterval = SmartInterval;
})(window.gl || (window.gl = {}));
app/assets/javascripts/subbable_resource.js.es6
0 → 100644
View file @
95068552
//= require vue
//= require vue-resource
((global) => {
/*
* SubbableResource can be extended to provide a pubsub-style service for one-off REST
* calls. Subscribe by passing a callback or render method you will use to handle responses.
*
* */
class SubbableResource {
constructor(resourcePath) {
this.endpoint = resourcePath;
// TODO: Switch to axios.create
this.resource = $.ajax;
this.subscribers = [];
}
subscribe(callback) {
this.subscribers.push(callback);
}
publish(newResponse) {
const responseCopy = _.extend({}, newResponse);
this.subscribers.forEach((fn) => {
fn(responseCopy);
});
return newResponse;
}
get(data) {
return this.resource(data)
.then(data => this.publish(data));
}
post(data) {
return this.resource(data)
.then(data => this.publish(data));
}
put(data) {
return this.resource(data)
.then(data => this.publish(data));
}
delete(data) {
return this.resource(data)
.then(data => this.publish(data));
}
}
gl.SubbableResource = SubbableResource;
})(window.gl || (window.gl = {}));
app/assets/javascripts/weight_select.js
View file @
95068552
...
...
@@ -12,6 +12,20 @@
$value
=
$block
.
find
(
'
.value
'
);
abilityName
=
$dropdown
.
data
(
'
ability-name
'
);
$loading
=
$block
.
find
(
'
.block-loading
'
).
fadeOut
();
var
ajaxResource
=
gl
.
IssuableResource
?
gl
.
IssuableResource
.
put
.
bind
(
gl
.
IssuableResource
)
:
$
.
ajax
;
var
renderMethod
=
function
(
data
)
{
if
(
data
.
weight
!=
null
)
{
$value
.
html
(
data
.
weight
);
}
else
{
$value
.
html
(
'
None
'
);
}
return
$sidebarCollapsedValue
.
html
(
data
.
weight
);
};
gl
.
IssuableResource
&&
gl
.
IssuableResource
.
subscribe
(
renderMethod
);
updateWeight
=
function
(
selected
)
{
var
data
;
data
=
{};
...
...
@@ -19,7 +33,7 @@
data
[
abilityName
].
weight
=
selected
!=
null
?
selected
:
null
;
$loading
.
fadeIn
();
$dropdown
.
trigger
(
'
loading.gl.dropdown
'
);
return
$
.
ajax
({
return
ajaxResource
({
type
:
'
PUT
'
,
dataType
:
'
json
'
,
url
:
updateUrl
,
...
...
@@ -28,12 +42,7 @@
$dropdown
.
trigger
(
'
loaded.gl.dropdown
'
);
$loading
.
fadeOut
();
$selectbox
.
hide
();
if
(
data
.
weight
!=
null
)
{
$value
.
html
(
data
.
weight
);
}
else
{
$value
.
html
(
'
None
'
);
}
return
$sidebarCollapsedValue
.
html
(
data
.
weight
);
renderMethod
(
data
);
});
};
return
$dropdown
.
glDropdown
({
...
...
app/assets/stylesheets/pages/issuable.scss
View file @
95068552
...
...
@@ -421,3 +421,58 @@
}
}
}
#issuable-time-tracker
{
.time-tracking-help-state
{
padding
:
10px
0
;
margin-top
:
10px
;
border-top
:
1px
solid
#dcdcdc
;
}
.meter-container
{
background
:
$gray-lighter
;
border-radius
:
2px
;
}
.meter-fill
{
max-width
:
100%
;
height
:
4px
;
background
:
$gl-text-green
;
}
.help-button
,
.close-help-button
{
cursor
:
pointer
;
}
.over_estimate
{
.meter-fill
{
background
:
$red-light
;
}
.time-remaining
,
.compare-value.spent
{
color
:
$red-light
;
}
}
.sidebar-collapsed-icon
{
svg
{
width
:
16px
;
height
:
16px
;
fill
:
#999
;
}
}
.within_estimate
{
.meter-fill
{
background
:
$gl-text-green
;
}
}
.compare-display-container
{
margin-top
:
5px
;
}
.compare-display
{
font-size
:
13px
;
color
:
$gl-gray-light
;
.compare-value
{
color
:
$gl-gray
;
}
}
}
app/views/shared/icons/_icon_stopwatch.svg
0 → 100644
View file @
95068552
<svg
xmlns=
"http://www.w3.org/2000/svg"
viewBox=
"0 0 12 14"
enable-background=
"new 0 0 12 14"
><path
d=
"m11.5 2.4l-1.3-1.1-1 1.1 1.4 1.1.9-1.1"
/><path
d=
"m6.8 2v-.5h.5v-1.5h-2.6v1.5h.5v.5c-2.9.4-5.2 2.9-5.2 6 0 3.3 2.7 6 6 6s6-2.7 6-6c0-3-2.3-5.6-5.2-6m-.8 10.5c-2.5 0-4.5-2-4.5-4.5s2-4.5 4.5-4.5 4.5 2 4.5 4.5-2 4.5-4.5 4.5"
/><path
d=
"m6.2 8.9h-.5c-.1 0-.2-.1-.2-.2v-3.5c0-.1.1-.2.2-.2h.5c.1 0 .2.1.2.2v3.5c0 .1-.1.2-.2.2"
/></svg>
\ No newline at end of file
app/views/shared/issuable/_sidebar.html.haml
View file @
95068552
-
todo
=
issuable_todo
(
issuable
)
%aside
.right-sidebar
{
class:
sidebar_gutter_collapsed_class
}
%aside
.right-sidebar
{
class:
sidebar_gutter_collapsed_class
,
'aria-live'
=>
'polite'
}
.issuable-sidebar
-
can_edit_issuable
=
can?
(
current_user
,
:"admin_
#{
issuable
.
to_ability_name
}
"
,
@project
)
.block.issuable-sidebar-header
...
...
@@ -72,7 +72,10 @@
.selectbox.hide-collapsed
=
f
.
hidden_field
'milestone_id'
,
value:
issuable
.
milestone_id
,
id:
nil
=
dropdown_tag
(
'Milestone'
,
options:
{
title:
'Assign milestone'
,
toggle_class:
'js-milestone-select js-extra-options'
,
filter:
true
,
dropdown_class:
'dropdown-menu-selectable'
,
placeholder:
'Search milestones'
,
data:
{
show_no:
true
,
field_name:
"
#{
issuable
.
to_ability_name
}
[milestone_id]"
,
project_id:
@project
.
id
,
issuable_id:
issuable
.
id
,
milestones:
namespace_project_milestones_path
(
@project
.
namespace
,
@project
,
:json
),
ability_name:
issuable
.
to_ability_name
,
issue_update:
issuable_json_path
(
issuable
),
use_id:
true
}})
// TODO: Need to add check for time_estimated - if issuable.has_attribute?(:time_estimated) once hooked up to backend
-
if
issuable
.
has_attribute?
(
:due_date
)
#issuable-time-tracker
.block
%issuable-time-tracker
{
':time_estimated'
=>
'time_estimated'
,
':time_spent'
=>
'time_spent'
}
-
if
issuable
.
has_attribute?
(
:due_date
)
.block.due_date
.sidebar-collapsed-icon
...
...
@@ -195,6 +198,7 @@
=
clipboard_button
(
clipboard_text:
project_ref
)
:javascript
gl
.
IssuableResource
=
new
gl
.
SubbableResource
(
'
#{
issuable_json_path
(
issuable
)
}
'
);
new
MilestoneSelect
(
'
{"namespace":"
#{
@project
.
namespace
.
path
}
","path":"
#{
@project
.
path
}
"}
'
);
new
LabelsSelect
();
new
WeightSelect
();
...
...
spec/javascripts/issuable_time_tracker_spec.js.es6
0 → 100644
View file @
95068552
/* eslint-disable */
//= require jquery
//= require vue
//= require vue-resource
//= require issuable_time_tracker
((gl) => {
function generateTimeObject (weeks, days, hours, minutes, totalMinutes) {
return { weeks, days, hours, minutes, totalMinutes };
}
describe('Issuable Time Tracker', function() {
beforeEach(function() {
const time_estimated = generateTimeObject(2, 2, 2, 0, 5880);
const time_spent = generateTimeObject(1, 1, 1, 0, 2940);
const timeTrackingComponent = Vue.extend(gl.TimeTrackingDisplay);
this.timeTracker = new timeTrackingComponent({ data: { time_estimated, time_spent }}).$mount();
});
// show the correct pane
// stringify a time value
// the percent is being calculated and displayed correctly on the compare meter
// differ works, if needed
//
it('should parse a time diff based on total minutes', function() {
const parsedDiff = this.timeTracker.parsedDiff;
expect(parsedDiff.weeks).toBe(1);
expect(parsedDiff.days).toBe(1);
expect(parsedDiff.hours).toBe(1);
expect(parsedDiff.minutes).toBe(0);
});
it('should stringify a time value', function() {
const timeTracker = this.timeTracker;
const noZeroes = generateTimeObject(1, 1, 1, 2, 2940);
const someZeroes = generateTimeObject(1, 0, 1, 0, 2940);
expect(timeTracker.stringifyTime(noZeroes)).toBe('1w 1d 1h 2m');
expect(timeTracker.stringifyTime(someZeroes)).toBe('1w 1h');
});
it('should abbreviate a stringified value', function() {
const stringifyTime = this.timeTracker.stringifyTime;
const oneWeek = stringifyTime(generateTimeObject(1, 1, 1, 1, 2940));
const oneDay = stringifyTime(generateTimeObject(0, 1, 1, 1, 2940));
const oneHour = stringifyTime(generateTimeObject(0, 0, 1, 1, 2940));
const oneMinute = stringifyTime(generateTimeObject(0, 0, 0, 1, 2940));
const abbreviateTimeFilter = Vue.filter('abbreviate-time');
expect(abbreviateTimeFilter(oneWeek)).toBe('1w');
expect(abbreviateTimeFilter(oneDay)).toBe('1d');
expect(abbreviateTimeFilter(oneHour)).toBe('1h');
expect(abbreviateTimeFilter(oneMinute)).toBe('1m');
});
it('should toggle the help state', function() {
const timeTracker = this.timeTracker;
expect(timeTracker.displayHelp).toBe(false);
timeTracker.toggleHelpState(true);
expect(timeTracker.displayHelp).toBe(true);
timeTracker.toggleHelpState(false);
expect(timeTracker.displayHelp).toBe(false);
});
});
})(window.gl || (window.gl = {}));
spec/javascripts/smart_interval_spec.js.es6
0 → 100644
View file @
95068552
/* eslint-disable */
//= require jquery
//= require smart_interval
((global) => {
describe('SmartInterval', function () {
const DEFAULT_MAX_INTERVAL = 100;
const DEFAULT_STARTING_INTERVAL = 5;
const DEFAULT_SHORT_TIMEOUT = 75;
const DEFAULT_LONG_TIMEOUT = 1000;
const DEFAULT_INCREMENT_FACTOR = 2;
describe('Increment Interval', function () {
beforeEach(function () {
this.smartInterval = createDefaultSmartInterval();
});
it('should increment the interval delay', function (done) {
const interval = this.smartInterval;
setTimeout(() => {
const intervalConfig = this.smartInterval.cfg;
const iterationCount = 4;
const maxIntervalAfterIterations = intervalConfig.startingInterval *
Math.pow(intervalConfig.incrementByFactorOf, (iterationCount - 1)); // 40
const currentInterval = interval.getCurrentInterval();
// Provide some flexibility for performance of testing environment
expect(currentInterval).toBeGreaterThan(intervalConfig.startingInterval);
expect(currentInterval <= maxIntervalAfterIterations).toBeTruthy();
done();
}, DEFAULT_SHORT_TIMEOUT); // 4 iterations, increment by 2x = (5 + 10 + 20 + 40)
});
it('should not increment past maxInterval', function (done) {
const interval = this.smartInterval;
setTimeout(() => {
const currentInterval = interval.getCurrentInterval();
expect(currentInterval).toBe(interval.cfg.maxInterval);
done();
}, DEFAULT_LONG_TIMEOUT);
});
});
describe('Public methods', function () {
beforeEach(function () {
this.smartInterval = createDefaultSmartInterval();
});
it('should cancel an interval', function (done) {
const interval = this.smartInterval;
setTimeout(() => {
interval.cancel();
const intervalId = interval.state.intervalId;
const currentInterval = interval.getCurrentInterval();
const intervalLowerLimit = interval.cfg.startingInterval;
expect(intervalId).toBeUndefined();
expect(currentInterval).toBe(intervalLowerLimit);
done();
}, DEFAULT_SHORT_TIMEOUT);
});
it('should pause an interval', function (done) {
const interval = this.smartInterval;
setTimeout(() => {
interval.pause();
const intervalId = interval.state.intervalId;
const currentInterval = interval.getCurrentInterval();
const intervalLowerLimit = interval.cfg.startingInterval;
expect(intervalId).toBeUndefined();
expect(currentInterval).toBeGreaterThan(intervalLowerLimit);
done();
}, DEFAULT_SHORT_TIMEOUT);
});
it('should resume an interval', function (done) {
const interval = this.smartInterval;
setTimeout(() => {
interval.pause();
const lastInterval = interval.getCurrentInterval();
interval.resume();
const nextInterval = interval.getCurrentInterval();
const intervalId = interval.state.intervalId;
expect(intervalId).toBeTruthy();
expect(nextInterval).toBe(lastInterval);
done();
}, DEFAULT_SHORT_TIMEOUT);
});
});
describe('DOM Events', function () {
beforeEach(function () {
// This ensures DOM and DOM events are initialized for these specs.
fixture.set('<div></div>');
this.smartInterval = createDefaultSmartInterval();
});
it('should pause when page is not visible', function (done) {
const interval = this.smartInterval;
setTimeout(() => {
expect(interval.state.intervalId).toBeTruthy();
// simulates triggering of visibilitychange event
interval.state.pageVisibility = 'hidden';
interval.handleVisibilityChange();
expect(interval.state.intervalId).toBeUndefined();
done();
}, DEFAULT_SHORT_TIMEOUT);
});
it('should resume when page is becomes visible at the previous interval', function (done) {
const interval = this.smartInterval;
setTimeout(() => {
expect(interval.state.intervalId).toBeTruthy();
// simulates triggering of visibilitychange event
interval.state.pageVisibility = 'hidden';
interval.handleVisibilityChange();
const pausedIntervalLength = interval.getCurrentInterval();
expect(interval.state.intervalId).toBeUndefined();
// simulates triggering of visibilitychange event
interval.state.pageVisibility = 'visible';
interval.handleVisibilityChange();
expect(interval.state.intervalId).toBeTruthy();
expect(interval.getCurrentInterval()).toBe(pausedIntervalLength);
done();
}, DEFAULT_SHORT_TIMEOUT);
});
it('should cancel on page unload', function (done) {
const interval = this.smartInterval;
setTimeout(() => {
$(document).trigger('page:before-unload');
expect(interval.state.intervalId).toBeUndefined();
expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval);
done();
}, DEFAULT_SHORT_TIMEOUT);
});
});
});
function createDefaultSmartInterval(config) {
const defaultParams = {
callback: () => {},
startingInterval: DEFAULT_STARTING_INTERVAL,
maxInterval: DEFAULT_MAX_INTERVAL,
incrementByFactorOf: DEFAULT_INCREMENT_FACTOR,
delayStartBy: 0,
lazyStart: false,
};
if (config) {
_.extend(defaultParams, config);
}
return new gl.SmartInterval(defaultParams);
}
})(window.gl || (window.gl = {}));
spec/javascripts/subbable_resource_spec.js.es6
0 → 100644
View file @
95068552
/* eslint-disable */
//= vue
//= vue-resource
//= require jquery
//= require subbable_resource
/*
* Test that each rest verb calls the publish and subscribe function and passes the correct value back
*
*
* */
((global) => {
describe('Subbable Resource', function () {
describe('PubSub', function () {
beforeEach(function () {
this.MockResource = new global.SubbableResource('https://example.com');
});
it('should successfully add a single subscriber', function () {
const callback = () => {};
this.MockResource.subscribe(callback);
expect(this.MockResource.subscribers.length).toBe(1);
expect(this.MockResource.subscribers[0]).toBe(callback);
});
it('should successfully add multiple subscribers', function () {
const callbackOne = () => {};
const callbackTwo = () => {};
const callbackThree = () => {};
this.MockResource.subscribe(callbackOne);
this.MockResource.subscribe(callbackTwo);
this.MockResource.subscribe(callbackThree);
expect(this.MockResource.subscribers.length).toBe(3);
});
it('should successfully publish an update to a single subscriber', function () {
const state = { myprop: 1 };
const callbacks = {
one: (data) => expect(data.myprop).toBe(2),
two: (data) => expect(data.myprop).toBe(2),
three: (data) => expect(data.myprop).toBe(2)
};
const spyOne = spyOn(callbacks, 'one');
const spyTwo = spyOn(callbacks, 'two');
const spyThree = spyOn(callbacks, 'three');
this.MockResource.subscribe(callbacks.one);
this.MockResource.subscribe(callbacks.two);
this.MockResource.subscribe(callbacks.three);
state.myprop++;
this.MockResource.publish(state);
expect(spyOne).toHaveBeenCalled();
expect(spyTwo).toHaveBeenCalled();
expect(spyThree).toHaveBeenCalled();
});
});
});
})(window.gl || (window.gl = {}));
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment