Merge "Show "Loading..." while the autocomplete query is loading."
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts
index 91e601c..661ffa0 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts
@@ -35,6 +35,16 @@
selected: HTMLElement | null;
}
+export enum AutocompleteQueryStatusType {
+ LOADING = 'loading',
+ ERROR = 'error',
+}
+
+export interface AutocompleteQueryStatus {
+ type: AutocompleteQueryStatusType;
+ message: string;
+}
+
@customElement('gr-autocomplete-dropdown')
export class GrAutocompleteDropdown extends LitElement {
/**
@@ -58,8 +68,8 @@
/** If specified a single non-interactable line is shown instead of
* suggestions.
*/
- @property({type: String})
- errorMessage?: String;
+ @property({type: Object})
+ queryStatus?: AutocompleteQueryStatus;
@property({type: Number})
verticalOffset = 0;
@@ -117,10 +127,12 @@
li.selected {
background-color: var(--hover-background-color);
}
- li.query-error {
+ li.query-status {
background-color: var(--disabled-background);
- color: var(--error-foreground);
cursor: default;
+ }
+ li.query-status.error {
+ color: var(--error-foreground);
white-space: pre-wrap;
}
@media only screen and (max-height: 35em) {
@@ -140,7 +152,7 @@
}
private isSuggestionListInteractible() {
- return !this.isHidden && !this.errorMessage;
+ return !this.isHidden && !this.queryStatus;
}
constructor() {
@@ -172,7 +184,8 @@
override updated(changedProperties: PropertyValues) {
if (
changedProperties.has('suggestions') ||
- changedProperties.has('isHidden')
+ changedProperties.has('isHidden') ||
+ changedProperties.has('queryStatus')
) {
if (!this.isHidden) {
this.computeCursorStopsAndRefit();
@@ -180,15 +193,19 @@
}
}
- private renderError() {
+ private renderStatus() {
return html`
<li
tabindex="-1"
- aria-label="autocomplete query error"
- class="query-error"
+ aria-label="autocomplete query status"
+ class="query-status ${this.queryStatus?.type}"
>
- <span>${this.errorMessage}</span>
- <span class="label">ERROR</span>
+ <span>${this.queryStatus?.message}</span>
+ <span class="label"
+ >${this.queryStatus?.type === AutocompleteQueryStatusType.ERROR
+ ? 'ERROR'
+ : ''}</span
+ >
</li>
`;
}
@@ -198,8 +215,8 @@
<div class="dropdown-content" id="suggestions" role="listbox">
<ul>
${when(
- this.errorMessage,
- () => this.renderError(),
+ this.queryStatus,
+ () => this.renderStatus(),
() => html`
${repeat(
this.suggestions,
@@ -236,7 +253,7 @@
}
getCurrentText() {
- if (!this.errorMessage) {
+ if (!this.queryStatus) {
return this.getCursorTarget()?.dataset['value'] || '';
}
return '';
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.ts
index 54d054b..10ba5d0 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.ts
@@ -5,7 +5,10 @@
*/
import '../../../test/common-test-setup';
import './gr-autocomplete-dropdown';
-import {GrAutocompleteDropdown} from './gr-autocomplete-dropdown';
+import {
+ AutocompleteQueryStatusType,
+ GrAutocompleteDropdown,
+} from './gr-autocomplete-dropdown';
import {
pressKey,
queryAll,
@@ -177,7 +180,7 @@
});
});
- suite('error tests', () => {
+ suite('status tests', () => {
let element: GrAutocompleteDropdown;
setup(async () => {
@@ -185,7 +188,10 @@
html`<gr-autocomplete-dropdown></gr-autocomplete-dropdown>`
);
element.open();
- element.errorMessage = 'Failed query error';
+ element.queryStatus = {
+ type: AutocompleteQueryStatusType.ERROR,
+ message: 'Failed query error',
+ };
await waitEventLoop();
});
@@ -200,8 +206,8 @@
<div class="dropdown-content" id="suggestions" role="listbox">
<ul>
<li
- aria-label="autocomplete query error"
- class="query-error"
+ aria-label="autocomplete query status"
+ class="query-status error"
tabindex="-1"
>
<span>Failed query error</span>
@@ -213,6 +219,31 @@
);
});
+ test('renders loading', async () => {
+ element.queryStatus = {
+ type: AutocompleteQueryStatusType.LOADING,
+ message: 'Loading...',
+ };
+ await waitEventLoop();
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ `
+ <div class="dropdown-content" id="suggestions" role="listbox">
+ <ul>
+ <li
+ aria-label="autocomplete query status"
+ class="query-status loading"
+ tabindex="-1"
+ >
+ <span>Loading...</span>
+ <span class="label"></span>
+ </li>
+ </ul>
+ </div>
+ `
+ );
+ });
+
test('escape key close dropdown with error', async () => {
const closeSpy = sinon.spy(element, 'close');
pressKey(element, Key.ESC);
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 f8f7f9d..8711f14 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
@@ -7,7 +7,11 @@
import '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
import '../gr-cursor-manager/gr-cursor-manager';
import '../../../styles/shared-styles';
-import {GrAutocompleteDropdown} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
+import {
+ AutocompleteQueryStatus,
+ AutocompleteQueryStatusType,
+ GrAutocompleteDropdown,
+} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
import {fire, fireEvent} from '../../../utils/event-util';
import {
debounce,
@@ -166,7 +170,7 @@
@state() suggestions: AutocompleteSuggestion[] = [];
- @state() queryErrorMessage?: string;
+ @state() queryStatus?: AutocompleteQueryStatus;
@state() index: number | null = null;
@@ -179,8 +183,23 @@
@state() selected: HTMLElement | null = null;
+ /**
+ * The query id that status or suggestions correspond to.
+ */
+ private activeQueryId = 0;
+
+ /**
+ * Last scheduled update suggestions task.
+ */
private updateSuggestionsTask?: DelayedTask;
+ // Generate ids for scheduled suggestion queries to easily distinguish them.
+ private static NEXT_QUERY_ID = 1;
+
+ private static getNextQueryId() {
+ return GrAutocomplete.NEXT_QUERY_ID++;
+ }
+
/**
* @return Promise that resolves when suggestions are update.
*/
@@ -266,7 +285,7 @@
}
if (
changedProperties.has('suggestions') ||
- changedProperties.has('queryErrorMessage')
+ changedProperties.has('queryStatus')
) {
this.updateDropdownVisibility();
}
@@ -310,7 +329,7 @@
@item-selected=${this.handleItemSelect}
@dropdown-closed=${this.focusWithoutDisplayingSuggestions}
.suggestions=${this.suggestions}
- .errorMessage=${this.queryErrorMessage}
+ .queryStatus=${this.queryStatus}
role="listbox"
.index=${this.index}
>
@@ -412,8 +431,7 @@
// Reset suggestions for every update
// This will also prevent from carrying over suggestions:
// @see Issue 12039
- this.suggestions = [];
- this.queryErrorMessage = undefined;
+ this.resetQueryOutput();
// TODO(taoalpha): Also skip if text has not changed
@@ -421,8 +439,7 @@
return;
}
- const query = this.query;
- if (!query) {
+ if (!this.query) {
return;
}
@@ -435,50 +452,69 @@
return;
}
- const requestText = this.text;
- const update = () => {
- query(this.text)
- .then(suggestions => {
- if (requestText !== this.text) {
- // Late response.
- return;
- }
- for (const suggestion of suggestions) {
- suggestion.text = suggestion?.name ?? '';
- }
- this.suggestions = suggestions;
- if (this.index === -1) {
- this.value = '';
- }
- })
- .catch(e => {
- this.value = '';
- if (typeof e === 'string') {
- this.queryErrorMessage = e;
- } else if (e instanceof Error) {
- this.queryErrorMessage = e.message;
- }
- });
- };
-
+ const queryId = GrAutocomplete.getNextQueryId();
+ this.activeQueryId = queryId;
+ this.setQueryStatus({
+ type: AutocompleteQueryStatusType.LOADING,
+ message: 'Loading...',
+ });
this.updateSuggestionsTask = debounce(
this.updateSuggestionsTask,
- update,
+ this.createUpdateTask(queryId, this.query, this.text),
DEBOUNCE_WAIT_MS
);
}
+ private createUpdateTask(
+ queryId: number,
+ query: AutocompleteQuery,
+ text: string
+ ): () => Promise<void> {
+ return async () => {
+ let suggestions: AutocompleteSuggestion[];
+ try {
+ suggestions = await query(text);
+ } catch (e) {
+ this.value = '';
+ if (typeof e === 'string') {
+ this.setQueryStatus({
+ type: AutocompleteQueryStatusType.ERROR,
+ message: e,
+ });
+ } else if (e instanceof Error) {
+ this.setQueryStatus({
+ type: AutocompleteQueryStatusType.ERROR,
+ message: e.message,
+ });
+ }
+ return;
+ }
+ if (queryId !== this.activeQueryId) {
+ // Late response.
+ return;
+ }
+ for (const suggestion of suggestions) {
+ suggestion.text = suggestion?.name ?? '';
+ }
+ this.setSuggestions(suggestions);
+ if (this.index === -1) {
+ this.value = '';
+ }
+ };
+ }
+
setFocus(focused: boolean) {
if (focused === this.focused) return;
this.focused = focused;
this.updateDropdownVisibility();
}
+ private shouldShowDropdown() {
+ return (this.suggestions.length > 0 || this.queryStatus) && this.focused;
+ }
+
updateDropdownVisibility() {
- if (
- (this.suggestions.length > 0 || this.queryErrorMessage) &&
- this.focused
- ) {
+ if (this.shouldShowDropdown()) {
this.suggestionsDropdown?.open();
return;
}
@@ -513,8 +549,8 @@
case 'Tab':
if (this.suggestions.length > 0 && this.tabComplete) {
e.preventDefault();
- this.focus();
this.handleInputCommit(true);
+ this.focus();
} else {
this.setFocus(false);
}
@@ -541,7 +577,8 @@
// been based on a previous input. Clear them. This prevents an
// outdated suggestion from being used if the input keystroke is
// immediately followed by a commit keystroke. @see Issue 8655
- this.suggestions = [];
+ this.resetQueryOutput();
+ this.activeQueryId = 0;
}
this.dispatchEvent(
new CustomEvent('input-keydown', {
@@ -553,9 +590,11 @@
}
cancel() {
- if (this.suggestions.length || this.queryErrorMessage) {
- this.suggestions = [];
- this.queryErrorMessage = undefined;
+ if (this.shouldShowDropdown()) {
+ this.resetQueryOutput();
+ // If query is in flight by setting id to 0 we indicate that the results
+ // are outdated.
+ this.activeQueryId = 0;
this.requestUpdate();
} else {
fireEvent(this, 'cancel');
@@ -566,7 +605,7 @@
// Nothing to do if no suggestions.
if (
!this.allowNonSuggestedValues &&
- (this.suggestionsDropdown?.isHidden || this.queryErrorMessage)
+ (this.suggestionsDropdown?.isHidden || this.suggestions.length === 0)
) {
return;
}
@@ -608,6 +647,7 @@
}
}
this.setFocus(false);
+ this.activeQueryId = 0;
};
/**
@@ -644,8 +684,7 @@
}
}
- this.suggestions = [];
- this.queryErrorMessage = undefined;
+ this.resetQueryOutput();
// we need willUpdate to send text-changed event before we can send the
// 'commit' event
await this.updateComplete;
@@ -659,6 +698,23 @@
);
}
}
+
+ // resetQueryOutput, setSuggestions and setQueryStatus insure that suggestions
+ // and queryStatus are never set at the same time.
+ private resetQueryOutput() {
+ this.suggestions = [];
+ this.queryStatus = undefined;
+ }
+
+ private setSuggestions(suggestions: AutocompleteSuggestion[]) {
+ this.suggestions = suggestions;
+ this.queryStatus = undefined;
+ }
+
+ private setQueryStatus(queryStatus: AutocompleteQueryStatus) {
+ this.suggestions = [];
+ this.queryStatus = queryStatus;
+ }
}
/**
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 81949c7..a3da953 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
@@ -8,11 +8,15 @@
import {AutocompleteSuggestion, GrAutocomplete} from './gr-autocomplete';
import {
assertFails,
+ mockPromise,
pressKey,
queryAndAssert,
waitUntil,
} from '../../../test/test-utils';
-import {GrAutocompleteDropdown} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
+import {
+ AutocompleteQueryStatusType,
+ GrAutocompleteDropdown,
+} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
import {PaperInputElement} from '@polymer/paper-input/paper-input';
import {fixture, html, assert} from '@open-wc/testing';
import {Key, Modifier} from '../../../utils/dom-util';
@@ -30,9 +34,7 @@
const inputEl = () => queryAndAssert<HTMLInputElement>(element, '#input');
setup(async () => {
- element = await fixture(
- html`<gr-autocomplete no-debounce></gr-autocomplete>`
- );
+ element = await fixture(html`<gr-autocomplete></gr-autocomplete>`);
});
test('renders', () => {
@@ -151,7 +153,10 @@
],
}
);
- assert.equal(element.suggestionsDropdown?.errorMessage, 'blah not allowed');
+ assert.equal(
+ element.suggestionsDropdown?.queryStatus?.message,
+ 'blah not allowed'
+ );
});
test('cursor starts on suggestions', async () => {
@@ -240,17 +245,21 @@
await element.updateComplete;
return assertFails(promise).then(async () => {
+ await element.latestSuggestionUpdateComplete;
await waitUntil(() => !suggestionsEl().isHidden);
const cancelHandler = sinon.spy();
element.addEventListener('cancel', cancelHandler);
- assert.equal(element.queryErrorMessage, 'Test error');
+ assert.deepEqual(element.queryStatus, {
+ type: AutocompleteQueryStatusType.ERROR,
+ message: 'Test error',
+ });
pressKey(inputEl(), Key.ESC);
await waitUntil(() => suggestionsEl().isHidden);
assert.isFalse(cancelHandler.called);
- assert.isUndefined(element.queryErrorMessage);
+ assert.isUndefined(element.queryStatus);
pressKey(inputEl(), Key.ESC);
await element.updateComplete;
@@ -260,16 +269,14 @@
});
test('emits commit and handles cursor movement', async () => {
- let promise: Promise<AutocompleteSuggestion[]> = Promise.resolve([]);
- const queryStub = sinon.spy(
- (input: string) =>
- (promise = Promise.resolve([
- {name: input + ' 0', value: '0'},
- {name: input + ' 1', value: '1'},
- {name: input + ' 2', value: '2'},
- {name: input + ' 3', value: '3'},
- {name: input + ' 4', value: '4'},
- ] as AutocompleteSuggestion[]))
+ const queryStub = sinon.spy((input: string) =>
+ Promise.resolve([
+ {name: input + ' 0', value: '0'},
+ {name: input + ' 1', value: '1'},
+ {name: input + ' 2', value: '2'},
+ {name: input + ' 3', value: '3'},
+ {name: input + ' 4', value: '4'},
+ ] as AutocompleteSuggestion[])
);
element.query = queryStub;
await element.updateComplete;
@@ -280,7 +287,7 @@
element.text = 'blah';
await element.updateComplete;
- return promise.then(async () => {
+ return element.latestSuggestionUpdateComplete!.then(async () => {
await waitUntil(() => !suggestionsEl().isHidden);
const commitHandler = sinon.spy();
@@ -456,24 +463,22 @@
});
test('suggestions should not carry over', async () => {
- let promise: Promise<AutocompleteSuggestion[]> = Promise.resolve([]);
const queryStub = sinon
.stub()
- .returns(
- (promise = Promise.resolve([
- {name: 'suggestion', value: '0'},
- ] as AutocompleteSuggestion[]))
- );
+ .resolves([{name: 'suggestion', value: '0'}] as AutocompleteSuggestion[]);
element.query = queryStub;
focusOnInput();
element.text = 'bla';
await element.updateComplete;
- return promise.then(async () => {
+ return element.latestSuggestionUpdateComplete!.then(async () => {
await waitUntil(() => element.suggestions.length > 0);
assert.equal(element.suggestions.length, 1);
+
+ queryStub.resolves([] as AutocompleteSuggestion[]);
element.text = '';
element.threshold = 0;
await element.updateComplete;
+ await element.latestSuggestionUpdateComplete;
assert.equal(element.suggestions.length, 0);
});
});
@@ -488,11 +493,15 @@
element.text = 'bla';
await element.updateComplete;
return assertFails(promise).then(async () => {
- await waitUntil(() => element.queryErrorMessage === 'Test error');
+ await element.latestSuggestionUpdateComplete;
+ await waitUntil(() => element.queryStatus?.message === 'Test error');
+
+ queryStub.resolves([] as AutocompleteSuggestion[]);
element.text = '';
element.threshold = 0;
await element.updateComplete;
- assert.isUndefined(element.queryErrorMessage);
+ await element.latestSuggestionUpdateComplete;
+ assert.isUndefined(element.queryStatus);
});
});
@@ -514,6 +523,7 @@
return promise.then(async () => {
const commitHandler = sinon.spy();
element.addEventListener('commit', commitHandler);
+ await element.latestSuggestionUpdateComplete;
await waitUntil(() => element.suggestionsDropdown?.isHidden === false);
pressKey(inputEl(), Key.ENTER);
@@ -524,15 +534,24 @@
});
test('tabComplete flag functions', async () => {
+ element.query = sinon
+ .stub()
+ .resolves([
+ {name: 'tunnel snakes rule!', value: 'snakes'},
+ ] as AutocompleteSuggestion[]);
+
// commitHandler checks for the commit event, whereas commitSpy checks for
// the _commit function of the element.
const commitHandler = sinon.spy();
element.addEventListener('commit', commitHandler);
const commitSpy = sinon.spy(element, '_commit');
element.setFocus(true);
-
- element.suggestions = [{text: 'tunnel snakes rule!', name: ''}];
element.tabComplete = false;
+ element.text = 'text1';
+ await element.updateComplete;
+
+ await element.latestSuggestionUpdateComplete;
+ await element.updateComplete;
pressKey(inputEl(), Key.TAB);
await element.updateComplete;
@@ -540,9 +559,12 @@
assert.isFalse(commitSpy.called);
assert.isFalse(element.focused);
- element.tabComplete = true;
- await element.updateComplete;
element.setFocus(true);
+ element.tabComplete = true;
+ element.text = 'text2';
+ await element.updateComplete;
+
+ await element.latestSuggestionUpdateComplete;
await element.updateComplete;
pressKey(inputEl(), Key.TAB);
@@ -597,7 +619,11 @@
' allowNonSuggestedValues',
() => {
const commitStub = sinon.stub(element, '_commit');
- element.queryErrorMessage = 'Error';
+ element.queryStatus = {
+ type: AutocompleteQueryStatusType.ERROR,
+ message: 'Error',
+ };
+ element.suggestions = [];
element.handleInputCommit();
assert.isFalse(commitStub.called);
}
@@ -620,7 +646,11 @@
() => {
const commitStub = sinon.stub(element, '_commit');
element.allowNonSuggestedValues = true;
- element.queryErrorMessage = 'Error';
+ element.queryStatus = {
+ type: AutocompleteQueryStatusType.ERROR,
+ message: 'Error',
+ };
+ element.suggestions = [];
element.handleInputCommit();
assert.isTrue(commitStub.called);
}
@@ -629,6 +659,7 @@
test('handleInputCommit with autocomplete open calls commit', () => {
const commitStub = sinon.stub(element, '_commit');
suggestionsEl().isHidden = false;
+ element.suggestions = [{name: 'first suggestion'}];
element.handleInputCommit();
assert.isTrue(commitStub.calledOnce);
});
@@ -671,6 +702,79 @@
assert.equal(element.text, 'file:x');
});
+ test('render loading replace with suggestions when done', async () => {
+ 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...',
+ });
+
+ queryPromise.resolve([{name: 'suggestion 1'}] as AutocompleteSuggestion[]);
+ await element.latestSuggestionUpdateComplete;
+ await element.updateComplete;
+
+ assert.equal(element.suggestions.length, 1);
+ assert.isUndefined(element.queryStatus);
+ });
+
+ test('render loading replace with error when done', async () => {
+ 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...',
+ });
+
+ queryPromise.reject(new Error('Test error'));
+ await assertFails(queryPromise);
+ await element.latestSuggestionUpdateComplete;
+ await element.updateComplete;
+
+ assert.equal(element.suggestions.length, 0);
+ assert.deepEqual(element.queryStatus, {
+ type: AutocompleteQueryStatusType.ERROR,
+ message: 'Test error',
+ });
+ });
+
+ test('render loading esc cancels', async () => {
+ 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...',
+ });
+
+ const cancelHandler = sinon.spy();
+ element.addEventListener('cancel', cancelHandler);
+ pressKey(inputEl(), Key.ESC);
+ await waitUntil(() => suggestionsEl().isHidden);
+
+ assert.isFalse(cancelHandler.called);
+ assert.isUndefined(element.queryStatus);
+
+ pressKey(inputEl(), Key.ESC);
+ await element.updateComplete;
+
+ assert.isTrue(cancelHandler.called);
+ });
+
suite('focus', () => {
let commitSpy: sinon.SinonSpy;
let focusSpy: sinon.SinonSpy;
@@ -688,13 +792,16 @@
await element.updateComplete;
assert.equal(element.suggestions.length, 0);
- assert.isUndefined(element.queryErrorMessage);
+ assert.isUndefined(element.queryStatus);
assert.isTrue(suggestionsEl().isHidden);
});
test('enter in input does not re-render error', async () => {
element.allowNonSuggestedValues = true;
- element.queryErrorMessage = 'Error message';
+ element.queryStatus = {
+ type: AutocompleteQueryStatusType.ERROR,
+ message: 'Error message',
+ };
pressKey(inputEl(), Key.ENTER);
@@ -702,7 +809,7 @@
await element.updateComplete;
assert.equal(element.suggestions.length, 0);
- assert.isUndefined(element.queryErrorMessage);
+ assert.isUndefined(element.queryStatus);
assert.isTrue(suggestionsEl().isHidden);
});
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.ts b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.ts
index d916118..3bb058e 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.ts
@@ -295,7 +295,10 @@
suggestions = [{name: 'value text 1'}, {name: 'value text 2'}];
await element.open();
+ // Waiting until dropdown not hidden, will ensure dialog is open and input
+ // is focused, but not that the suggestion has loaded.
await waitUntil(() => !autocomplete.suggestionsDropdown!.isHidden);
+ await autocomplete.latestSuggestionUpdateComplete;
pressKey(autocomplete.input!, Key.ENTER);
@@ -312,7 +315,11 @@
test('autocomplete suggestions closed enter saves suggestion', async () => {
suggestions = [{name: 'value text 1'}, {name: 'value text 2'}];
await element.open();
+ // Waiting until dropdown not hidden, will ensure dialog is open and input
+ // is focused, but not that the suggestion has loaded.
await waitUntil(() => !autocomplete.suggestionsDropdown!.isHidden);
+ await autocomplete.latestSuggestionUpdateComplete;
+
// Press enter to close suggestions.
pressKey(autocomplete.input!, Key.ENTER);
diff --git a/polygerrit-ui/app/utils/async-util.ts b/polygerrit-ui/app/utils/async-util.ts
index 752de62..905b16c 100644
--- a/polygerrit-ui/app/utils/async-util.ts
+++ b/polygerrit-ui/app/utils/async-util.ts
@@ -62,6 +62,8 @@
/**
* Promise that is resolved after the callback is run or the task is
* cancelled.
+ *
+ * If callback returns a Promise this resolves after the promise is settled.
*/
public readonly promise: Promise<ResolvedDelayedTaskStatus>;
@@ -69,14 +71,25 @@
value: ResolvedDelayedTaskStatus | PromiseLike<ResolvedDelayedTaskStatus>
) => void;
- constructor(private callback: () => void, waitMs = 0) {
+ private callCallbackAndResolveOnCompletion() {
+ let callbackResult;
+ if (this.callback) callbackResult = this.callback();
+ if (callbackResult instanceof Promise) {
+ callbackResult.finally(() =>
+ this.resolvePromise!(ResolvedDelayedTaskStatus.CALLBACK_EXECUTED)
+ );
+ } else {
+ this.resolvePromise!(ResolvedDelayedTaskStatus.CALLBACK_EXECUTED);
+ }
+ }
+
+ constructor(private callback: () => void | Promise<void>, waitMs = 0) {
this.promise = new Promise(resolve => {
this.resolvePromise = resolve;
this.timerId = window.setTimeout(() => {
if (this.timerId) _testOnly_allTasks.delete(this.timerId);
this.timerId = undefined;
- if (this.callback) this.callback();
- resolve(ResolvedDelayedTaskStatus.CALLBACK_EXECUTED);
+ this.callCallbackAndResolveOnCompletion();
}, waitMs);
_testOnly_allTasks.set(this.timerId, this);
});
@@ -98,8 +111,7 @@
flush() {
if (this.isActive()) {
this.cancelTimer();
- if (this.callback) this.callback();
- this.resolvePromise?.(ResolvedDelayedTaskStatus.CALLBACK_EXECUTED);
+ this.callCallbackAndResolveOnCompletion();
}
}
diff --git a/polygerrit-ui/app/utils/async-util_test.ts b/polygerrit-ui/app/utils/async-util_test.ts
index 9f029b8..ee4f73a 100644
--- a/polygerrit-ui/app/utils/async-util_test.ts
+++ b/polygerrit-ui/app/utils/async-util_test.ts
@@ -6,8 +6,8 @@
import {assert} from '@open-wc/testing';
import {SinonFakeTimers} from 'sinon';
import '../test/common-test-setup';
-import {waitEventLoop} from '../test/test-utils';
-import {asyncForeach, debounceP} from './async-util';
+import {mockPromise, waitEventLoop, waitUntil} from '../test/test-utils';
+import {asyncForeach, debounceP, DelayedTask} from './async-util';
suite('async-util tests', () => {
suite('asyncForeach', () => {
@@ -205,4 +205,16 @@
await waitEventLoop();
});
});
+
+ test('DelayedTask promise resolved when callback is done', async () => {
+ const callbackPromise = mockPromise<void>();
+ const task = new DelayedTask(() => callbackPromise);
+ let completed = false;
+ task.promise.then(() => (completed = true));
+ await waitUntil(() => !task.isActive());
+
+ assert.isFalse(completed);
+ callbackPromise.resolve();
+ await waitUntil(() => completed);
+ });
});