Commit 95068552 authored by Bryce Johnson's avatar Bryce Johnson

Prepare timetracking frontend for backend.

parent 3b5f3c61
......@@ -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 */
......
//= 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 = {}));
//= 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 = {}));
/*
* 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 = {}));
//= 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 = {}));
......@@ -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({
......
......@@ -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;
}
}
}
<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
- 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();
......
/* 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 = {}));
/* 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 = {}));
/* 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 = {}));
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment