Skip to content

Commit

Permalink
feat: add selectOptions function (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
timdeschryver authored Aug 30, 2019
1 parent 6d3d71a commit ddbc1fc
Show file tree
Hide file tree
Showing 13 changed files with 666 additions and 12 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@
},
"dependencies": {
"@angular/animations": "^8.0.0",
"@angular/cdk": "^8.1.4",
"@angular/common": "^8.0.0",
"@angular/compiler": "^8.0.0",
"@angular/core": "^8.0.0",
"@angular/forms": "^8.0.0",
"@angular/material": "^8.1.4",
"@angular/platform-browser": "^8.0.0",
"@angular/platform-browser-dynamic": "^8.0.0",
"@angular/router": "^8.0.0",
Expand Down
3 changes: 2 additions & 1 deletion projects/testing-library/src/lib/testing-library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { NoopAnimationsModule, BrowserAnimationsModule } from '@angular/platform
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { getQueriesForElement, prettyDOM, fireEvent, FireObject, FireFunction } from '@testing-library/dom';
import { RenderResult, RenderOptions } from './models';
import { createType } from './user-events';
import { createType, createSelectOptions } from './user-events';

@Component({ selector: 'wrapper-component', template: '' })
class WrapperComponent implements OnInit {
Expand Down Expand Up @@ -87,6 +87,7 @@ export async function render<T>(
...getQueriesForElement(fixture.nativeElement, queries),
...eventsWithDetectChanges,
type: createType(eventsWithDetectChanges),
selectOptions: createSelectOptions(eventsWithDetectChanges),
} as any;
}

Expand Down
5 changes: 4 additions & 1 deletion projects/testing-library/src/lib/user-events/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { fireEvent } from '@testing-library/dom';
import { createType } from './type';
import { createSelectOptions } from './selectOptions';

export interface UserEvents {
type: ReturnType<typeof createType>;
selectOptions: ReturnType<typeof createSelectOptions>;
}

const type = createType(fireEvent);
const selectOptions = createSelectOptions(fireEvent);

export { createType, type };
export { createType, type, createSelectOptions, selectOptions };
67 changes: 67 additions & 0 deletions projects/testing-library/src/lib/user-events/selectOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {
FireFunction,
FireObject,
Matcher,
getByText,
SelectorMatcherOptions,
queryByText,
} from '@testing-library/dom';

// implementation from https://github.com/testing-library/user-event
export function createSelectOptions(fireEvent: FireFunction & FireObject) {
function clickElement(element: HTMLElement) {
fireEvent.mouseOver(element);
fireEvent.mouseMove(element);
fireEvent.mouseDown(element);
fireEvent.focus(element);
fireEvent.mouseUp(element);
fireEvent.click(element);
}

function selectOption(select: HTMLSelectElement, index: number, matcher: Matcher, options?: SelectorMatcherOptions) {
// fallback to document.body, because libraries as Angular Material will have their custom select component
const option = (queryByText(select, matcher, options) ||
getByText(document.body, matcher, options)) as HTMLOptionElement;

fireEvent.mouseOver(option);
fireEvent.mouseMove(option);
fireEvent.mouseDown(option);
fireEvent.focus(option);
fireEvent.mouseUp(option);
fireEvent.click(option, { ctrlKey: index > 0 });

option.selected = true;
fireEvent.change(select);
}

return async function selectOptions(
element: HTMLElement,
matcher: Matcher | Matcher[],
matcherOptions?: SelectorMatcherOptions,
) {
const selectElement = element as HTMLSelectElement;

if (selectElement.selectedOptions) {
Array.from(selectElement.selectedOptions).forEach(option => (option.selected = false));
}

const focusedElement = document.activeElement;
const wasAnotherElementFocused = focusedElement !== document.body && focusedElement !== selectElement;

if (wasAnotherElementFocused) {
fireEvent.mouseMove(focusedElement);
fireEvent.mouseLeave(focusedElement);
}

clickElement(selectElement);

const values = Array.isArray(matcher) ? matcher : [matcher];
values
.filter((_, index) => index === 0 || selectElement.multiple)
.forEach((val, index) => selectOption(selectElement, index, val, matcherOptions));

if (wasAnotherElementFocused) {
fireEvent.blur(focusedElement);
}
};
}
236 changes: 236 additions & 0 deletions projects/testing-library/tests/user-events/selectOptions.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
import { ReactiveFormsModule, FormsModule, FormControl } from '@angular/forms';
import { render, RenderResult } from '../../src/public_api';
import { Component, ViewChild, Input } from '@angular/core';

describe('selectOption: single', () => {
test('with a template-driven form', async () => {
@Component({
selector: 'fixture',
template: `
<select data-testid="select" [(ngModel)]="value">
<option value="1" data-testid="apples">Apples</option>
<option value="2" data-testid="oranges">Oranges</option>
<option value="3" data-testid="lemons">Lemons</option>
</select>
<p data-testid="text">{{ value }}</p>
`,
})
class FixtureComponent {
value: string;
}

const component = await render(FixtureComponent, {
imports: [FormsModule],
});

assertSelectOptions(component, () => component.fixture.componentInstance.value);
});

test('with a reactive form', async () => {
@Component({
selector: 'fixture',
template: `
<select data-testid="select" [formControl]="value">
<option value="1" data-testid="apples">Apples</option>
<option value="2" data-testid="oranges">Oranges</option>
<option value="3" data-testid="lemons">Lemons</option>
</select>
<p data-testid="text">{{ value.value }}</p>
`,
})
class FixtureComponent {
value = new FormControl('');
}

const component = await render(FixtureComponent, {
imports: [ReactiveFormsModule],
});

assertSelectOptions(component, () => component.fixture.componentInstance.value.value);
});

test('with change event', async () => {
@Component({
selector: 'fixture',
template: `
<select data-testid="select" (change)="onChange($event)">
<option value="1" data-testid="apples">Apples</option>
<option value="2" data-testid="oranges">Oranges</option>
<option value="3" data-testid="lemons">Lemons</option>
</select>
<p data-testid="text">{{ value }}</p>
`,
})
class FixtureComponent {
value = '';

onChange(event: KeyboardEvent) {
this.value = (<HTMLInputElement>event.target).value;
}
}

const component = await render(FixtureComponent);

assertSelectOptions(component, () => component.fixture.componentInstance.value);
});

test('by reference', async () => {
@Component({
selector: 'fixture',
template: `
<select data-testid="select" #input>
<option value="1" data-testid="apples">Apples</option>
<option value="2" data-testid="oranges">Oranges</option>
<option value="3" data-testid="lemons">Lemons</option>
</select>
<p data-testid="text">{{ input.value }}</p>
`,
})
class FixtureComponent {
@ViewChild('input', { static: false }) value;
}

const component = await render(FixtureComponent);

assertSelectOptions(component, () => component.fixture.componentInstance.value.nativeElement.value);
});

function assertSelectOptions(component: RenderResult, value: () => string) {
const inputControl = component.getByTestId('select') as HTMLSelectElement;
component.selectOptions(inputControl, /apples/i);
component.selectOptions(inputControl, 'Oranges');

expect(value()).toBe('2');
expect(component.getByTestId('text').textContent).toBe('2');
expect(inputControl.value).toBe('2');

expect((component.getByTestId('apples') as HTMLOptionElement).selected).toBe(false);
expect((component.getByTestId('oranges') as HTMLOptionElement).selected).toBe(true);
expect((component.getByTestId('lemons') as HTMLOptionElement).selected).toBe(false);
}
});

describe('selectOption: multiple', () => {
test('with a template-driven form', async () => {
@Component({
selector: 'fixture',
template: `
<select data-testid="select" multiple [(ngModel)]="value">
<option value="1" data-testid="apples">Apples</option>
<option value="2" data-testid="oranges">Oranges</option>
<option value="3" data-testid="lemons">Lemons</option>
</select>
<p data-testid="text">{{ value }}</p>
`,
})
class FixtureComponent {
value: string;
}

const component = await render(FixtureComponent, {
imports: [FormsModule],
});
assertSelectOptions(component, () => component.fixture.componentInstance.value);
});

test('with a reactive form', async () => {
@Component({
selector: 'fixture',
template: `
<select data-testid="select" multiple [formControl]="value">
<option value="1" data-testid="apples">Apples</option>
<option value="2" data-testid="oranges">Oranges</option>
<option value="3" data-testid="lemons">Lemons</option>
</select>
<p data-testid="text">{{ value.value }}</p>
`,
})
class FixtureComponent {
value = new FormControl('');
}

const component = await render(FixtureComponent, {
imports: [ReactiveFormsModule],
});

assertSelectOptions(component, () => component.fixture.componentInstance.value.value);
});

test('with change event', async () => {
@Component({
selector: 'fixture',
template: `
<select data-testid="select" multiple (change)="onChange($event)">
<option value="1" data-testid="apples">Apples</option>
<option value="2" data-testid="oranges">Oranges</option>
<option value="3" data-testid="lemons">Lemons</option>
</select>
<p data-testid="text">{{ value }}</p>
`,
})
class FixtureComponent {
value = [];

onChange(event: KeyboardEvent) {
this.value = Array.from((<HTMLSelectElement>event.target).selectedOptions).map(o => o.value);
}
}

const component = await render(FixtureComponent);

assertSelectOptions(component, () => component.fixture.componentInstance.value);
});

test('by reference', async () => {
@Component({
selector: 'fixture',
template: `
<select data-testid="select" multiple #input>
<option value="1" data-testid="apples">Apples</option>
<option value="2" data-testid="oranges">Oranges</option>
<option value="3" data-testid="lemons">Lemons</option>
</select>
<p data-testid="text">{{ input.value }}</p>
`,
})
class FixtureComponent {
@ViewChild('input', { static: false }) value;
}

const component = await render(FixtureComponent);

const inputControl = component.getByTestId('select') as HTMLSelectElement;
component.selectOptions(inputControl, /apples/i);
component.selectOptions(inputControl, ['Oranges', 'Lemons']);

const options = component.fixture.componentInstance.value.nativeElement.selectedOptions;
const value = Array.from(options).map((o: any) => o.value);

expect(value).toEqual(['2', '3']);
// shouldn't this be an empty string? - https://stackblitz.com/edit/angular-pdvm9n
expect(component.getByTestId('text').textContent).toBe('2');
expect((component.getByTestId('apples') as HTMLOptionElement).selected).toBe(false);
expect((component.getByTestId('oranges') as HTMLOptionElement).selected).toBe(true);
expect((component.getByTestId('lemons') as HTMLOptionElement).selected).toBe(true);
});

function assertSelectOptions(component: RenderResult, value: () => string) {
const inputControl = component.getByTestId('select') as HTMLSelectElement;
component.selectOptions(inputControl, /apples/i);
component.selectOptions(inputControl, ['Oranges', 'Lemons']);

expect(value()).toEqual(['2', '3']);
expect(component.getByTestId('text').textContent).toBe('2,3');
expect((component.getByTestId('apples') as HTMLOptionElement).selected).toBe(false);
expect((component.getByTestId('oranges') as HTMLOptionElement).selected).toBe(true);
expect((component.getByTestId('lemons') as HTMLOptionElement).selected).toBe(true);
}
});
Loading

0 comments on commit ddbc1fc

Please sign in to comment.