Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/elements/issue-list/mr-chart/mr-chart.test.js b/static_src/elements/issue-list/mr-chart/mr-chart.test.js
new file mode 100644
index 0000000..8c079fd
--- /dev/null
+++ b/static_src/elements/issue-list/mr-chart/mr-chart.test.js
@@ -0,0 +1,524 @@
+import {assert} from 'chai';
+import sinon from 'sinon';
+
+import MrChart, {
+  subscribedQuery,
+} from 'elements/issue-list/mr-chart/mr-chart.js';
+import {prpcClient} from 'prpc-client-instance.js';
+
+let element;
+let dataLoadedPromise;
+
+const beforeEachElement = () => {
+  if (element && document.body.contains(element)) {
+    // Avoid setting up multiple versions of the same element.
+    document.body.removeChild(element);
+    element = null;
+  }
+  const el = document.createElement('mr-chart');
+  el.setAttribute('projectName', 'rutabaga');
+  dataLoadedPromise = new Promise((resolve) => {
+    el.addEventListener('allDataLoaded', resolve);
+  });
+
+  document.body.appendChild(el);
+  return el;
+};
+
+describe('mr-chart', () => {
+  beforeEach(() => {
+    window.CS_env = {
+      token: 'rutabaga-token',
+      tokenExpiresSec: 0,
+      app_version: 'rutabaga-version',
+    };
+    sinon.stub(prpcClient, 'call').callsFake(async () => {
+      return {
+        snapshotCount: [{count: 8}],
+        unsupportedField: [],
+        searchLimitReached: false,
+      };
+    });
+
+    element = beforeEachElement();
+  });
+
+  afterEach(async () => {
+    // _fetchData is always called when the element is connected, so we have to
+    // wait until all data has been loaded.
+    // Otherwise prpcClient.call will be restored and we will make actual XHR
+    // calls.
+    await dataLoadedPromise;
+
+    document.body.removeChild(element);
+
+    prpcClient.call.restore();
+  });
+
+  describe('initializes', () => {
+    it('renders', () => {
+      assert.instanceOf(element, MrChart);
+    });
+
+    it('sets this.projectname', () => {
+      assert.equal(element.projectName, 'rutabaga');
+    });
+  });
+
+  describe('data loading', () => {
+    beforeEach(() => {
+      // Stub MrChart.makeTimestamps to return 6, not 30 data points.
+      const originalMakeTimestamps = MrChart.makeTimestamps;
+      sinon.stub(MrChart, 'makeTimestamps').callsFake((endDate) => {
+        return originalMakeTimestamps(endDate, 1, 6);
+      });
+      sinon.stub(MrChart, 'getEndDate').callsFake(() => {
+        return new Date(Date.UTC(2018, 10, 3, 23, 59, 59));
+      });
+
+      // Re-instantiate element after stubs.
+      element = beforeEachElement();
+    });
+
+    afterEach(() => {
+      MrChart.makeTimestamps.restore();
+      MrChart.getEndDate.restore();
+    });
+
+    it('makes a series of XHR calls', async () => {
+      await dataLoadedPromise;
+      for (let i = 0; i < 6; i++) {
+        assert.deepEqual(element.values[i], new Map());
+      }
+    });
+
+    it('sets indices and correctly re-orders values', async () => {
+      await dataLoadedPromise;
+
+      const timestampMap = new Map([
+        [1540857599, 0], [1540943999, 1], [1541030399, 2], [1541116799, 3],
+        [1541203199, 4], [1541289599, 5],
+      ]);
+      sinon.stub(MrChart.prototype, '_fetchDataAtTimestamp').callsFake(
+          async (ts) => ({issues: {'Issue Count': timestampMap.get(ts)}}));
+
+      element.endDate = new Date(Date.UTC(2018, 10, 3, 23, 59, 59));
+      await element._fetchData();
+
+      assert.deepEqual(element.indices, [
+        '10/29/2018', '10/30/2018', '10/31/2018',
+        '11/1/2018', '11/2/2018', '11/3/2018',
+      ]);
+      for (let i = 0; i < 6; i++) {
+        assert.deepEqual(element.values[i], {'Issue Count': i});
+      }
+      MrChart.prototype._fetchDataAtTimestamp.restore();
+    });
+
+    it('if issue count is null, defaults to 0', async () => {
+      prpcClient.call.restore();
+      sinon.stub(prpcClient, 'call').callsFake(async () => {
+        return {snapshotCount: [{}]};
+      });
+      MrChart.makeTimestamps.restore();
+      sinon.stub(MrChart, 'makeTimestamps').callsFake((endDate) => {
+        return [1234567, 2345678, 3456789];
+      });
+
+      await element._fetchData(new Date());
+      assert.deepEqual(element.values[0], new Map());
+    });
+
+    it('Retrieve data under groupby feature', async () => {
+      const data = new Map([['Type-1', 0], ['Type-2', 1]]);
+      sinon.stub(MrChart.prototype, '_fetchDataAtTimestamp').callsFake(
+          () => ({issues: data}));
+
+      element = beforeEachElement();
+
+      await element._fetchData(new Date());
+      for (let i = 0; i < 3; i++) {
+        assert.deepEqual(element.values[i], data);
+      }
+      MrChart.prototype._fetchDataAtTimestamp.restore();
+    });
+
+    it('_fetchDataAtTimestamp has no default query or can', async () => {
+      await element._fetchData();
+
+      sinon.assert.calledWith(
+          prpcClient.call,
+          'monorail.Issues',
+          'IssueSnapshot',
+          {
+            cannedQuery: undefined,
+            groupBy: undefined,
+            hotlistId: undefined,
+            query: undefined,
+            projectName: 'rutabaga',
+            timestamp: 1540857599,
+          });
+    });
+  });
+
+  describe('start date change detection', () => {
+    it('illegal query: start-date is greater than end-date', async () => {
+      await element.updateComplete;
+
+      element.startDate = new Date('2199-11-06');
+      element._fetchData();
+
+      assert.equal(element.dateRange, 90);
+      assert.equal(element.frequency, 7);
+      assert.equal(element.dateRangeNotLegal, true);
+    });
+
+    it('illegal query: end_date - start_date requires more than 90 queries',
+        async () => {
+          await element.updateComplete;
+
+          element.startDate = new Date('2016-10-03');
+          element._fetchData();
+
+          assert.equal(element.dateRange, 90 * 7);
+          assert.equal(element.frequency, 7);
+          assert.equal(element.maxQuerySizeReached, true);
+        });
+  });
+
+  describe('date change behavior', () => {
+    it('pushes to history API via pageJS', async () => {
+      sinon.stub(element, '_page');
+      sinon.spy(element, '_setDateRange');
+      sinon.spy(element, '_onDateChanged');
+      sinon.spy(element, '_changeUrlParams');
+
+      await element.updateComplete;
+
+      const thirtyButton = element.shadowRoot
+          .querySelector('#two-toggle').children[2];
+      thirtyButton.click();
+
+      sinon.assert.calledOnce(element._setDateRange);
+      sinon.assert.calledOnce(element._onDateChanged);
+      sinon.assert.calledOnce(element._changeUrlParams);
+      sinon.assert.calledOnce(element._page);
+
+      element._page.restore();
+      element._setDateRange.restore();
+      element._onDateChanged.restore();
+      element._changeUrlParams.restore();
+    });
+  });
+
+  describe('progress bar', () => {
+    it('visible based on loading progress', async () => {
+      // Check for visible progress bar and hidden input after initial render
+      await element.updateComplete;
+      const progressBar = element.shadowRoot.querySelector('progress');
+      const endDateInput = element.shadowRoot.querySelector('#end-date');
+      assert.isFalse(progressBar.hasAttribute('hidden'));
+      assert.isTrue(endDateInput.disabled);
+
+      // Check for hidden progress bar and enabled input after fetch and render
+      await dataLoadedPromise;
+      await element.updateComplete;
+      assert.isTrue(progressBar.hasAttribute('hidden'));
+      assert.isFalse(endDateInput.disabled);
+
+      // Trigger another data fetch and render, but prior to fetch complete
+      // Check progress bar is visible again
+      element.queryParams['start-date'] = '2012-01-01';
+      await element.requestUpdate('queryParams');
+      await element.updateComplete;
+      assert.isFalse(progressBar.hasAttribute('hidden'));
+
+      await dataLoadedPromise;
+      await element.updateComplete;
+      assert.isTrue(progressBar.hasAttribute('hidden'));
+    });
+  });
+
+  describe('static methods', () => {
+    describe('sortInBisectOrder', () => {
+      it('orders first, last, median recursively', () => {
+        assert.deepEqual(MrChart.sortInBisectOrder([]), []);
+        assert.deepEqual(MrChart.sortInBisectOrder([9]), [9]);
+        assert.deepEqual(MrChart.sortInBisectOrder([8, 9]), [8, 9]);
+        assert.deepEqual(MrChart.sortInBisectOrder([7, 8, 9]), [7, 9, 8]);
+        assert.deepEqual(
+            MrChart.sortInBisectOrder([1, 2, 3, 4, 5]), [1, 5, 3, 2, 4]);
+      });
+    });
+
+    describe('makeTimestamps', () => {
+      it('throws an error if endDate not passed', () => {
+        assert.throws(() => {
+          MrChart.makeTimestamps();
+        }, 'endDate required');
+      });
+      it('returns an array of in seconds', () => {
+        const endDate = new Date(Date.UTC(2018, 10, 3, 23, 59, 59));
+        const secondsInDay = 24 * 60 * 60;
+
+        assert.deepEqual(MrChart.makeTimestamps(endDate, 1, 6), [
+          1541289599 - (secondsInDay * 5), 1541289599 - (secondsInDay * 4),
+          1541289599 - (secondsInDay * 3), 1541289599 - (secondsInDay * 2),
+          1541289599 - (secondsInDay * 1), 1541289599 - (secondsInDay * 0),
+        ]);
+      });
+      it('tests frequency greater than 1', () => {
+        const endDate = new Date(Date.UTC(2018, 10, 3, 23, 59, 59));
+        const secondsInDay = 24 * 60 * 60;
+
+        assert.deepEqual(MrChart.makeTimestamps(endDate, 2, 6), [
+          1541289599 - (secondsInDay * 4),
+          1541289599 - (secondsInDay * 2),
+          1541289599 - (secondsInDay * 0),
+        ]);
+      });
+      it('tests frequency greater than 1', () => {
+        const endDate = new Date(Date.UTC(2018, 10, 3, 23, 59, 59));
+        const secondsInDay = 24 * 60 * 60;
+
+        assert.deepEqual(MrChart.makeTimestamps(endDate, 2, 7), [
+          1541289599 - (secondsInDay * 6),
+          1541289599 - (secondsInDay * 4),
+          1541289599 - (secondsInDay * 2),
+          1541289599 - (secondsInDay * 0),
+        ]);
+      });
+    });
+
+    describe('dateStringToDate', () => {
+      it('returns null if no input', () => {
+        assert.isNull(MrChart.dateStringToDate());
+      });
+
+      it('returns a new Date at EOD UTC', () => {
+        const actualDate = MrChart.dateStringToDate('2018-11-03');
+        const expectedDate = new Date(Date.UTC(2018, 10, 3, 23, 59, 59));
+        assert.equal(expectedDate.getTime(), 1541289599000, 'Sanity check.');
+
+        assert.equal(actualDate.getTime(), expectedDate.getTime());
+      });
+    });
+
+    describe('getEndDate', () => {
+      let clock;
+
+      beforeEach(() => {
+        clock = sinon.useFakeTimers(10000);
+      });
+
+      afterEach(() => {
+        clock.restore();
+      });
+
+      it('returns parsed input date', () => {
+        const input = '2018-11-03';
+
+        const expectedDate = new Date(Date.UTC(2018, 10, 3, 23, 59, 59));
+        // Time sanity check.
+        assert.equal(Math.round(expectedDate.getTime() / 1e3), 1541289599);
+
+        const actual = MrChart.getEndDate(input);
+        assert.equal(actual.getTime(), expectedDate.getTime());
+      });
+
+      it('returns EOD of current date by default', () => {
+        const expectedDate = new Date();
+        expectedDate.setHours(23);
+        expectedDate.setMinutes(59);
+        expectedDate.setSeconds(59);
+
+        assert.equal(MrChart.getEndDate().getTime(),
+            expectedDate.getTime());
+      });
+    });
+
+    describe('getStartDate', () => {
+      let clock;
+
+      beforeEach(() => {
+        clock = sinon.useFakeTimers(10000);
+      });
+
+      afterEach(() => {
+        clock.restore();
+      });
+
+      it('returns parsed input date', () => {
+        const input = '2018-07-03';
+
+        const expectedDate = new Date(Date.UTC(2018, 6, 3, 23, 59, 59));
+        // Time sanity check.
+        assert.equal(Math.round(expectedDate.getTime() / 1e3), 1530662399);
+
+        const actual = MrChart.getStartDate(input);
+        assert.equal(actual.getTime(), expectedDate.getTime());
+      });
+
+      it('returns EOD of current date by default', () => {
+        const today = new Date();
+        today.setHours(23);
+        today.setMinutes(59);
+        today.setSeconds(59);
+
+        const secondsInDay = 24 * 60 * 60;
+        const expectedDate = new Date(today.getTime() -
+            1000 * 90 * secondsInDay);
+        assert.equal(MrChart.getStartDate(undefined, today, 90).getTime(),
+            expectedDate.getTime());
+      });
+    });
+
+    describe('makeIndices', () => {
+      it('returns dates in mm/dd/yyy format', () => {
+        const timestamps = [
+          1540857599, 1540943999, 1541030399,
+          1541116799, 1541203199, 1541289599,
+        ];
+        assert.deepEqual(MrChart.makeIndices(timestamps), [
+          '10/29/2018', '10/30/2018', '10/31/2018',
+          '11/1/2018', '11/2/2018', '11/3/2018',
+        ]);
+      });
+    });
+
+    describe('getPredictedData', () => {
+      it('get predicted data shown in daily', () => {
+        const values = [0, 1, 2, 3, 4, 5, 6];
+        const result = MrChart.getPredictedData(
+            values, values.length, 3, 1, new Date('10-02-2017'));
+        assert.deepEqual(result[0], ['10/4/2017', '10/5/2017', '10/6/2017']);
+        assert.deepEqual(result[1], [7, 8, 9]);
+        assert.deepEqual(result[2], [0, 1, 2, 3, 4, 5, 6]);
+      });
+
+      it('get predicted data shown in weekly', () => {
+        const values = [0, 7, 14, 21, 28, 35, 42, 49, 56, 63, 70, 77, 84];
+        const result = MrChart.getPredictedData(
+            values, 91, 13, 7, new Date('10-02-2017'));
+        assert.deepEqual(result[1], values.map((x) => x+91));
+        assert.deepEqual(result[2], values);
+      });
+    });
+
+    describe('getErrorData', () => {
+      it('get error data with perfect regression', () => {
+        const values = [0, 1, 2, 3, 4, 5, 6];
+        const result = MrChart.getErrorData(values, values, [7, 8, 9]);
+        assert.deepEqual(result[0], [7, 8, 9]);
+        assert.deepEqual(result[1], [7, 8, 9]);
+      });
+
+      it('get error data with nonperfect regression', () => {
+        const values = [0, 1, 3, 4, 6, 6, 7];
+        const result = MrChart.getPredictedData(
+            values, values.length, 3, 1, new Date('10-02-2017'));
+        const error = MrChart.getErrorData(result[2], values, result[1]);
+        assert.isTrue(error[0][0] > result[1][0]);
+        assert.isTrue(error[1][0] < result[1][0]);
+      });
+    });
+
+    describe('getSortedLines', () => {
+      it('return all lines for less than n lines', () => {
+        const arrayValues = [
+          {label: 'line1', data: [0, 0, 1]},
+          {label: 'line2', data: [0, 1, 2]},
+          {label: 'line3', data: [0, 1, 0]},
+          {label: 'line4', data: [4, 0, 3]},
+        ];
+        const expectedValues = [
+          {label: 'line1', data: [0, 0, 1]},
+          {label: 'line2', data: [0, 1, 2]},
+          {label: 'line3', data: [0, 1, 0]},
+          {label: 'line4', data: [4, 0, 3]},
+        ];
+        const actualValues = MrChart.getSortedLines(arrayValues, 4);
+        for (let i = 0; i < 4; i++) {
+          assert.deepEqual(expectedValues[i], actualValues[i]);
+        }
+      });
+
+      it('return top n lines in sorted order for more than n lines',
+          () => {
+            const arrayValues = [
+              {label: 'line1', data: [0, 0, 1]},
+              {label: 'line2', data: [0, 1, 2]},
+              {label: 'line3', data: [0, 4, 0]},
+              {label: 'line4', data: [4, 0, 3]},
+              {label: 'line5', data: [0, 2, 3]},
+            ];
+            const expectedValues = [
+              {label: 'line5', data: [0, 2, 3]},
+              {label: 'line4', data: [4, 0, 3]},
+              {label: 'line2', data: [0, 1, 2]},
+            ];
+            const actualValues = MrChart.getSortedLines(arrayValues, 3);
+            for (let i = 0; i < 3; i++) {
+              assert.deepEqual(expectedValues[i], actualValues[i]);
+            }
+          });
+    });
+
+    describe('getGroupByFromQuery', () => {
+      it('get group by label object from URL', () => {
+        const input = {'groupby': 'label', 'labelprefix': 'Type'};
+
+        const expectedGroupBy = {
+          value: 'label',
+          labelPrefix: 'Type',
+          display: 'Type',
+        };
+        assert.deepEqual(MrChart.getGroupByFromQuery(input), expectedGroupBy);
+      });
+
+      it('get group by is open object from URL', () => {
+        const input = {'groupby': 'open'};
+
+        const expectedGroupBy = {value: 'open', display: 'Is open'};
+        assert.deepEqual(MrChart.getGroupByFromQuery(input), expectedGroupBy);
+      });
+
+      it('get group by none object from URL', () => {
+        const input = {'groupby': ''};
+
+        const expectedGroupBy = {value: '', display: 'None'};
+        assert.deepEqual(MrChart.getGroupByFromQuery(input), expectedGroupBy);
+      });
+
+      it('only returns valid groupBy values', () => {
+        const invalidKeys = ['pri', 'reporter', 'stars'];
+
+        const queryParams = {groupBy: ''};
+
+        invalidKeys.forEach((key) => {
+          queryParams.groupBy = key;
+          const expected = {value: '', display: 'None'};
+          const result = MrChart.getGroupByFromQuery(queryParams);
+          assert.deepEqual(result, expected);
+        });
+      });
+    });
+  });
+
+  describe('subscribedQuery', () => {
+    it('includes start and end date', () => {
+      assert.isTrue(subscribedQuery.has('start-date'));
+      assert.isTrue(subscribedQuery.has('start-date'));
+    });
+
+    it('includes groupby and labelprefix', () => {
+      assert.isTrue(subscribedQuery.has('groupby'));
+      assert.isTrue(subscribedQuery.has('labelprefix'));
+    });
+
+    it('includes q and can', () => {
+      assert.isTrue(subscribedQuery.has('q'));
+      assert.isTrue(subscribedQuery.has('can'));
+    });
+  });
+});