Merge "Add the ability to display error to gr-autocomplete."
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 26c8a50..91e601c 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
@@ -11,6 +11,7 @@
import {FitController} from '../../lit/fit-controller';
import {css, html, LitElement, PropertyValues} from 'lit';
import {customElement, property, query} from 'lit/decorators.js';
+import {when} from 'lit/directives/when.js';
import {repeat} from 'lit/directives/repeat.js';
import {sharedStyles} from '../../../styles/shared-styles';
import {ShortcutController} from '../../lit/shortcut-controller';
@@ -54,6 +55,12 @@
@property({type: Boolean, reflect: true, attribute: 'is-hidden'})
isHidden = true;
+ /** If specified a single non-interactable line is shown instead of
+ * suggestions.
+ */
+ @property({type: String})
+ errorMessage?: String;
+
@property({type: Number})
verticalOffset = 0;
@@ -110,6 +117,12 @@
li.selected {
background-color: var(--hover-background-color);
}
+ li.query-error {
+ background-color: var(--disabled-background);
+ color: var(--error-foreground);
+ cursor: default;
+ white-space: pre-wrap;
+ }
@media only screen and (max-height: 35em) {
.dropdown-content {
max-height: 80vh;
@@ -126,25 +139,25 @@
];
}
+ private isSuggestionListInteractible() {
+ return !this.isHidden && !this.errorMessage;
+ }
+
constructor() {
super();
this.cursor.cursorTargetClass = 'selected';
this.cursor.focusOnMove = true;
this.shortcuts.addLocal({key: Key.UP, allowRepeat: true}, () =>
- this.handleUp()
+ this.cursorUp()
);
this.shortcuts.addLocal({key: Key.DOWN, allowRepeat: true}, () =>
- this.handleDown()
+ this.cursorDown()
);
this.shortcuts.addLocal({key: Key.ENTER}, () => this.handleEnter());
this.shortcuts.addLocal({key: Key.ESC}, () => this.handleEscape());
this.shortcuts.addLocal({key: Key.TAB}, () => this.handleTab());
}
- override connectedCallback() {
- super.connectedCallback();
- }
-
override disconnectedCallback() {
this.cursor.unsetCursor();
super.disconnectedCallback();
@@ -167,27 +180,46 @@
}
}
+ private renderError() {
+ return html`
+ <li
+ tabindex="-1"
+ aria-label="autocomplete query error"
+ class="query-error"
+ >
+ <span>${this.errorMessage}</span>
+ <span class="label">ERROR</span>
+ </li>
+ `;
+ }
+
override render() {
return html`
<div class="dropdown-content" id="suggestions" role="listbox">
<ul>
- ${repeat(
- this.suggestions,
- (item, index) => html`
- <li
- data-index=${index}
- data-value=${item.dataValue ?? ''}
- tabindex="-1"
- aria-label=${item.name ?? ''}
- class="autocompleteOption"
- role="option"
- @click=${this.handleClickItem}
- >
- <span>${item.text}</span>
- <span class="label ${this.computeLabelClass(item)}"
- >${item.label}</span
- >
- </li>
+ ${when(
+ this.errorMessage,
+ () => this.renderError(),
+ () => html`
+ ${repeat(
+ this.suggestions,
+ (item, index) => html`
+ <li
+ data-index=${index}
+ data-value=${item.dataValue ?? ''}
+ tabindex="-1"
+ aria-label=${item.name ?? ''}
+ class="autocompleteOption"
+ role="option"
+ @click=${this.handleClickItem}
+ >
+ <span>${item.text}</span>
+ <span class="label ${this.computeLabelClass(item)}"
+ >${item.label}</span
+ >
+ </li>
+ `
+ )}
`
)}
</ul>
@@ -204,55 +236,54 @@
}
getCurrentText() {
- return this.getCursorTarget()?.dataset['value'] || '';
+ if (!this.errorMessage) {
+ return this.getCursorTarget()?.dataset['value'] || '';
+ }
+ return '';
}
setPositionTarget(target: HTMLElement) {
this.fitController.setPositionTarget(target);
}
- private handleUp() {
- if (!this.isHidden) this.cursorUp();
- }
-
- private handleDown() {
- if (!this.isHidden) this.cursorDown();
- }
-
cursorDown() {
- if (!this.isHidden) this.cursor.next();
+ if (this.isSuggestionListInteractible()) this.cursor.next();
}
cursorUp() {
- if (!this.isHidden) this.cursor.previous();
+ if (this.isSuggestionListInteractible()) this.cursor.previous();
}
// private but used in tests
handleTab() {
- this.dispatchEvent(
- new CustomEvent<ItemSelectedEvent>('item-selected', {
- detail: {
- trigger: 'tab',
- selected: this.cursor.target,
- },
- composed: true,
- bubbles: true,
- })
- );
+ if (this.isSuggestionListInteractible()) {
+ this.dispatchEvent(
+ new CustomEvent<ItemSelectedEvent>('item-selected', {
+ detail: {
+ trigger: 'tab',
+ selected: this.cursor.target,
+ },
+ composed: true,
+ bubbles: true,
+ })
+ );
+ }
}
// private but used in tests
handleEnter() {
- this.dispatchEvent(
- new CustomEvent<ItemSelectedEvent>('item-selected', {
- detail: {
- trigger: 'enter',
- selected: this.cursor.target,
- },
- composed: true,
- bubbles: true,
- })
- );
+ if (this.isSuggestionListInteractible()) {
+ this.dispatchEvent(
+ new CustomEvent<ItemSelectedEvent>('item-selected', {
+ detail: {
+ trigger: 'enter',
+ selected: this.cursor.target,
+ },
+ composed: true,
+ bubbles: true,
+ })
+ );
+ }
}
private handleEscape() {
@@ -293,7 +324,7 @@
computeCursorStopsAndRefit() {
if (this.suggestions.length > 0) {
this.cursor.stops = Array.from(
- this.suggestionsDiv?.querySelectorAll('li') ?? []
+ this.suggestionsDiv?.querySelectorAll('li.autocompleteOption') ?? []
);
this.resetCursorIndex();
} else {
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 cb83396..54d054b 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
@@ -18,155 +18,233 @@
import {Key} from '../../../utils/dom-util';
suite('gr-autocomplete-dropdown', () => {
- let element: GrAutocompleteDropdown;
+ suite('suggestion tests', () => {
+ let element: GrAutocompleteDropdown;
- const suggestionsEl = () => queryAndAssert(element, '#suggestions');
+ const suggestionsEl = () => queryAndAssert(element, '#suggestions');
- setup(async () => {
- element = await fixture(
- html`<gr-autocomplete-dropdown></gr-autocomplete-dropdown>`
- );
- element.open();
- element.suggestions = [
- {dataValue: 'test value 1', name: 'test name 1', text: '1', label: 'hi'},
- {dataValue: 'test value 2', name: 'test name 2', text: '2'},
- ];
- await waitEventLoop();
- });
+ setup(async () => {
+ element = await fixture(
+ html`<gr-autocomplete-dropdown></gr-autocomplete-dropdown>`
+ );
+ element.open();
+ element.suggestions = [
+ {
+ dataValue: 'test value 1',
+ name: 'test name 1',
+ text: '1',
+ label: 'hi',
+ },
+ {dataValue: 'test value 2', name: 'test name 2', text: '2'},
+ ];
+ await waitEventLoop();
+ });
- teardown(() => {
- element.close();
- });
+ teardown(() => {
+ element.close();
+ });
- test('renders', () => {
- assert.shadowDom.equal(
- element,
- /* HTML */ `
- <div class="dropdown-content" id="suggestions" role="listbox">
- <ul>
- <li
- aria-label="test name 1"
- class="autocompleteOption selected"
- data-index="0"
- data-value="test value 1"
- role="option"
- tabindex="-1"
- >
- <span> 1 </span>
- <span class="label"> hi </span>
- </li>
- <li
- aria-label="test name 2"
- class="autocompleteOption"
- data-index="1"
- data-value="test value 2"
- role="option"
- tabindex="-1"
- >
- <span> 2 </span>
- <span class="hide label"> </span>
- </li>
- </ul>
- </div>
- `
- );
- });
+ test('renders', () => {
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ `
+ <div class="dropdown-content" id="suggestions" role="listbox">
+ <ul>
+ <li
+ aria-label="test name 1"
+ class="autocompleteOption selected"
+ data-index="0"
+ data-value="test value 1"
+ role="option"
+ tabindex="-1"
+ >
+ <span> 1 </span>
+ <span class="label"> hi </span>
+ </li>
+ <li
+ aria-label="test name 2"
+ class="autocompleteOption"
+ data-index="1"
+ data-value="test value 2"
+ role="option"
+ tabindex="-1"
+ >
+ <span> 2 </span>
+ <span class="hide label"> </span>
+ </li>
+ </ul>
+ </div>
+ `
+ );
+ });
- test('shows labels', () => {
- const els = queryAll<HTMLElement>(suggestionsEl(), 'li');
- assert.equal(els[0].innerText.trim(), '1\nhi');
- assert.equal(els[1].innerText.trim(), '2');
- });
+ test('shows labels', () => {
+ const els = queryAll<HTMLElement>(suggestionsEl(), 'li');
+ assert.equal(els[0].innerText.trim(), '1\nhi');
+ assert.equal(els[1].innerText.trim(), '2');
+ });
- test('escape key', async () => {
- const closeSpy = sinon.spy(element, 'close');
- pressKey(element, Key.ESC);
- await waitEventLoop();
- assert.isTrue(closeSpy.called);
- });
+ test('escape key close suggestions', async () => {
+ const closeSpy = sinon.spy(element, 'close');
+ pressKey(element, Key.ESC);
+ await waitEventLoop();
+ assert.isTrue(closeSpy.called);
+ });
- test('tab key', () => {
- const handleTabSpy = sinon.spy(element, 'handleTab');
- const itemSelectedStub = sinon.stub();
- element.addEventListener('item-selected', itemSelectedStub);
- pressKey(element, Key.TAB);
- assert.isTrue(handleTabSpy.called);
- assert.equal(element.cursor.index, 0);
- assert.isTrue(itemSelectedStub.called);
- assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, {
- trigger: 'tab',
- selected: element.getCursorTarget(),
+ test('tab key', () => {
+ const handleTabSpy = sinon.spy(element, 'handleTab');
+ const itemSelectedStub = sinon.stub();
+ element.addEventListener('item-selected', itemSelectedStub);
+ pressKey(element, Key.TAB);
+ assert.isTrue(handleTabSpy.called);
+ assert.equal(element.cursor.index, 0);
+ assert.isTrue(itemSelectedStub.called);
+ assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, {
+ trigger: 'tab',
+ selected: element.getCursorTarget(),
+ });
+ });
+
+ test('enter key', () => {
+ const handleEnterSpy = sinon.spy(element, 'handleEnter');
+ const itemSelectedStub = sinon.stub();
+ element.addEventListener('item-selected', itemSelectedStub);
+ pressKey(element, Key.ENTER);
+ assert.isTrue(handleEnterSpy.called);
+ assert.equal(element.cursor.index, 0);
+ assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, {
+ trigger: 'enter',
+ selected: element.getCursorTarget(),
+ });
+ });
+
+ test('down key', () => {
+ element.isHidden = true;
+ const nextSpy = sinon.spy(element.cursor, 'next');
+ pressKey(element, 'ArrowDown');
+ assert.isFalse(nextSpy.called);
+ assert.equal(element.cursor.index, 0);
+ element.isHidden = false;
+ pressKey(element, 'ArrowDown');
+ assert.isTrue(nextSpy.called);
+ assert.equal(element.cursor.index, 1);
+ });
+
+ test('up key', () => {
+ element.isHidden = true;
+ const prevSpy = sinon.spy(element.cursor, 'previous');
+ pressKey(element, 'ArrowUp');
+ assert.isFalse(prevSpy.called);
+ assert.equal(element.cursor.index, 0);
+ element.isHidden = false;
+ element.cursor.setCursorAtIndex(1);
+ assert.equal(element.cursor.index, 1);
+ pressKey(element, 'ArrowUp');
+ assert.isTrue(prevSpy.called);
+ assert.equal(element.cursor.index, 0);
+ });
+
+ test('tapping selects item', async () => {
+ const itemSelectedStub = sinon.stub();
+ element.addEventListener('item-selected', itemSelectedStub);
+
+ suggestionsEl().querySelectorAll('li')[1].click();
+ await waitEventLoop();
+ assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, {
+ trigger: 'click',
+ selected: suggestionsEl().querySelectorAll('li')[1],
+ });
+ });
+
+ test('tapping child still selects item', async () => {
+ const itemSelectedStub = sinon.stub();
+ element.addEventListener('item-selected', itemSelectedStub);
+ const lastElChild = queryAll<HTMLLIElement>(suggestionsEl(), 'li')[0]
+ ?.lastElementChild;
+ assertIsDefined(lastElChild);
+ (lastElChild as HTMLSpanElement).click();
+ await waitEventLoop();
+ assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, {
+ trigger: 'click',
+ selected: queryAll<HTMLElement>(suggestionsEl(), 'li')[0],
+ });
+ });
+
+ test('updated suggestions resets cursor stops', async () => {
+ const resetStopsSpy = sinon.spy(element, 'computeCursorStopsAndRefit');
+ element.suggestions = [];
+ await waitUntil(() => resetStopsSpy.called);
});
});
- test('enter key', () => {
- const handleEnterSpy = sinon.spy(element, 'handleEnter');
- const itemSelectedStub = sinon.stub();
- element.addEventListener('item-selected', itemSelectedStub);
- pressKey(element, Key.ENTER);
- assert.isTrue(handleEnterSpy.called);
- assert.equal(element.cursor.index, 0);
- assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, {
- trigger: 'enter',
- selected: element.getCursorTarget(),
+ suite('error tests', () => {
+ let element: GrAutocompleteDropdown;
+
+ setup(async () => {
+ element = await fixture(
+ html`<gr-autocomplete-dropdown></gr-autocomplete-dropdown>`
+ );
+ element.open();
+ element.errorMessage = 'Failed query error';
+ await waitEventLoop();
});
- });
- test('down key', () => {
- element.isHidden = true;
- const nextSpy = sinon.spy(element.cursor, 'next');
- pressKey(element, 'ArrowDown');
- assert.isFalse(nextSpy.called);
- assert.equal(element.cursor.index, 0);
- element.isHidden = false;
- pressKey(element, 'ArrowDown');
- assert.isTrue(nextSpy.called);
- assert.equal(element.cursor.index, 1);
- });
-
- test('up key', () => {
- element.isHidden = true;
- const prevSpy = sinon.spy(element.cursor, 'previous');
- pressKey(element, 'ArrowUp');
- assert.isFalse(prevSpy.called);
- assert.equal(element.cursor.index, 0);
- element.isHidden = false;
- element.cursor.setCursorAtIndex(1);
- assert.equal(element.cursor.index, 1);
- pressKey(element, 'ArrowUp');
- assert.isTrue(prevSpy.called);
- assert.equal(element.cursor.index, 0);
- });
-
- test('tapping selects item', async () => {
- const itemSelectedStub = sinon.stub();
- element.addEventListener('item-selected', itemSelectedStub);
-
- suggestionsEl().querySelectorAll('li')[1].click();
- await waitEventLoop();
- assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, {
- trigger: 'click',
- selected: suggestionsEl().querySelectorAll('li')[1],
+ teardown(() => {
+ element.close();
});
- });
- test('tapping child still selects item', async () => {
- const itemSelectedStub = sinon.stub();
- element.addEventListener('item-selected', itemSelectedStub);
- const lastElChild = queryAll<HTMLLIElement>(suggestionsEl(), 'li')[0]
- ?.lastElementChild;
- assertIsDefined(lastElChild);
- (lastElChild as HTMLSpanElement).click();
- await waitEventLoop();
- assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, {
- trigger: 'click',
- selected: queryAll<HTMLElement>(suggestionsEl(), 'li')[0],
+ test('renders error', () => {
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ `
+ <div class="dropdown-content" id="suggestions" role="listbox">
+ <ul>
+ <li
+ aria-label="autocomplete query error"
+ class="query-error"
+ tabindex="-1"
+ >
+ <span>Failed query error</span>
+ <span class="label">ERROR</span>
+ </li>
+ </ul>
+ </div>
+ `
+ );
});
- });
- test('updated suggestions resets cursor stops', async () => {
- const resetStopsSpy = sinon.spy(element, 'computeCursorStopsAndRefit');
- element.suggestions = [];
- await waitUntil(() => resetStopsSpy.called);
+ test('escape key close dropdown with error', async () => {
+ const closeSpy = sinon.spy(element, 'close');
+ pressKey(element, Key.ESC);
+ await waitEventLoop();
+ assert.isTrue(closeSpy.called);
+ });
+
+ test('tab key when error shown sends no event', () => {
+ const handleTabSpy = sinon.spy(element, 'handleTab');
+ const itemSelectedStub = sinon.stub();
+ element.addEventListener('item-selected', itemSelectedStub);
+ pressKey(element, Key.TAB);
+ assert.isTrue(handleTabSpy.called);
+ assert.isFalse(itemSelectedStub.called);
+ });
+
+ test('enter key when error shown sends no event', () => {
+ const handleEnterSpy = sinon.spy(element, 'handleEnter');
+ const itemSelectedStub = sinon.stub();
+ element.addEventListener('item-selected', itemSelectedStub);
+ pressKey(element, Key.ENTER);
+ assert.isTrue(handleEnterSpy.called);
+ assert.isFalse(itemSelectedStub.called);
+ });
+
+ test('up/down disabled when error', () => {
+ const nextSpy = sinon.spy(element.cursor, 'next');
+ const prevSpy = sinon.spy(element.cursor, 'previous');
+ pressKey(element, 'ArrowUp');
+ pressKey(element, 'ArrowDown');
+ assert.isFalse(nextSpy.called);
+ assert.isFalse(prevSpy.called);
+ });
});
});
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 34e67b9..f7f8ea5 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
@@ -81,6 +81,9 @@
* next to the "name" as label text. The "value" property will be emitted
* if that suggestion is selected.
*
+ * If query fails, the function should return rejected promise containing
+ * an Error. The "message" property will be shown in a dropdown instead of
+ * rendering suggestions.
*/
@property({type: Object})
query?: AutocompleteQuery = () => Promise.resolve([]);
@@ -169,6 +172,8 @@
@state() suggestions: AutocompleteSuggestion[] = [];
+ @state() queryErrorMessage?: string;
+
@state() index: number | null = null;
// Enabled to suppress showing/updating suggestions when changing properties
@@ -269,7 +274,10 @@
) {
this.updateSuggestions();
}
- if (changedProperties.has('suggestions')) {
+ if (
+ changedProperties.has('suggestions') ||
+ changedProperties.has('queryErrorMessage')
+ ) {
this.updateDropdownVisibility();
}
if (changedProperties.has('text')) {
@@ -317,6 +325,7 @@
@item-selected=${this.handleItemSelect}
@dropdown-closed=${this.focusWithoutDisplayingSuggestions}
.suggestions=${this.suggestions}
+ .errorMessage=${this.queryErrorMessage}
role="listbox"
.index=${this.index}
>
@@ -424,6 +433,7 @@
// This will also prevent from carrying over suggestions:
// @see Issue 12039
this.suggestions = [];
+ this.queryErrorMessage = undefined;
// TODO(taoalpha): Also skip if text has not changed
@@ -446,19 +456,28 @@
}
const update = () => {
- query(this.text).then(suggestions => {
- if (this.text !== this.text) {
- // Late response.
- return;
- }
- for (const suggestion of suggestions) {
- suggestion.text = suggestion?.name ?? '';
- }
- this.suggestions = suggestions;
- if (this.index === -1) {
+ query(this.text)
+ .then(suggestions => {
+ if (this.text !== 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;
+ }
+ });
};
if (this.noDebounce) {
@@ -479,7 +498,10 @@
}
updateDropdownVisibility() {
- if (this.suggestions.length > 0 && this.focused) {
+ if (
+ (this.suggestions.length > 0 || this.queryErrorMessage) &&
+ this.focused
+ ) {
this.suggestionsDropdown?.open();
return;
}
@@ -526,6 +548,7 @@
}
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();
@@ -553,8 +576,9 @@
}
cancel() {
- if (this.suggestions.length) {
+ if (this.suggestions.length || this.queryErrorMessage) {
this.suggestions = [];
+ this.queryErrorMessage = undefined;
this.requestUpdate();
} else {
fireEvent(this, 'cancel');
@@ -562,8 +586,11 @@
}
handleInputCommit(_tabComplete?: boolean) {
- // Nothing to do if the dropdown is not open.
- if (!this.allowNonSuggestedValues && this.suggestionsDropdown?.isHidden) {
+ // Nothing to do if no suggestions.
+ if (
+ !this.allowNonSuggestedValues &&
+ (this.suggestionsDropdown?.isHidden || this.queryErrorMessage)
+ ) {
return;
}
@@ -641,6 +668,7 @@
}
this.suggestions = [];
+ this.queryErrorMessage = undefined;
// we need willUpdate to send text-changed event before we can send the
// 'commit' event
await this.updateComplete;
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 d991180..e593c0d 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
@@ -6,7 +6,12 @@
import '../../../test/common-test-setup';
import './gr-autocomplete';
import {AutocompleteSuggestion, GrAutocomplete} from './gr-autocomplete';
-import {pressKey, queryAndAssert, waitUntil} from '../../../test/test-utils';
+import {
+ assertFails,
+ pressKey,
+ queryAndAssert,
+ waitUntil,
+} from '../../../test/test-utils';
import {GrAutocompleteDropdown} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
import {PaperInputElement} from '@polymer/paper-input/paper-input';
import {fixture, html, assert} from '@open-wc/testing';
@@ -109,6 +114,46 @@
);
});
+ test('renders with error', async () => {
+ const queryStub = sinon.spy((input: string) =>
+ Promise.reject(new Error(`${input} not allowed`))
+ );
+ element.query = queryStub;
+
+ focusOnInput();
+ element.text = 'blah';
+ await waitUntil(() => queryStub.called);
+ await element.updateComplete;
+
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ `
+ <paper-input
+ aria-disabled="false"
+ autocomplete="off"
+ id="input"
+ tabindex="0"
+ >
+ <div slot="prefix">
+ <gr-icon icon="search" class="searchIcon"></gr-icon>
+ </div>
+ <div slot="suffix">
+ <slot name="suffix"> </slot>
+ </div>
+ </paper-input>
+ <gr-autocomplete-dropdown id="suggestions" role="listbox">
+ </gr-autocomplete-dropdown>
+ `,
+ {
+ // gr-autocomplete-dropdown sizing seems to vary between local & CI
+ ignoreAttributes: [
+ {tags: ['gr-autocomplete-dropdown'], attributes: ['style']},
+ ],
+ }
+ );
+ assert.equal(element.suggestionsDropdown?.errorMessage, 'blah not allowed');
+ });
+
test('cursor starts on suggestions', async () => {
const queryStub = sinon.spy((input: string) =>
Promise.resolve([
@@ -181,6 +226,39 @@
});
});
+ test('esc key behavior on error', async () => {
+ let promise: Promise<AutocompleteSuggestion[]> = Promise.resolve([]);
+ const queryStub = sinon.spy(
+ (_: string) => (promise = Promise.reject(new Error('Test error')))
+ );
+ element.query = queryStub;
+
+ assert.isTrue(suggestionsEl().isHidden);
+
+ element.setFocus(true);
+ element.text = 'blah';
+ await element.updateComplete;
+
+ return assertFails(promise).then(async () => {
+ await waitUntil(() => !suggestionsEl().isHidden);
+
+ const cancelHandler = sinon.spy();
+ element.addEventListener('cancel', cancelHandler);
+ assert.equal(element.queryErrorMessage, 'Test error');
+
+ pressKey(inputEl(), Key.ESC);
+ await waitUntil(() => suggestionsEl().isHidden);
+
+ assert.isFalse(cancelHandler.called);
+ assert.isUndefined(element.queryErrorMessage);
+
+ pressKey(inputEl(), Key.ESC);
+ await element.updateComplete;
+
+ assert.isTrue(cancelHandler.called);
+ });
+ });
+
test('emits commit and handles cursor movement', async () => {
let promise: Promise<AutocompleteSuggestion[]> = Promise.resolve([]);
const queryStub = sinon.spy(
@@ -403,6 +481,25 @@
});
});
+ test('error should not carry over', async () => {
+ let promise: Promise<AutocompleteSuggestion[]> = Promise.resolve([]);
+ const queryStub = sinon
+ .stub()
+ .returns((promise = Promise.reject(new Error('Test error'))));
+ element.query = queryStub;
+ focusOnInput();
+ element.text = 'bla';
+ await element.updateComplete;
+ return assertFails(promise).then(async () => {
+ await waitUntil(() => element.queryErrorMessage === 'Test error');
+ element.text = '';
+ element.threshold = 0;
+ element.noDebounce = false;
+ await element.updateComplete;
+ assert.isUndefined(element.queryErrorMessage);
+ });
+ });
+
test('multi completes only the last part of the query', async () => {
let promise;
const queryStub = sinon
@@ -504,7 +601,7 @@
test(
'handleInputCommit with autocomplete hidden does nothing without' +
- 'without allowNonSuggestedValues',
+ ' allowNonSuggestedValues',
() => {
const commitStub = sinon.stub(element, '_commit');
suggestionsEl().isHidden = true;
@@ -514,6 +611,17 @@
);
test(
+ 'handleInputCommit with query error does nothing without' +
+ ' allowNonSuggestedValues',
+ () => {
+ const commitStub = sinon.stub(element, '_commit');
+ element.queryErrorMessage = 'Error';
+ element.handleInputCommit();
+ assert.isFalse(commitStub.called);
+ }
+ );
+
+ test(
'handleInputCommit with autocomplete hidden with' +
'allowNonSuggestedValues',
() => {
@@ -525,6 +633,17 @@
}
);
+ test(
+ 'handleInputCommit with query error with' + 'allowNonSuggestedValues',
+ () => {
+ const commitStub = sinon.stub(element, '_commit');
+ element.allowNonSuggestedValues = true;
+ element.queryErrorMessage = 'Error';
+ element.handleInputCommit();
+ assert.isTrue(commitStub.called);
+ }
+ );
+
test('handleInputCommit with autocomplete open calls commit', () => {
const commitStub = sinon.stub(element, '_commit');
suggestionsEl().isHidden = false;
@@ -587,6 +706,21 @@
await element.updateComplete;
assert.equal(element.suggestions.length, 0);
+ assert.isUndefined(element.queryErrorMessage);
+ assert.isTrue(suggestionsEl().isHidden);
+ });
+
+ test('enter in input does not re-render error', async () => {
+ element.allowNonSuggestedValues = true;
+ element.queryErrorMessage = 'Error message';
+
+ pressKey(inputEl(), Key.ENTER);
+
+ await waitUntil(() => commitSpy.called);
+ await element.updateComplete;
+
+ assert.equal(element.suggestions.length, 0);
+ assert.isUndefined(element.queryErrorMessage);
assert.isTrue(suggestionsEl().isHidden);
});