Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/elements/issue-detail/metadata/mr-edit-metadata/mr-edit-metadata.test.js b/static_src/elements/issue-detail/metadata/mr-edit-metadata/mr-edit-metadata.test.js
new file mode 100644
index 0000000..2e4554f
--- /dev/null
+++ b/static_src/elements/issue-detail/metadata/mr-edit-metadata/mr-edit-metadata.test.js
@@ -0,0 +1,1078 @@
+// 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 sinon from 'sinon';
+import {fireEvent} from '@testing-library/react';
+
+import {MrEditMetadata} from './mr-edit-metadata.js';
+import {ISSUE_EDIT_PERMISSION, ISSUE_EDIT_SUMMARY_PERMISSION,
+  ISSUE_EDIT_STATUS_PERMISSION, ISSUE_EDIT_OWNER_PERMISSION,
+  ISSUE_EDIT_CC_PERMISSION,
+} from 'shared/consts/permissions.js';
+import {FIELD_DEF_VALUE_EDIT} from 'reducers/permissions.js';
+import {store, resetState} from 'reducers/base.js';
+import {enterInput} from 'shared/test/helpers.js';
+
+let element;
+
+xdescribe('mr-edit-metadata', () => {
+  beforeEach(() => {
+    store.dispatch(resetState());
+    element = document.createElement('mr-edit-metadata');
+    document.body.appendChild(element);
+
+    element.issuePermissions = [ISSUE_EDIT_PERMISSION];
+
+    sinon.stub(store, 'dispatch');
+  });
+
+  afterEach(() => {
+    document.body.removeChild(element);
+    store.dispatch.restore();
+  });
+
+  it('initializes', () => {
+    assert.instanceOf(element, MrEditMetadata);
+  });
+
+  describe('updated sets initial values', () => {
+    it('updates owner', async () => {
+      element.ownerName = 'goose@bird.org';
+      await element.updateComplete;
+
+      assert.equal(element._values.owner, 'goose@bird.org');
+    });
+
+    it('updates cc', async () => {
+      element.cc = [
+        {displayName: 'initial-cc@bird.org', userId: '1234'},
+      ];
+      await element.updateComplete;
+
+      assert.deepEqual(element._values.cc, ['initial-cc@bird.org']);
+    });
+
+    it('updates components', async () => {
+      element.components = [{path: 'Hello>World'}];
+
+      await element.updateComplete;
+
+      assert.deepEqual(element._values.components, ['Hello>World']);
+    });
+
+    it('updates labels', async () => {
+      element.labelNames = ['test-label'];
+
+      await element.updateComplete;
+
+      assert.deepEqual(element._values.labels, ['test-label']);
+    });
+  });
+
+  describe('saves edit form', () => {
+    let saveStub;
+
+    beforeEach(() => {
+      saveStub = sinon.stub();
+      element.addEventListener('save', saveStub);
+    });
+
+    it('saves on form submit', async () => {
+      await element.updateComplete;
+
+      element.querySelector('#editForm').dispatchEvent(
+          new Event('submit', {bubbles: true, cancelable: true}));
+
+      sinon.assert.calledOnce(saveStub);
+    });
+
+    it('saves when clicking the save button', async () => {
+      await element.updateComplete;
+
+      element.querySelector('.save-changes').click();
+
+      sinon.assert.calledOnce(saveStub);
+    });
+
+    it('does not save on random keydowns', async () => {
+      await element.updateComplete;
+
+      element.querySelector('#editForm').dispatchEvent(
+          new KeyboardEvent('keydown', {key: 'a', ctrlKey: true}));
+      element.querySelector('#editForm').dispatchEvent(
+          new KeyboardEvent('keydown', {key: 'b', ctrlKey: false}));
+      element.querySelector('#editForm').dispatchEvent(
+          new KeyboardEvent('keydown', {key: 'c', metaKey: true}));
+
+      sinon.assert.notCalled(saveStub);
+    });
+
+    it('does not save on Enter without Ctrl', async () => {
+      await element.updateComplete;
+
+      element.querySelector('#editForm').dispatchEvent(
+          new KeyboardEvent('keydown', {key: 'Enter', ctrlKey: false}));
+
+      sinon.assert.notCalled(saveStub);
+    });
+
+    it('saves on Ctrl+Enter', async () => {
+      await element.updateComplete;
+
+      element.querySelector('#editForm').dispatchEvent(
+          new KeyboardEvent('keydown', {key: 'Enter', ctrlKey: true}));
+
+      sinon.assert.calledOnce(saveStub);
+    });
+
+    it('saves on Ctrl+Meta', async () => {
+      await element.updateComplete;
+
+      element.querySelector('#editForm').dispatchEvent(
+          new KeyboardEvent('keydown', {key: 'Enter', metaKey: true}));
+
+      sinon.assert.calledOnce(saveStub);
+    });
+  });
+
+  it('disconnecting element reports form is not dirty', () => {
+    element.formName = 'test';
+
+    assert.isFalse(store.dispatch.calledOnce);
+
+    document.body.removeChild(element);
+
+    assert.isTrue(store.dispatch.calledOnce);
+    sinon.assert.calledWith(
+        store.dispatch,
+        {
+          type: 'REPORT_DIRTY_FORM',
+          name: 'test',
+          isDirty: false,
+        },
+    );
+
+    document.body.appendChild(element);
+  });
+
+  it('_processChanges fires change event', async () => {
+    await element.updateComplete;
+
+    const changeStub = sinon.stub();
+    element.addEventListener('change', changeStub);
+
+    element._processChanges();
+
+    sinon.assert.calledOnce(changeStub);
+  });
+
+  it('save button disabled when disabled is true', async () => {
+    // Check that save button is initially disabled.
+    await element.updateComplete;
+
+    const button = element.querySelector('.save-changes');
+
+    assert.isTrue(element.disabled);
+    assert.isTrue(button.disabled);
+
+    element.isDirty = true;
+
+    await element.updateComplete;
+
+    assert.isFalse(element.disabled);
+    assert.isFalse(button.disabled);
+  });
+
+  it('editing form sets isDirty to true or false', async () => {
+    await element.updateComplete;
+
+    assert.isFalse(element.isDirty);
+
+    // User makes some changes.
+    const comment = element.querySelector('#commentText');
+    comment.value = 'Value';
+    comment.dispatchEvent(new Event('keyup'));
+
+    assert.isTrue(element.isDirty);
+
+    // User undoes the changes.
+    comment.value = '';
+    comment.dispatchEvent(new Event('keyup'));
+
+    assert.isFalse(element.isDirty);
+  });
+
+  it('reseting form disables save button', async () => {
+    // Check that save button is initially disabled.
+    assert.isTrue(element.disabled);
+
+    // User makes some changes.
+    element.isDirty = true;
+
+    // Check that save button is not disabled.
+    assert.isFalse(element.disabled);
+
+    // Reset form.
+    await element.updateComplete;
+    await element.reset();
+
+    // Check that save button is still disabled.
+    assert.isTrue(element.disabled);
+  });
+
+  it('save button is enabled if request fails', async () => {
+    // Check that save button is initially disabled.
+    assert.isTrue(element.disabled);
+
+    // User makes some changes.
+    element.isDirty = true;
+
+    // Check that save button is not disabled.
+    assert.isFalse(element.disabled);
+
+    // User submits the change.
+    element.saving = true;
+
+    // Check that save button is disabled.
+    assert.isTrue(element.disabled);
+
+    // Request fails.
+    element.saving = false;
+    element.error = 'error';
+
+    // Check that save button is re-enabled.
+    assert.isFalse(element.disabled);
+  });
+
+  it('delta empty when no changes', async () => {
+    await element.updateComplete;
+    assert.deepEqual(element.delta, {});
+  });
+
+  it('toggling checkbox toggles sendEmail', async () => {
+    element.sendEmail = false;
+
+    await element.updateComplete;
+    const checkbox = element.querySelector('#sendEmail');
+
+    await checkbox.updateComplete;
+
+    checkbox.click();
+    await element.updateComplete;
+
+    assert.equal(checkbox.checked, true);
+    assert.equal(element.sendEmail, true);
+
+    checkbox.click();
+    await element.updateComplete;
+
+    assert.equal(checkbox.checked, false);
+    assert.equal(element.sendEmail, false);
+
+    checkbox.click();
+    await element.updateComplete;
+
+    assert.equal(checkbox.checked, true);
+    assert.equal(element.sendEmail, true);
+  });
+
+  it('changing status produces delta change (lit-element)', async () => {
+    element.statuses = [
+      {'status': 'New'},
+      {'status': 'Old'},
+      {'status': 'Test'},
+    ];
+    element.status = 'New';
+
+    await element.updateComplete;
+
+    const statusComponent = element.querySelector('#statusInput');
+    statusComponent.status = 'Old';
+
+    await element.updateComplete;
+
+    assert.deepEqual(element.delta, {
+      status: 'Old',
+    });
+  });
+
+  it('changing owner produces delta change (React)', async () => {
+    element.ownerName = 'initial-owner@bird.org';
+    await element.updateComplete;
+
+    const input = element.querySelector('#ownerInput');
+    enterInput(input, 'new-owner@bird.org');
+    await element.updateComplete;
+
+    const expected = {ownerRef: {displayName: 'new-owner@bird.org'}};
+    assert.deepEqual(element.delta, expected);
+  });
+
+  it('adding CC produces delta change (React)', async () => {
+    element.cc = [
+      {displayName: 'initial-cc@bird.org', userId: '1234'},
+    ];
+
+    await element.updateComplete;
+
+    const input = element.querySelector('#ccInput');
+    enterInput(input, 'another@bird.org');
+    await element.updateComplete;
+
+    const expected = {
+      ccRefsAdd: [{displayName: 'another@bird.org'}],
+      ccRefsRemove: [{displayName: 'initial-cc@bird.org'}],
+    };
+    assert.deepEqual(element.delta, expected);
+  });
+
+  it('invalid status throws', async () => {
+    element.statuses = [
+      {'status': 'New'},
+      {'status': 'Old'},
+      {'status': 'Duplicate'},
+    ];
+    element.status = 'Duplicate';
+
+    await element.updateComplete;
+
+    const statusComponent = element.querySelector('#statusInput');
+    statusComponent.shadowRoot.querySelector('#mergedIntoInput').value = 'xx';
+    assert.deepEqual(element.delta, {});
+    assert.equal(
+        element.error,
+        'Invalid issue ref: xx. Expected [projectName:]issueId.');
+  });
+
+  it('cannot block an issue on itself', async () => {
+    element.projectName = 'proj';
+    element.issueRef = {projectName: 'proj', localId: 123};
+
+    await element.updateComplete;
+
+    for (const fieldName of ['blockedOn', 'blocking']) {
+      const input =
+        element.querySelector(`#${fieldName}Input`);
+      enterInput(input, '123');
+      await element.updateComplete;
+
+      assert.deepEqual(element.delta, {});
+      assert.equal(
+          element.error,
+          `Invalid issue ref: 123. Cannot merge or block an issue on itself.`);
+      fireEvent.keyDown(input, {key: 'Backspace', code: 'Backspace'});
+      await element.updateComplete;
+
+      enterInput(input, 'proj:123');
+      await element.updateComplete;
+
+      assert.deepEqual(element.delta, {});
+      assert.equal(
+          element.error,
+          `Invalid issue ref: proj:123. ` +
+        'Cannot merge or block an issue on itself.');
+      fireEvent.keyDown(input, {key: 'Backspace', code: 'Backspace'});
+      await element.updateComplete;
+
+      enterInput(input, 'proj2:123');
+      await element.updateComplete;
+
+      assert.notDeepEqual(element.delta, {});
+      assert.equal(element.error, '');
+
+      fireEvent.keyDown(input, {key: 'Backspace', code: 'Backspace'});
+      await element.updateComplete;
+    }
+  });
+
+  it('cannot merge an issue into itself', async () => {
+    element.statuses = [
+      {'status': 'New'},
+      {'status': 'Duplicate'},
+    ];
+    element.status = 'New';
+    element.projectName = 'proj';
+    element.issueRef = {projectName: 'proj', localId: 123};
+
+    await element.updateComplete;
+
+    const statusComponent = element.querySelector('#statusInput');
+    const root = statusComponent.shadowRoot;
+    const statusInput = root.querySelector('#statusInput');
+    statusInput.value = 'Duplicate';
+    statusInput.dispatchEvent(new Event('change'));
+
+    await element.updateComplete;
+
+    root.querySelector('#mergedIntoInput').value = 'proj:123';
+    assert.deepEqual(element.delta, {});
+    assert.equal(
+        element.error,
+        `Invalid issue ref: proj:123. Cannot merge or block an issue on itself.`);
+
+    root.querySelector('#mergedIntoInput').value = '123';
+    assert.deepEqual(element.delta, {});
+    assert.equal(
+        element.error,
+        `Invalid issue ref: 123. Cannot merge or block an issue on itself.`);
+
+    root.querySelector('#mergedIntoInput').value = 'proj2:123';
+    assert.notDeepEqual(element.delta, {});
+    assert.equal(element.error, '');
+  });
+
+  it('cannot set invalid emails', async () => {
+    await element.updateComplete;
+
+    const ccInput = element.querySelector('#ccInput');
+    enterInput(ccInput, 'invalid!email');
+    await element.updateComplete;
+
+    assert.deepEqual(element.delta, {});
+    assert.equal(
+        element.error,
+        `Invalid email address: invalid!email`);
+
+    const input = element.querySelector('#ownerInput');
+    enterInput(input, 'invalid!email2');
+    await element.updateComplete;
+
+    assert.deepEqual(element.delta, {});
+    assert.equal(
+        element.error,
+        `Invalid email address: invalid!email2`);
+  });
+
+  it('can remove invalid values', async () => {
+    element.projectName = 'proj';
+    element.issueRef = {projectName: 'proj', localId: 123};
+
+    element.statuses = [
+      {'status': 'Duplicate'},
+    ];
+    element.status = 'Duplicate';
+    element.mergedInto = element.issueRef;
+
+    element.blockedOn = [element.issueRef];
+    element.blocking = [element.issueRef];
+
+    await element.updateComplete;
+
+    const blockedOnInput = element.querySelector('#blockedOnInput');
+    const blockingInput = element.querySelector('#blockingInput');
+    const statusInput = element.querySelector('#statusInput');
+
+    await element.updateComplete;
+
+    const mergedIntoInput =
+      statusInput.shadowRoot.querySelector('#mergedIntoInput');
+
+    fireEvent.keyDown(blockedOnInput, {key: 'Backspace', code: 'Backspace'});
+    await element.updateComplete;
+    fireEvent.keyDown(blockingInput, {key: 'Backspace', code: 'Backspace'});
+    await element.updateComplete;
+    mergedIntoInput.value = 'proj:124';
+    await element.updateComplete;
+
+    assert.deepEqual(
+        element.delta,
+        {
+          blockedOnRefsRemove: [{projectName: 'proj', localId: 123}],
+          blockingRefsRemove: [{projectName: 'proj', localId: 123}],
+          mergedIntoRef: {projectName: 'proj', localId: 124},
+        });
+    assert.equal(element.error, '');
+  });
+
+  it('not changing status produces no delta', async () => {
+    element.statuses = [
+      {'status': 'Duplicate'},
+    ];
+    element.status = 'Duplicate';
+
+    element.mergedInto = {
+      projectName: 'chromium',
+      localId: 1234,
+    };
+
+    element.projectName = 'chromium';
+
+    await element.updateComplete;
+    await element.updateComplete; // Merged input updates its value.
+
+    assert.deepEqual(element.delta, {});
+  });
+
+  it('changing status to duplicate produces delta change', async () => {
+    element.statuses = [
+      {'status': 'New'},
+      {'status': 'Duplicate'},
+    ];
+    element.status = 'New';
+
+    await element.updateComplete;
+
+    const statusComponent = element.querySelector(
+        '#statusInput');
+    const root = statusComponent.shadowRoot;
+    const statusInput = root.querySelector('#statusInput');
+    statusInput.value = 'Duplicate';
+    statusInput.dispatchEvent(new Event('change'));
+
+    await element.updateComplete;
+
+    root.querySelector('#mergedIntoInput').value = 'chromium:1234';
+    assert.deepEqual(element.delta, {
+      status: 'Duplicate',
+      mergedIntoRef: {
+        projectName: 'chromium',
+        localId: 1234,
+      },
+    });
+  });
+
+  it('changing summary produces delta change', async () => {
+    element.summary = 'Old summary';
+
+    await element.updateComplete;
+
+    element.querySelector(
+        '#summaryInput').value = 'newfangled fancy summary';
+    assert.deepEqual(element.delta, {
+      summary: 'newfangled fancy summary',
+    });
+  });
+
+  it('custom fields the user cannot edit should be hidden', async () => {
+    element.projectName = 'proj';
+    const fieldName = 'projects/proj/fieldDefs/1';
+    const restrictedFieldName = 'projects/proj/fieldDefs/2';
+    element._permissions = {
+      [fieldName]: {permissions: [FIELD_DEF_VALUE_EDIT]},
+      [restrictedFieldName]: {permissions: []}};
+    element.fieldDefs = [
+      {
+        fieldRef: {
+          fieldName: 'normalFd',
+          fieldId: 1,
+          type: 'ENUM_TYPE',
+        },
+      },
+      {
+        fieldRef: {
+          fieldName: 'cantEditFd',
+          fieldId: 2,
+          type: 'ENUM_TYPE',
+        },
+      },
+    ];
+
+    await element.updateComplete;
+    assert.isFalse(element.querySelector('#normalFdInput').hidden);
+    assert.isTrue(element.querySelector('#cantEditFdInput').hidden);
+  });
+
+  it('changing enum custom fields produces delta', async () => {
+    element.fieldValueMap = new Map([['fakefield', ['prev value']]]);
+    element.fieldDefs = [
+      {
+        fieldRef: {
+          fieldName: 'testField',
+          fieldId: 1,
+          type: 'ENUM_TYPE',
+        },
+      },
+      {
+        fieldRef: {
+          fieldName: 'fakeField',
+          fieldId: 2,
+          type: 'ENUM_TYPE',
+        },
+      },
+    ];
+
+    await element.updateComplete;
+
+    const input1 = element.querySelector('#testFieldInput');
+    const input2 = element.querySelector('#fakeFieldInput');
+
+    input1.values = ['test value'];
+    input2.values = [];
+
+    await element.updateComplete;
+
+    assert.deepEqual(element.delta, {
+      fieldValsAdd: [
+        {
+          fieldRef: {
+            fieldName: 'testField',
+            fieldId: 1,
+            type: 'ENUM_TYPE',
+          },
+          value: 'test value',
+        },
+      ],
+      fieldValsRemove: [
+        {
+          fieldRef: {
+            fieldName: 'fakeField',
+            fieldId: 2,
+            type: 'ENUM_TYPE',
+          },
+          value: 'prev value',
+        },
+      ],
+    });
+  });
+
+  it('changing approvers produces delta', async () => {
+    element.isApproval = true;
+    element.hasApproverPrivileges = true;
+    element.approvers = [
+      {displayName: 'foo@example.com', userId: '1'},
+      {displayName: 'bar@example.com', userId: '2'},
+      {displayName: 'baz@example.com', userId: '3'},
+    ];
+
+    await element.updateComplete;
+    await element.updateComplete;
+
+    element.querySelector('#approversInput').values =
+        ['chicken@example.com', 'foo@example.com', 'dog@example.com'];
+
+    await element.updateComplete;
+
+    assert.deepEqual(element.delta, {
+      approverRefsAdd: [
+        {displayName: 'chicken@example.com'},
+        {displayName: 'dog@example.com'},
+      ],
+      approverRefsRemove: [
+        {displayName: 'bar@example.com'},
+        {displayName: 'baz@example.com'},
+      ],
+    });
+  });
+
+  it('changing blockedon produces delta change (React)', async () => {
+    element.blockedOn = [
+      {projectName: 'chromium', localId: '1234'},
+      {projectName: 'monorail', localId: '4567'},
+    ];
+    element.projectName = 'chromium';
+
+    await element.updateComplete;
+    await element.updateComplete;
+
+    const input = element.querySelector('#blockedOnInput');
+
+    fireEvent.keyDown(input, {key: 'Backspace', code: 'Backspace'});
+    await element.updateComplete;
+
+    enterInput(input, 'v8:5678');
+    await element.updateComplete;
+
+    assert.deepEqual(element.delta, {
+      blockedOnRefsAdd: [{
+        projectName: 'v8',
+        localId: 5678,
+      }],
+      blockedOnRefsRemove: [{
+        projectName: 'monorail',
+        localId: 4567,
+      }],
+    });
+  });
+
+  it('_optionsForField computes options', () => {
+    const optionsPerEnumField = new Map([
+      ['enumfield', [{optionName: 'one'}, {optionName: 'two'}]],
+    ]);
+    assert.deepEqual(
+        element._optionsForField(optionsPerEnumField, new Map(), 'enumField'), [
+          {
+            optionName: 'one',
+          },
+          {
+            optionName: 'two',
+          },
+        ]);
+  });
+
+  it('changing enum fields produces delta', async () => {
+    element.fieldDefs = [
+      {
+        fieldRef: {
+          fieldName: 'enumField',
+          fieldId: 1,
+          type: 'ENUM_TYPE',
+        },
+        isMultivalued: true,
+      },
+    ];
+
+    element.optionsPerEnumField = new Map([
+      ['enumfield', [{optionName: 'one'}, {optionName: 'two'}]],
+    ]);
+
+    await element.updateComplete;
+    await element.updateComplete;
+
+    element.querySelector(
+        '#enumFieldInput').values = ['one', 'two'];
+
+    await element.updateComplete;
+
+    assert.deepEqual(element.delta, {
+      fieldValsAdd: [
+        {
+          fieldRef: {
+            fieldName: 'enumField',
+            fieldId: 1,
+            type: 'ENUM_TYPE',
+          },
+          value: 'one',
+        },
+        {
+          fieldRef: {
+            fieldName: 'enumField',
+            fieldId: 1,
+            type: 'ENUM_TYPE',
+          },
+          value: 'two',
+        },
+      ],
+    });
+  });
+
+  it('changing multiple single valued enum fields', async () => {
+    element.fieldDefs = [
+      {
+        fieldRef: {
+          fieldName: 'enumField',
+          fieldId: 1,
+          type: 'ENUM_TYPE',
+        },
+      },
+      {
+        fieldRef: {
+          fieldName: 'enumField2',
+          fieldId: 2,
+          type: 'ENUM_TYPE',
+        },
+      },
+    ];
+
+    element.optionsPerEnumField = new Map([
+      ['enumfield', [{optionName: 'one'}, {optionName: 'two'}]],
+      ['enumfield2', [{optionName: 'three'}, {optionName: 'four'}]],
+    ]);
+
+    await element.updateComplete;
+
+    element.querySelector('#enumFieldInput').values = ['two'];
+    element.querySelector('#enumField2Input').values = ['three'];
+
+    await element.updateComplete;
+
+    assert.deepEqual(element.delta, {
+      fieldValsAdd: [
+        {
+          fieldRef: {
+            fieldName: 'enumField',
+            fieldId: 1,
+            type: 'ENUM_TYPE',
+          },
+          value: 'two',
+        },
+        {
+          fieldRef: {
+            fieldName: 'enumField2',
+            fieldId: 2,
+            type: 'ENUM_TYPE',
+          },
+          value: 'three',
+        },
+      ],
+    });
+  });
+
+  it('adding components produces delta', async () => {
+    await element.updateComplete;
+
+    element.isApproval = false;
+    element.issuePermissions = [ISSUE_EDIT_PERMISSION];
+
+    element.components = [];
+
+    await element.updateComplete;
+
+    element._values.components = ['Hello>World'];
+
+    await element.updateComplete;
+
+    assert.deepEqual(element.delta, {
+      compRefsAdd: [
+        {path: 'Hello>World'},
+      ],
+    });
+
+    element._values.components = ['Hello>World', 'Test', 'Multi'];
+
+    await element.updateComplete;
+
+    assert.deepEqual(element.delta, {
+      compRefsAdd: [
+        {path: 'Hello>World'},
+        {path: 'Test'},
+        {path: 'Multi'},
+      ],
+    });
+
+    element._values.components = [];
+
+    await element.updateComplete;
+
+    assert.deepEqual(element.delta, {});
+  });
+
+  it('removing components produces delta', async () => {
+    await element.updateComplete;
+
+    element.isApproval = false;
+    element.issuePermissions = [ISSUE_EDIT_PERMISSION];
+
+    element.components = [{path: 'Hello>World'}];
+
+    await element.updateComplete;
+
+    element._values.components = [];
+
+    await element.updateComplete;
+
+    assert.deepEqual(element.delta, {
+      compRefsRemove: [
+        {path: 'Hello>World'},
+      ],
+    });
+  });
+
+  it('approver input appears when user has privileges', async () => {
+    assert.isNull(element.querySelector('#approversInput'));
+    element.isApproval = true;
+    element.hasApproverPrivileges = true;
+
+    await element.updateComplete;
+
+    assert.isNotNull(element.querySelector('#approversInput'));
+  });
+
+  it('reset sets controlled values to default', async () => {
+    element.ownerName = 'burb@bird.com';
+    element.cc = [
+      {displayName: 'flamingo@bird.com', userId: '1234'},
+      {displayName: 'penguin@bird.com', userId: '5678'},
+    ];
+    element.components = [{path: 'Bird>Penguin'}];
+    element.labelNames = ['chickadee-chirp'];
+    element.blockedOn = [{localId: 1234, projectName: 'project'}];
+    element.blocking = [{localId: 5678, projectName: 'other-project'}];
+    element.projectName = 'project';
+
+    // Update cycle is needed because <mr-edit-metadata> initializes
+    // this.values in updated().
+    await element.updateComplete;
+
+    const initialValues = {
+      owner: 'burb@bird.com',
+      cc: ['flamingo@bird.com', 'penguin@bird.com'],
+      components: ['Bird>Penguin'],
+      labels: ['chickadee-chirp'],
+      blockedOn: ['1234'],
+      blocking: ['other-project:5678'],
+    };
+
+    assert.deepEqual(element._values, initialValues);
+
+    element._values = {
+      owner: 'newburb@hello.com',
+      cc: ['noburbs@wings.com'],
+    };
+    element.reset();
+
+    assert.deepEqual(element._values, initialValues);
+  })
+
+  it('reset empties form values', async () => {
+    element.fieldDefs = [
+      {
+        fieldRef: {
+          fieldName: 'testField',
+          fieldId: 1,
+          type: 'ENUM_TYPE',
+        },
+      },
+      {
+        fieldRef: {
+          fieldName: 'fakeField',
+          fieldId: 2,
+          type: 'ENUM_TYPE',
+        },
+      },
+    ];
+
+    await element.updateComplete;
+
+    const uploader = element.querySelector('mr-upload');
+    uploader.files = [
+      {name: 'test.png'},
+      {name: 'rutabaga.png'},
+    ];
+
+    element.querySelector('#testFieldInput').values = 'testy test';
+    element.querySelector('#fakeFieldInput').values = 'hello world';
+
+    await element.reset();
+
+    assert.lengthOf(element.querySelector('#testFieldInput').value, 0);
+    assert.lengthOf(element.querySelector('#fakeFieldInput').value, 0);
+    assert.lengthOf(uploader.files, 0);
+  });
+
+  it('reset results in empty delta', async () => {
+    element.ownerName = 'goose@bird.org';
+    await element.updateComplete;
+
+    element._values.owner = 'penguin@bird.org';
+    const expected = {ownerRef: {displayName: 'penguin@bird.org'}};
+    assert.deepEqual(element.delta, expected);
+
+    await element.reset();
+    assert.deepEqual(element.delta, {});
+  });
+
+  it('edit issue permissions', async () => {
+    const allFields = ['summary', 'status', 'owner', 'cc'];
+    const testCases = [
+      {permissions: [], nonNull: []},
+      {permissions: [ISSUE_EDIT_PERMISSION], nonNull: allFields},
+      {permissions: [ISSUE_EDIT_SUMMARY_PERMISSION], nonNull: ['summary']},
+      {permissions: [ISSUE_EDIT_STATUS_PERMISSION], nonNull: ['status']},
+      {permissions: [ISSUE_EDIT_OWNER_PERMISSION], nonNull: ['owner']},
+      {permissions: [ISSUE_EDIT_CC_PERMISSION], nonNull: ['cc']},
+    ];
+    element.statuses = [{'status': 'Foo'}];
+
+    for (const testCase of testCases) {
+      element.issuePermissions = testCase.permissions;
+      await element.updateComplete;
+
+      allFields.forEach((fieldName) => {
+        const field = element.querySelector(`#${fieldName}Input`);
+        if (testCase.nonNull.includes(fieldName)) {
+          assert.isNotNull(field);
+        } else {
+          assert.isNull(field);
+        }
+      });
+    }
+  });
+
+  it('duplicate issue is rendered correctly', async () => {
+    element.statuses = [
+      {'status': 'Duplicate'},
+    ];
+    element.status = 'Duplicate';
+    element.projectName = 'chromium';
+    element.mergedInto = {
+      projectName: 'chromium',
+      localId: 1234,
+    };
+
+    await element.updateComplete;
+    await element.updateComplete;
+
+    const statusComponent = element.querySelector('#statusInput');
+    const root = statusComponent.shadowRoot;
+    assert.equal(
+        root.querySelector('#mergedIntoInput').value, '1234');
+  });
+
+  it('duplicate issue on different project is rendered correctly', async () => {
+    element.statuses = [
+      {'status': 'Duplicate'},
+    ];
+    element.status = 'Duplicate';
+    element.projectName = 'chromium';
+    element.mergedInto = {
+      projectName: 'monorail',
+      localId: 1234,
+    };
+
+    await element.updateComplete;
+    await element.updateComplete;
+
+    const statusComponent = element.querySelector('#statusInput');
+    const root = statusComponent.shadowRoot;
+    assert.equal(
+        root.querySelector('#mergedIntoInput').value, 'monorail:1234');
+  });
+
+  it('filter out deleted users', async () => {
+    element.cc = [
+      {displayName: 'test@example.com', userId: '1234'},
+      {displayName: 'a_deleted_user'},
+      {displayName: 'someone@example.com', userId: '5678'},
+    ];
+
+    await element.updateComplete;
+
+    assert.deepEqual(element._values.cc, [
+      'test@example.com',
+      'someone@example.com',
+    ]);
+  });
+
+  it('renders valid markdown description with preview', async () => {
+    await element.updateComplete;
+
+    element.prefs = new Map([['render_markdown', true]]);
+    element.projectName = 'monkeyrail';
+    sinon.stub(element, 'getCommentContent').returns('# h1');
+
+    await element.updateComplete;
+
+    assert.isTrue(element._renderMarkdown);
+
+    const previewMarkdown = element.querySelector('.markdown-preview');
+    assert.isNotNull(previewMarkdown);
+
+    const headerText = previewMarkdown.querySelector('h1').textContent;
+    assert.equal(headerText, 'h1');
+  });
+
+  it('does not show preview when markdown is disabled', async () => {
+    element.prefs = new Map([['render_markdown', false]]);
+    element.projectName = 'monkeyrail';
+    sinon.stub(element, 'getCommentContent').returns('# h1');
+
+    await element.updateComplete;
+
+    const previewMarkdown = element.querySelector('.markdown-preview');
+    assert.isNull(previewMarkdown);
+  });
+
+  it('does not show preview when no input', async () => {
+    element.prefs = new Map([['render_markdown', true]]);
+    element.projectName = 'monkeyrail';
+    sinon.stub(element, 'getCommentContent').returns('');
+
+    await element.updateComplete;
+
+    const previewMarkdown = element.querySelector('.markdown-preview');
+    assert.isNull(previewMarkdown);
+  });
+});
+