| /** |
| * @license |
| * Copyright 2016 Google LLC |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| import * as sinon from 'sinon'; |
| import '../../../test/common-test-setup'; |
| import './gr-autocomplete'; |
| import {AutocompleteSuggestion, GrAutocomplete} from './gr-autocomplete'; |
| import { |
| assertFails, |
| mockPromise, |
| pressKey, |
| queryAndAssert, |
| waitUntil, |
| } from '../../../test/test-utils'; |
| 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'; |
| |
| suite('gr-autocomplete tests', () => { |
| let element: GrAutocomplete; |
| |
| const focusOnInput = () => { |
| pressKey(inputEl(), Key.ENTER); |
| }; |
| |
| const suggestionsEl = () => |
| queryAndAssert<GrAutocompleteDropdown>(element, '#suggestions'); |
| |
| const inputEl = () => queryAndAssert<HTMLInputElement>(element, '#input'); |
| |
| setup(async () => { |
| element = await fixture(html`<gr-autocomplete></gr-autocomplete>`); |
| }); |
| |
| test('renders', () => { |
| assert.shadowDom.equal( |
| element, |
| /* HTML */ ` |
| <paper-input |
| aria-disabled="false" |
| autocomplete="off" |
| id="input" |
| tabindex="0" |
| > |
| <div slot="prefix"> |
| <slot name="prefix"> </slot> |
| </div> |
| <div slot="suffix"> |
| <slot name="suffix"> </slot> |
| </div> |
| </paper-input> |
| <gr-autocomplete-dropdown |
| id="suggestions" |
| is-hidden="" |
| role="listbox" |
| style="position: fixed; top: 300px; left: 392.5px; box-sizing: border-box; max-height: 600px; max-width: 785px;" |
| > |
| </gr-autocomplete-dropdown> |
| `, |
| { |
| // gr-autocomplete-dropdown sizing seems to vary between local & CI |
| ignoreAttributes: [ |
| {tags: ['gr-autocomplete-dropdown'], attributes: ['style']}, |
| ], |
| } |
| ); |
| }); |
| |
| test('renders with suggestions', async () => { |
| 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; |
| |
| 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"> |
| <slot name="prefix"> </slot> |
| </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']}, |
| ], |
| } |
| ); |
| }); |
| |
| 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"> |
| <slot name="prefix"> </slot> |
| </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?.queryStatus?.message, |
| 'blah not allowed' |
| ); |
| }); |
| |
| test('cursor starts on suggestions', async () => { |
| 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; |
| |
| assert.equal(suggestionsEl().cursor.index, -1); |
| |
| focusOnInput(); |
| element.text = 'blah'; |
| await waitUntil(() => queryStub.called); |
| await element.updateComplete; |
| |
| assert.notEqual(suggestionsEl().cursor.index, -1); |
| }); |
| |
| test('selectAll', async () => { |
| await element.updateComplete; |
| const nativeInput = element.nativeInput; |
| const selectionStub = sinon.stub(nativeInput, 'setSelectionRange'); |
| |
| element.selectAll(); |
| await element.updateComplete; |
| assert.isFalse(selectionStub.called); |
| |
| inputEl().value = 'test'; |
| await element.updateComplete; |
| element.selectAll(); |
| assert.isTrue(selectionStub.called); |
| }); |
| |
| test('esc key behavior', async () => { |
| let promise: Promise<AutocompleteSuggestion[]> = Promise.resolve([]); |
| const queryStub = sinon.spy( |
| (_: string) => |
| (promise = Promise.resolve([ |
| {name: 'blah', value: '123'}, |
| ] as AutocompleteSuggestion[])) |
| ); |
| element.query = queryStub; |
| |
| assert.isTrue(suggestionsEl().isHidden); |
| |
| element.setFocus(true); |
| element.text = 'blah'; |
| await element.updateComplete; |
| |
| return promise.then(async () => { |
| await waitUntil(() => !suggestionsEl().isHidden); |
| |
| const cancelHandler = sinon.spy(); |
| element.addEventListener('cancel', cancelHandler); |
| |
| pressKey(inputEl(), Key.ESC); |
| await waitUntil(() => suggestionsEl().isHidden); |
| |
| assert.isFalse(cancelHandler.called); |
| assert.equal(element.suggestions.length, 0); |
| |
| pressKey(inputEl(), Key.ESC); |
| await element.updateComplete; |
| |
| assert.isTrue(cancelHandler.called); |
| }); |
| }); |
| |
| 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 element.latestSuggestionUpdateComplete; |
| await waitUntil(() => !suggestionsEl().isHidden); |
| |
| const cancelHandler = sinon.spy(); |
| element.addEventListener('cancel', cancelHandler); |
| 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.queryStatus); |
| |
| pressKey(inputEl(), Key.ESC); |
| await element.updateComplete; |
| |
| assert.isTrue(cancelHandler.called); |
| }); |
| }); |
| |
| test('emits commit and handles cursor movement', async () => { |
| 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; |
| assert.isTrue(suggestionsEl().isHidden); |
| assert.equal(suggestionsEl().cursor.index, -1); |
| element.setFocus(true); |
| |
| element.text = 'blah'; |
| await element.updateComplete; |
| |
| return element.latestSuggestionUpdateComplete!.then(async () => { |
| await waitUntil(() => !suggestionsEl().isHidden); |
| |
| const commitHandler = sinon.spy(); |
| element.addEventListener('commit', commitHandler); |
| |
| assert.equal(suggestionsEl().cursor.index, 0); |
| |
| pressKey(inputEl(), 'ArrowDown'); |
| await element.updateComplete; |
| |
| assert.equal(suggestionsEl().cursor.index, 1); |
| |
| pressKey(inputEl(), 'ArrowDown'); |
| await element.updateComplete; |
| |
| assert.equal(suggestionsEl().cursor.index, 2); |
| |
| pressKey(inputEl(), 'ArrowUp'); |
| await element.updateComplete; |
| |
| assert.equal(suggestionsEl().cursor.index, 1); |
| |
| pressKey(inputEl(), Key.ENTER); |
| await element.updateComplete; |
| |
| assert.equal(element.value, '1'); |
| |
| await waitUntil(() => commitHandler.called); |
| assert.equal(commitHandler.getCall(0).args[0].detail.value, 1); |
| assert.isTrue(suggestionsEl().isHidden); |
| assert.isTrue(element.focused); |
| }); |
| }); |
| |
| test('clear-on-commit behavior (off)', async () => { |
| let promise: Promise<AutocompleteSuggestion[]> = Promise.resolve([]); |
| const queryStub = sinon.spy(() => { |
| promise = Promise.resolve([ |
| {name: 'suggestion', value: '0'}, |
| ] as AutocompleteSuggestion[]); |
| return promise; |
| }); |
| element.query = queryStub; |
| focusOnInput(); |
| element.text = 'blah'; |
| await waitUntil(() => element.suggestions.length > 0); |
| |
| return promise.then(async () => { |
| const commitHandler = sinon.spy(); |
| element.addEventListener('commit', commitHandler); |
| |
| pressKey(inputEl(), Key.ENTER); |
| |
| await waitUntil(() => commitHandler.called); |
| |
| assert.equal(element.text, 'suggestion'); |
| }); |
| }); |
| |
| test('clear-on-commit behavior (on)', async () => { |
| let promise: Promise<AutocompleteSuggestion[]> = Promise.resolve([]); |
| const queryStub = sinon.spy(() => { |
| promise = Promise.resolve([ |
| {name: 'suggestion', value: '0'}, |
| ] as AutocompleteSuggestion[]); |
| return promise; |
| }); |
| element.query = queryStub; |
| focusOnInput(); |
| element.text = 'blah'; |
| |
| await waitUntil(() => element.suggestions.length > 0); |
| |
| element.clearOnCommit = true; |
| |
| return promise.then(async () => { |
| const commitHandler = sinon.spy(); |
| element.addEventListener('commit', commitHandler); |
| |
| pressKey(inputEl(), Key.ENTER); |
| |
| await waitUntil(() => commitHandler.called); |
| |
| assert.equal(element.text, ''); |
| }); |
| }); |
| |
| test('threshold guards the query', async () => { |
| const queryStub = sinon.spy(() => |
| Promise.resolve([] as AutocompleteSuggestion[]) |
| ); |
| element.query = queryStub; |
| element.threshold = 2; |
| focusOnInput(); |
| element.text = 'a'; |
| await element.updateComplete; |
| assert.isFalse(queryStub.called); |
| |
| element.text = 'ab'; |
| await element.updateComplete; |
| await waitUntil(() => queryStub.called); |
| }); |
| |
| test('noDebounce=false debounces the query', async () => { |
| const queryStub = sinon.spy(() => |
| Promise.resolve([] as AutocompleteSuggestion[]) |
| ); |
| |
| element.query = queryStub; |
| await element.updateComplete; |
| focusOnInput(); |
| element.text = 'a'; |
| |
| // not called right away |
| assert.isFalse(queryStub.called); |
| |
| await waitUntil(() => queryStub.called); |
| }); |
| |
| test('computeClass respects border property', () => { |
| element.borderless = false; |
| assert.equal(element.computeClass(), ''); |
| element.borderless = true; |
| assert.equal(element.computeClass(), 'borderless'); |
| element.showBlueFocusBorder = true; |
| assert.equal(element.computeClass(), 'borderless showBlueFocusBorder'); |
| }); |
| |
| test('empty text results in no suggestions', async () => { |
| element.text = ''; |
| element.threshold = 0; |
| await element.updateComplete; |
| assert.equal(element.suggestions.length, 0); |
| }); |
| |
| test('when focused', async () => { |
| let promise: Promise<AutocompleteSuggestion[]> = Promise.resolve([]); |
| const queryStub = sinon |
| .stub() |
| .returns( |
| (promise = Promise.resolve([ |
| {name: 'suggestion', value: '0'}, |
| ] as AutocompleteSuggestion[])) |
| ); |
| element.query = queryStub; |
| focusOnInput(); |
| element.text = 'bla'; |
| assert.equal(element.focused, true); |
| await element.updateComplete; |
| return promise.then(async () => { |
| await waitUntil(() => element.suggestions.length > 0); |
| assert.equal(element.suggestions.length, 1); |
| assert.equal(queryStub.notCalled, false); |
| }); |
| }); |
| |
| test('when not focused', async () => { |
| let promise: Promise<AutocompleteSuggestion[]> = Promise.resolve([]); |
| const queryStub = sinon |
| .stub() |
| .returns( |
| (promise = Promise.resolve([ |
| {name: 'suggestion', value: '0'}, |
| ] as AutocompleteSuggestion[])) |
| ); |
| element.query = queryStub; |
| element.text = 'bla'; |
| assert.equal(element.focused, false); |
| await element.updateComplete; |
| return promise.then(() => { |
| assert.equal(element.suggestions.length, 0); |
| }); |
| }); |
| |
| test('suggestions should not carry over', async () => { |
| const queryStub = sinon |
| .stub() |
| .resolves([{name: 'suggestion', value: '0'}] as AutocompleteSuggestion[]); |
| element.query = queryStub; |
| focusOnInput(); |
| element.text = 'bla'; |
| await element.updateComplete; |
| 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); |
| }); |
| }); |
| |
| 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 element.latestSuggestionUpdateComplete; |
| await waitUntil(() => element.queryStatus?.message === 'Test error'); |
| |
| queryStub.resolves([] as AutocompleteSuggestion[]); |
| element.text = ''; |
| element.threshold = 0; |
| await element.updateComplete; |
| await element.latestSuggestionUpdateComplete; |
| assert.isUndefined(element.queryStatus); |
| }); |
| }); |
| |
| test('multi completes only the last part of the query', async () => { |
| let promise; |
| const queryStub = sinon |
| .stub() |
| .returns( |
| (promise = Promise.resolve([ |
| {name: 'suggestion', value: '0'}, |
| ] as AutocompleteSuggestion[])) |
| ); |
| element.query = queryStub; |
| focusOnInput(); |
| element.text = 'blah blah'; |
| element.multi = true; |
| await element.updateComplete; |
| |
| 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); |
| |
| await waitUntil(() => commitHandler.called); |
| assert.equal(element.text, 'blah 0'); |
| }); |
| }); |
| |
| 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.tabComplete = false; |
| element.text = 'text1'; |
| await element.updateComplete; |
| |
| await element.latestSuggestionUpdateComplete; |
| await element.updateComplete; |
| pressKey(inputEl(), Key.TAB); |
| await element.updateComplete; |
| |
| assert.isFalse(commitHandler.called); |
| assert.isFalse(commitSpy.called); |
| assert.isFalse(element.focused); |
| |
| element.setFocus(true); |
| element.tabComplete = true; |
| element.text = 'text2'; |
| await element.updateComplete; |
| |
| await element.latestSuggestionUpdateComplete; |
| await element.updateComplete; |
| pressKey(inputEl(), Key.TAB); |
| |
| await waitUntil(() => commitSpy.called); |
| assert.isFalse(commitHandler.called); |
| assert.isTrue(element.focused); |
| }); |
| |
| test('focused flag properly triggered', async () => { |
| await element.updateComplete; |
| assert.isFalse(element.focused); |
| const input = queryAndAssert<PaperInputElement>(element, 'paper-input'); |
| input.focus(); |
| assert.isTrue(element.focused); |
| }); |
| |
| test('vertical offset overridden by param if it exists', async () => { |
| assert.equal(suggestionsEl().verticalOffset, 31); |
| |
| element.verticalOffset = 30; |
| await element.updateComplete; |
| |
| assert.equal(suggestionsEl().verticalOffset, 30); |
| }); |
| |
| test('focused flag shows/hides the suggestions', async () => { |
| const openStub = sinon.stub(suggestionsEl(), 'open'); |
| const closedStub = sinon.stub(suggestionsEl(), 'close'); |
| element.suggestions = [{text: 'hello'}, {text: 'its me'}]; |
| assert.isFalse(openStub.called); |
| await waitUntil(() => closedStub.calledOnce); |
| element.setFocus(true); |
| await waitUntil(() => openStub.calledOnce); |
| element.suggestions = []; |
| await waitUntil(() => closedStub.calledTwice); |
| assert.isTrue(openStub.calledOnce); |
| }); |
| |
| test( |
| 'handleInputCommit with autocomplete hidden does nothing without' + |
| ' allowNonSuggestedValues', |
| () => { |
| const commitStub = sinon.stub(element, '_commit'); |
| suggestionsEl().isHidden = true; |
| element.handleInputCommit(); |
| assert.isFalse(commitStub.called); |
| } |
| ); |
| |
| test( |
| 'handleInputCommit with query error does nothing without' + |
| ' allowNonSuggestedValues', |
| () => { |
| const commitStub = sinon.stub(element, '_commit'); |
| element.queryStatus = { |
| type: AutocompleteQueryStatusType.ERROR, |
| message: 'Error', |
| }; |
| element.suggestions = []; |
| element.handleInputCommit(); |
| assert.isFalse(commitStub.called); |
| } |
| ); |
| |
| test( |
| 'handleInputCommit with autocomplete hidden with' + |
| 'allowNonSuggestedValues', |
| () => { |
| const commitStub = sinon.stub(element, '_commit'); |
| element.allowNonSuggestedValues = true; |
| suggestionsEl().isHidden = true; |
| element.handleInputCommit(); |
| assert.isTrue(commitStub.called); |
| } |
| ); |
| |
| test( |
| 'handleInputCommit with query error with' + 'allowNonSuggestedValues', |
| () => { |
| const commitStub = sinon.stub(element, '_commit'); |
| element.allowNonSuggestedValues = true; |
| element.queryStatus = { |
| type: AutocompleteQueryStatusType.ERROR, |
| message: 'Error', |
| }; |
| element.suggestions = []; |
| element.handleInputCommit(); |
| assert.isTrue(commitStub.called); |
| } |
| ); |
| |
| 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); |
| }); |
| |
| test( |
| 'handleInputCommit with autocomplete open calls commit' + |
| 'with allowNonSuggestedValues', |
| () => { |
| const commitStub = sinon.stub(element, '_commit'); |
| element.allowNonSuggestedValues = true; |
| suggestionsEl().isHidden = false; |
| element.handleInputCommit(); |
| assert.isTrue(commitStub.calledOnce); |
| } |
| ); |
| |
| test('issue 8655', async () => { |
| function makeSuggestion(s: string) { |
| return {name: s, text: s, value: s}; |
| } |
| const keydownSpy = sinon.spy(element, 'handleKeydown'); |
| element.requestUpdate(); |
| await element.updateComplete; |
| |
| // const dispatchEventStub = sinon.stub(element, 'dispatchEvent'); |
| element.setText('file:'); |
| element.suggestions = [makeSuggestion('file:'), makeSuggestion('-file:')]; |
| await element.updateComplete; |
| |
| pressKey(inputEl(), 'x'); |
| // Must set the value, because the MockInteraction does not. |
| inputEl().value = 'file:x'; |
| |
| assert.isTrue(keydownSpy.calledOnce); |
| |
| pressKey(inputEl(), Key.ENTER); |
| await element.updateComplete; |
| assert.isTrue(keydownSpy.calledTwice); |
| |
| 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); |
| }); |
| |
| 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; |
| |
| setup(() => { |
| commitSpy = sinon.spy(element, '_commit'); |
| }); |
| |
| test('enter in input does not re-render suggestions', async () => { |
| element.suggestions = [{text: 'sugar bombs'}]; |
| |
| pressKey(inputEl(), Key.ENTER); |
| |
| await waitUntil(() => commitSpy.called); |
| await element.updateComplete; |
| |
| assert.equal(element.suggestions.length, 0); |
| assert.isUndefined(element.queryStatus); |
| assert.isTrue(suggestionsEl().isHidden); |
| }); |
| |
| test('enter in input does not re-render error', async () => { |
| element.allowNonSuggestedValues = true; |
| element.queryStatus = { |
| type: AutocompleteQueryStatusType.ERROR, |
| message: 'Error message', |
| }; |
| |
| pressKey(inputEl(), Key.ENTER); |
| |
| await waitUntil(() => commitSpy.called); |
| await element.updateComplete; |
| |
| assert.equal(element.suggestions.length, 0); |
| assert.isUndefined(element.queryStatus); |
| assert.isTrue(suggestionsEl().isHidden); |
| }); |
| |
| test('enter in suggestion does not re-render suggestions', async () => { |
| element.suggestions = [{text: 'sugar bombs'}]; |
| element.setFocus(true); |
| |
| await element.updateComplete; |
| assert.isFalse(suggestionsEl().isHidden); |
| |
| focusSpy = sinon.spy(element, 'focus'); |
| pressKey(suggestionsEl(), Key.ENTER); |
| |
| await waitUntil(() => commitSpy.called); |
| await element.updateComplete; |
| |
| assert.equal(element.suggestions.length, 0); |
| assert.isTrue(suggestionsEl().isHidden); |
| }); |
| |
| test('tab in input, tabComplete = true', async () => { |
| focusSpy = sinon.spy(element, 'focus'); |
| const commitHandler = sinon.stub(); |
| element.addEventListener('commit', commitHandler); |
| element.tabComplete = true; |
| element.suggestions = [{text: 'tunnel snakes drool'}]; |
| |
| pressKey(inputEl(), Key.TAB); |
| |
| await waitUntil(() => commitSpy.called); |
| |
| assert.isTrue(focusSpy.called); |
| assert.isFalse(commitHandler.called); |
| assert.equal(element.suggestions.length, 0); |
| }); |
| |
| test('tab in input, tabComplete = false', async () => { |
| element.suggestions = [{text: 'sugar bombs'}]; |
| focusSpy = sinon.spy(element, 'focus'); |
| pressKey(inputEl(), Key.TAB); |
| await element.updateComplete; |
| |
| assert.isFalse(commitSpy.called); |
| assert.isFalse(focusSpy.called); |
| await waitUntil(() => element.suggestions.length > 0); |
| assert.equal(element.suggestions.length, 1); |
| }); |
| |
| test('tab on suggestion, tabComplete = false', async () => { |
| element.suggestions = [{name: 'sugar bombs'}]; |
| element.setFocus(true); |
| // When tabComplete is false, do not focus. |
| element.tabComplete = false; |
| focusSpy = sinon.spy(element, 'focus'); |
| |
| await element.updateComplete; |
| |
| assert.isFalse(suggestionsEl().isHidden); |
| |
| pressKey(queryAndAssert(suggestionsEl(), 'li:first-child'), Key.TAB); |
| await element.updateComplete; |
| assert.isFalse(commitSpy.called); |
| assert.isFalse(element.focused); |
| }); |
| |
| test('tab on suggestion, tabComplete = true', async () => { |
| element.suggestions = [{name: 'sugar bombs'}]; |
| element.setFocus(true); |
| // When tabComplete is true, focus. |
| element.tabComplete = true; |
| focusSpy = sinon.spy(element, 'focus'); |
| |
| await element.updateComplete; |
| |
| assert.isFalse(suggestionsEl().isHidden); |
| |
| pressKey(queryAndAssert(suggestionsEl(), 'li:first-child'), Key.TAB); |
| await element.updateComplete; |
| |
| assert.isTrue(commitSpy.called); |
| assert.isTrue(element.focused); |
| }); |
| |
| test('tap on suggestion commits, calls focus', async () => { |
| focusSpy = sinon.spy(element, 'focus'); |
| element.setFocus(true); |
| element.suggestions = [{name: 'first suggestion'}]; |
| |
| await element.updateComplete; |
| |
| await waitUntil(() => !suggestionsEl().isHidden); |
| queryAndAssert<HTMLLIElement>(suggestionsEl(), 'li:first-child').click(); |
| |
| await waitUntil(() => suggestionsEl().isHidden); |
| assert.isTrue(focusSpy.called); |
| assert.isTrue(commitSpy.called); |
| }); |
| |
| test('esc on suggestion clears suggestions, calls focus', async () => { |
| element.suggestions = [{name: 'sugar bombs'}]; |
| element.setFocus(true); |
| focusSpy = sinon.spy(element, 'focus'); |
| |
| await element.updateComplete; |
| |
| assert.isFalse(suggestionsEl().isHidden); |
| |
| pressKey(queryAndAssert(suggestionsEl(), 'li:first-child'), Key.ESC); |
| |
| await waitUntil(() => suggestionsEl().isHidden); |
| await element.updateComplete; |
| |
| assert.isFalse(commitSpy.called); |
| assert.isTrue(focusSpy.called); |
| }); |
| }); |
| |
| test('enter with modifier does not complete', async () => { |
| const commitStub = sinon.stub(element, 'handleInputCommit'); |
| |
| pressKey(inputEl(), Key.ENTER, Modifier.CTRL_KEY); |
| await element.updateComplete; |
| |
| assert.isFalse(commitStub.called); |
| |
| pressKey(inputEl(), Key.ENTER); |
| await element.updateComplete; |
| |
| assert.isTrue(commitStub.called); |
| }); |
| |
| test('enter with dropdown does not propagate', async () => { |
| const event = new KeyboardEvent('keydown', {key: Key.ENTER}); |
| const stopPropagationStub = sinon.stub(event, 'stopPropagation'); |
| |
| element.suggestions = [{name: 'first suggestion'}]; |
| |
| inputEl().dispatchEvent(event); |
| await element.updateComplete; |
| |
| assert.isTrue(stopPropagationStub.called); |
| }); |
| |
| test('enter with no dropdown propagates', async () => { |
| const event = new KeyboardEvent('keydown', {key: Key.ENTER}); |
| const stopPropagationStub = sinon.stub(event, 'stopPropagation'); |
| |
| inputEl().dispatchEvent(event); |
| await element.updateComplete; |
| |
| assert.isFalse(stopPropagationStub.called); |
| }); |
| |
| suite('warnUncommitted', () => { |
| let inputClassList: DOMTokenList; |
| setup(() => { |
| inputClassList = inputEl().classList; |
| }); |
| |
| test('enabled', () => { |
| element.warnUncommitted = true; |
| element.text = 'blah blah blah'; |
| inputEl().dispatchEvent(new Event('blur')); |
| assert.isTrue(inputClassList.contains('warnUncommitted')); |
| inputEl().focus(); |
| assert.isFalse(inputClassList.contains('warnUncommitted')); |
| }); |
| |
| test('disabled', () => { |
| element.warnUncommitted = false; |
| element.text = 'blah blah blah'; |
| inputEl().blur(); |
| assert.isFalse(inputClassList.contains('warnUncommitted')); |
| }); |
| |
| test('no text', () => { |
| element.warnUncommitted = true; |
| element.text = ''; |
| inputEl().blur(); |
| assert.isFalse(inputClassList.contains('warnUncommitted')); |
| }); |
| }); |
| }); |