@financial-times/n-myft-ui 23.0.1 → 24.0.0
Sign up to get free protection for your applications and to get access to all the features.
- package/.circleci/config.yml +13 -16
- package/.circleci/shared-helpers/helper-npm-install-peer-deps +6 -5
- package/.github/settings.yml +1 -1
- package/.scss-lint.yml +3 -3
- package/Makefile +1 -0
- package/README.md +11 -8
- package/build-state/npm-shrinkwrap.json +11383 -10733
- package/components/collections/collections.html +3 -11
- package/components/concept-list/concept-list.html +1 -4
- package/components/csrf-token/__tests__/input.test.js +23 -0
- package/components/csrf-token/input.jsx +26 -0
- package/components/follow-button/__tests__/follow-button.test.js +40 -0
- package/components/follow-button/follow-button.jsx +174 -0
- package/components/instant-alert/instant-alert.html +1 -1
- package/components/pin-button/pin-button.html +1 -1
- package/components/save-for-later/save-for-later.html +6 -9
- package/components/unread-articles-indicator/README.md +2 -62
- package/components/unread-articles-indicator/date-fns.js +5 -12
- package/components/unread-articles-indicator/index.js +1 -62
- package/components/unread-articles-indicator/storage.js +0 -44
- package/demos/app.js +30 -3
- package/demos/templates/demo.html +2 -4
- package/demos/templates/demo.jsx +33 -0
- package/demos/templates/digest-on-follow.html +1 -1
- package/jest.config.js +8 -0
- package/mixins/lozenge/_themes.scss +8 -4
- package/mixins/lozenge/main.scss +50 -4
- package/myft/main.scss +0 -1
- package/myft/ui/index.js +0 -2
- package/package.json +20 -6
- package/test/unread-articles-indicator/index.spec.js +13 -65
- package/test/unread-articles-indicator/storage.spec.js +0 -93
- package/components/csrf-token/input.html +0 -5
- package/components/follow-button/follow-button.html +0 -79
- package/components/header-tooltip/index.js +0 -37
- package/components/header-tooltip/main.scss +0 -12
- package/components/unread-articles-indicator/constants.js +0 -4
- package/components/unread-articles-indicator/count-unread-articles.js +0 -26
- package/components/unread-articles-indicator/main.scss +0 -59
- package/components/unread-articles-indicator/tracking.js +0 -15
- package/components/unread-articles-indicator/ui.js +0 -99
- package/components/unread-articles-indicator/update-count.js +0 -40
- package/test/unread-articles-indicator/count-unread-articles.spec.js +0 -72
- package/test/unread-articles-indicator/tracking.spec.js +0 -26
- package/test/unread-articles-indicator/ui.spec.js +0 -123
- package/test/unread-articles-indicator/update-count.spec.js +0 -156
@@ -1,59 +0,0 @@
|
|
1
|
-
$circle-radius-desktop: 17px;
|
2
|
-
$circle-radius-mobile-single-digit: 14px;
|
3
|
-
$circle-radius-mobile: 8px;
|
4
|
-
|
5
|
-
.myft__indicator-container {
|
6
|
-
position: relative;
|
7
|
-
}
|
8
|
-
|
9
|
-
.myft__indicator {
|
10
|
-
padding: 0;
|
11
|
-
position: absolute;
|
12
|
-
top: 18px;
|
13
|
-
right: -3px;
|
14
|
-
|
15
|
-
border: 1px solid oColorsByName("paper");
|
16
|
-
background-color: oColorsByName("claret");
|
17
|
-
|
18
|
-
font-family: MetricWeb, sans-serif;
|
19
|
-
vertical-align: middle;
|
20
|
-
overflow: hidden;
|
21
|
-
text-align: center;
|
22
|
-
|
23
|
-
font-size: 12px;
|
24
|
-
font-weight: 500;
|
25
|
-
line-height: $circle-radius-mobile;
|
26
|
-
border-radius: $circle-radius-mobile;
|
27
|
-
width: $circle-radius-mobile;
|
28
|
-
height: $circle-radius-mobile;
|
29
|
-
|
30
|
-
// On mobile hide text unless its only one digit
|
31
|
-
color: transparent;
|
32
|
-
|
33
|
-
&.myft__indicator--single-digit {
|
34
|
-
top: 16px;
|
35
|
-
right: -6px;
|
36
|
-
color: oColorsByName("white");
|
37
|
-
line-height: $circle-radius-mobile-single-digit;
|
38
|
-
border-radius: $circle-radius-mobile-single-digit;
|
39
|
-
width: $circle-radius-mobile-single-digit;
|
40
|
-
height: $circle-radius-mobile-single-digit;
|
41
|
-
}
|
42
|
-
@include oGridRespondTo(M) {
|
43
|
-
top: 16px;
|
44
|
-
right: -8px;
|
45
|
-
color: oColorsByName("white");
|
46
|
-
line-height: $circle-radius-desktop;
|
47
|
-
border-radius: $circle-radius-desktop;
|
48
|
-
width: $circle-radius-desktop;
|
49
|
-
height: $circle-radius-desktop;
|
50
|
-
&.myft__indicator--single-digit {
|
51
|
-
// shows single digit counts slightly larger for more legibility
|
52
|
-
font-size: 13px;
|
53
|
-
}
|
54
|
-
}
|
55
|
-
}
|
56
|
-
|
57
|
-
.myft__indicator--hidden {
|
58
|
-
display: none;
|
59
|
-
}
|
@@ -1,15 +0,0 @@
|
|
1
|
-
function dispatchEvent (detail) {
|
2
|
-
const event = new CustomEvent('oTracking.event', {
|
3
|
-
detail,
|
4
|
-
bubbles: true
|
5
|
-
});
|
6
|
-
|
7
|
-
document.body.dispatchEvent(event);
|
8
|
-
}
|
9
|
-
|
10
|
-
export const onCountChange = (count, newArticlesSinceTime) => dispatchEvent({
|
11
|
-
category: 'unread-articles-indicator',
|
12
|
-
action: 'render',
|
13
|
-
count,
|
14
|
-
newArticlesSinceTime
|
15
|
-
});
|
@@ -1,99 +0,0 @@
|
|
1
|
-
class Indicator {
|
2
|
-
constructor (container, {onClick} = {}) {
|
3
|
-
this.count = undefined;
|
4
|
-
|
5
|
-
this.container = container;
|
6
|
-
this.container.classList.add('myft__indicator-container');
|
7
|
-
|
8
|
-
this.el = document.createElement('span');
|
9
|
-
this.el.classList.add('myft__indicator');
|
10
|
-
this.el.classList.add('myft__indicator--hidden');
|
11
|
-
|
12
|
-
container.appendChild(this.el);
|
13
|
-
|
14
|
-
if (typeof onClick === 'function') {
|
15
|
-
this.container.addEventListener('click', () => onClick());
|
16
|
-
}
|
17
|
-
}
|
18
|
-
|
19
|
-
setCount (count) {
|
20
|
-
if( count === this.count ) {
|
21
|
-
return;
|
22
|
-
}
|
23
|
-
if( count < 1 ) {
|
24
|
-
this.el.classList.add('myft__indicator--hidden');
|
25
|
-
} else {
|
26
|
-
this.el.classList.remove('myft__indicator--hidden');
|
27
|
-
if( count < 10 ) {
|
28
|
-
this.el.classList.add('myft__indicator--single-digit');
|
29
|
-
} else {
|
30
|
-
this.el.classList.remove('myft__indicator--single-digit');
|
31
|
-
}
|
32
|
-
this.el.innerText = count > 0 && count < 200 ? count : '';
|
33
|
-
}
|
34
|
-
this.count = count;
|
35
|
-
}
|
36
|
-
}
|
37
|
-
|
38
|
-
class Favicon {
|
39
|
-
constructor () {
|
40
|
-
this.count = undefined;
|
41
|
-
this.faviconLinks = Array.from(document.querySelectorAll('head link[rel=icon]'));
|
42
|
-
this.showDot = false;
|
43
|
-
}
|
44
|
-
|
45
|
-
setCount (count) {
|
46
|
-
if( count === this.count ) {
|
47
|
-
return;
|
48
|
-
}
|
49
|
-
this.showDot = count > 0;
|
50
|
-
const newImage = this.showDot ? 'brand-ft-logo-square-coloured-dot' : 'brand-ft-logo-square-coloured-no-dot';
|
51
|
-
this.faviconLinks.forEach(link => {
|
52
|
-
link.href = link.href.replace(/brand-ft-logo-square-coloured(-dot|-no-dot)?/, newImage);
|
53
|
-
});
|
54
|
-
this.count = count;
|
55
|
-
}
|
56
|
-
}
|
57
|
-
|
58
|
-
class Title {
|
59
|
-
constructor () {
|
60
|
-
this.count = undefined;
|
61
|
-
this.originalTitle = document.title;
|
62
|
-
}
|
63
|
-
|
64
|
-
setCount (count) {
|
65
|
-
if( count === this.count ) {
|
66
|
-
return;
|
67
|
-
}
|
68
|
-
document.title = count > 0 ? `(${count}) ${this.originalTitle}` : this.originalTitle;
|
69
|
-
this.count = count;
|
70
|
-
}
|
71
|
-
}
|
72
|
-
|
73
|
-
let indicators;
|
74
|
-
let favicon;
|
75
|
-
let title;
|
76
|
-
|
77
|
-
export const createIndicators = (targets, options = {}) => {
|
78
|
-
indicators = [...targets].map(target => new Indicator(target, options));
|
79
|
-
|
80
|
-
if (options.flags && options.flags.myftUnreadFavicon) {
|
81
|
-
favicon = new Favicon();
|
82
|
-
title = new Title();
|
83
|
-
}
|
84
|
-
};
|
85
|
-
|
86
|
-
export const setCount = count => {
|
87
|
-
indicators.forEach(indicator => indicator.setCount(count));
|
88
|
-
if (favicon) {
|
89
|
-
favicon.setCount(count);
|
90
|
-
}
|
91
|
-
if (title) {
|
92
|
-
title.setCount(count);
|
93
|
-
}
|
94
|
-
};
|
95
|
-
|
96
|
-
export const getState = () => ({
|
97
|
-
faviconHasDot: favicon ? favicon.showDot : false,
|
98
|
-
numberInTitle: title ? title.count : 0
|
99
|
-
});
|
@@ -1,40 +0,0 @@
|
|
1
|
-
import {isAfter} from './date-fns';
|
2
|
-
import * as storage from './storage';
|
3
|
-
import countUnreadArticles from './count-unread-articles';
|
4
|
-
import * as tracking from './tracking';
|
5
|
-
import {UPDATE_INTERVAL, UPDATE_TIMEOUT} from './constants';
|
6
|
-
|
7
|
-
|
8
|
-
function latest (a, b) {
|
9
|
-
if (!a) return b;
|
10
|
-
if (!b) return a;
|
11
|
-
return isAfter(a, b) ? a : b;
|
12
|
-
}
|
13
|
-
|
14
|
-
export default async function updateCount (userId, now) {
|
15
|
-
let doingUpdate;
|
16
|
-
try {
|
17
|
-
const lastUpdate = storage.getLastUpdate();
|
18
|
-
|
19
|
-
const isFirstUpdate = !lastUpdate; // Always update if there has never been an update before.
|
20
|
-
const noUpdateInProgress = !isFirstUpdate && (!lastUpdate.time || now.getTime() - lastUpdate.time.getTime() > UPDATE_INTERVAL);
|
21
|
-
const updateInProgressHasTimedOut = !isFirstUpdate && (!lastUpdate.updateStarted || now.getTime() - lastUpdate.updateStarted.getTime() > UPDATE_TIMEOUT);
|
22
|
-
|
23
|
-
if (isFirstUpdate || noUpdateInProgress && updateInProgressHasTimedOut) {
|
24
|
-
|
25
|
-
storage.updateLastUpdate({updateStarted: now});
|
26
|
-
|
27
|
-
const startTime = latest(storage.getFeedStartTime(), storage.getIndicatorDismissedTime());
|
28
|
-
|
29
|
-
doingUpdate = true;
|
30
|
-
const count = await countUnreadArticles(userId, startTime);
|
31
|
-
if (!lastUpdate || count !== lastUpdate.count) {
|
32
|
-
tracking.onCountChange(count, startTime);
|
33
|
-
}
|
34
|
-
storage.setLastUpdate({time: now, count, updateStarted: false});
|
35
|
-
}
|
36
|
-
} catch (error) {
|
37
|
-
if (doingUpdate) storage.updateLastUpdate({updateStarted: false});
|
38
|
-
throw error;
|
39
|
-
}
|
40
|
-
};
|
@@ -1,72 +0,0 @@
|
|
1
|
-
/* global expect */
|
2
|
-
|
3
|
-
import fetchMock from 'fetch-mock';
|
4
|
-
|
5
|
-
const START_TIME = new Date('2018-06-05T06:48:26.635Z');
|
6
|
-
const userId = '00000000-0000-0000-0000-000000000000';
|
7
|
-
|
8
|
-
const timestamps = {
|
9
|
-
BEFORE: '2018-06-04T06:48:26.635Z',
|
10
|
-
AFTER: '2018-06-05T07:48:26.635Z'
|
11
|
-
};
|
12
|
-
|
13
|
-
const articleGenerator = () => {
|
14
|
-
const counters = {};
|
15
|
-
return (read, when) => {
|
16
|
-
if (counters[read] === undefined) {
|
17
|
-
counters[read] = {};
|
18
|
-
}
|
19
|
-
if (counters[read][when] === undefined) {
|
20
|
-
counters[read][when] = 0;
|
21
|
-
}
|
22
|
-
const count = counters[read][when]++;
|
23
|
-
return {
|
24
|
-
id: `${read}_${when}_${count}`,
|
25
|
-
contentTimeStamp: timestamps[when],
|
26
|
-
userCompletion: read === 'READ' ? 1 : 0
|
27
|
-
};
|
28
|
-
};
|
29
|
-
};
|
30
|
-
|
31
|
-
|
32
|
-
const generateFeedArticle = articleGenerator();
|
33
|
-
const mockPersonalisedFeedData = {
|
34
|
-
results: [
|
35
|
-
generateFeedArticle('READ', 'AFTER'),
|
36
|
-
generateFeedArticle('UNREAD', 'BEFORE'),
|
37
|
-
generateFeedArticle('READ', 'AFTER'),
|
38
|
-
generateFeedArticle('UNREAD', 'BEFORE'),
|
39
|
-
generateFeedArticle('UNREAD', 'BEFORE'),
|
40
|
-
generateFeedArticle('UNREAD', 'AFTER'),
|
41
|
-
generateFeedArticle('UNREAD', 'BEFORE'),
|
42
|
-
generateFeedArticle('UNREAD', 'AFTER'),
|
43
|
-
generateFeedArticle('UNREAD', 'AFTER'),
|
44
|
-
generateFeedArticle('UNREAD', 'AFTER'),
|
45
|
-
generateFeedArticle('UNREAD', 'AFTER'),
|
46
|
-
generateFeedArticle('UNREAD', 'AFTER'),
|
47
|
-
generateFeedArticle('UNREAD', 'AFTER'),
|
48
|
-
generateFeedArticle('UNREAD', 'AFTER'),
|
49
|
-
]
|
50
|
-
};
|
51
|
-
|
52
|
-
describe('count-unread-articles', () => {
|
53
|
-
let count;
|
54
|
-
|
55
|
-
const countUnreadArticles = require('../../components/unread-articles-indicator/count-unread-articles');
|
56
|
-
|
57
|
-
beforeEach(() => {
|
58
|
-
count = null;
|
59
|
-
fetchMock.get('begin:/__myft/api/onsite/feed/', mockPersonalisedFeedData);
|
60
|
-
|
61
|
-
return countUnreadArticles(userId, START_TIME)
|
62
|
-
.then(resolvedValue => {
|
63
|
-
count = resolvedValue;
|
64
|
-
});
|
65
|
-
});
|
66
|
-
|
67
|
-
afterEach(fetchMock.restore);
|
68
|
-
|
69
|
-
it('should return only the unread articles after the start time', () => {
|
70
|
-
expect(count).to.equal(8);
|
71
|
-
});
|
72
|
-
});
|
@@ -1,26 +0,0 @@
|
|
1
|
-
/* global expect */
|
2
|
-
import * as tracking from '../../components/unread-articles-indicator/tracking';
|
3
|
-
|
4
|
-
describe('unread-articles-indicator tracking', () => {
|
5
|
-
describe('onCountChange', () => {
|
6
|
-
let dispatchedEvent;
|
7
|
-
const COUNT = 3;
|
8
|
-
const NEW_ARTICLES_SINCE_TIME = '2018-06-18T14:22:51.098Z';
|
9
|
-
|
10
|
-
beforeEach(() => {
|
11
|
-
dispatchedEvent = null;
|
12
|
-
document.addEventListener('oTracking.event', event => dispatchedEvent = event);
|
13
|
-
});
|
14
|
-
|
15
|
-
it('should should dispatch the event', () => {
|
16
|
-
tracking.onCountChange(COUNT, NEW_ARTICLES_SINCE_TIME);
|
17
|
-
|
18
|
-
expect(dispatchedEvent.detail).to.deep.equal({
|
19
|
-
category: 'unread-articles-indicator',
|
20
|
-
action: 'render',
|
21
|
-
count: COUNT,
|
22
|
-
newArticlesSinceTime: NEW_ARTICLES_SINCE_TIME
|
23
|
-
});
|
24
|
-
});
|
25
|
-
});
|
26
|
-
});
|
@@ -1,123 +0,0 @@
|
|
1
|
-
/* global expect */
|
2
|
-
import sinon from 'sinon';
|
3
|
-
import * as ui from '../../components/unread-articles-indicator/ui';
|
4
|
-
|
5
|
-
describe('unread stories indicator - ui', () => {
|
6
|
-
|
7
|
-
describe('createIndicators', () => {
|
8
|
-
let containers;
|
9
|
-
let options;
|
10
|
-
const mockContainer = document.createElement('div');
|
11
|
-
|
12
|
-
mockContainer.classList.add('o-header__top-link--myft');
|
13
|
-
|
14
|
-
beforeEach(() => {
|
15
|
-
containers = [0, 1].map(() => {
|
16
|
-
const cont = mockContainer.cloneNode(false);
|
17
|
-
|
18
|
-
document.body.appendChild(cont);
|
19
|
-
|
20
|
-
return cont;
|
21
|
-
});
|
22
|
-
});
|
23
|
-
|
24
|
-
afterEach(() => {
|
25
|
-
containers.forEach(container => container.remove());
|
26
|
-
});
|
27
|
-
|
28
|
-
describe('createIndicators', () => {
|
29
|
-
beforeEach(() => {
|
30
|
-
ui.createIndicators(containers);
|
31
|
-
});
|
32
|
-
|
33
|
-
it('should set a class on each container', () => {
|
34
|
-
containers.forEach(container => {
|
35
|
-
expect(container.classList.contains('myft__indicator-container')).to.equal(true);
|
36
|
-
});
|
37
|
-
});
|
38
|
-
|
39
|
-
it('should add a count element to each container', () => {
|
40
|
-
containers.forEach(container => {
|
41
|
-
const els = container.querySelectorAll('.myft__indicator');
|
42
|
-
|
43
|
-
expect(els.length).to.equal(1);
|
44
|
-
});
|
45
|
-
});
|
46
|
-
});
|
47
|
-
|
48
|
-
describe('setCount', () => {
|
49
|
-
beforeEach(() => {
|
50
|
-
ui.createIndicators(containers);
|
51
|
-
ui.setCount(3);
|
52
|
-
});
|
53
|
-
|
54
|
-
it('should show the count in the count element', () => {
|
55
|
-
containers.forEach(container => {
|
56
|
-
const el = container.querySelector('.myft__indicator');
|
57
|
-
|
58
|
-
expect(el.innerText).to.equal('3');
|
59
|
-
});
|
60
|
-
});
|
61
|
-
|
62
|
-
it('should hide the indicator when count is zero', () => {
|
63
|
-
ui.setCount(0);
|
64
|
-
containers.forEach(container => {
|
65
|
-
const el = container.querySelector('.myft__indicator');
|
66
|
-
|
67
|
-
expect(el.classList.contains('myft__indicator--hidden')).to.equal(true);
|
68
|
-
});
|
69
|
-
});
|
70
|
-
|
71
|
-
it('should set a single digit class when count is less than 10', () => {
|
72
|
-
ui.setCount(9);
|
73
|
-
containers.forEach(container => {
|
74
|
-
const el = container.querySelector('.myft__indicator');
|
75
|
-
|
76
|
-
expect(el.classList.contains('myft__indicator--hidden')).to.equal(false);
|
77
|
-
expect(el.classList.contains('myft__indicator--single-digit')).to.equal(true);
|
78
|
-
});
|
79
|
-
});
|
80
|
-
|
81
|
-
it('should remove a single digit class when count is more than 10', () => {
|
82
|
-
ui.setCount(10);
|
83
|
-
containers.forEach(container => {
|
84
|
-
const el = container.querySelector('.myft__indicator');
|
85
|
-
|
86
|
-
expect(el.classList.contains('myft__indicator--hidden')).to.equal(false);
|
87
|
-
expect(el.classList.contains('myft__indicator--single-digit')).to.equal(false);
|
88
|
-
});
|
89
|
-
});
|
90
|
-
|
91
|
-
it('should not display number when count is more than 200', () => {
|
92
|
-
ui.setCount(200);
|
93
|
-
containers.forEach(container => {
|
94
|
-
const el = container.querySelector('.myft__indicator');
|
95
|
-
|
96
|
-
expect(el.classList.contains('myft__indicator--hidden')).to.equal(false);
|
97
|
-
expect(el.classList.contains('myft__indicator--single-digit')).to.equal(false);
|
98
|
-
expect(el.innerText).to.equal('');
|
99
|
-
});
|
100
|
-
});
|
101
|
-
});
|
102
|
-
|
103
|
-
describe('click handling', () => {
|
104
|
-
beforeEach(() => {
|
105
|
-
options = {
|
106
|
-
onClick: sinon.stub()
|
107
|
-
};
|
108
|
-
ui.createIndicators(containers, options);
|
109
|
-
ui.setCount(3);
|
110
|
-
});
|
111
|
-
|
112
|
-
describe('given an indicator is clicked', () => {
|
113
|
-
beforeEach(() => {
|
114
|
-
containers[0].click();
|
115
|
-
});
|
116
|
-
|
117
|
-
it('should set the indicator dismissed time', () => {
|
118
|
-
expect(options.onClick.called).to.equal(true);
|
119
|
-
});
|
120
|
-
});
|
121
|
-
});
|
122
|
-
});
|
123
|
-
});
|
@@ -1,156 +0,0 @@
|
|
1
|
-
/* global expect */
|
2
|
-
import sinon from 'sinon';
|
3
|
-
|
4
|
-
const MOCK_COUNT = 1234;
|
5
|
-
const MOCK_PREVIOUS_COUNT = 1230;
|
6
|
-
const MOCK_NOW = new Date('2019-09-23T11:29:00Z');
|
7
|
-
const MOCK_EARLIEST_CUTOFF_TIME = new Date('2019-09-22T00:00:00Z');
|
8
|
-
const MOCK_OVERDUE_REFRESH_TIME = new Date('2019-09-23T11:23:59Z');
|
9
|
-
const MOCK_UNDUE_REFRESH_TIME = new Date('2019-09-23T11:24:01Z');
|
10
|
-
const USER_ID = '00000000-0000-0000-0000-000000000000';
|
11
|
-
|
12
|
-
const mocks = {
|
13
|
-
countUnreadArticles: sinon.stub().resolves(MOCK_COUNT),
|
14
|
-
setCount: sinon.stub(),
|
15
|
-
onCountChange: sinon.stub()
|
16
|
-
};
|
17
|
-
|
18
|
-
const mockStorage = {
|
19
|
-
_lastUpdate: undefined,
|
20
|
-
_setLastUpdate: sinon.spy( function (x) { this.default.lastUpdate = x; } ),
|
21
|
-
getLastUpdate () { return this.default.lastUpdate; },
|
22
|
-
setLastUpdate (x) { this._setLastUpdate(x); },
|
23
|
-
updateLastUpdate (x) { this._setLastUpdate(x); },
|
24
|
-
getFeedStartTime () { return MOCK_EARLIEST_CUTOFF_TIME; },
|
25
|
-
getIndicatorDismissedTime () { return MOCK_EARLIEST_CUTOFF_TIME; }
|
26
|
-
};
|
27
|
-
|
28
|
-
function resetMocks () {
|
29
|
-
mocks.countUnreadArticles.resetHistory();
|
30
|
-
mocks.setCount.resetHistory();
|
31
|
-
mockStorage._setLastUpdate.resetHistory();
|
32
|
-
}
|
33
|
-
|
34
|
-
describe('update function', () => {
|
35
|
-
|
36
|
-
const injector = require('inject-loader!../../components/unread-articles-indicator/update-count');
|
37
|
-
const updateCount = injector({
|
38
|
-
'./storage': mockStorage,
|
39
|
-
'./count-unread-articles': mocks.countUnreadArticles,
|
40
|
-
'./tracking': {onCountChange: mocks.onCountChange}
|
41
|
-
});
|
42
|
-
|
43
|
-
context('when there is no previous update', () => {
|
44
|
-
before( function () {
|
45
|
-
resetMocks();
|
46
|
-
mockStorage.lastUpdate = undefined;
|
47
|
-
return updateCount(USER_ID, MOCK_NOW);
|
48
|
-
} );
|
49
|
-
|
50
|
-
it('checks for new articles', () => {
|
51
|
-
expect(mocks.countUnreadArticles).calledWith(USER_ID, MOCK_EARLIEST_CUTOFF_TIME);
|
52
|
-
});
|
53
|
-
|
54
|
-
it('marked an update as in progress', () => {
|
55
|
-
expect(mockStorage._setLastUpdate.firstCall.args[0].updateStarted.toISOString()).equal(MOCK_NOW.toISOString());
|
56
|
-
});
|
57
|
-
|
58
|
-
it('updates the local storage', () => {
|
59
|
-
expect(mockStorage.lastUpdate.count).equal(MOCK_COUNT);
|
60
|
-
});
|
61
|
-
|
62
|
-
});
|
63
|
-
|
64
|
-
|
65
|
-
context('when an update is due', () => {
|
66
|
-
before( function () {
|
67
|
-
resetMocks();
|
68
|
-
mockStorage.lastUpdate = {time: MOCK_OVERDUE_REFRESH_TIME};
|
69
|
-
return updateCount(USER_ID, MOCK_NOW);
|
70
|
-
} );
|
71
|
-
|
72
|
-
it('checks for new articles', () => {
|
73
|
-
expect(mocks.countUnreadArticles).calledWith(USER_ID, MOCK_EARLIEST_CUTOFF_TIME);
|
74
|
-
});
|
75
|
-
|
76
|
-
it('updates the local storage', () => {
|
77
|
-
expect(mockStorage.lastUpdate.count).equal(MOCK_COUNT);
|
78
|
-
});
|
79
|
-
|
80
|
-
it('marked an update as in progress', () => {
|
81
|
-
expect(mockStorage._setLastUpdate.firstCall.args[0].updateStarted.toISOString()).equal(MOCK_NOW.toISOString());
|
82
|
-
});
|
83
|
-
|
84
|
-
it('doesn\'t block further updates', () => {
|
85
|
-
expect(mockStorage.lastUpdate.updateStarted).equal(false);
|
86
|
-
});
|
87
|
-
});
|
88
|
-
|
89
|
-
context('when an update is not due', () => {
|
90
|
-
const lastUpdate = {time: MOCK_UNDUE_REFRESH_TIME, count: MOCK_PREVIOUS_COUNT};
|
91
|
-
before( function () {
|
92
|
-
resetMocks();
|
93
|
-
mockStorage.lastUpdate = lastUpdate;
|
94
|
-
return updateCount(USER_ID, MOCK_NOW);
|
95
|
-
} );
|
96
|
-
|
97
|
-
it('does not check for new articles', () => {
|
98
|
-
expect(mocks.countUnreadArticles.notCalled).equal(true,'Unexpected call to countUnreadArticles()');
|
99
|
-
});
|
100
|
-
|
101
|
-
it('did not mark an update as in progress', () => {
|
102
|
-
expect(mockStorage._setLastUpdate.notCalled).equal(true,'Unexpected storage update');
|
103
|
-
});
|
104
|
-
|
105
|
-
it('doesn\'t update the local storage', () => {
|
106
|
-
expect(mockStorage.lastUpdate).equal(lastUpdate);
|
107
|
-
});
|
108
|
-
});
|
109
|
-
|
110
|
-
context('when an update fails', () => {
|
111
|
-
let err;
|
112
|
-
before( async () => {
|
113
|
-
resetMocks();
|
114
|
-
mocks.countUnreadArticles.rejects(new Error('boom'));
|
115
|
-
mockStorage.lastUpdate = undefined;
|
116
|
-
try {
|
117
|
-
await updateCount(USER_ID, MOCK_NOW);
|
118
|
-
} catch(e) {
|
119
|
-
err = e;
|
120
|
-
}
|
121
|
-
} );
|
122
|
-
|
123
|
-
after( () => {
|
124
|
-
mocks.countUnreadArticles.resolves(MOCK_COUNT);
|
125
|
-
});
|
126
|
-
|
127
|
-
it('marks the update as in progress', () => {
|
128
|
-
expect(mockStorage._setLastUpdate.firstCall.args[0].updateStarted.toISOString()).equal(MOCK_NOW.toISOString());
|
129
|
-
});
|
130
|
-
|
131
|
-
it('doesn\'t block further updates', () => {
|
132
|
-
expect(mockStorage.lastUpdate.updateStarted).equal(false);
|
133
|
-
});
|
134
|
-
|
135
|
-
it('should throw an error', () => {
|
136
|
-
expect(err.message).to.equal('boom');
|
137
|
-
});
|
138
|
-
});
|
139
|
-
|
140
|
-
context('when an update is due but already happening', () => {
|
141
|
-
before( function () {
|
142
|
-
resetMocks();
|
143
|
-
mockStorage.lastUpdate = {time: MOCK_OVERDUE_REFRESH_TIME, count: MOCK_PREVIOUS_COUNT, updateStarted: MOCK_OVERDUE_REFRESH_TIME};
|
144
|
-
return updateCount(USER_ID, MOCK_NOW);
|
145
|
-
} );
|
146
|
-
|
147
|
-
it('does not check for new articles', () => {
|
148
|
-
expect(mocks.countUnreadArticles.notCalled).equal(true,'Unexpected call to countUnreadArticles()');
|
149
|
-
});
|
150
|
-
|
151
|
-
it('does not touch local storage', () => {
|
152
|
-
expect(mockStorage._setLastUpdate.notCalled).equal(true,'Unexpected storage update');
|
153
|
-
});
|
154
|
-
|
155
|
-
});
|
156
|
-
});
|