Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/shared/test/constants-hotlists.js b/static_src/shared/test/constants-hotlists.js
new file mode 100644
index 0000000..a496905
--- /dev/null
+++ b/static_src/shared/test/constants-hotlists.js
@@ -0,0 +1,76 @@
+// 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 * as issueV0 from './constants-issueV0.js';
+import * as users from './constants-users.js';
+import 'shared/typedef.js';
+
+/** @type {string} */
+export const NAME = 'hotlists/1234';
+
+/** @type {Hotlist} */
+export const HOTLIST = Object.freeze({
+  name: NAME,
+  displayName: 'Hotlist-Name',
+  owner: users.NAME,
+  editors: [users.NAME_2],
+  summary: 'Summary',
+  description: 'Description',
+  defaultColumns: [{column: 'Rank'}, {column: 'ID'}, {column: 'Summary'}],
+  hotlistPrivacy: 'PUBLIC',
+});
+
+export const HOTLIST_ITEM_NAME = NAME + '/items/56';
+
+/** @type {HotlistItem} */
+export const HOTLIST_ITEM = Object.freeze({
+  name: HOTLIST_ITEM_NAME,
+  issue: issueV0.NAME,
+  // rank: The API excludes the rank field if it's 0.
+  adder: users.NAME,
+  createTime: '2020-01-01T12:00:00Z',
+});
+
+/** @type {HotlistIssue} */
+export const HOTLIST_ISSUE = Object.freeze({
+  ...issueV0.ISSUE, ...HOTLIST_ITEM, adder: users.USER,
+});
+
+/** @type {HotlistItem} */
+export const HOTLIST_ITEM_OTHER_PROJECT = Object.freeze({
+  name: NAME + '/items/78',
+  issue: issueV0.NAME_OTHER_PROJECT,
+  rank: 1,
+  adder: users.NAME,
+  createTime: '2020-01-01T12:00:00Z',
+});
+
+/** @type {HotlistIssue} */
+export const HOTLIST_ISSUE_OTHER_PROJECT = Object.freeze({
+  ...issueV0.ISSUE_OTHER_PROJECT,
+  ...HOTLIST_ITEM_OTHER_PROJECT,
+  adder: users.USER,
+});
+
+/** @type {HotlistItem} */
+export const HOTLIST_ITEM_CLOSED = Object.freeze({
+  name: NAME + '/items/90',
+  issue: issueV0.NAME_CLOSED,
+  rank: 2,
+  adder: users.NAME,
+  createTime: '2020-01-01T12:00:00Z',
+});
+
+/** @type {HotlistIssue} */
+export const HOTLIST_ISSUE_CLOSED = Object.freeze({
+  ...issueV0.ISSUE_CLOSED, ...HOTLIST_ITEM_CLOSED, adder: users.USER,
+});
+
+/** @type {Object<string, Hotlist>} */
+export const BY_NAME = Object.freeze({[NAME]: HOTLIST});
+
+/** @type {Object<string, Array<HotlistItem>>} */
+export const HOTLIST_ITEMS = Object.freeze({
+  [NAME]: [HOTLIST_ITEM],
+});
diff --git a/static_src/shared/test/constants-issueV0.js b/static_src/shared/test/constants-issueV0.js
new file mode 100644
index 0000000..4f52aef
--- /dev/null
+++ b/static_src/shared/test/constants-issueV0.js
@@ -0,0 +1,42 @@
+// 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 'shared/typedef.js';
+
+export const NAME = 'projects/project-name/issues/1234';
+
+export const ISSUE_REF_STRING = 'project-name:1234';
+
+/** @type {IssueRef} */
+export const ISSUE_REF = Object.freeze({
+  projectName: 'project-name',
+  localId: 1234,
+});
+
+/** @type {Issue} */
+export const ISSUE = Object.freeze({
+  projectName: 'project-name',
+  localId: 1234,
+  statusRef: {status: 'Available', meansOpen: true},
+});
+
+export const NAME_OTHER_PROJECT = 'projects/other-project-name/issues/1234';
+
+export const ISSUE_OTHER_PROJECT_REF_STRING = 'other-project-name:1234';
+
+/** @type {Issue} */
+export const ISSUE_OTHER_PROJECT = Object.freeze({
+  projectName: 'other-project-name',
+  localId: 1234,
+  statusRef: {status: 'Available', meansOpen: true},
+});
+
+export const NAME_CLOSED = 'projects/project-name/issues/5678';
+
+/** @type {Issue} */
+export const ISSUE_CLOSED = Object.freeze({
+  projectName: 'project-name',
+  localId: 5678,
+  statusRef: {status: 'Fixed', meansOpen: false},
+});
diff --git a/static_src/shared/test/constants-permissions.js b/static_src/shared/test/constants-permissions.js
new file mode 100644
index 0000000..f4b09c0
--- /dev/null
+++ b/static_src/shared/test/constants-permissions.js
@@ -0,0 +1,23 @@
+// Copyright 2020 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 * as issue from './constants-issueV0.js';
+import 'shared/typedef.js';
+
+/** @type {Permission} */
+export const PERMISSION_ISSUE_EDIT = 'ISSUE_EDIT';
+
+/** @type {PermissionSet} */
+export const PERMISSION_SET_ISSUE = {
+  resource: issue.NAME,
+  permissions: [PERMISSION_ISSUE_EDIT],
+};
+
+/** @type {Object<string, PermissionSet>} */
+export const BY_NAME = {
+  [issue.NAME]: PERMISSION_SET_ISSUE,
+};
+
+/** @type {Array<Permission>} */
+export const PERMISSION_HOTLIST_EDIT = ['HOTLIST_EDIT'];
diff --git a/static_src/shared/test/constants-projectV0.js b/static_src/shared/test/constants-projectV0.js
new file mode 100644
index 0000000..4a46af8
--- /dev/null
+++ b/static_src/shared/test/constants-projectV0.js
@@ -0,0 +1,80 @@
+// 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 {fieldTypes} from 'shared/issue-fields.js';
+import {USER_REF} from './constants-users.js';
+import 'shared/typedef.js';
+
+/** @type {string} */
+export const PROJECT_NAME = 'project-name';
+
+/** @type {FieldDef} */
+export const FIELD_DEF_INT = Object.freeze({
+  fieldRef: Object.freeze({
+    fieldId: 123,
+    fieldName: 'field-name',
+    type: fieldTypes.INT_TYPE,
+  }),
+});
+
+/** @type {FieldDef} */
+export const FIELD_DEF_ENUM = Object.freeze({
+  fieldRef: Object.freeze({
+    fieldId: 456,
+    fieldName: 'enum',
+    type: fieldTypes.ENUM_TYPE,
+  }),
+});
+
+/** @type {Array<FieldDef>} */
+export const FIELD_DEFS = [
+  FIELD_DEF_INT,
+  FIELD_DEF_ENUM,
+];
+
+/** @type {Config} */
+export const CONFIG = Object.freeze({
+  projectName: PROJECT_NAME,
+  fieldDefs: FIELD_DEFS,
+  labelDefs: [
+    {label: 'One'},
+    {label: 'EnUm'},
+    {label: 'eNuM-Options'},
+    {label: 'hello-world', docstring: 'hmmm'},
+    {label: 'hello-me', docstring: 'hmmm'},
+  ],
+});
+
+/** @type {string} */
+export const DEFAULT_QUERY = 'owner:me';
+
+/** @type {PresentationConfig} */
+export const PRESENTATION_CONFIG = Object.freeze({
+  projectThumbnailUrl: 'test.png',
+  defaultColSpec: 'ID+Summary+AllLabels',
+  defaultQuery: DEFAULT_QUERY,
+});
+
+/** @type {Array<string>} */
+export const CUSTOM_PERMISSIONS = ['google', 'security'];
+
+/** @type {{userRefs: Array<UserRef>, groupRefs: Array<UserRef>}} */
+export const VISIBLE_MEMBERS = Object.freeze({
+  userRefs: [USER_REF],
+  groupRefs: [],
+});
+
+/** @type {TemplateDef} */
+export const TEMPLATE_DEF = Object.freeze({
+  templateName: 'Template Name',
+});
+
+export const STATE = Object.freeze({projectV0: {
+  name: PROJECT_NAME,
+  configs: {[PROJECT_NAME]: CONFIG},
+  presentationConfigs: {[PROJECT_NAME]: PRESENTATION_CONFIG},
+  customPermissions: {[PROJECT_NAME]: CUSTOM_PERMISSIONS},
+  visibleMembers: {[PROJECT_NAME]: VISIBLE_MEMBERS},
+  templates: {[PROJECT_NAME]: TEMPLATE_DEF},
+}});
diff --git a/static_src/shared/test/constants-projects.js b/static_src/shared/test/constants-projects.js
new file mode 100644
index 0000000..c25f46b
--- /dev/null
+++ b/static_src/shared/test/constants-projects.js
@@ -0,0 +1,27 @@
+// 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 'shared/typedef.js';
+
+/** @type {ProjectName} */
+export const NAME = 'projects/chromium';
+
+/** @type {Project} */
+export const PROJECT = Object.freeze({
+  name: NAME,
+  displayName: 'Chromium',
+  summary: 'A great open source project.',
+  thumbnailUrl: 'chromium.png',
+});
+
+/** @type {ProjectName} */
+export const NAME_2 = 'projects/monorail';
+
+/** @type {Project} */
+export const PROJECT_2 = Object.freeze({
+  name: NAME_2,
+  displayName: 'mOnOrAiL',
+  summary: 'Best issue tracker.',
+  thumbnailUrl: 'dogtrain.gif',
+});
diff --git a/static_src/shared/test/constants-stars.js b/static_src/shared/test/constants-stars.js
new file mode 100644
index 0000000..42e7012
--- /dev/null
+++ b/static_src/shared/test/constants-stars.js
@@ -0,0 +1,18 @@
+// 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 'shared/typedef.js';
+
+export const PROJECT_STAR_NAME = 'users/1234/projectStars/monorail';
+export const PROJECT_STAR_NAME_2 = 'users/1234/projectStars/chromium';
+
+/** @type {ProjectStar} */
+export const PROJECT_STAR = Object.freeze({
+  name: PROJECT_STAR_NAME,
+});
+
+/** @type {ProjectStar} */
+export const PROJECT_STAR_2 = Object.freeze({
+  name: PROJECT_STAR_NAME_2,
+});
diff --git a/static_src/shared/test/constants-users.js b/static_src/shared/test/constants-users.js
new file mode 100644
index 0000000..0a9bbf8
--- /dev/null
+++ b/static_src/shared/test/constants-users.js
@@ -0,0 +1,43 @@
+// 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 'shared/typedef.js';
+
+export const NAME = 'users/1234';
+
+export const DISPLAY_NAME = 'example@example.com';
+
+export const ID = 1234;
+
+/** @type {UserRef} */
+export const USER_REF = Object.freeze({
+  userId: ID,
+  displayName: DISPLAY_NAME,
+});
+
+/** @type {User} */
+export const USER = Object.freeze({
+  name: NAME,
+  displayName: DISPLAY_NAME,
+});
+
+export const NAME_2 = 'users/5678';
+
+export const DISPLAY_NAME_2 = 'other_user@example.com';
+
+/** @type {User} */
+export const USER_2 = Object.freeze({
+  name: NAME_2,
+  displayName: DISPLAY_NAME_2,
+});
+
+/** @type {Object<string, User>} */
+export const BY_NAME = Object.freeze({[NAME]: USER, [NAME_2]: USER_2});
+
+/** @type {ProjectMember} */
+export const PROJECT_MEMBER = Object.freeze({
+  name: 'projects/proj/members/1234',
+  role: 'CONTRIBUTOR',
+});
+
diff --git a/static_src/shared/test/fakes.js b/static_src/shared/test/fakes.js
new file mode 100644
index 0000000..d506f6a
--- /dev/null
+++ b/static_src/shared/test/fakes.js
@@ -0,0 +1,12 @@
+// 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 sinon from 'sinon';
+
+export const clientLoggerFake = () => ({
+  logStart: sinon.stub(),
+  logEnd: sinon.stub(),
+  logPause: sinon.stub(),
+  started: sinon.stub().returns(true),
+});
diff --git a/static_src/shared/test/helpers.js b/static_src/shared/test/helpers.js
new file mode 100644
index 0000000..63a1e12
--- /dev/null
+++ b/static_src/shared/test/helpers.js
@@ -0,0 +1,57 @@
+// 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 axe from 'axe-core';
+import userEvent from '@testing-library/user-event';
+import {fireEvent} from '@testing-library/react';
+
+// TODO(seanmccullough): Move this into crdx/chopsui-npm if we decide this
+// is worth using in other projects.
+
+/**
+ * @param {HTMLElement} element The element to audit accessibility for.
+ */
+export async function auditA11y(element) {
+  // Performance tip: try restricting the analysis using
+  // https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#use-resulttypes
+  const options = {};
+
+  // Adjust this set to make tests more/less permissible.
+  const reportImpact = new Set(['critical', 'serious', 'moderate', 'minor']);
+  const results = await axe.run(element, options);
+
+  if (results.violations.length == 0) {
+    return;
+  }
+
+  const msgs = ['Accessibility violations:'];
+  results.violations.forEach((result) => {
+    if (reportImpact.has(result.impact)) {
+      msgs.push(`\n[${result.impact}] ${result.help}`);
+      for (const node of result.nodes) {
+        if (node.failureSummary) {
+          msgs.push(node.failureSummary);
+        }
+        msgs.push(node.html);
+      }
+      msgs.push('---');
+    }
+  });
+
+  throw new Error(msgs.join('\n'));
+}
+
+/**
+ * Types text into an input field and presses Enter.
+ * @param {HTMLInputElement} input The input field to enter text in.
+ * @param {string} value The text to enter in the input field.
+ */
+export function enterInput(input, value) {
+  userEvent.clear(input);
+
+  userEvent.type(input, value);
+
+  fireEvent.keyDown(input, {key: 'Enter', code: 'Enter'});
+}
+