Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/shared/issue-fields.test.js b/static_src/shared/issue-fields.test.js
new file mode 100644
index 0000000..c37faa9
--- /dev/null
+++ b/static_src/shared/issue-fields.test.js
@@ -0,0 +1,491 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {assert} from 'chai';
+import {parseColSpec, fieldsForIssue,
+  stringValuesForIssueField} from './issue-fields.js';
+import sinon from 'sinon';
+
+let issue;
+let clock;
+
+describe('parseColSpec', () => {
+  it('empty spec produces empty list', () => {
+    assert.deepEqual(parseColSpec(),
+        []);
+    assert.deepEqual(parseColSpec(''),
+        []);
+    assert.deepEqual(parseColSpec(' + + + '),
+        []);
+    assert.deepEqual(parseColSpec('          '),
+        []);
+    assert.deepEqual(parseColSpec('+++++'),
+        []);
+  });
+
+  it('parses spec correctly', () => {
+    assert.deepEqual(parseColSpec('ID+Summary+AllLabels+Priority'),
+        ['ID', 'Summary', 'AllLabels', 'Priority']);
+  });
+
+  it('parses spaces correctly', () => {
+    assert.deepEqual(parseColSpec('ID Summary AllLabels Priority'),
+        ['ID', 'Summary', 'AllLabels', 'Priority']);
+    assert.deepEqual(parseColSpec('ID + Summary + AllLabels + Priority'),
+        ['ID', 'Summary', 'AllLabels', 'Priority']);
+    assert.deepEqual(parseColSpec('ID   Summary AllLabels     Priority'),
+        ['ID', 'Summary', 'AllLabels', 'Priority']);
+  });
+
+  it('spec parsing preserves dashed parameters', () => {
+    assert.deepEqual(parseColSpec('ID+Summary+Test-Label+Another-Label'),
+        ['ID', 'Summary', 'Test-Label', 'Another-Label']);
+  });
+});
+
+describe('fieldsForIssue', () => {
+  const issue = {
+    projectName: 'proj',
+    localId: 1,
+  };
+
+  const issueWithLabels = {
+    projectName: 'proj',
+    localId: 1,
+    labelRefs: [
+      {label: 'test'},
+      {label: 'hello-world'},
+      {label: 'multi-label-field'},
+    ],
+  };
+
+  const issueWithFieldValues = {
+    projectName: 'proj',
+    localId: 1,
+    fieldValues: [
+      {fieldRef: {fieldName: 'number', type: 'INT_TYPE'}},
+      {fieldRef: {fieldName: 'string', type: 'STR_TYPE'}},
+    ],
+  };
+
+  const issueWithPhases = {
+    projectName: 'proj',
+    localId: 1,
+    fieldValues: [
+      {fieldRef: {fieldName: 'phase-number', type: 'INT_TYPE'},
+        phaseRef: {phaseName: 'phase1'}},
+      {fieldRef: {fieldName: 'phase-string', type: 'STR_TYPE'},
+        phaseRef: {phaseName: 'phase2'}},
+    ],
+  };
+
+  const issueWithApprovals = {
+    projectName: 'proj',
+    localId: 1,
+    approvalValues: [
+      {fieldRef: {fieldName: 'approval', type: 'APPROVAL_TYPE'}},
+    ],
+  };
+
+  it('empty issue issue produces no field names', () => {
+    assert.deepEqual(fieldsForIssue(issue), []);
+    assert.deepEqual(fieldsForIssue(issue, true), []);
+  });
+
+  it('includes label prefixes', () => {
+    assert.deepEqual(fieldsForIssue(issueWithLabels), [
+      'hello',
+      'multi',
+      'multi-label',
+    ]);
+  });
+
+  it('includes field values', () => {
+    assert.deepEqual(fieldsForIssue(issueWithFieldValues), [
+      'number',
+      'string',
+    ]);
+  });
+
+  it('excludes high cardinality field values', () => {
+    assert.deepEqual(fieldsForIssue(issueWithFieldValues, true), [
+      'number',
+    ]);
+  });
+
+  it('includes phase fields', () => {
+    assert.deepEqual(fieldsForIssue(issueWithPhases), [
+      'phase1.phase-number',
+      'phase2.phase-string',
+    ]);
+  });
+
+  it('excludes high cardinality phase fields', () => {
+    assert.deepEqual(fieldsForIssue(issueWithPhases, true), [
+      'phase1.phase-number',
+    ]);
+  });
+
+  it('includes approval values', () => {
+    assert.deepEqual(fieldsForIssue(issueWithApprovals), [
+      'approval',
+      'approval-Approver',
+    ]);
+  });
+});
+
+describe('stringValuesForIssueField', () => {
+  describe('built-in fields', () => {
+    beforeEach(() => {
+      // Set clock to some specified date for relative time.
+      const initialTime = 365 * 24 * 60 * 60;
+
+      clock = sinon.useFakeTimers({
+        now: new Date(initialTime * 1000),
+        shouldAdvanceTime: false,
+      });
+
+      issue = {
+        localId: 33,
+        projectName: 'chromium',
+        summary: 'Test summary',
+        attachmentCount: 22,
+        starCount: 2,
+        componentRefs: [{path: 'Infra'}, {path: 'Monorail>UI'}],
+        blockedOnIssueRefs: [{localId: 30, projectName: 'chromium'}],
+        blockingIssueRefs: [{localId: 60, projectName: 'chromium'}],
+        labelRefs: [{label: 'Restrict-View-Google'}, {label: 'Type-Defect'}],
+        reporterRef: {displayName: 'test@example.com'},
+        ccRefs: [{displayName: 'test@example.com'}],
+        ownerRef: {displayName: 'owner@example.com'},
+        closedTimestamp: initialTime - 120, // 2 minutes ago
+        modifiedTimestamp: initialTime - 60, // a minute ago
+        openedTimestamp: initialTime - 24 * 60 * 60, // a day ago
+        componentModifiedTimestamp: initialTime - 60, // a minute ago
+        statusModifiedTimestamp: initialTime - 60, // a minute ago
+        ownerModifiedTimestamp: initialTime - 60, // a minute ago
+        statusRef: {status: 'Duplicate'},
+        mergedIntoIssueRef: {localId: 31, projectName: 'chromium'},
+      };
+    });
+
+    afterEach(() => {
+      clock.restore();
+    });
+
+    it('computes strings for ID', () => {
+      const fieldName = 'ID';
+
+      assert.deepEqual(stringValuesForIssueField(issue, fieldName),
+          ['chromium:33']);
+    });
+
+    it('computes strings for Project', () => {
+      const fieldName = 'Project';
+
+      assert.deepEqual(stringValuesForIssueField(issue, fieldName),
+          ['chromium']);
+    });
+
+    it('computes strings for Attachments', () => {
+      const fieldName = 'Attachments';
+
+      assert.deepEqual(stringValuesForIssueField(issue, fieldName),
+          ['22']);
+    });
+
+    it('computes strings for AllLabels', () => {
+      const fieldName = 'AllLabels';
+
+      assert.deepEqual(stringValuesForIssueField(issue, fieldName),
+          ['Restrict-View-Google', 'Type-Defect']);
+    });
+
+    it('computes strings for Blocked when issue is blocked', () => {
+      const fieldName = 'Blocked';
+
+      assert.deepEqual(stringValuesForIssueField(issue, fieldName),
+          ['Yes']);
+    });
+
+    it('computes strings for Blocked when issue is not blocked', () => {
+      const fieldName = 'Blocked';
+      issue.blockedOnIssueRefs = [];
+
+      assert.deepEqual(stringValuesForIssueField(issue, fieldName),
+          ['No']);
+    });
+
+    it('computes strings for BlockedOn', () => {
+      const fieldName = 'BlockedOn';
+
+      assert.deepEqual(stringValuesForIssueField(issue, fieldName),
+          ['chromium:30']);
+    });
+
+    it('computes strings for Blocking', () => {
+      const fieldName = 'Blocking';
+
+      assert.deepEqual(stringValuesForIssueField(issue, fieldName),
+          ['chromium:60']);
+    });
+
+    it('computes strings for CC', () => {
+      const fieldName = 'CC';
+
+      assert.deepEqual(stringValuesForIssueField(issue, fieldName),
+          ['test@example.com']);
+    });
+
+    it('computes strings for Closed', () => {
+      const fieldName = 'Closed';
+
+      assert.deepEqual(stringValuesForIssueField(issue, fieldName),
+          ['2 minutes ago']);
+    });
+
+    it('computes strings for Component', () => {
+      const fieldName = 'Component';
+
+      assert.deepEqual(stringValuesForIssueField(issue, fieldName),
+          ['Infra', 'Monorail>UI']);
+    });
+
+    it('computes strings for ComponentModified', () => {
+      const fieldName = 'ComponentModified';
+
+      assert.deepEqual(stringValuesForIssueField(issue, fieldName),
+          ['a minute ago']);
+    });
+
+    it('computes strings for MergedInto', () => {
+      const fieldName = 'MergedInto';
+
+      assert.deepEqual(stringValuesForIssueField(issue, fieldName),
+          ['chromium:31']);
+    });
+
+    it('computes strings for Modified', () => {
+      const fieldName = 'Modified';
+
+      assert.deepEqual(stringValuesForIssueField(issue, fieldName),
+          ['a minute ago']);
+    });
+
+    it('computes strings for Reporter', () => {
+      const fieldName = 'Reporter';
+
+      assert.deepEqual(stringValuesForIssueField(issue, fieldName),
+          ['test@example.com']);
+    });
+
+    it('computes strings for Stars', () => {
+      const fieldName = 'Stars';
+
+      assert.deepEqual(stringValuesForIssueField(issue, fieldName),
+          ['2']);
+    });
+
+    it('computes strings for Status', () => {
+      const fieldName = 'Status';
+
+      assert.deepEqual(stringValuesForIssueField(issue, fieldName),
+          ['Duplicate']);
+    });
+
+    it('computes strings for StatusModified', () => {
+      const fieldName = 'StatusModified';
+
+      assert.deepEqual(stringValuesForIssueField(issue, fieldName),
+          ['a minute ago']);
+    });
+
+    it('computes strings for Summary', () => {
+      const fieldName = 'Summary';
+
+      assert.deepEqual(stringValuesForIssueField(issue, fieldName),
+          ['Test summary']);
+    });
+
+    it('computes strings for Type', () => {
+      const fieldName = 'Type';
+
+      assert.deepEqual(stringValuesForIssueField(issue, fieldName),
+          ['Defect']);
+    });
+
+    it('computes strings for Owner', () => {
+      const fieldName = 'Owner';
+
+      assert.deepEqual(stringValuesForIssueField(issue, fieldName),
+          ['owner@example.com']);
+    });
+
+    it('computes strings for OwnerModified', () => {
+      const fieldName = 'OwnerModified';
+
+      assert.deepEqual(stringValuesForIssueField(issue, fieldName),
+          ['a minute ago']);
+    });
+
+    it('computes strings for Opened', () => {
+      const fieldName = 'Opened';
+
+      assert.deepEqual(stringValuesForIssueField(issue, fieldName),
+          ['a day ago']);
+    });
+  });
+
+  describe('custom approval fields', () => {
+    beforeEach(() => {
+      issue = {
+        localId: 33,
+        projectName: 'bird',
+        approvalValues: [
+          {fieldRef: {type: 'APPROVAL_TYPE', fieldName: 'Goose-Approval'},
+            approverRefs: []},
+          {fieldRef: {type: 'APPROVAL_TYPE', fieldName: 'Chicken-Approval'},
+            status: 'APPROVED'},
+          {fieldRef: {type: 'APPROVAL_TYPE', fieldName: 'Dodo-Approval'},
+            status: 'NEED_INFO', approverRefs: [{displayName: 'kiwi@bird.test'},
+              {displayName: 'mini-dino@bird.test'}],
+          },
+        ],
+      };
+    });
+
+    it('handles approval approver columns', () => {
+      const projectName = 'bird';
+      assert.deepEqual(stringValuesForIssueField(
+          issue, 'goose-approval-approver',
+          projectName), []);
+      assert.deepEqual(stringValuesForIssueField(
+          issue, 'chicken-approval-approver',
+          projectName), []);
+      assert.deepEqual(stringValuesForIssueField(
+          issue, 'dodo-approval-approver',
+          projectName), ['kiwi@bird.test', 'mini-dino@bird.test']);
+    });
+
+    it('handles approval value columns', () => {
+      const projectName = 'bird';
+      assert.deepEqual(stringValuesForIssueField(issue, 'goose-approval',
+          projectName), ['NotSet']);
+      assert.deepEqual(stringValuesForIssueField(issue, 'chicken-approval',
+          projectName), ['Approved']);
+      assert.deepEqual(stringValuesForIssueField(issue, 'dodo-approval',
+          projectName), ['NeedInfo']);
+    });
+  });
+
+  describe('custom fields', () => {
+    beforeEach(() => {
+      issue = {
+        localId: 33,
+        projectName: 'chromium',
+        fieldValues: [
+          {fieldRef: {type: 'STR_TYPE', fieldName: 'aString'}, value: 'test'},
+          {fieldRef: {type: 'STR_TYPE', fieldName: 'aString'}, value: 'test2'},
+          {fieldRef: {type: 'ENUM_TYPE', fieldName: 'ENUM'}, value: 'a-value'},
+          {fieldRef: {type: 'INT_TYPE', fieldId: '6', fieldName: 'Cow-Number'},
+            phaseRef: {phaseName: 'Cow-Phase'},
+            value: '55'},
+          {fieldRef: {type: 'INT_TYPE', fieldId: '6', fieldName: 'Cow-Number'},
+            phaseRef: {phaseName: 'Cow-Phase'},
+            value: '54'},
+          {fieldRef: {type: 'INT_TYPE', fieldId: '6', fieldName: 'Cow-Number'},
+            phaseRef: {phaseName: 'MilkCow-Phase'},
+            value: '56'},
+        ],
+      };
+    });
+
+    it('gets values for custom fields', () => {
+      const projectName = 'chromium';
+      assert.deepEqual(stringValuesForIssueField(issue, 'aString',
+          projectName), ['test', 'test2']);
+      assert.deepEqual(stringValuesForIssueField(issue, 'enum',
+          projectName), ['a-value']);
+      assert.deepEqual(stringValuesForIssueField(issue, 'cow-phase.cow-number',
+          projectName), ['55', '54']);
+      assert.deepEqual(stringValuesForIssueField(issue,
+          'milkcow-phase.cow-number', projectName), ['56']);
+    });
+
+    it('custom fields get precedence over label fields', () => {
+      const projectName = 'chromium';
+      issue.labelRefs = [{label: 'aString-ignore'}];
+      assert.deepEqual(stringValuesForIssueField(issue, 'aString',
+          projectName), ['test', 'test2']);
+    });
+  });
+
+  describe('label prefix fields', () => {
+    beforeEach(() => {
+      issue = {
+        localId: 33,
+        projectName: 'chromium',
+        labelRefs: [
+          {label: 'test-label'},
+          {label: 'test-label-2'},
+          {label: 'ignore-me'},
+          {label: 'Milestone-UI'},
+          {label: 'Milestone-Goodies'},
+        ],
+      };
+    });
+
+    it('gets values for label prefixes', () => {
+      const projectName = 'chromium';
+      assert.deepEqual(stringValuesForIssueField(issue, 'test',
+          projectName), ['label', 'label-2']);
+      assert.deepEqual(stringValuesForIssueField(issue, 'Milestone',
+          projectName), ['UI', 'Goodies']);
+      assert.deepEqual(stringValuesForIssueField(issue, 'ignore',
+          projectName), ['me']);
+    });
+  });
+
+  describe('composite fields', () => {
+    beforeEach(() => {
+      // Set clock to some specified date for relative time.
+      const initialTime = 365 * 24 * 60 * 60;
+
+      clock = sinon.useFakeTimers({
+        now: new Date(initialTime * 1000),
+        shouldAdvanceTime: false,
+      });
+
+      issue = {
+        localId: 33,
+        projectName: 'chromium',
+        summary: 'Test summary',
+        closedTimestamp: initialTime - 120, // 2 minutes ago
+        modifiedTimestamp: initialTime - 60, // a minute ago
+        openedTimestamp: initialTime - 24 * 60 * 60, // a day ago
+        statusModifiedTimestamp: initialTime - 60, // a minute ago
+        statusRef: {status: 'Duplicate'},
+      };
+    });
+
+    afterEach(() => {
+      clock.restore();
+    });
+
+    it('computes strings for Status/Closed', () => {
+      const fieldName = 'Status/Closed';
+
+      assert.deepEqual(stringValuesForIssueField(issue, fieldName),
+          ['Duplicate', '2 minutes ago']);
+    });
+
+    it('ignores nonexistant fields', () => {
+      const fieldName = 'Owner/Status';
+
+      assert.isFalse(issue.hasOwnProperty('ownerRef'));
+      assert.deepEqual(stringValuesForIssueField(issue, fieldName),
+          ['Duplicate']);
+    });
+  });
+});