Skip to content

Commit

Permalink
feat(type): handle maxLength for textinput/textarea (#185)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
klujanrosas committed Jan 29, 2020
1 parent 0ad9eed commit 259fb3a
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 11 deletions.
51 changes: 51 additions & 0 deletions __tests__/react/type.js
Expand Up @@ -135,4 +135,55 @@ describe("userEvent.type", () => {
expect(getByTestId("input")).toHaveProperty("value", text);
}
);

it.each(["input", "textarea"])(
"should type text in <%s> up to maxLength if provided",
type => {
const onChange = jest.fn();
const onKeyDown = jest.fn();
const onKeyPress = jest.fn();
const onKeyUp = jest.fn();
const maxLength = 10;

const { getByTestId } = render(
React.createElement(type, {
"data-testid": "input",
onChange,
onKeyDown,
onKeyPress,
onKeyUp,
maxLength
})
);

const text = "superlongtext";
const slicedText = text.slice(0, maxLength);

const inputEl = getByTestId("input");

userEvent.type(inputEl, text);

expect(inputEl).toHaveProperty("value", slicedText);
expect(onChange).toHaveBeenCalledTimes(slicedText.length);
expect(onKeyPress).toHaveBeenCalledTimes(text.length);
expect(onKeyDown).toHaveBeenCalledTimes(text.length);
expect(onKeyUp).toHaveBeenCalledTimes(text.length);

inputEl.value = "";
onChange.mockClear();
onKeyPress.mockClear();
onKeyDown.mockClear();
onKeyUp.mockClear();

userEvent.type(inputEl, text, {
allAtOnce: true
});

expect(inputEl).toHaveProperty("value", slicedText);
expect(onChange).toHaveBeenCalledTimes(1);
expect(onKeyPress).not.toHaveBeenCalled();
expect(onKeyDown).not.toHaveBeenCalled();
expect(onKeyUp).not.toHaveBeenCalled();
}
);
});
44 changes: 44 additions & 0 deletions __tests__/vue/type.js
Expand Up @@ -145,4 +145,48 @@ describe("userEvent.type", () => {
expect(input).toHaveBeenCalledTimes(1);
}
);

it.each(["input", "textarea"])(
"should type text in <%s> up to maxLength if provided",
type => {
const input = jest.fn();
const keydown = jest.fn();
const keypress = jest.fn();
const keyup = jest.fn();
const maxLength = 10;

const { getByTestId } = renderComponent(
type,
{ input, keydown, keypress, keyup },
{ maxLength }
);

const text = "superlongtext";
const slicedText = text.slice(0, maxLength);

const inputEl = getByTestId("input");

userEvent.type(inputEl, text);
expect(inputEl).toHaveProperty("value", slicedText);
expect(keydown).toHaveBeenCalledTimes(text.length);
expect(keypress).toHaveBeenCalledTimes(text.length);
expect(keyup).toHaveBeenCalledTimes(text.length);

inputEl.value = "";
input.mockClear();
keydown.mockClear();
keypress.mockClear();
keyup.mockClear();

userEvent.type(inputEl, text, {
allAtOnce: true
});

expect(inputEl).toHaveProperty("value", slicedText);
expect(input).toHaveBeenCalledTimes(1);
expect(keydown).not.toHaveBeenCalled();
expect(keypress).not.toHaveBeenCalled();
expect(keyup).not.toHaveBeenCalled();
}
);
});
32 changes: 21 additions & 11 deletions src/index.js
@@ -1,7 +1,7 @@
import { fireEvent } from "@testing-library/dom";

function wait(time) {
return new Promise(function (resolve) {
return new Promise(function(resolve) {
setTimeout(() => resolve(), time);
});
}
Expand Down Expand Up @@ -171,9 +171,9 @@ const userEvent = {
clickElement(element);

const valArray = Array.isArray(values) ? values : [values];
const selectedOptions = Array.from(element.querySelectorAll('option')).filter(
opt => valArray.includes(opt.value)
);
const selectedOptions = Array.from(
element.querySelectorAll("option")
).filter(opt => valArray.includes(opt.value));

if (selectedOptions.length > 0) {
if (element.multiple) {
Expand All @@ -193,9 +193,12 @@ const userEvent = {
delay: 0
};
const opts = Object.assign(defaultOpts, userOpts);

const computedText = text.slice(0, element.maxLength || text.length);

if (opts.allAtOnce) {
if (element.readOnly) return;
fireEvent.input(element, { target: { value: text } });
fireEvent.input(element, { target: { value: computedText } });
} else {
let actuallyTyped = "";
for (let index = 0; index < text.length; index++) {
Expand All @@ -210,13 +213,18 @@ const userEvent = {
keyCode: keyCode,
which: keyCode
});

if (downEvent) {
const pressEvent = fireEvent.keyPress(element, {
key: key,
keyCode,
charCode: keyCode
});
if (pressEvent) {

const isTextPastThreshold =
(actuallyTyped + key).length > computedText.length;

if (pressEvent && !isTextPastThreshold) {
actuallyTyped += key;
if (!element.readOnly)
fireEvent.input(element, {
Expand Down Expand Up @@ -244,24 +252,26 @@ const userEvent = {
"input, button, select, textarea, a[href], [tabindex]"
);

let list = Array.prototype.filter.call(focusableElements, function (item) {
return item.getAttribute("tabindex") !== "-1"&& !item.disabled;
}).map((el, idx) => ({ el, idx }))
let list = Array.prototype.filter
.call(focusableElements, function(item) {
return item.getAttribute("tabindex") !== "-1" && !item.disabled;
})
.map((el, idx) => ({ el, idx }))
.sort((a, b) => {
const tabIndexA = a.el.getAttribute("tabindex");
const tabIndexB = b.el.getAttribute("tabindex");

const diff = tabIndexA - tabIndexB;

return diff !== 0 ? diff : a.idx - b.idx;
})
});

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

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

const { el: next } = (list[nextIndex] || list[defaultIndex]);
const { el: next } = list[nextIndex] || list[defaultIndex];

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

0 comments on commit 259fb3a

Please sign in to comment.