blob: 8c079fda0d17c744c279393df847baeed37ad0fc [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001import {assert} from 'chai';
2import sinon from 'sinon';
3
4import MrChart, {
5 subscribedQuery,
6} from 'elements/issue-list/mr-chart/mr-chart.js';
7import {prpcClient} from 'prpc-client-instance.js';
8
9let element;
10let dataLoadedPromise;
11
12const beforeEachElement = () => {
13 if (element && document.body.contains(element)) {
14 // Avoid setting up multiple versions of the same element.
15 document.body.removeChild(element);
16 element = null;
17 }
18 const el = document.createElement('mr-chart');
19 el.setAttribute('projectName', 'rutabaga');
20 dataLoadedPromise = new Promise((resolve) => {
21 el.addEventListener('allDataLoaded', resolve);
22 });
23
24 document.body.appendChild(el);
25 return el;
26};
27
28describe('mr-chart', () => {
29 beforeEach(() => {
30 window.CS_env = {
31 token: 'rutabaga-token',
32 tokenExpiresSec: 0,
33 app_version: 'rutabaga-version',
34 };
35 sinon.stub(prpcClient, 'call').callsFake(async () => {
36 return {
37 snapshotCount: [{count: 8}],
38 unsupportedField: [],
39 searchLimitReached: false,
40 };
41 });
42
43 element = beforeEachElement();
44 });
45
46 afterEach(async () => {
47 // _fetchData is always called when the element is connected, so we have to
48 // wait until all data has been loaded.
49 // Otherwise prpcClient.call will be restored and we will make actual XHR
50 // calls.
51 await dataLoadedPromise;
52
53 document.body.removeChild(element);
54
55 prpcClient.call.restore();
56 });
57
58 describe('initializes', () => {
59 it('renders', () => {
60 assert.instanceOf(element, MrChart);
61 });
62
63 it('sets this.projectname', () => {
64 assert.equal(element.projectName, 'rutabaga');
65 });
66 });
67
68 describe('data loading', () => {
69 beforeEach(() => {
70 // Stub MrChart.makeTimestamps to return 6, not 30 data points.
71 const originalMakeTimestamps = MrChart.makeTimestamps;
72 sinon.stub(MrChart, 'makeTimestamps').callsFake((endDate) => {
73 return originalMakeTimestamps(endDate, 1, 6);
74 });
75 sinon.stub(MrChart, 'getEndDate').callsFake(() => {
76 return new Date(Date.UTC(2018, 10, 3, 23, 59, 59));
77 });
78
79 // Re-instantiate element after stubs.
80 element = beforeEachElement();
81 });
82
83 afterEach(() => {
84 MrChart.makeTimestamps.restore();
85 MrChart.getEndDate.restore();
86 });
87
88 it('makes a series of XHR calls', async () => {
89 await dataLoadedPromise;
90 for (let i = 0; i < 6; i++) {
91 assert.deepEqual(element.values[i], new Map());
92 }
93 });
94
95 it('sets indices and correctly re-orders values', async () => {
96 await dataLoadedPromise;
97
98 const timestampMap = new Map([
99 [1540857599, 0], [1540943999, 1], [1541030399, 2], [1541116799, 3],
100 [1541203199, 4], [1541289599, 5],
101 ]);
102 sinon.stub(MrChart.prototype, '_fetchDataAtTimestamp').callsFake(
103 async (ts) => ({issues: {'Issue Count': timestampMap.get(ts)}}));
104
105 element.endDate = new Date(Date.UTC(2018, 10, 3, 23, 59, 59));
106 await element._fetchData();
107
108 assert.deepEqual(element.indices, [
109 '10/29/2018', '10/30/2018', '10/31/2018',
110 '11/1/2018', '11/2/2018', '11/3/2018',
111 ]);
112 for (let i = 0; i < 6; i++) {
113 assert.deepEqual(element.values[i], {'Issue Count': i});
114 }
115 MrChart.prototype._fetchDataAtTimestamp.restore();
116 });
117
118 it('if issue count is null, defaults to 0', async () => {
119 prpcClient.call.restore();
120 sinon.stub(prpcClient, 'call').callsFake(async () => {
121 return {snapshotCount: [{}]};
122 });
123 MrChart.makeTimestamps.restore();
124 sinon.stub(MrChart, 'makeTimestamps').callsFake((endDate) => {
125 return [1234567, 2345678, 3456789];
126 });
127
128 await element._fetchData(new Date());
129 assert.deepEqual(element.values[0], new Map());
130 });
131
132 it('Retrieve data under groupby feature', async () => {
133 const data = new Map([['Type-1', 0], ['Type-2', 1]]);
134 sinon.stub(MrChart.prototype, '_fetchDataAtTimestamp').callsFake(
135 () => ({issues: data}));
136
137 element = beforeEachElement();
138
139 await element._fetchData(new Date());
140 for (let i = 0; i < 3; i++) {
141 assert.deepEqual(element.values[i], data);
142 }
143 MrChart.prototype._fetchDataAtTimestamp.restore();
144 });
145
146 it('_fetchDataAtTimestamp has no default query or can', async () => {
147 await element._fetchData();
148
149 sinon.assert.calledWith(
150 prpcClient.call,
151 'monorail.Issues',
152 'IssueSnapshot',
153 {
154 cannedQuery: undefined,
155 groupBy: undefined,
156 hotlistId: undefined,
157 query: undefined,
158 projectName: 'rutabaga',
159 timestamp: 1540857599,
160 });
161 });
162 });
163
164 describe('start date change detection', () => {
165 it('illegal query: start-date is greater than end-date', async () => {
166 await element.updateComplete;
167
168 element.startDate = new Date('2199-11-06');
169 element._fetchData();
170
171 assert.equal(element.dateRange, 90);
172 assert.equal(element.frequency, 7);
173 assert.equal(element.dateRangeNotLegal, true);
174 });
175
176 it('illegal query: end_date - start_date requires more than 90 queries',
177 async () => {
178 await element.updateComplete;
179
180 element.startDate = new Date('2016-10-03');
181 element._fetchData();
182
183 assert.equal(element.dateRange, 90 * 7);
184 assert.equal(element.frequency, 7);
185 assert.equal(element.maxQuerySizeReached, true);
186 });
187 });
188
189 describe('date change behavior', () => {
190 it('pushes to history API via pageJS', async () => {
191 sinon.stub(element, '_page');
192 sinon.spy(element, '_setDateRange');
193 sinon.spy(element, '_onDateChanged');
194 sinon.spy(element, '_changeUrlParams');
195
196 await element.updateComplete;
197
198 const thirtyButton = element.shadowRoot
199 .querySelector('#two-toggle').children[2];
200 thirtyButton.click();
201
202 sinon.assert.calledOnce(element._setDateRange);
203 sinon.assert.calledOnce(element._onDateChanged);
204 sinon.assert.calledOnce(element._changeUrlParams);
205 sinon.assert.calledOnce(element._page);
206
207 element._page.restore();
208 element._setDateRange.restore();
209 element._onDateChanged.restore();
210 element._changeUrlParams.restore();
211 });
212 });
213
214 describe('progress bar', () => {
215 it('visible based on loading progress', async () => {
216 // Check for visible progress bar and hidden input after initial render
217 await element.updateComplete;
218 const progressBar = element.shadowRoot.querySelector('progress');
219 const endDateInput = element.shadowRoot.querySelector('#end-date');
220 assert.isFalse(progressBar.hasAttribute('hidden'));
221 assert.isTrue(endDateInput.disabled);
222
223 // Check for hidden progress bar and enabled input after fetch and render
224 await dataLoadedPromise;
225 await element.updateComplete;
226 assert.isTrue(progressBar.hasAttribute('hidden'));
227 assert.isFalse(endDateInput.disabled);
228
229 // Trigger another data fetch and render, but prior to fetch complete
230 // Check progress bar is visible again
231 element.queryParams['start-date'] = '2012-01-01';
232 await element.requestUpdate('queryParams');
233 await element.updateComplete;
234 assert.isFalse(progressBar.hasAttribute('hidden'));
235
236 await dataLoadedPromise;
237 await element.updateComplete;
238 assert.isTrue(progressBar.hasAttribute('hidden'));
239 });
240 });
241
242 describe('static methods', () => {
243 describe('sortInBisectOrder', () => {
244 it('orders first, last, median recursively', () => {
245 assert.deepEqual(MrChart.sortInBisectOrder([]), []);
246 assert.deepEqual(MrChart.sortInBisectOrder([9]), [9]);
247 assert.deepEqual(MrChart.sortInBisectOrder([8, 9]), [8, 9]);
248 assert.deepEqual(MrChart.sortInBisectOrder([7, 8, 9]), [7, 9, 8]);
249 assert.deepEqual(
250 MrChart.sortInBisectOrder([1, 2, 3, 4, 5]), [1, 5, 3, 2, 4]);
251 });
252 });
253
254 describe('makeTimestamps', () => {
255 it('throws an error if endDate not passed', () => {
256 assert.throws(() => {
257 MrChart.makeTimestamps();
258 }, 'endDate required');
259 });
260 it('returns an array of in seconds', () => {
261 const endDate = new Date(Date.UTC(2018, 10, 3, 23, 59, 59));
262 const secondsInDay = 24 * 60 * 60;
263
264 assert.deepEqual(MrChart.makeTimestamps(endDate, 1, 6), [
265 1541289599 - (secondsInDay * 5), 1541289599 - (secondsInDay * 4),
266 1541289599 - (secondsInDay * 3), 1541289599 - (secondsInDay * 2),
267 1541289599 - (secondsInDay * 1), 1541289599 - (secondsInDay * 0),
268 ]);
269 });
270 it('tests frequency greater than 1', () => {
271 const endDate = new Date(Date.UTC(2018, 10, 3, 23, 59, 59));
272 const secondsInDay = 24 * 60 * 60;
273
274 assert.deepEqual(MrChart.makeTimestamps(endDate, 2, 6), [
275 1541289599 - (secondsInDay * 4),
276 1541289599 - (secondsInDay * 2),
277 1541289599 - (secondsInDay * 0),
278 ]);
279 });
280 it('tests frequency greater than 1', () => {
281 const endDate = new Date(Date.UTC(2018, 10, 3, 23, 59, 59));
282 const secondsInDay = 24 * 60 * 60;
283
284 assert.deepEqual(MrChart.makeTimestamps(endDate, 2, 7), [
285 1541289599 - (secondsInDay * 6),
286 1541289599 - (secondsInDay * 4),
287 1541289599 - (secondsInDay * 2),
288 1541289599 - (secondsInDay * 0),
289 ]);
290 });
291 });
292
293 describe('dateStringToDate', () => {
294 it('returns null if no input', () => {
295 assert.isNull(MrChart.dateStringToDate());
296 });
297
298 it('returns a new Date at EOD UTC', () => {
299 const actualDate = MrChart.dateStringToDate('2018-11-03');
300 const expectedDate = new Date(Date.UTC(2018, 10, 3, 23, 59, 59));
301 assert.equal(expectedDate.getTime(), 1541289599000, 'Sanity check.');
302
303 assert.equal(actualDate.getTime(), expectedDate.getTime());
304 });
305 });
306
307 describe('getEndDate', () => {
308 let clock;
309
310 beforeEach(() => {
311 clock = sinon.useFakeTimers(10000);
312 });
313
314 afterEach(() => {
315 clock.restore();
316 });
317
318 it('returns parsed input date', () => {
319 const input = '2018-11-03';
320
321 const expectedDate = new Date(Date.UTC(2018, 10, 3, 23, 59, 59));
322 // Time sanity check.
323 assert.equal(Math.round(expectedDate.getTime() / 1e3), 1541289599);
324
325 const actual = MrChart.getEndDate(input);
326 assert.equal(actual.getTime(), expectedDate.getTime());
327 });
328
329 it('returns EOD of current date by default', () => {
330 const expectedDate = new Date();
331 expectedDate.setHours(23);
332 expectedDate.setMinutes(59);
333 expectedDate.setSeconds(59);
334
335 assert.equal(MrChart.getEndDate().getTime(),
336 expectedDate.getTime());
337 });
338 });
339
340 describe('getStartDate', () => {
341 let clock;
342
343 beforeEach(() => {
344 clock = sinon.useFakeTimers(10000);
345 });
346
347 afterEach(() => {
348 clock.restore();
349 });
350
351 it('returns parsed input date', () => {
352 const input = '2018-07-03';
353
354 const expectedDate = new Date(Date.UTC(2018, 6, 3, 23, 59, 59));
355 // Time sanity check.
356 assert.equal(Math.round(expectedDate.getTime() / 1e3), 1530662399);
357
358 const actual = MrChart.getStartDate(input);
359 assert.equal(actual.getTime(), expectedDate.getTime());
360 });
361
362 it('returns EOD of current date by default', () => {
363 const today = new Date();
364 today.setHours(23);
365 today.setMinutes(59);
366 today.setSeconds(59);
367
368 const secondsInDay = 24 * 60 * 60;
369 const expectedDate = new Date(today.getTime() -
370 1000 * 90 * secondsInDay);
371 assert.equal(MrChart.getStartDate(undefined, today, 90).getTime(),
372 expectedDate.getTime());
373 });
374 });
375
376 describe('makeIndices', () => {
377 it('returns dates in mm/dd/yyy format', () => {
378 const timestamps = [
379 1540857599, 1540943999, 1541030399,
380 1541116799, 1541203199, 1541289599,
381 ];
382 assert.deepEqual(MrChart.makeIndices(timestamps), [
383 '10/29/2018', '10/30/2018', '10/31/2018',
384 '11/1/2018', '11/2/2018', '11/3/2018',
385 ]);
386 });
387 });
388
389 describe('getPredictedData', () => {
390 it('get predicted data shown in daily', () => {
391 const values = [0, 1, 2, 3, 4, 5, 6];
392 const result = MrChart.getPredictedData(
393 values, values.length, 3, 1, new Date('10-02-2017'));
394 assert.deepEqual(result[0], ['10/4/2017', '10/5/2017', '10/6/2017']);
395 assert.deepEqual(result[1], [7, 8, 9]);
396 assert.deepEqual(result[2], [0, 1, 2, 3, 4, 5, 6]);
397 });
398
399 it('get predicted data shown in weekly', () => {
400 const values = [0, 7, 14, 21, 28, 35, 42, 49, 56, 63, 70, 77, 84];
401 const result = MrChart.getPredictedData(
402 values, 91, 13, 7, new Date('10-02-2017'));
403 assert.deepEqual(result[1], values.map((x) => x+91));
404 assert.deepEqual(result[2], values);
405 });
406 });
407
408 describe('getErrorData', () => {
409 it('get error data with perfect regression', () => {
410 const values = [0, 1, 2, 3, 4, 5, 6];
411 const result = MrChart.getErrorData(values, values, [7, 8, 9]);
412 assert.deepEqual(result[0], [7, 8, 9]);
413 assert.deepEqual(result[1], [7, 8, 9]);
414 });
415
416 it('get error data with nonperfect regression', () => {
417 const values = [0, 1, 3, 4, 6, 6, 7];
418 const result = MrChart.getPredictedData(
419 values, values.length, 3, 1, new Date('10-02-2017'));
420 const error = MrChart.getErrorData(result[2], values, result[1]);
421 assert.isTrue(error[0][0] > result[1][0]);
422 assert.isTrue(error[1][0] < result[1][0]);
423 });
424 });
425
426 describe('getSortedLines', () => {
427 it('return all lines for less than n lines', () => {
428 const arrayValues = [
429 {label: 'line1', data: [0, 0, 1]},
430 {label: 'line2', data: [0, 1, 2]},
431 {label: 'line3', data: [0, 1, 0]},
432 {label: 'line4', data: [4, 0, 3]},
433 ];
434 const expectedValues = [
435 {label: 'line1', data: [0, 0, 1]},
436 {label: 'line2', data: [0, 1, 2]},
437 {label: 'line3', data: [0, 1, 0]},
438 {label: 'line4', data: [4, 0, 3]},
439 ];
440 const actualValues = MrChart.getSortedLines(arrayValues, 4);
441 for (let i = 0; i < 4; i++) {
442 assert.deepEqual(expectedValues[i], actualValues[i]);
443 }
444 });
445
446 it('return top n lines in sorted order for more than n lines',
447 () => {
448 const arrayValues = [
449 {label: 'line1', data: [0, 0, 1]},
450 {label: 'line2', data: [0, 1, 2]},
451 {label: 'line3', data: [0, 4, 0]},
452 {label: 'line4', data: [4, 0, 3]},
453 {label: 'line5', data: [0, 2, 3]},
454 ];
455 const expectedValues = [
456 {label: 'line5', data: [0, 2, 3]},
457 {label: 'line4', data: [4, 0, 3]},
458 {label: 'line2', data: [0, 1, 2]},
459 ];
460 const actualValues = MrChart.getSortedLines(arrayValues, 3);
461 for (let i = 0; i < 3; i++) {
462 assert.deepEqual(expectedValues[i], actualValues[i]);
463 }
464 });
465 });
466
467 describe('getGroupByFromQuery', () => {
468 it('get group by label object from URL', () => {
469 const input = {'groupby': 'label', 'labelprefix': 'Type'};
470
471 const expectedGroupBy = {
472 value: 'label',
473 labelPrefix: 'Type',
474 display: 'Type',
475 };
476 assert.deepEqual(MrChart.getGroupByFromQuery(input), expectedGroupBy);
477 });
478
479 it('get group by is open object from URL', () => {
480 const input = {'groupby': 'open'};
481
482 const expectedGroupBy = {value: 'open', display: 'Is open'};
483 assert.deepEqual(MrChart.getGroupByFromQuery(input), expectedGroupBy);
484 });
485
486 it('get group by none object from URL', () => {
487 const input = {'groupby': ''};
488
489 const expectedGroupBy = {value: '', display: 'None'};
490 assert.deepEqual(MrChart.getGroupByFromQuery(input), expectedGroupBy);
491 });
492
493 it('only returns valid groupBy values', () => {
494 const invalidKeys = ['pri', 'reporter', 'stars'];
495
496 const queryParams = {groupBy: ''};
497
498 invalidKeys.forEach((key) => {
499 queryParams.groupBy = key;
500 const expected = {value: '', display: 'None'};
501 const result = MrChart.getGroupByFromQuery(queryParams);
502 assert.deepEqual(result, expected);
503 });
504 });
505 });
506 });
507
508 describe('subscribedQuery', () => {
509 it('includes start and end date', () => {
510 assert.isTrue(subscribedQuery.has('start-date'));
511 assert.isTrue(subscribedQuery.has('start-date'));
512 });
513
514 it('includes groupby and labelprefix', () => {
515 assert.isTrue(subscribedQuery.has('groupby'));
516 assert.isTrue(subscribedQuery.has('labelprefix'));
517 });
518
519 it('includes q and can', () => {
520 assert.isTrue(subscribedQuery.has('q'));
521 assert.isTrue(subscribedQuery.has('can'));
522 });
523 });
524});