Merge "Ignore Trigger votes section in reply dialog when no permission"
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
index 8711f14..db0d236 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
@@ -547,9 +547,25 @@
         this.cancel();
         break;
       case 'Tab':
-        if (this.suggestions.length > 0 && this.tabComplete) {
+        if (
+          this.queryStatus?.type === AutocompleteQueryStatusType.LOADING &&
+          this.tabComplete
+        ) {
           e.preventDefault();
-          this.handleInputCommit(true);
+          // Queue tab on load.
+          this.queryStatus = {
+            type: AutocompleteQueryStatusType.LOADING,
+            message: 'Loading... (Handle Tab on load)',
+          };
+          const queryId = this.activeQueryId;
+          this.latestSuggestionUpdateComplete?.then(() => {
+            if (queryId === this.activeQueryId) {
+              this.handleInputCommit(/* _tabComplete=*/ true);
+            }
+          });
+        } else if (this.suggestions.length > 0 && this.tabComplete) {
+          e.preventDefault();
+          this.handleInputCommit(/* _tabComplete=*/ true);
           this.focus();
         } else {
           this.setFocus(false);
@@ -559,12 +575,24 @@
         if (modifierPressed(e)) {
           break;
         }
-        if (this.suggestions.length > 0) {
+        e.preventDefault();
+        if (this.queryStatus?.type === AutocompleteQueryStatusType.LOADING) {
+          // Queue enter on load.
+          this.queryStatus = {
+            type: AutocompleteQueryStatusType.LOADING,
+            message: 'Loading... (Handle Enter on load)',
+          };
+          const queryId = this.activeQueryId;
+          this.latestSuggestionUpdateComplete?.then(() => {
+            if (queryId === this.activeQueryId) {
+              this.handleItemSelectEnter(e);
+            }
+          });
+        } else if (this.suggestions.length > 0) {
           // If suggestions are shown, act as if the keypress is in dropdown.
           // suggestions length is 0 if error is shown.
           this.handleItemSelectEnter(e);
         } else {
-          e.preventDefault();
           this.handleInputCommit();
         }
         break;
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts
index a3da953..c59b8e8 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts
@@ -775,6 +775,142 @@
     assert.isTrue(cancelHandler.called);
   });
 
+  test('while loading queue enter commits', async () => {
+    const commitHandler = sinon.stub();
+    element.addEventListener('commit', commitHandler);
+    let resolvePromise: (value: AutocompleteSuggestion[]) => void;
+    const blockingPromise = new Promise<AutocompleteSuggestion[]>(resolve => {
+      resolvePromise = resolve;
+    });
+    element.query = (_: string) => blockingPromise;
+
+    element.setFocus(true);
+    element.text = 'blah';
+    await element.updateComplete;
+    await waitUntil(() => !suggestionsEl().isHidden);
+    assert.deepEqual(element.queryStatus, {
+      type: AutocompleteQueryStatusType.LOADING,
+      message: 'Loading...',
+    });
+
+    pressKey(inputEl(), Key.ENTER);
+    await element.updateComplete;
+    assert.deepEqual(element.queryStatus, {
+      type: AutocompleteQueryStatusType.LOADING,
+      message: 'Loading... (Handle Enter on load)',
+    });
+
+    resolvePromise!([{name: 'suggestion 1'}] as AutocompleteSuggestion[]);
+    await element.latestSuggestionUpdateComplete;
+    await element.updateComplete;
+
+    assert.equal(element.suggestions.length, 0);
+    assert.isUndefined(element.queryStatus);
+    assert.isTrue(commitHandler.called);
+  });
+
+  test('while loading queue tab completes', async () => {
+    element.tabComplete = true;
+    const commitHandler = sinon.stub();
+    element.addEventListener('commit', commitHandler);
+    const queryPromise = mockPromise<AutocompleteSuggestion[]>();
+    element.query = (_: string) => queryPromise;
+
+    element.setFocus(true);
+    element.text = 'blah';
+    await element.updateComplete;
+    await waitUntil(() => !suggestionsEl().isHidden);
+    assert.deepEqual(element.queryStatus, {
+      type: AutocompleteQueryStatusType.LOADING,
+      message: 'Loading...',
+    });
+
+    pressKey(inputEl(), Key.TAB);
+    await element.updateComplete;
+    assert.deepEqual(element.queryStatus, {
+      type: AutocompleteQueryStatusType.LOADING,
+      message: 'Loading... (Handle Tab on load)',
+    });
+
+    queryPromise.resolve([{name: 'suggestion 1'}] as AutocompleteSuggestion[]);
+    await element.latestSuggestionUpdateComplete;
+    await element.updateComplete;
+
+    assert.equal(element.suggestions.length, 0);
+    assert.isUndefined(element.queryStatus);
+    assert.isFalse(commitHandler.called);
+    assert.equal(element.text, 'suggestion 1');
+  });
+
+  test('while loading and queued update text cancels', async () => {
+    const commitHandler = sinon.stub();
+    element.addEventListener('commit', commitHandler);
+    const queryPromise = mockPromise<AutocompleteSuggestion[]>();
+    element.query = (_: string) => queryPromise;
+
+    element.setFocus(true);
+    element.text = 'blah';
+    await element.updateComplete;
+    await waitUntil(() => !suggestionsEl().isHidden);
+    assert.deepEqual(element.queryStatus, {
+      type: AutocompleteQueryStatusType.LOADING,
+      message: 'Loading...',
+    });
+
+    pressKey(inputEl(), Key.ENTER);
+    await element.updateComplete;
+    assert.deepEqual(element.queryStatus, {
+      type: AutocompleteQueryStatusType.LOADING,
+      message: 'Loading... (Handle Enter on load)',
+    });
+
+    element.text = 'more blah';
+    await element.updateComplete;
+
+    queryPromise.resolve([{name: 'suggestion 1'}] as AutocompleteSuggestion[]);
+    await element.latestSuggestionUpdateComplete;
+    await element.updateComplete;
+
+    // Commit for stale request is not called.
+    assert.isFalse(commitHandler.called);
+  });
+
+  test('while loading and queued esc cancels', async () => {
+    const commitHandler = sinon.stub();
+    element.addEventListener('commit', commitHandler);
+    const queryPromise = mockPromise<AutocompleteSuggestion[]>();
+    element.query = (_: string) => queryPromise;
+
+    element.setFocus(true);
+    element.text = 'blah';
+    await element.updateComplete;
+    await waitUntil(() => !suggestionsEl().isHidden);
+    assert.deepEqual(element.queryStatus, {
+      type: AutocompleteQueryStatusType.LOADING,
+      message: 'Loading...',
+    });
+
+    pressKey(inputEl(), Key.ENTER);
+    await element.updateComplete;
+    assert.deepEqual(element.queryStatus, {
+      type: AutocompleteQueryStatusType.LOADING,
+      message: 'Loading... (Handle Enter on load)',
+    });
+
+    pressKey(inputEl(), Key.ESC);
+    await element.updateComplete;
+
+    queryPromise.resolve([{name: 'suggestion 1'}] as AutocompleteSuggestion[]);
+    await element.latestSuggestionUpdateComplete;
+    await element.updateComplete;
+
+    // Commit for stale request is not called.
+    assert.isFalse(commitHandler.called);
+    // Query results and status are cleared
+    assert.equal(element.suggestions.length, 0);
+    assert.isUndefined(element.queryStatus);
+  });
+
   suite('focus', () => {
     let commitSpy: sinon.SinonSpy;
     let focusSpy: sinon.SinonSpy;