Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/elements/issue-detail/metadata/mr-edit-field/mr-edit-field.js b/static_src/elements/issue-detail/metadata/mr-edit-field/mr-edit-field.js
new file mode 100644
index 0000000..18bd963
--- /dev/null
+++ b/static_src/elements/issue-detail/metadata/mr-edit-field/mr-edit-field.js
@@ -0,0 +1,288 @@
+// 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 {LitElement, html} from 'lit-element';
+
+import deepEqual from 'deep-equal';
+import {fieldTypes, EMPTY_FIELD_VALUE} from 'shared/issue-fields.js';
+import {arrayDifference, equalsIgnoreCase} from 'shared/helpers.js';
+import {NON_EDITING_KEY_EVENTS} from 'shared/dom-helpers.js';
+
+import './mr-multi-checkbox.js';
+import 'react/mr-react-autocomplete.tsx';
+
+const AUTOCOMPLETE_INPUT = 'AUTOCOMPLETE_INPUT';
+const CHECKBOX_INPUT = 'CHECKBOX_INPUT';
+const SELECT_INPUT = 'SELECT_INPUT';
+
+/**
+ * `<mr-edit-field>`
+ *
+ * A single edit input for a fieldDef + the values of the field.
+ *
+ */
+export class MrEditField extends LitElement {
+  /** @override */
+  createRenderRoot() {
+    return this;
+  }
+
+  /** @override */
+  render() {
+    return html`
+      <link href="https://fonts.googleapis.com/icon?family=Material+Icons"
+            rel="stylesheet">
+      <style>
+        mr-edit-field {
+          display: block;
+        }
+        mr-edit-field[hidden] {
+          display: none;
+        }
+        mr-edit-field input,
+        mr-edit-field select {
+          width: var(--mr-edit-field-width);
+          padding: var(--mr-edit-field-padding);
+        }
+      </style>
+      ${this._renderInput()}
+    `;
+  }
+
+  /**
+   * Renders a single input field.
+   * @return {TemplateResult}
+   */
+  _renderInput() {
+    switch (this._widgetType) {
+      case CHECKBOX_INPUT:
+        return html`
+          <mr-multi-checkbox
+            .options=${this.options}
+            .values=${[...this.values]}
+            @change=${this._changeHandler}
+          ></mr-multi-checkbox>
+        `;
+      case SELECT_INPUT:
+        return html`
+          <select
+            id="${this.label}"
+            class="editSelect"
+            aria-label=${this.name}
+            @change=${this._changeHandler}
+          >
+            <option value="">${EMPTY_FIELD_VALUE}</option>
+            ${this.options.map((option) => html`
+              <option
+                value=${option.optionName}
+                .selected=${this.value === option.optionName}
+              >
+                ${option.optionName}
+                ${option.docstring ? ' = ' + option.docstring : ''}
+              </option>
+            `)}
+          </select>
+        `;
+      case AUTOCOMPLETE_INPUT:
+        return html`
+          <mr-react-autocomplete
+            .label=${this.label}
+            .vocabularyName=${this.acType || ''}
+            .inputType=${this._html5InputType}
+            .fixedValues=${this.derivedValues}
+            .value=${this.multi ? this.values : this.value}
+            .multiple=${this.multi}
+            .onChange=${this._changeHandlerReact.bind(this)}
+          ></mr-react-autocomplete>
+        `;
+      default:
+        return '';
+    }
+  }
+
+
+  /** @override */
+  static get properties() {
+    return {
+      // TODO(zhangtiff): Redesign this a bit so we don't need two separate
+      // ways of specifying "type" for a field. Right now, "type" is mapped to
+      // the Monorail custom field types whereas "acType" includes additional
+      // data types such as components, and labels.
+      // String specifying what kind of autocomplete to add to this field.
+      acType: {type: String},
+      // "type" is based on the various custom field types available in
+      // Monorail.
+      type: {type: String},
+      label: {type: String},
+      multi: {type: Boolean},
+      name: {type: String},
+      // Only used for basic, non-repeated fields.
+      placeholder: {type: String},
+      initialValues: {
+        type: Array,
+        hasChanged(newVal, oldVal) {
+          // Prevent extra recomputations of the same initial value causing
+          // values to be reset.
+          return !deepEqual(newVal, oldVal);
+        },
+      },
+      // The current user-inputted values for a field.
+      values: {type: Array},
+      derivedValues: {type: Array},
+      // For enum fields, the possible options that you have. Each entry is a
+      // label type with an additional optionName field added.
+      options: {type: Array},
+    };
+  }
+
+  /** @override */
+  constructor() {
+    super();
+    this.initialValues = [];
+    this.values = [];
+    this.derivedValues = [];
+    this.options = [];
+    this.multi = false;
+
+    this.actType = '';
+    this.placeholder = '';
+    this.type = '';
+  }
+
+  /** @override */
+  update(changedProperties) {
+    if (changedProperties.has('initialValues')) {
+      // Assume we always want to reset the user's input when initial
+      // values change.
+      this.reset();
+    }
+    super.update(changedProperties);
+  }
+
+  /**
+   * @return {string}
+   */
+  get value() {
+    return _getSingleValue(this.values);
+  }
+
+  /**
+   * @return {string}
+   */
+  get _widgetType() {
+    const type = this.type;
+    const multi = this.multi;
+    if (type === fieldTypes.ENUM_TYPE) {
+      if (multi) {
+        return CHECKBOX_INPUT;
+      }
+      return SELECT_INPUT;
+    } else {
+      return AUTOCOMPLETE_INPUT;
+    }
+  }
+
+  /**
+   * @return {string} HTML type for the input.
+   */
+  get _html5InputType() {
+    const type = this.type;
+    if (type === fieldTypes.INT_TYPE) {
+      return 'number';
+    } else if (type === fieldTypes.DATE_TYPE) {
+      return 'date';
+    }
+    return 'text';
+  }
+
+  /**
+   * Reset form values to initial state.
+   */
+  reset() {
+    this.values = _wrapInArray(this.initialValues);
+  }
+
+  /**
+   * Return the values that the user added to this input.
+   * @return {Array<string>}åß
+   */
+  getValuesAdded() {
+    if (!this.values || !this.values.length) return [];
+    return arrayDifference(
+        this.values, this.initialValues, equalsIgnoreCase);
+  }
+
+  /**
+   * Return the values that the userremoved from this input.
+   * @return {Array<string>}
+   */
+  getValuesRemoved() {
+    if (!this.multi && (!this.values || this.values.length > 0)) return [];
+    return arrayDifference(
+        this.initialValues, this.values, equalsIgnoreCase);
+  }
+
+  /**
+   * Syncs form values and fires a change event as the user edits the form.
+   * @param {Event} e
+   * @fires Event#change
+   * @private
+   */
+  _changeHandler(e) {
+    if (e instanceof KeyboardEvent) {
+      if (NON_EDITING_KEY_EVENTS.has(e.key)) return;
+    }
+    const input = e.target;
+
+    if (input.getValues) {
+      // <mr-multi-checkbox> support.
+      this.values = input.getValues();
+    } else {
+      // Is a native input element.
+      const value = input.value.trim();
+      this.values = _wrapInArray(value);
+    }
+
+    this.dispatchEvent(new Event('change'));
+  }
+
+  /**
+   * Syncs form values and fires a change event as the user edits the form.
+   * @param {React.SyntheticEvent} _e
+   * @param {string|Array<string>|null} value React autcoomplete form value.
+   * @fires Event#change
+   * @private
+   */
+  _changeHandlerReact(_e, value) {
+    this.values = _wrapInArray(value);
+
+    this.dispatchEvent(new Event('change'));
+  }
+}
+
+/**
+ * Returns the string value for a single field.
+ * @param {Array<string>} arr
+ * @return {string}
+ */
+function _getSingleValue(arr) {
+  return (arr && arr.length) ? arr[0] : '';
+}
+
+/**
+ * Returns the string value for a single field.
+ * @param {Array<string>|string} v
+ * @return {string}
+ */
+function _wrapInArray(v) {
+  if (!v) return [];
+
+  let values = v;
+  if (!Array.isArray(v)) {
+    values = !!v ? [v] : [];
+  }
+  return [...values];
+}
+
+customElements.define('mr-edit-field', MrEditField);
diff --git a/static_src/elements/issue-detail/metadata/mr-edit-field/mr-edit-field.test.js b/static_src/elements/issue-detail/metadata/mr-edit-field/mr-edit-field.test.js
new file mode 100644
index 0000000..a718203
--- /dev/null
+++ b/static_src/elements/issue-detail/metadata/mr-edit-field/mr-edit-field.test.js
@@ -0,0 +1,215 @@
+// 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 userEvent from '@testing-library/user-event';
+
+import {MrEditField} from './mr-edit-field.js';
+import {fieldTypes} from 'shared/issue-fields.js';
+
+import {enterInput} from 'shared/test/helpers.js';
+
+
+let element;
+let input;
+
+xdescribe('mr-edit-field', () => {
+  beforeEach(async () => {
+    element = document.createElement('mr-edit-field');
+    document.body.appendChild(element);
+
+    element.label = 'testInput';
+    await element.updateComplete;
+
+    input = element.querySelector('#testInput');
+  });
+
+  afterEach(async () => {
+    userEvent.clear(input);
+
+    document.body.removeChild(element);
+  });
+
+  it('initializes', () => {
+    assert.instanceOf(element, MrEditField);
+  });
+
+  it('reset input value', async () => {
+    element.initialValues = [];
+    await element.updateComplete;
+
+    enterInput(input, 'jackalope');
+    await element.updateComplete;
+
+    assert.equal(element.value, 'jackalope');
+
+    element.reset();
+    await element.updateComplete;
+
+    assert.equal(element.value, '');
+  });
+
+  it('input updates when initialValues change', async () => {
+    element.initialValues = ['hello'];
+
+    await element.updateComplete;
+
+    assert.equal(element.value, 'hello');
+  });
+
+  it('initial value does not change after value set', async () => {
+    element.initialValues = ['hello'];
+    element.label = 'testInput';
+    await element.updateComplete;
+
+    input = element.querySelector('#testInput');
+
+    enterInput(input, 'world');
+    await element.updateComplete;
+
+    assert.deepEqual(element.initialValues, ['hello']);
+    assert.equal(element.value, 'world');
+  });
+
+  it('value updates when input is updated', async () => {
+    element.initialValues = ['hello'];
+    await element.updateComplete;
+
+    enterInput(input, 'world');
+    await element.updateComplete;
+
+    assert.equal(element.value, 'world');
+  });
+
+  it('initial value does not change after user input', async () => {
+    element.initialValues = ['hello'];
+    await element.updateComplete;
+
+    enterInput(input, 'jackalope');
+    await element.updateComplete;
+
+    assert.deepEqual(element.initialValues, ['hello']);
+    assert.equal(element.value, 'jackalope');
+  });
+
+  it('get value after user input', async () => {
+    element.initialValues = ['hello'];
+    await element.updateComplete;
+
+    enterInput(input, 'jackalope');
+    await element.updateComplete;
+
+    assert.equal(element.value, 'jackalope');
+  });
+
+  it('input value was added', async () => {
+    // Simulate user input.
+    await element.updateComplete;
+
+    enterInput(input, 'jackalope');
+    await element.updateComplete;
+
+    assert.deepEqual(element.getValuesAdded(), ['jackalope']);
+    assert.deepEqual(element.getValuesRemoved(), []);
+  });
+
+  it('input value was removed', async () => {
+    await element.updateComplete;
+
+    element.initialValues = ['hello'];
+    await element.updateComplete;
+
+    enterInput(input, '');
+    await element.updateComplete;
+
+    assert.deepEqual(element.getValuesAdded(), []);
+    assert.deepEqual(element.getValuesRemoved(), ['hello']);
+  });
+
+  it('input value was changed', async () => {
+    element.initialValues = ['hello'];
+    await element.updateComplete;
+
+    enterInput(input, 'world');
+    await element.updateComplete;
+
+    assert.deepEqual(element.getValuesAdded(), ['world']);
+  });
+
+  it('edit select updates value when initialValues change', async () => {
+    element.multi = false;
+    element.type = fieldTypes.ENUM_TYPE;
+
+    element.options = [
+      {optionName: 'hello'},
+      {optionName: 'jackalope'},
+      {optionName: 'text'},
+    ];
+
+    element.initialValues = ['hello'];
+
+    await element.updateComplete;
+
+    assert.equal(element.value, 'hello');
+
+    const select = element.querySelector('select');
+    userEvent.selectOptions(select, 'jackalope');
+
+    // User input should not be overridden by the initialValue variable.
+    assert.equal(element.value, 'jackalope');
+    // Initial values should not change based on user input.
+    assert.deepEqual(element.initialValues, ['hello']);
+
+    element.initialValues = ['text'];
+    await element.updateComplete;
+
+    assert.equal(element.value, 'text');
+
+    element.initialValues = [];
+    await element.updateComplete;
+
+    assert.deepEqual(element.value, '');
+  });
+
+  it('multi enum updates value on reset', async () => {
+    element.multi = true;
+    element.type = fieldTypes.ENUM_TYPE;
+    element.options = [
+      {optionName: 'hello'},
+      {optionName: 'world'},
+      {optionName: 'fake'},
+    ];
+
+    await element.updateComplete;
+
+    element.initialValues = ['hello'];
+    element.reset();
+    await element.updateComplete;
+
+    assert.deepEqual(element.values, ['hello']);
+
+    const checkboxes = element.querySelector('mr-multi-checkbox');
+
+    // User checks all boxes.
+    checkboxes._inputRefs.forEach(
+        (checkbox) => {
+          checkbox.checked = true;
+        },
+    );
+    checkboxes._changeHandler();
+
+    await element.updateComplete;
+
+    // User input should not be overridden by the initialValues variable.
+    assert.deepEqual(element.values, ['hello', 'world', 'fake']);
+    // Initial values should not change based on user input.
+    assert.deepEqual(element.initialValues, ['hello']);
+
+    element.initialValues = ['hello', 'world'];
+    element.reset();
+    await element.updateComplete;
+
+    assert.deepEqual(element.values, ['hello', 'world']);
+  });
+});
diff --git a/static_src/elements/issue-detail/metadata/mr-edit-field/mr-edit-status.js b/static_src/elements/issue-detail/metadata/mr-edit-field/mr-edit-status.js
new file mode 100644
index 0000000..5303c57
--- /dev/null
+++ b/static_src/elements/issue-detail/metadata/mr-edit-field/mr-edit-status.js
@@ -0,0 +1,183 @@
+// 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 {LitElement, html, css} from 'lit-element';
+
+import {SHARED_STYLES} from 'shared/shared-styles';
+import './mr-edit-field.js';
+
+/**
+ * `<mr-edit-status>`
+ *
+ * Editing form for either an approval or the overall issue.
+ *
+ */
+export class MrEditStatus extends LitElement {
+  /** @override */
+  static get styles() {
+    return [
+      SHARED_STYLES,
+      css`
+        :host {
+          width: 100%;
+        }
+        select {
+          width: var(--mr-edit-field-width);
+          padding: var(--mr-edit-field-padding);
+        }
+        .grid-input {
+          margin-top: 8px;
+          display: grid;
+          grid-gap: var(--mr-input-grid-gap);
+          grid-template-columns: auto 1fr;
+        }
+        .grid-input[hidden] {
+          display: none;
+        }
+        label {
+          font-weight: bold;
+          word-wrap: break-word;
+          text-align: left;
+        }
+        #mergedIntoInput {
+          width: 160px;
+        }
+      `,
+    ];
+  }
+
+  /** @override */
+  render() {
+    return html`
+      <select
+        @change=${this._selectChangeHandler}
+        aria-label="Status"
+        id="statusInput"
+      >
+        ${this._statusesGrouped.map((group) => html`
+          <optgroup label=${group.name} ?hidden=${!group.name}>
+            ${group.statuses.map((item) => html`
+              <option
+                value=${item.status}
+                .selected=${this.status === item.status}
+              >
+                ${item.status}
+                ${item.docstring ? `= ${item.docstring}` : ''}
+              </option>
+            `)}
+          </optgroup>
+
+          ${!group.name ? html`
+            ${group.statuses.map((item) => html`
+              <option
+                value=${item.status}
+                .selected=${this.status === item.status}
+              >
+                ${item.status}
+                ${item.docstring ? `= ${item.docstring}` : ''}
+              </option>
+            `)}
+          ` : ''}
+        `)}
+      </select>
+
+      <div class="grid-input" ?hidden=${!this._showMergedInto}>
+        <label for="mergedIntoInput" id="mergedIntoLabel">Merged into:</label>
+        <input
+          id="mergedIntoInput"
+          value=${this.mergedInto || ''}
+          @change=${this._changeHandler}
+        ></input>
+      </div>`;
+  }
+
+  /** @override */
+  static get properties() {
+    return {
+      initialStatus: {type: String},
+      status: {type: String},
+      statuses: {type: Array},
+      isApproval: {type: Boolean},
+      mergedInto: {type: String},
+    };
+  }
+
+  /** @override */
+  update(changedProperties) {
+    if (changedProperties.has('initialStatus')) {
+      this.status = this.initialStatus;
+    }
+    super.update(changedProperties);
+  }
+
+  get _showMergedInto() {
+    const status = this.status || this.initialStatus;
+    return (status === 'Duplicate');
+  }
+
+  get _statusesGrouped() {
+    const statuses = this.statuses;
+    const isApproval = this.isApproval;
+    if (!statuses) return [];
+    if (isApproval) {
+      return [{statuses: statuses}];
+    }
+    return [
+      {
+        name: 'Open',
+        statuses: statuses.filter((s) => s.meansOpen),
+      },
+      {
+        name: 'Closed',
+        statuses: statuses.filter((s) => !s.meansOpen),
+      },
+    ];
+  }
+
+  async reset() {
+    await this.updateComplete;
+    const mergedIntoInput = this.shadowRoot.querySelector('#mergedIntoInput');
+    if (mergedIntoInput) {
+      mergedIntoInput.value = this.mergedInto || '';
+    }
+    this.status = this.initialStatus;
+  }
+
+  get delta() {
+    const result = {};
+
+    if (this.status !== this.initialStatus) {
+      result['status'] = this.status;
+    }
+
+    if (this._showMergedInto) {
+      const newMergedInto = this.shadowRoot.querySelector(
+          '#mergedIntoInput').value;
+      if (newMergedInto !== this.mergedInto) {
+        result['mergedInto'] = newMergedInto;
+      }
+    } else if (this.initialStatus === 'Duplicate') {
+      result['mergedInto'] = '';
+    }
+
+    return result;
+  }
+
+  _selectChangeHandler(e) {
+    const statusInput = e.target;
+    this.status = statusInput.value;
+    this._changeHandler(e);
+  }
+
+  /**
+   * @param {Event} e
+   * @fires CustomEvent#change
+   * @private
+   */
+  _changeHandler(e) {
+    this.dispatchEvent(new CustomEvent('change'));
+  }
+}
+
+customElements.define('mr-edit-status', MrEditStatus);
diff --git a/static_src/elements/issue-detail/metadata/mr-edit-field/mr-edit-status.test.js b/static_src/elements/issue-detail/metadata/mr-edit-field/mr-edit-status.test.js
new file mode 100644
index 0000000..ffa25e5
--- /dev/null
+++ b/static_src/elements/issue-detail/metadata/mr-edit-field/mr-edit-status.test.js
@@ -0,0 +1,83 @@
+// 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 {MrEditStatus} from './mr-edit-status.js';
+
+
+let element;
+
+describe('mr-edit-status', () => {
+  beforeEach(() => {
+    element = document.createElement('mr-edit-status');
+    element.statuses = [
+      {'status': 'New'},
+      {'status': 'Old'},
+      {'status': 'Duplicate'},
+    ];
+    document.body.appendChild(element);
+  });
+
+  afterEach(() => {
+    document.body.removeChild(element);
+  });
+
+  it('initializes', () => {
+    assert.instanceOf(element, MrEditStatus);
+  });
+
+  it('delta empty when no changes', () => {
+    assert.deepEqual(element.delta, {});
+  });
+
+  it('change status', async () => {
+    element.initialStatus = 'New';
+
+    await element.updateComplete;
+
+    const statusInput = element.shadowRoot.querySelector('select');
+    statusInput.value = 'Old';
+    statusInput.dispatchEvent(new Event('change'));
+
+    await element.updateComplete;
+
+    assert.deepEqual(element.delta, {status: 'Old'});
+  });
+
+  it('mark as duplicate', async () => {
+    element.initialStatus = 'New';
+
+    await element.updateComplete;
+
+    const statusInput = element.shadowRoot.querySelector('select');
+    statusInput.value = 'Duplicate';
+    statusInput.dispatchEvent(new Event('change'));
+
+    await element.updateComplete;
+
+    element.shadowRoot.querySelector('#mergedIntoInput').value = 'proj:123';
+    assert.deepEqual(element.delta, {
+      status: 'Duplicate',
+      mergedInto: 'proj:123',
+    });
+  });
+
+  it('remove mark as duplicate', async () => {
+    element.initialStatus = 'Duplicate';
+    element.mergedInto = 'chromium:1234';
+
+    await element.updateComplete;
+
+    const statusInput = element.shadowRoot.querySelector('select');
+    statusInput.value = 'New';
+    statusInput.dispatchEvent(new Event('change'));
+
+    await element.updateComplete;
+
+    assert.deepEqual(element.delta, {
+      status: 'New',
+      mergedInto: '',
+    });
+  });
+});
diff --git a/static_src/elements/issue-detail/metadata/mr-edit-field/mr-multi-checkbox.js b/static_src/elements/issue-detail/metadata/mr-edit-field/mr-multi-checkbox.js
new file mode 100644
index 0000000..881cced
--- /dev/null
+++ b/static_src/elements/issue-detail/metadata/mr-edit-field/mr-multi-checkbox.js
@@ -0,0 +1,96 @@
+// 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 {LitElement, html, css} from 'lit-element';
+
+/**
+ * `<mr-multi-checkbox>`
+ *
+ * A web component for managing values in a set of checkboxes.
+ *
+ */
+export class MrMultiCheckbox extends LitElement {
+  /** @override */
+  static get styles() {
+    return css`
+      input[type="checkbox"] {
+        width: auto;
+        height: auto;
+      }
+    `;
+  }
+
+  /** @override */
+  render() {
+    return html`
+      ${this.options.map((option) => html`
+        <label title=${option.docstring}>
+          <input
+            type="checkbox"
+            name=${this.name}
+            value=${option.optionName}
+            ?checked=${this.values.includes(option.optionName)}
+            @change=${this._changeHandler}
+          />
+          ${option.optionName}
+        </label>
+      `)}
+    `;
+  }
+
+  /** @override */
+  static get properties() {
+    return {
+      values: {type: Array},
+      options: {type: Array},
+      _inputRefs: {type: Object},
+    };
+  }
+
+
+  /** @override */
+  updated(changedProperties) {
+    if (changedProperties.has('options')) {
+      this._inputRefs = this.shadowRoot.querySelectorAll('input');
+    }
+
+    if (changedProperties.has('values')) {
+      this.reset();
+    }
+  }
+
+  reset() {
+    this.setValues(this.values);
+  }
+
+  getValues() {
+    if (!this._inputRefs) return;
+    const valueList = [];
+    this._inputRefs.forEach((c) => {
+      if (c.checked) {
+        valueList.push(c.value.trim());
+      }
+    });
+    return valueList;
+  }
+
+  setValues(values) {
+    if (!this._inputRefs) return;
+    this._inputRefs.forEach(
+        (checkbox) => {
+          checkbox.checked = values.includes(checkbox.value);
+        },
+    );
+  }
+
+  /**
+   * @fires CustomEvent#change
+   * @private
+   */
+  _changeHandler() {
+    this.dispatchEvent(new CustomEvent('change'));
+  }
+}
+
+customElements.define('mr-multi-checkbox', MrMultiCheckbox);
diff --git a/static_src/elements/issue-detail/metadata/mr-edit-field/mr-multi-checkbox.test.js b/static_src/elements/issue-detail/metadata/mr-edit-field/mr-multi-checkbox.test.js
new file mode 100644
index 0000000..33cce9e
--- /dev/null
+++ b/static_src/elements/issue-detail/metadata/mr-edit-field/mr-multi-checkbox.test.js
@@ -0,0 +1,23 @@
+// 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 {MrMultiCheckbox} from './mr-multi-checkbox.js';
+
+let element;
+
+describe('mr-multi-checkbox', () => {
+  beforeEach(() => {
+    element = document.createElement('mr-multi-checkbox');
+    document.body.appendChild(element);
+  });
+
+  afterEach(() => {
+    document.body.removeChild(element);
+  });
+
+  it('initializes', () => {
+    assert.instanceOf(element, MrMultiCheckbox);
+  });
+});