blob: 9be28e729c2d48f12ee3e9ee623da2f109ffb64d [file] [log] [blame]
/**
* @license
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import '../../../test/common-test-setup';
import './gr-autocomplete';
import {AutocompleteSuggestion, GrAutocomplete} from './gr-autocomplete';
import {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';
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 no-debounce></gr-autocomplete>`
);
});
test('renders', () => {
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
horizontal-align="left"
id="suggestions"
is-hidden=""
role="listbox"
style="position: fixed; top: 300px; left: 392.5px; box-sizing: border-box; max-height: 600px; max-width: 785px;"
vertical-align="top"
>
</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">
<gr-icon icon="search" class="searchIcon"></gr-icon>
</div>
<div slot="suffix">
<slot name="suffix"> </slot>
</div>
</paper-input>
<gr-autocomplete-dropdown
horizontal-align="left"
id="suggestions"
role="listbox"
vertical-align="top"
>
</gr-autocomplete-dropdown>
`,
{
// gr-autocomplete-dropdown sizing seems to vary between local & CI
ignoreAttributes: [
{tags: ['gr-autocomplete-dropdown'], attributes: ['style']},
],
}
);
});
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('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[]))
);
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 promise.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;
element.noDebounce = false;
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;
element.noDebounce = false;
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 () => {
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';
await element.updateComplete;
return promise.then(async () => {
await waitUntil(() => element.suggestions.length > 0);
assert.equal(element.suggestions.length, 1);
element.text = '';
element.threshold = 0;
element.noDebounce = false;
await element.updateComplete;
assert.equal(element.suggestions.length, 0);
});
});
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 waitUntil(() => element.suggestionsDropdown?.isHidden === false);
pressKey(inputEl(), Key.ENTER);
await waitUntil(() => commitHandler.called);
assert.equal(element.text, 'blah 0');
});
});
test('tabComplete flag functions', async () => {
// 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;
pressKey(inputEl(), Key.TAB);
await element.updateComplete;
assert.isFalse(commitHandler.called);
assert.isFalse(commitSpy.called);
assert.isFalse(element.focused);
element.tabComplete = true;
await element.updateComplete;
element.setFocus(true);
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('search icon shows with showSearchIcon property', async () => {
assert.equal(
getComputedStyle(queryAndAssert(element, 'gr-icon')).display,
'none'
);
element.showSearchIcon = true;
await element.updateComplete;
assert.notEqual(
getComputedStyle(queryAndAssert(element, 'gr-icon')).display,
'none'
);
});
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' +
'without allowNonSuggestedValues',
() => {
const commitStub = sinon.stub(element, '_commit');
suggestionsEl().isHidden = true;
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 autocomplete open calls commit', () => {
const commitStub = sinon.stub(element, '_commit');
suggestionsEl().isHidden = false;
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');
});
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.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('input-keydown event fired', async () => {
const listener = sinon.spy();
element.addEventListener('input-keydown', listener);
pressKey(inputEl(), Key.TAB);
await element.updateComplete;
assert.isTrue(listener.called);
});
test('enter with modifier does not complete', async () => {
const dispatchEventStub = sinon.stub(element, 'dispatchEvent');
const commitStub = sinon.stub(element, 'handleInputCommit');
pressKey(inputEl(), Key.ENTER, Modifier.CTRL_KEY);
await element.updateComplete;
assert.equal(dispatchEventStub.lastCall.args[0].type, 'input-keydown');
assert.equal(
(dispatchEventStub.lastCall.args[0] as CustomEvent).detail.key,
Key.ENTER
);
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'));
});
});
});