Skip to content

Commit 259fb3a

Browse files
authored
feat(type): handle maxLength for textinput/textarea (#185)
* feat(type): handle maxLength for textinput/textarea * test(type): can't write past maxLength * feat: fire keyup/keypress/keydown events * test: keydown/keypress/keydown events
1 parent 0ad9eed commit 259fb3a

File tree

3 files changed

+116
-11
lines changed

3 files changed

+116
-11
lines changed

__tests__/react/type.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,55 @@ describe("userEvent.type", () => {
135135
expect(getByTestId("input")).toHaveProperty("value", text);
136136
}
137137
);
138+
139+
it.each(["input", "textarea"])(
140+
"should type text in <%s> up to maxLength if provided",
141+
type => {
142+
const onChange = jest.fn();
143+
const onKeyDown = jest.fn();
144+
const onKeyPress = jest.fn();
145+
const onKeyUp = jest.fn();
146+
const maxLength = 10;
147+
148+
const { getByTestId } = render(
149+
React.createElement(type, {
150+
"data-testid": "input",
151+
onChange,
152+
onKeyDown,
153+
onKeyPress,
154+
onKeyUp,
155+
maxLength
156+
})
157+
);
158+
159+
const text = "superlongtext";
160+
const slicedText = text.slice(0, maxLength);
161+
162+
const inputEl = getByTestId("input");
163+
164+
userEvent.type(inputEl, text);
165+
166+
expect(inputEl).toHaveProperty("value", slicedText);
167+
expect(onChange).toHaveBeenCalledTimes(slicedText.length);
168+
expect(onKeyPress).toHaveBeenCalledTimes(text.length);
169+
expect(onKeyDown).toHaveBeenCalledTimes(text.length);
170+
expect(onKeyUp).toHaveBeenCalledTimes(text.length);
171+
172+
inputEl.value = "";
173+
onChange.mockClear();
174+
onKeyPress.mockClear();
175+
onKeyDown.mockClear();
176+
onKeyUp.mockClear();
177+
178+
userEvent.type(inputEl, text, {
179+
allAtOnce: true
180+
});
181+
182+
expect(inputEl).toHaveProperty("value", slicedText);
183+
expect(onChange).toHaveBeenCalledTimes(1);
184+
expect(onKeyPress).not.toHaveBeenCalled();
185+
expect(onKeyDown).not.toHaveBeenCalled();
186+
expect(onKeyUp).not.toHaveBeenCalled();
187+
}
188+
);
138189
});

__tests__/vue/type.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,48 @@ describe("userEvent.type", () => {
145145
expect(input).toHaveBeenCalledTimes(1);
146146
}
147147
);
148+
149+
it.each(["input", "textarea"])(
150+
"should type text in <%s> up to maxLength if provided",
151+
type => {
152+
const input = jest.fn();
153+
const keydown = jest.fn();
154+
const keypress = jest.fn();
155+
const keyup = jest.fn();
156+
const maxLength = 10;
157+
158+
const { getByTestId } = renderComponent(
159+
type,
160+
{ input, keydown, keypress, keyup },
161+
{ maxLength }
162+
);
163+
164+
const text = "superlongtext";
165+
const slicedText = text.slice(0, maxLength);
166+
167+
const inputEl = getByTestId("input");
168+
169+
userEvent.type(inputEl, text);
170+
expect(inputEl).toHaveProperty("value", slicedText);
171+
expect(keydown).toHaveBeenCalledTimes(text.length);
172+
expect(keypress).toHaveBeenCalledTimes(text.length);
173+
expect(keyup).toHaveBeenCalledTimes(text.length);
174+
175+
inputEl.value = "";
176+
input.mockClear();
177+
keydown.mockClear();
178+
keypress.mockClear();
179+
keyup.mockClear();
180+
181+
userEvent.type(inputEl, text, {
182+
allAtOnce: true
183+
});
184+
185+
expect(inputEl).toHaveProperty("value", slicedText);
186+
expect(input).toHaveBeenCalledTimes(1);
187+
expect(keydown).not.toHaveBeenCalled();
188+
expect(keypress).not.toHaveBeenCalled();
189+
expect(keyup).not.toHaveBeenCalled();
190+
}
191+
);
148192
});

src/index.js

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { fireEvent } from "@testing-library/dom";
22

33
function wait(time) {
4-
return new Promise(function (resolve) {
4+
return new Promise(function(resolve) {
55
setTimeout(() => resolve(), time);
66
});
77
}
@@ -171,9 +171,9 @@ const userEvent = {
171171
clickElement(element);
172172

173173
const valArray = Array.isArray(values) ? values : [values];
174-
const selectedOptions = Array.from(element.querySelectorAll('option')).filter(
175-
opt => valArray.includes(opt.value)
176-
);
174+
const selectedOptions = Array.from(
175+
element.querySelectorAll("option")
176+
).filter(opt => valArray.includes(opt.value));
177177

178178
if (selectedOptions.length > 0) {
179179
if (element.multiple) {
@@ -193,9 +193,12 @@ const userEvent = {
193193
delay: 0
194194
};
195195
const opts = Object.assign(defaultOpts, userOpts);
196+
197+
const computedText = text.slice(0, element.maxLength || text.length);
198+
196199
if (opts.allAtOnce) {
197200
if (element.readOnly) return;
198-
fireEvent.input(element, { target: { value: text } });
201+
fireEvent.input(element, { target: { value: computedText } });
199202
} else {
200203
let actuallyTyped = "";
201204
for (let index = 0; index < text.length; index++) {
@@ -210,13 +213,18 @@ const userEvent = {
210213
keyCode: keyCode,
211214
which: keyCode
212215
});
216+
213217
if (downEvent) {
214218
const pressEvent = fireEvent.keyPress(element, {
215219
key: key,
216220
keyCode,
217221
charCode: keyCode
218222
});
219-
if (pressEvent) {
223+
224+
const isTextPastThreshold =
225+
(actuallyTyped + key).length > computedText.length;
226+
227+
if (pressEvent && !isTextPastThreshold) {
220228
actuallyTyped += key;
221229
if (!element.readOnly)
222230
fireEvent.input(element, {
@@ -244,24 +252,26 @@ const userEvent = {
244252
"input, button, select, textarea, a[href], [tabindex]"
245253
);
246254

247-
let list = Array.prototype.filter.call(focusableElements, function (item) {
248-
return item.getAttribute("tabindex") !== "-1"&& !item.disabled;
249-
}).map((el, idx) => ({ el, idx }))
255+
let list = Array.prototype.filter
256+
.call(focusableElements, function(item) {
257+
return item.getAttribute("tabindex") !== "-1" && !item.disabled;
258+
})
259+
.map((el, idx) => ({ el, idx }))
250260
.sort((a, b) => {
251261
const tabIndexA = a.el.getAttribute("tabindex");
252262
const tabIndexB = b.el.getAttribute("tabindex");
253263

254264
const diff = tabIndexA - tabIndexB;
255265

256266
return diff !== 0 ? diff : a.idx - b.idx;
257-
})
267+
});
258268

259269
const index = list.findIndex(({ el }) => el === document.activeElement);
260270

261271
let nextIndex = shift ? index - 1 : index + 1;
262272
let defaultIndex = shift ? list.length - 1 : 0;
263273

264-
const { el: next } = (list[nextIndex] || list[defaultIndex]);
274+
const { el: next } = list[nextIndex] || list[defaultIndex];
265275

266276
if (next.getAttribute("tabindex") === null) {
267277
next.setAttribute("tabindex", "0"); // jsdom requires tabIndex=0 for an item to become 'document.activeElement' (the browser does not)

0 commit comments

Comments
 (0)