user-event icon indicating copy to clipboard operation
user-event copied to clipboard

Provide a convenient way to select the whole text with type()

Open glenjamin opened this issue 3 years ago • 7 comments

Problem description

When using .type() I've been finding a common pattern that emerges is variations on the following

        await userEvent.type(titleInput, 'Edited title', {
          initialSelectionStart: 0,
          initialSelectionEnd: titleInput.value.length,
        });

To represent selecting the whole content and then overwriting it when typing

There was some related discussion in https://github.com/testing-library/user-event/issues/755#issuecomment-950689400

Suggested solution

Adding a new property like selectAll would be convenient, but probably opens up some awkward interactions if initialSelectionStart or initialSelectionEnd are also supplied.

From poking around in the code it looks like if I simply set a large value for initialSelectionEnd then it'll get clamped to the length of the input - which is basically what I want (although it's still quite verbose in a test). Perhaps the answer would be document the pattern of using a large number or Infinity as the selection end?

Additional context

No response

glenjamin avatar Aug 22 '22 11:08 glenjamin

I'd rather not add any options to .type(), but remove the existing options in the future. Reasons from the linked issue regarding a clear option apply.

The magic happening when these options are used is not obvious to many users and isn't in line with the philosophy to simulate a user interaction.

The .tripleClick() API provides a convenient way to describe a workflow that can actually be performed by a real user:

await user.tripleClick(element) // selects a whole line - in <input/> this is everything
await user.keyboard('somethingNew')

ph-fritsche avatar Aug 22 '22 11:08 ph-fritsche

That makes sense, but presumably that can't be combined with type as it would reset the selection?

The description for type says

You can use type() if you just want to conveniently insert some text into an input field or textarea.

Is it no longer a goal to have a convenient way to suffinctly fill in a form?

glenjamin avatar Aug 22 '22 11:08 glenjamin

Without the options, .type(element, 'foo') is:

await user.click(element)
await user.keyboard('foo')

If that's what you want, you can use .type().

If you use skipClick option, just use .keyboard(). If you use initialSelection*, consider setting the selection like a user either per .pointer() or .keyboard(). (For shortcuts see https://testing-library.com/docs/user-event/convenience)

To me

await user.type(element, 'foo', {initialSelectionStart: 0, initialSelectionEnd: 1000})

doesn't seem more convenient than

await user.tripleClick(element)
await user.keyboard('foo')

But the latter is unambiguous regarding the events that should happen.

ph-fritsche avatar Aug 22 '22 12:08 ph-fritsche

I see what you're saying - I think my mental model was that the convenience helpers are a way of succinctly expressing what the user is doing at a higher level.

My app doesn't care that they use triple-click to select the text, the user interaction I care about is that the user replaces the content of the text box with new text.

I can write a wrapper for my own tests but that ends up being quite specific to my own code.

glenjamin avatar Aug 22 '22 13:08 glenjamin

Would a .selectAndType(element, 'text') API satisfy your use case?

ph-fritsche avatar Sep 01 '22 10:09 ph-fritsche

Yeah, something specifically intended to replace a whole textbox would be great! 👍

glenjamin avatar Sep 01 '22 10:09 glenjamin

I created my own custom clearAndType utility like this:

// custom-user-events.ts

import {
  UserEvent,
  UserEventApi,
} from "@testing-library/user-event/dist/types/setup/setup";

type CustomUserEvents = {
  clearAndType: (
    ...args: Parameters<UserEventApi["type"]>
  ) => ReturnType<UserEventApi["type"]>;
};

export async function clearAndType(
  user: UserEvent,
  ...args: Parameters<CustomUserEvents["clearAndType"]>
): Promise<void> {
  const [element] = args;

  await user.clear(element);
  await user.type(...args);
}

export default CustomUserEvents;
// utils.tsx

import * as customUserEvents from "./custom-user-events";
import type CustomUserEventTypes from "./custom-user-events";

function setup(jsx) {
  const baseUser = userEvent.setup();
  const customUser = Object.entries(customUserEvents).reduce(
    (accumulator, [key, customUserEvent]) => {
      return {
        ...accumulator,
        [key]: customUserEvent.bind(null, baseUser),
      };
    },
    {}
  ) as CustomUserEventTypes;

  const user = {
    ...baseUser,
    ...customUser,
  };

  return {
    user,
    ...render(jsx),
  };
}
// foo-spec.tsx

import { setup } from './utils'

test('edits name', async () => {
  const { user } = setup(<MyComponent />)

  user.type(screen.getByLabelText("Name"), "Dwight Schrute")
  user.clearAndType(screen.getByLabelText("Name"), "Michael Scott")
  expect(screen.getByLabelText("Name")).toHaveValue("Michael Scott")
})

Works for me and can't really think of any reason why this would be a bad idea. Feels kind of similar to custom queries.

phegman avatar Aug 30 '23 17:08 phegman