Skip to content

Commit ddbc1fc

Browse files
feat: add selectOptions function (#41)
1 parent 6d3d71a commit ddbc1fc

File tree

13 files changed

+666
-12
lines changed

13 files changed

+666
-12
lines changed

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@
2727
},
2828
"dependencies": {
2929
"@angular/animations": "^8.0.0",
30+
"@angular/cdk": "^8.1.4",
3031
"@angular/common": "^8.0.0",
3132
"@angular/compiler": "^8.0.0",
3233
"@angular/core": "^8.0.0",
3334
"@angular/forms": "^8.0.0",
35+
"@angular/material": "^8.1.4",
3436
"@angular/platform-browser": "^8.0.0",
3537
"@angular/platform-browser-dynamic": "^8.0.0",
3638
"@angular/router": "^8.0.0",

projects/testing-library/src/lib/testing-library.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { NoopAnimationsModule, BrowserAnimationsModule } from '@angular/platform
44
import { TestBed, ComponentFixture } from '@angular/core/testing';
55
import { getQueriesForElement, prettyDOM, fireEvent, FireObject, FireFunction } from '@testing-library/dom';
66
import { RenderResult, RenderOptions } from './models';
7-
import { createType } from './user-events';
7+
import { createType, createSelectOptions } from './user-events';
88

99
@Component({ selector: 'wrapper-component', template: '' })
1010
class WrapperComponent implements OnInit {
@@ -87,6 +87,7 @@ export async function render<T>(
8787
...getQueriesForElement(fixture.nativeElement, queries),
8888
...eventsWithDetectChanges,
8989
type: createType(eventsWithDetectChanges),
90+
selectOptions: createSelectOptions(eventsWithDetectChanges),
9091
} as any;
9192
}
9293

Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import { fireEvent } from '@testing-library/dom';
22
import { createType } from './type';
3+
import { createSelectOptions } from './selectOptions';
34

45
export interface UserEvents {
56
type: ReturnType<typeof createType>;
7+
selectOptions: ReturnType<typeof createSelectOptions>;
68
}
79

810
const type = createType(fireEvent);
11+
const selectOptions = createSelectOptions(fireEvent);
912

10-
export { createType, type };
13+
export { createType, type, createSelectOptions, selectOptions };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import {
2+
FireFunction,
3+
FireObject,
4+
Matcher,
5+
getByText,
6+
SelectorMatcherOptions,
7+
queryByText,
8+
} from '@testing-library/dom';
9+
10+
// implementation from https://github.com/testing-library/user-event
11+
export function createSelectOptions(fireEvent: FireFunction & FireObject) {
12+
function clickElement(element: HTMLElement) {
13+
fireEvent.mouseOver(element);
14+
fireEvent.mouseMove(element);
15+
fireEvent.mouseDown(element);
16+
fireEvent.focus(element);
17+
fireEvent.mouseUp(element);
18+
fireEvent.click(element);
19+
}
20+
21+
function selectOption(select: HTMLSelectElement, index: number, matcher: Matcher, options?: SelectorMatcherOptions) {
22+
// fallback to document.body, because libraries as Angular Material will have their custom select component
23+
const option = (queryByText(select, matcher, options) ||
24+
getByText(document.body, matcher, options)) as HTMLOptionElement;
25+
26+
fireEvent.mouseOver(option);
27+
fireEvent.mouseMove(option);
28+
fireEvent.mouseDown(option);
29+
fireEvent.focus(option);
30+
fireEvent.mouseUp(option);
31+
fireEvent.click(option, { ctrlKey: index > 0 });
32+
33+
option.selected = true;
34+
fireEvent.change(select);
35+
}
36+
37+
return async function selectOptions(
38+
element: HTMLElement,
39+
matcher: Matcher | Matcher[],
40+
matcherOptions?: SelectorMatcherOptions,
41+
) {
42+
const selectElement = element as HTMLSelectElement;
43+
44+
if (selectElement.selectedOptions) {
45+
Array.from(selectElement.selectedOptions).forEach(option => (option.selected = false));
46+
}
47+
48+
const focusedElement = document.activeElement;
49+
const wasAnotherElementFocused = focusedElement !== document.body && focusedElement !== selectElement;
50+
51+
if (wasAnotherElementFocused) {
52+
fireEvent.mouseMove(focusedElement);
53+
fireEvent.mouseLeave(focusedElement);
54+
}
55+
56+
clickElement(selectElement);
57+
58+
const values = Array.isArray(matcher) ? matcher : [matcher];
59+
values
60+
.filter((_, index) => index === 0 || selectElement.multiple)
61+
.forEach((val, index) => selectOption(selectElement, index, val, matcherOptions));
62+
63+
if (wasAnotherElementFocused) {
64+
fireEvent.blur(focusedElement);
65+
}
66+
};
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import { ReactiveFormsModule, FormsModule, FormControl } from '@angular/forms';
2+
import { render, RenderResult } from '../../src/public_api';
3+
import { Component, ViewChild, Input } from '@angular/core';
4+
5+
describe('selectOption: single', () => {
6+
test('with a template-driven form', async () => {
7+
@Component({
8+
selector: 'fixture',
9+
template: `
10+
<select data-testid="select" [(ngModel)]="value">
11+
<option value="1" data-testid="apples">Apples</option>
12+
<option value="2" data-testid="oranges">Oranges</option>
13+
<option value="3" data-testid="lemons">Lemons</option>
14+
</select>
15+
16+
<p data-testid="text">{{ value }}</p>
17+
`,
18+
})
19+
class FixtureComponent {
20+
value: string;
21+
}
22+
23+
const component = await render(FixtureComponent, {
24+
imports: [FormsModule],
25+
});
26+
27+
assertSelectOptions(component, () => component.fixture.componentInstance.value);
28+
});
29+
30+
test('with a reactive form', async () => {
31+
@Component({
32+
selector: 'fixture',
33+
template: `
34+
<select data-testid="select" [formControl]="value">
35+
<option value="1" data-testid="apples">Apples</option>
36+
<option value="2" data-testid="oranges">Oranges</option>
37+
<option value="3" data-testid="lemons">Lemons</option>
38+
</select>
39+
40+
<p data-testid="text">{{ value.value }}</p>
41+
`,
42+
})
43+
class FixtureComponent {
44+
value = new FormControl('');
45+
}
46+
47+
const component = await render(FixtureComponent, {
48+
imports: [ReactiveFormsModule],
49+
});
50+
51+
assertSelectOptions(component, () => component.fixture.componentInstance.value.value);
52+
});
53+
54+
test('with change event', async () => {
55+
@Component({
56+
selector: 'fixture',
57+
template: `
58+
<select data-testid="select" (change)="onChange($event)">
59+
<option value="1" data-testid="apples">Apples</option>
60+
<option value="2" data-testid="oranges">Oranges</option>
61+
<option value="3" data-testid="lemons">Lemons</option>
62+
</select>
63+
64+
<p data-testid="text">{{ value }}</p>
65+
`,
66+
})
67+
class FixtureComponent {
68+
value = '';
69+
70+
onChange(event: KeyboardEvent) {
71+
this.value = (<HTMLInputElement>event.target).value;
72+
}
73+
}
74+
75+
const component = await render(FixtureComponent);
76+
77+
assertSelectOptions(component, () => component.fixture.componentInstance.value);
78+
});
79+
80+
test('by reference', async () => {
81+
@Component({
82+
selector: 'fixture',
83+
template: `
84+
<select data-testid="select" #input>
85+
<option value="1" data-testid="apples">Apples</option>
86+
<option value="2" data-testid="oranges">Oranges</option>
87+
<option value="3" data-testid="lemons">Lemons</option>
88+
</select>
89+
90+
<p data-testid="text">{{ input.value }}</p>
91+
`,
92+
})
93+
class FixtureComponent {
94+
@ViewChild('input', { static: false }) value;
95+
}
96+
97+
const component = await render(FixtureComponent);
98+
99+
assertSelectOptions(component, () => component.fixture.componentInstance.value.nativeElement.value);
100+
});
101+
102+
function assertSelectOptions(component: RenderResult, value: () => string) {
103+
const inputControl = component.getByTestId('select') as HTMLSelectElement;
104+
component.selectOptions(inputControl, /apples/i);
105+
component.selectOptions(inputControl, 'Oranges');
106+
107+
expect(value()).toBe('2');
108+
expect(component.getByTestId('text').textContent).toBe('2');
109+
expect(inputControl.value).toBe('2');
110+
111+
expect((component.getByTestId('apples') as HTMLOptionElement).selected).toBe(false);
112+
expect((component.getByTestId('oranges') as HTMLOptionElement).selected).toBe(true);
113+
expect((component.getByTestId('lemons') as HTMLOptionElement).selected).toBe(false);
114+
}
115+
});
116+
117+
describe('selectOption: multiple', () => {
118+
test('with a template-driven form', async () => {
119+
@Component({
120+
selector: 'fixture',
121+
template: `
122+
<select data-testid="select" multiple [(ngModel)]="value">
123+
<option value="1" data-testid="apples">Apples</option>
124+
<option value="2" data-testid="oranges">Oranges</option>
125+
<option value="3" data-testid="lemons">Lemons</option>
126+
</select>
127+
128+
<p data-testid="text">{{ value }}</p>
129+
`,
130+
})
131+
class FixtureComponent {
132+
value: string;
133+
}
134+
135+
const component = await render(FixtureComponent, {
136+
imports: [FormsModule],
137+
});
138+
assertSelectOptions(component, () => component.fixture.componentInstance.value);
139+
});
140+
141+
test('with a reactive form', async () => {
142+
@Component({
143+
selector: 'fixture',
144+
template: `
145+
<select data-testid="select" multiple [formControl]="value">
146+
<option value="1" data-testid="apples">Apples</option>
147+
<option value="2" data-testid="oranges">Oranges</option>
148+
<option value="3" data-testid="lemons">Lemons</option>
149+
</select>
150+
151+
<p data-testid="text">{{ value.value }}</p>
152+
`,
153+
})
154+
class FixtureComponent {
155+
value = new FormControl('');
156+
}
157+
158+
const component = await render(FixtureComponent, {
159+
imports: [ReactiveFormsModule],
160+
});
161+
162+
assertSelectOptions(component, () => component.fixture.componentInstance.value.value);
163+
});
164+
165+
test('with change event', async () => {
166+
@Component({
167+
selector: 'fixture',
168+
template: `
169+
<select data-testid="select" multiple (change)="onChange($event)">
170+
<option value="1" data-testid="apples">Apples</option>
171+
<option value="2" data-testid="oranges">Oranges</option>
172+
<option value="3" data-testid="lemons">Lemons</option>
173+
</select>
174+
175+
<p data-testid="text">{{ value }}</p>
176+
`,
177+
})
178+
class FixtureComponent {
179+
value = [];
180+
181+
onChange(event: KeyboardEvent) {
182+
this.value = Array.from((<HTMLSelectElement>event.target).selectedOptions).map(o => o.value);
183+
}
184+
}
185+
186+
const component = await render(FixtureComponent);
187+
188+
assertSelectOptions(component, () => component.fixture.componentInstance.value);
189+
});
190+
191+
test('by reference', async () => {
192+
@Component({
193+
selector: 'fixture',
194+
template: `
195+
<select data-testid="select" multiple #input>
196+
<option value="1" data-testid="apples">Apples</option>
197+
<option value="2" data-testid="oranges">Oranges</option>
198+
<option value="3" data-testid="lemons">Lemons</option>
199+
</select>
200+
201+
<p data-testid="text">{{ input.value }}</p>
202+
`,
203+
})
204+
class FixtureComponent {
205+
@ViewChild('input', { static: false }) value;
206+
}
207+
208+
const component = await render(FixtureComponent);
209+
210+
const inputControl = component.getByTestId('select') as HTMLSelectElement;
211+
component.selectOptions(inputControl, /apples/i);
212+
component.selectOptions(inputControl, ['Oranges', 'Lemons']);
213+
214+
const options = component.fixture.componentInstance.value.nativeElement.selectedOptions;
215+
const value = Array.from(options).map((o: any) => o.value);
216+
217+
expect(value).toEqual(['2', '3']);
218+
// shouldn't this be an empty string? - https://stackblitz.com/edit/angular-pdvm9n
219+
expect(component.getByTestId('text').textContent).toBe('2');
220+
expect((component.getByTestId('apples') as HTMLOptionElement).selected).toBe(false);
221+
expect((component.getByTestId('oranges') as HTMLOptionElement).selected).toBe(true);
222+
expect((component.getByTestId('lemons') as HTMLOptionElement).selected).toBe(true);
223+
});
224+
225+
function assertSelectOptions(component: RenderResult, value: () => string) {
226+
const inputControl = component.getByTestId('select') as HTMLSelectElement;
227+
component.selectOptions(inputControl, /apples/i);
228+
component.selectOptions(inputControl, ['Oranges', 'Lemons']);
229+
230+
expect(value()).toEqual(['2', '3']);
231+
expect(component.getByTestId('text').textContent).toBe('2,3');
232+
expect((component.getByTestId('apples') as HTMLOptionElement).selected).toBe(false);
233+
expect((component.getByTestId('oranges') as HTMLOptionElement).selected).toBe(true);
234+
expect((component.getByTestId('lemons') as HTMLOptionElement).selected).toBe(true);
235+
}
236+
});

0 commit comments

Comments
 (0)