Merge "Convert gr-change-reply-js-api_test to Typescript"
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.js b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.js
deleted file mode 100644
index 43bbcd3..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.js
+++ /dev/null
@@ -1,75 +0,0 @@
-/**
- * @license
- * Copyright 2017 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import '../../../test/common-test-setup';
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn';
-// eslint-disable-next-line import/named
-import {fixture, html, assert} from '@open-wc/testing';
-
-Polymer({
-  is: 'gr-attribute-helper-some-element',
-  properties: {
-    fooBar: {
-      type: Object,
-      notify: true,
-    },
-  },
-});
-
-suite('gr-attribute-helper tests', () => {
-  let element;
-  let instance;
-
-  setup(async () => {
-    let plugin;
-    window.Gerrit.install(
-        p => {
-          plugin = p;
-        },
-        '0.1',
-        'http://test.com/plugins/testplugin/static/test.js'
-    );
-    element = await fixture(
-        html`
-        <gr-attribute-helper-some-element></gr-attribute-helper-some-element>
-      `
-    );
-    instance = plugin.attributeHelper(element);
-  });
-
-  test('resolved on value change from undefined', () => {
-    const promise = instance.get('fooBar').then(value => {
-      assert.equal(value, 'foo! bar!');
-    });
-    element.fooBar = 'foo! bar!';
-    return promise;
-  });
-
-  test('resolves to current attribute value', () => {
-    element.fooBar = 'foo-foo-bar';
-    const promise = instance.get('fooBar').then(value => {
-      assert.equal(value, 'foo-foo-bar');
-    });
-    element.fooBar = 'no bar';
-    return promise;
-  });
-
-  test('bind', () => {
-    const stub = sinon.stub();
-    element.fooBar = 'bar foo';
-    const unbind = instance.bind('fooBar', stub);
-    element.fooBar = 'partridge in a foo tree';
-    element.fooBar = 'five gold bars';
-    assert.equal(stub.callCount, 3);
-    assert.deepEqual(stub.args[0], ['bar foo']);
-    assert.deepEqual(stub.args[1], ['partridge in a foo tree']);
-    assert.deepEqual(stub.args[2], ['five gold bars']);
-    stub.reset();
-    unbind();
-    instance.fooBar = 'ladies dancing';
-    assert.isFalse(stub.called);
-  });
-});
-
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.ts b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.ts
new file mode 100644
index 0000000..5c15816
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.ts
@@ -0,0 +1,82 @@
+/**
+ * @license
+ * Copyright 2017 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn';
+import {fixture, html, assert} from '@open-wc/testing';
+import {PluginApi} from '../../../api/plugin';
+import {AttributeHelperPluginApi} from '../../../api/attribute-helper';
+
+// Attribute helper only works on Polymer notify events, so we cannot use a Lit
+// element for the test.
+Polymer({
+  is: 'foo-bar',
+  properties: {
+    fooBar: {
+      type: Object,
+      notify: true,
+    },
+  },
+});
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'foo-bar': HTMLElement;
+  }
+}
+
+suite('gr-attribute-helper tests', () => {
+  let element: HTMLElement & {fooBar?: string};
+  let instance: AttributeHelperPluginApi;
+
+  setup(async () => {
+    let plugin: PluginApi;
+    window.Gerrit.install(
+      p => {
+        plugin = p;
+      },
+      '0.1',
+      'http://test.com/plugins/testplugin/static/test.js'
+    );
+    element = await fixture(html`<foo-bar></foo-bar>`);
+    instance = plugin!.attributeHelper(element);
+  });
+
+  test('get resolves on value change from undefined', async () => {
+    const fooBarWatch = instance.get('fooBar');
+    element.fooBar = 'foo! bar!';
+    const value = await fooBarWatch;
+
+    assert.equal(value, 'foo! bar!');
+  });
+
+  test('get resolves to current attribute value', async () => {
+    element.fooBar = 'foo-foo-bar';
+    const fooBarWatch = instance.get('fooBar');
+    element.fooBar = 'no bar';
+    const value = await fooBarWatch;
+
+    assert.equal(value, 'foo-foo-bar');
+  });
+
+  test('bind', () => {
+    const stub = sinon.stub();
+    element.fooBar = 'bar foo';
+    const unbind = instance.bind('fooBar', stub);
+    element.fooBar = 'partridge in a foo tree';
+    element.fooBar = 'five gold bars';
+
+    assert.equal(stub.callCount, 3);
+    assert.deepEqual(stub.args[0], ['bar foo']);
+    assert.deepEqual(stub.args[1], ['partridge in a foo tree']);
+    assert.deepEqual(stub.args[2], ['five gold bars']);
+
+    stub.reset();
+    unbind();
+    element.fooBar = 'ladies dancing';
+
+    assert.isFalse(stub.called);
+  });
+});
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.js
deleted file mode 100644
index c6e1fa4..0000000
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.js
+++ /dev/null
@@ -1,120 +0,0 @@
-/**
- * @license
- * Copyright 2017 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import '../../../test/common-test-setup';
-import './gr-js-api-interface';
-import {GrPluginRestApi} from './gr-plugin-rest-api';
-import {stubRestApi} from '../../../test/test-utils';
-import {assert} from '@open-wc/testing';
-
-suite('gr-plugin-rest-api tests', () => {
-  let instance;
-  let getResponseObjectStub;
-  let sendStub;
-
-  setup(() => {
-    stubRestApi('getAccount').returns(Promise.resolve({name: 'Judy Hopps'}));
-    getResponseObjectStub = stubRestApi('getResponseObject').returns(
-        Promise.resolve());
-    sendStub = stubRestApi('send').returns(Promise.resolve({status: 200}));
-    window.Gerrit.install(p => {}, '0.1',
-        'http://test.com/plugins/testplugin/static/test.js');
-    instance = new GrPluginRestApi();
-  });
-
-  test('fetch', () => {
-    const payload = {foo: 'foo'};
-    return instance.fetch('HTTP_METHOD', '/url', payload).then(r => {
-      assert.isTrue(sendStub.calledWith('HTTP_METHOD', '/url', payload));
-      assert.equal(r.status, 200);
-      assert.isFalse(getResponseObjectStub.called);
-    });
-  });
-
-  test('send', () => {
-    const payload = {foo: 'foo'};
-    const response = {bar: 'bar'};
-    getResponseObjectStub.returns(Promise.resolve(response));
-    return instance.send('HTTP_METHOD', '/url', payload).then(r => {
-      assert.isTrue(sendStub.calledWith('HTTP_METHOD', '/url', payload));
-      assert.strictEqual(r, response);
-    });
-  });
-
-  test('get', () => {
-    const response = {foo: 'foo'};
-    getResponseObjectStub.returns(Promise.resolve(response));
-    return instance.get('/url').then(r => {
-      assert.isTrue(sendStub.calledWith('GET', '/url'));
-      assert.strictEqual(r, response);
-    });
-  });
-
-  test('post', () => {
-    const payload = {foo: 'foo'};
-    const response = {bar: 'bar'};
-    getResponseObjectStub.returns(Promise.resolve(response));
-    return instance.post('/url', payload).then(r => {
-      assert.isTrue(sendStub.calledWith('POST', '/url', payload));
-      assert.strictEqual(r, response);
-    });
-  });
-
-  test('put', () => {
-    const payload = {foo: 'foo'};
-    const response = {bar: 'bar'};
-    getResponseObjectStub.returns(Promise.resolve(response));
-    return instance.put('/url', payload).then(r => {
-      assert.isTrue(sendStub.calledWith('PUT', '/url', payload));
-      assert.strictEqual(r, response);
-    });
-  });
-
-  test('delete works', () => {
-    const response = {status: 204};
-    sendStub.returns(Promise.resolve(response));
-    return instance.delete('/url').then(r => {
-      assert.isTrue(sendStub.calledWith('DELETE', '/url'));
-      assert.strictEqual(r, response);
-    });
-  });
-
-  test('delete fails', () => {
-    sendStub.returns(Promise.resolve(
-        {status: 400, text() { return Promise.resolve('text'); }}));
-    return instance.delete('/url').then(r => {
-      throw new Error('Should not resolve');
-    })
-        .catch(err => {
-          assert.isTrue(sendStub.calledWith('DELETE', '/url'));
-          assert.equal('text', err.message);
-        });
-  });
-
-  test('getLoggedIn', () => {
-    const stub = stubRestApi('getLoggedIn').returns(Promise.resolve(true));
-    return instance.getLoggedIn().then(result => {
-      assert.isTrue(stub.calledOnce);
-      assert.isTrue(result);
-    });
-  });
-
-  test('getVersion', () => {
-    const stub = stubRestApi('getVersion').returns(Promise.resolve('foo bar'));
-    return instance.getVersion().then(result => {
-      assert.isTrue(stub.calledOnce);
-      assert.equal(result, 'foo bar');
-    });
-  });
-
-  test('getConfig', () => {
-    const stub = stubRestApi('getConfig').returns(Promise.resolve('foo bar'));
-    return instance.getConfig().then(result => {
-      assert.isTrue(stub.calledOnce);
-      assert.equal(result, 'foo bar');
-    });
-  });
-});
-
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.ts
new file mode 100644
index 0000000..d6d7fc2
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.ts
@@ -0,0 +1,122 @@
+/**
+ * @license
+ * Copyright 2017 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-js-api-interface';
+import {GrPluginRestApi} from './gr-plugin-rest-api';
+import {assertFails, stubRestApi} from '../../../test/test-utils';
+import {assert} from '@open-wc/testing';
+import {PluginApi} from '../../../api/plugin';
+import {
+  createAccountDetailWithId,
+  createServerInfo,
+} from '../../../test/test-data-generators';
+import {HttpMethod} from '../../../api/rest-api';
+
+suite('gr-plugin-rest-api tests', () => {
+  let instance: GrPluginRestApi;
+  let getResponseObjectStub: sinon.SinonStub;
+  let sendStub: sinon.SinonStub;
+
+  setup(() => {
+    stubRestApi('getAccount').resolves(createAccountDetailWithId());
+    getResponseObjectStub = stubRestApi('getResponseObject').resolves();
+    sendStub = stubRestApi('send').resolves({...new Response(), status: 200});
+    let pluginApi: PluginApi;
+    window.Gerrit.install(
+      p => {
+        pluginApi = p;
+      },
+      '0.1',
+      'http://test.com/plugins/testplugin/static/test.js'
+    );
+    instance = new GrPluginRestApi(pluginApi!);
+  });
+
+  test('fetch', async () => {
+    const payload = {foo: 'foo'};
+    const r = await instance.fetch(HttpMethod.POST, '/url', payload);
+    assert.isTrue(sendStub.calledWith(HttpMethod.POST, '/url', payload));
+    assert.equal(r.status, 200);
+    assert.isFalse(getResponseObjectStub.called);
+  });
+
+  test('send', async () => {
+    const payload = {foo: 'foo'};
+    const response = {bar: 'bar'};
+    getResponseObjectStub.resolves(response);
+    const r = await instance.send(HttpMethod.POST, '/url', payload);
+    assert.isTrue(sendStub.calledWith(HttpMethod.POST, '/url', payload));
+    assert.strictEqual(r, response);
+  });
+
+  test('get', async () => {
+    const response = {foo: 'foo'};
+    getResponseObjectStub.resolves(response);
+    const r = await instance.get('/url');
+    assert.isTrue(sendStub.calledWith('GET', '/url'));
+    assert.strictEqual(r, response);
+  });
+
+  test('post', async () => {
+    const payload = {foo: 'foo'};
+    const response = {bar: 'bar'};
+    getResponseObjectStub.resolves(response);
+    const r = await instance.post('/url', payload);
+    assert.isTrue(sendStub.calledWith('POST', '/url', payload));
+    assert.strictEqual(r, response);
+  });
+
+  test('put', async () => {
+    const payload = {foo: 'foo'};
+    const response = {bar: 'bar'};
+    getResponseObjectStub.resolves(response);
+    const r = await instance.put('/url', payload);
+    assert.isTrue(sendStub.calledWith('PUT', '/url', payload));
+    assert.strictEqual(r, response);
+  });
+
+  test('delete works', async () => {
+    const response = {status: 204};
+    sendStub.resolves(response);
+    const r = await instance.delete('/url');
+    assert.isTrue(sendStub.calledWith('DELETE', '/url'));
+    assert.strictEqual(r, response);
+  });
+
+  test('delete fails', async () => {
+    sendStub.resolves({
+      status: 400,
+      text() {
+        return Promise.resolve('text');
+      },
+    });
+    const error = await assertFails(instance.delete('/url'));
+    assert.equal('text', (error as Error).message);
+    assert.isTrue(sendStub.calledWith('DELETE', '/url'));
+  });
+
+  test('getLoggedIn', async () => {
+    const stub = stubRestApi('getLoggedIn').resolves(true);
+    const loggedIn = await instance.getLoggedIn();
+    assert.isTrue(stub.calledOnce);
+    assert.isTrue(loggedIn);
+  });
+
+  test('getVersion', async () => {
+    const stub = stubRestApi('getVersion').resolves('foo bar');
+    const version = await instance.getVersion();
+    assert.isTrue(stub.calledOnce);
+    assert.equal(version, 'foo bar');
+  });
+
+  test('getConfig', async () => {
+    const info = createServerInfo();
+    const stub = stubRestApi('getConfig').resolves(info);
+    const config = await instance.getConfig();
+    assert.isTrue(stub.calledOnce);
+    assert.equal(config, info);
+  });
+});
diff --git a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts
index c7d63c2..b58fc3f 100644
--- a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts
+++ b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts
@@ -35,6 +35,7 @@
 }
 export interface BulkActionsState {
   loadingState: LoadingState;
+  selectableChangeNums: NumericChangeId[];
   selectedChangeNums: NumericChangeId[];
   allChanges: Map<NumericChangeId, ChangeInfo>;
 }
@@ -42,6 +43,7 @@
 const initialState: BulkActionsState = {
   loadingState: LoadingState.NOT_SYNCED,
   selectedChangeNums: [],
+  selectableChangeNums: [],
   allChanges: new Map(),
 };
 
@@ -68,11 +70,6 @@
     bulkActionsState => bulkActionsState.loadingState
   );
 
-  public readonly allChanges$ = select(
-    this.state$,
-    bulkActionsState => bulkActionsState.allChanges
-  );
-
   public readonly selectedChanges$ = select(this.state$, bulkActionsState => {
     const result = [];
     for (const changeNum of bulkActionsState.selectedChangeNums) {
@@ -90,7 +87,7 @@
 
   addSelectedChangeNum(changeNum: NumericChangeId) {
     const current = this.getState();
-    if (!current.allChanges.has(changeNum)) {
+    if (!current.selectableChangeNums.includes(changeNum)) {
       throw new Error(
         `Trying to add change ${changeNum} that is not part of bulk-actions model`
       );
@@ -102,7 +99,7 @@
 
   removeSelectedChangeNum(changeNum: NumericChangeId) {
     const current = this.getState();
-    if (!current.allChanges.has(changeNum)) {
+    if (!current.selectableChangeNums.includes(changeNum)) {
       throw new Error(
         `Trying to remove change ${changeNum} that is not part of bulk-actions model`
       );
@@ -242,10 +239,12 @@
     const selectedChangeNums = currentState.selectedChangeNums.filter(
       changeNum => basicChanges.has(changeNum)
     );
+    const selectableChangeNums = changes.map(c => c._number);
     this.updateState({
       loadingState: LoadingState.LOADING,
       selectedChangeNums,
-      allChanges: basicChanges,
+      selectableChangeNums,
+      allChanges: new Map(),
     });
 
     if (changes.length === 0) {
@@ -257,7 +256,7 @@
       );
     currentState = this.getState();
     // Return early if sync has been called again since starting the load.
-    if (basicChanges !== currentState.allChanges) return;
+    if (selectableChangeNums !== currentState.selectableChangeNums) return;
     const allDetailedChanges: Map<NumericChangeId, ChangeInfo> = new Map();
     for (const detailedChange of changeDetails ?? []) {
       const basicChange = basicChanges.get(detailedChange._number)!;
diff --git a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts
index 686f9d9..7bc37d6 100644
--- a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts
+++ b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts
@@ -79,6 +79,7 @@
     bulkActionsModel.sync([c1, c2]);
 
     assert.isEmpty(bulkActionsModel.getState().selectedChangeNums);
+    assert.deepEqual(bulkActionsModel.getState().selectableChangeNums, [1, 2]);
 
     bulkActionsModel.addSelectedChangeNum(c1._number);
     assert.sameMembers(bulkActionsModel.getState().selectedChangeNums, [
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index f4c0091..d6ad434 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -325,7 +325,7 @@
 }
 
 export function assertFails(promise: Promise<unknown>, error?: unknown) {
-  promise
+  return promise
     .then((_v: unknown) => {
       assert.fail('Promise resolved but should have failed');
     })
@@ -333,6 +333,7 @@
       if (error) {
         assert.equal(e, error);
       }
+      return e;
     });
 }