Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
fix(filter-field): Fixes an issue with instant submission of free-text.
Browse files Browse the repository at this point in the history
When selecting a freetext node from the autocomplete with the keyboard
the the different eventHandlers of the autocomplete and the filterfield
raced to a broken state. To prevent the instant submission of an empty
filter we are handling the locking of the input field and synching that
lock with the zone cycle.
Additionally this commit implements a fix for submission of empty values
if the value and submission happend in a shorter time than the
DT_FILTER_FIELD_TYPING_DEBOUNCE.

Fixes #294
  • Loading branch information
tomheller committed Jan 13, 2020
1 parent d8614c3 commit 3f6802b
Show file tree
Hide file tree
Showing 6 changed files with 271 additions and 32 deletions.
146 changes: 145 additions & 1 deletion apps/components-e2e/src/components/filter-field/filter-field.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ import {
input,
clearAll,
filterTags,
focusFilterFieldInput,
getFilterfieldTags,
} from './filter-field.po';
import { Selector } from 'testcafe';

fixture('Filter Field').page('http://localhost:4200/filter-field');
fixture.only('Filter Field').page('http://localhost:4200/filter-field');

test('should not show a error box if there is no validator provided', async (testController: TestController) => {
await clickOption(1);
Expand All @@ -37,6 +39,10 @@ test('should not show a error box if there is no validator provided', async (tes
test('should show a error box if does not meet the validation function', async (testController: TestController) => {
await clickOption(3);
await testController.typeText(input, 'a');

// Wiat for the filter field to refresh the error message.
await testController.wait(250);

await testController.expect(await errorBox.exists).ok();
await testController
.expect(await errorBox.innerText)
Expand Down Expand Up @@ -76,3 +82,141 @@ test('should remove all filters when clicking the clear-all button', async (test
await testController.wait(300);
await testController.expect(await filterTags.exists).notOk();
});

test('should choose a freetext node with the keyboard and submit the correct value', async (testController: TestController) => {
// Wait for the input before it can be focussed.
await input.exists;
await focusFilterFieldInput();

// Select the test autocomplete
await testController.pressKey('down down down down enter');

// Wait for a certain amount of time to let the filterfield refresh
await testController.wait(250);

// Select the free text node and start typing
await testController.pressKey('down down down enter');

await testController.typeText(input, 'Custom selection');

// Wait for a certain amout fo time to let the filterfield refresh
await testController.wait(250);

// Confirm the text typed in
await testController.pressKey('enter');

const tags = await getFilterfieldTags();

await testController.expect(tags.length).eql(1);
await testController
.expect(tags[0])
.eql('Autocomplete with free text optionsCustom selection');
});

test('should choose a freetext node with the keyboard and submit an empty value', async (testController: TestController) => {
// Wait for the input before it can be focussed.
await input.exists;
await focusFilterFieldInput();

// Select the test autocomplete
await testController.pressKey('down down down down enter');

// Wait for a certain amount of time to let the filterfield refresh
await testController.wait(250);

// Select the free text node and start typing
await testController.pressKey('down down down enter');

// Wait for a certain amout fo time to let the filterfield refresh
await testController.wait(250);

// Confirm the text typed in
await testController.pressKey('enter');

const tags = await getFilterfieldTags();

await testController.expect(tags.length).eql(1);
await testController
.expect(tags[0])
.eql('Autocomplete with free text options');
});

test('should choose a freetext node with the keyboard and submit an empyty value immediately', async (testController: TestController) => {
await focusFilterFieldInput();

// Select the test autocomplete
await testController.pressKey('down down down down enter');

// Wait for a certain amount of time to let the filterfield refresh
await testController.wait(250);

// Select the free text node and start typing
await testController.pressKey('down down down enter');

// Focus the filter field
await testController.pressKey('enter');

const tags = await getFilterfieldTags();

await testController.expect(tags.length).eql(1);
await testController
.expect(tags[0])
.eql('Autocomplete with free text options');
});

test('should choose a freetext node with the mouse and submit the correct value immediately', async (testController: TestController) => {
// Select the test autocomplete
await clickOption(5);

// Wait for a certain amount of time to let the filterfield refresh
await testController.wait(250);

// Select the free text node and start typing
await clickOption(4);

// Wait for a certain amout fo time to let the filterfield refresh
await testController.wait(250);

// Send the correct value into the input field
await testController.typeText(input, 'Custom selection');

// Focus the filter field
await focusFilterFieldInput();

// Submit the value immediately
await testController.pressKey('enter');

const tags = await getFilterfieldTags();

await testController.expect(tags.length).eql(1);
await testController
.expect(tags[0])
.eql('Autocomplete with free text optionsCustom selection');
});

test('should choose a freetext node with the mouse and submit an empty value immediately', async (testController: TestController) => {
// Select the test autocomplete
await clickOption(5);

// Wait for a certain amount of time to let the filterfield refresh
await testController.wait(250);

// Select the free text node and start typing
await clickOption(4);

// Wait for a certain amout fo time to let the filterfield refresh
await testController.wait(250);

// Confirm the text typed in
await focusFilterFieldInput();

// Submit the empty value immediately
await testController.pressKey('enter');

const tags = await getFilterfieldTags();

await testController.expect(tags.length).eql(1);
await testController
.expect(tags[0])
.eql('Autocomplete with free text options');
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { Selector, t } from 'testcafe';
import { Selector, t, ClientFunction } from 'testcafe';

export const errorBox = Selector('.dt-filter-field-error');
export const filterField = Selector('#filter-field');
Expand All @@ -33,3 +33,20 @@ export async function clickOption(
await controller.click(filterField);
await controller.click(option(nth));
}

/** Focus the input of the filter field to send key events to it. */
export const focusFilterFieldInput = ClientFunction(() => {
(document.querySelector('#filter-field input') as HTMLElement).focus();
});

/** Retreive all set tags in the filter field and their values. */
export const getFilterfieldTags = ClientFunction(() => {
const filterFieldTags: HTMLElement[] = [].slice.call(
document.querySelectorAll('.dt-filter-field-tag'),
);
const contents: string[] = [];
for (const tag of filterFieldTags) {
contents.push(tag.textContent || '');
}
return contents;
});
13 changes: 13 additions & 0 deletions apps/components-e2e/src/components/filter-field/filter-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@ const TEST_DATA = {
},
],
},
{
name: 'Autocomplete with free text options',
autocomplete: [
'Autocomplete option 1',
'Autocomplete option 2',
'Autocomplete option 3',
{
name: 'Autocomplete free text',
suggestions: ['Suggestion 1', 'Suggestion 2', 'Suggestion 3'],
validators: [],
},
],
},
],
};

Expand Down
1 change: 0 additions & 1 deletion components/filter-field/src/filter-field.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
[dtFilterFieldRange]="range"
[dtFilterFieldRangeDisabled]="!(_currentDef && !!_currentDef!.range) || loading"
(keydown)="_handleInputKeyDown($event)"
(keyup)="_handleInputKeyUp($event)"
[value]="_inputValue"
/>
<dt-autocomplete
Expand Down
54 changes: 50 additions & 4 deletions components/filter-field/src/filter-field.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
// tslint:disable no-lifecycle-call no-use-before-declare no-magic-numbers
// tslint:disable no-any max-file-line-count no-unbound-method use-component-selector

import { BACKSPACE, ENTER, ESCAPE } from '@angular/cdk/keycodes';
import { BACKSPACE, ENTER, ESCAPE, DOWN_ARROW } from '@angular/cdk/keycodes';
import { OverlayContainer } from '@angular/cdk/overlay';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { Component, DebugElement, NgZone, ViewChild } from '@angular/core';
Expand Down Expand Up @@ -540,10 +540,51 @@ describe('DtFilterField', () => {
tick(DT_FILTER_FIELD_TYPING_DEBOUNCE);

fixture.detectChanges();
dispatchKeyboardEvent(inputEl, 'keyup', ENTER);
dispatchKeyboardEvent(inputEl, 'keydown', ENTER);

tick(DT_FILTER_FIELD_TYPING_DEBOUNCE);
fixture.detectChanges();

const tags = getFilterTags(fixture);
expect(tags.length).toBe(1);
expect(tags[0].key).toBe('Free');
expect(tags[0].separator).toBe('~');
expect(tags[0].value).toBe('abc');
expect(spy).toHaveBeenCalledTimes(1);
subscription.unsubscribe();
}));

it('should switch to free text with keyboard interaction and on enter fire a filterChanges event and create a tag', fakeAsync(() => {
const spy = jest.fn();
const subscription = filterField.filterChanges.subscribe(spy);
filterField.focus();
zone.simulateMicrotasksEmpty();
zone.simulateZoneExit();
fixture.detectChanges();

const inputEl = getInput(fixture);

// Select the free text option with the keyboard and hit enter
dispatchKeyboardEvent(inputEl, 'keydown', DOWN_ARROW);
dispatchKeyboardEvent(inputEl, 'keydown', DOWN_ARROW);

// Use keydown and keyup arrow, as this is what happens in the real world as well.
dispatchKeyboardEvent(inputEl, 'keydown', ENTER);

zone.simulateMicrotasksEmpty();
fixture.detectChanges();

typeInElement('abc', inputEl);
tick(DT_FILTER_FIELD_TYPING_DEBOUNCE);

fixture.detectChanges();
dispatchKeyboardEvent(inputEl, 'keydown', ENTER);

tick(DT_FILTER_FIELD_TYPING_DEBOUNCE);
fixture.detectChanges();

const tags = getFilterTags(fixture);

expect(tags.length).toBe(1);
expect(tags[0].key).toBe('Free');
expect(tags[0].separator).toBe('~');
Expand Down Expand Up @@ -2065,8 +2106,13 @@ function getFilterTags(
key && key.getAttribute('data-separator')
? key.getAttribute('data-separator')!
: '';
const value = ele.nativeElement.querySelector('.dt-filter-field-tag-value')
.textContent;

// Get the value of the filter element, if no filter value element is rendered
// assume an empty string.
const valueElement = ele.nativeElement.querySelector(
'.dt-filter-field-tag-value',
);
const value = valueElement ? valueElement.textContent : '';

return {
ele,
Expand Down
Loading

0 comments on commit 3f6802b

Please sign in to comment.