/* * Copyright 2016 Palantir Technologies, Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { fireEvent, render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, expect, it, vi } from "@blueprintjs/test-commons/vitest"; import { Classes } from "../../common"; import { EditableText } from "./editableText"; describe("", () => { it("renders value", () => { render(); expect(screen.queryByText("alphabet")).toBeInTheDocument(); }); it("renders defaultValue", () => { render(); expect(screen.queryByText("default")).toBeInTheDocument(); }); it("value takes precedence over defaultValue", () => { render(); expect(screen.queryByText("controlled")).toBeInTheDocument(); expect(screen.queryByText("uncontrolled")).not.toBeInTheDocument(); }); it("renders placeholder", () => { render(); expect(screen.queryByText("Edit...")).toBeInTheDocument(); }); it("cannot be edited when disabled", () => { const { container } = render(); // When disabled + isEditing, the component should NOT be in editing state const input = container.querySelector("input"); expect(input).not.toBeInTheDocument(); }); it("allows resetting controlled value to undefined", () => { const { rerender } = render(); expect(screen.queryByText("alphabet")).toBeInTheDocument(); rerender(); expect(screen.queryByText("placeholder")).toBeInTheDocument(); }); it("passes an ID to the underlying span", () => { const { container } = render(); const span = container.querySelector(`#my-id`); expect(span).toBeInTheDocument(); }); describe("when editing", () => { it('renders when editing', () => { render(); const textbox = screen.getByRole("textbox"); expect(textbox).toHaveAttribute("type", "text"); }); it("unrenders input when done editing", () => { const { rerender } = render(); expect(screen.queryByRole("textbox")).toBeInTheDocument(); rerender(); expect(screen.queryByRole("textbox")).not.toBeInTheDocument(); }); it("calls onChange when input is changed", () => { const changeSpy = vi.fn(); // Note: using controlled component (value prop), so fireEvent.change is needed // to directly set values since user.clear() won't work on controlled inputs render(); const textbox = screen.getByRole("textbox"); fireEvent.change(textbox, { target: { value: "hello" } }); fireEvent.change(textbox, { target: { value: " " } }); fireEvent.change(textbox, { target: { value: "world" } }); expect(changeSpy).toHaveBeenCalledTimes(3); expect(changeSpy.mock.calls).toEqual([["hello"], [" "], ["world"]]); }); it("calls onChange when escape key pressed and value is unconfirmed", async () => { const user = userEvent.setup(); const changeSpy = vi.fn(); render( , ); const textbox = screen.getByRole("textbox"); await user.clear(textbox); await user.type(textbox, "hello"); await user.keyboard("{Escape}"); // Last call should be the revert to original value expect(changeSpy).toHaveBeenLastCalledWith("alphabet"); }); it("calls onCancel, does not call onConfirm, and reverts value when escape key pressed", async () => { const user = userEvent.setup(); const cancelSpy = vi.fn(); const confirmSpy = vi.fn(); const OLD_VALUE = "alphabet"; const NEW_VALUE = "hello"; const { container } = render( , ); const textbox = screen.getByRole("textbox"); await user.clear(textbox); await user.type(textbox, NEW_VALUE); await user.keyboard("{Escape}"); expect(confirmSpy).not.toHaveBeenCalled(); expect(cancelSpy).toHaveBeenCalledOnce(); expect(cancelSpy).toHaveBeenCalledWith(OLD_VALUE); // After escape, the component exits edit mode and displays the reverted value in the span const content = container.querySelector(`.${Classes.EDITABLE_TEXT_CONTENT}`); expect(content).toBeInTheDocument(); expect(content).toHaveTextContent(OLD_VALUE); }); it("calls onConfirm, does not call onCancel, and saves value when enter key pressed", async () => { const user = userEvent.setup(); const cancelSpy = vi.fn(); const confirmSpy = vi.fn(); const OLD_VALUE = "alphabet"; const NEW_VALUE = "hello"; render( , ); const textbox = screen.getByRole("textbox"); await user.clear(textbox); await user.type(textbox, NEW_VALUE); await user.keyboard("{Enter}"); expect(cancelSpy).not.toHaveBeenCalled(); expect(confirmSpy).toHaveBeenCalledOnce(); expect(confirmSpy).toHaveBeenCalledWith(NEW_VALUE); }); it("calls onConfirm when enter key pressed even if value didn't change", async () => { const user = userEvent.setup(); const cancelSpy = vi.fn(); const confirmSpy = vi.fn(); const OLD_VALUE = "alphabet"; const NEW_VALUE = "hello"; render( , ); const textbox = screen.getByRole("textbox"); await user.clear(textbox); await user.type(textbox, NEW_VALUE); // change await user.clear(textbox); await user.type(textbox, OLD_VALUE); // revert await user.keyboard("{Enter}"); expect(cancelSpy).not.toHaveBeenCalled(); expect(confirmSpy).toHaveBeenCalledOnce(); expect(confirmSpy).toHaveBeenCalledWith(OLD_VALUE); }); it("calls onEdit when entering edit mode and passes the initial value to the callback", async () => { const user = userEvent.setup(); const editSpy = vi.fn(); const INIT_VALUE = "hello"; const { container } = render(); const div = container.querySelector(`.${Classes.EDITABLE_TEXT}`); expect(div).toBeInTheDocument(); await user.click(div!); expect(editSpy).toHaveBeenCalledOnce(); expect(editSpy).toHaveBeenCalledWith(INIT_VALUE); }); it("caret is placed at the end of the input box", () => { render(); const textbox = screen.getByRole("textbox"); expect(textbox.selectionStart).toBe(8); expect(textbox.selectionEnd).toBe(8); }); it("controlled mode can only change value via props", async () => { const user = userEvent.setup(); let expected = "alphabet"; const { rerender } = render(); const textbox = screen.getByRole("textbox"); await user.type(textbox, "hello"); expect(textbox).toHaveValue(expected); expected = "hello world"; rerender(); expect(textbox).toHaveValue(expected); }); it("applies defaultValue only on initial render", async () => { const user = userEvent.setup(); const { rerender } = render( , ); const textbox = screen.getByDisplayValue("default"); // type new value, then change a prop to cause re-render await user.clear(textbox); await user.type(textbox, "hello"); rerender(); expect(screen.queryByDisplayValue("hello")).toBeInTheDocument(); }); it("the full input box is highlighted when selectAllOnFocus is true", () => { render(); const textbox = screen.getByRole("textbox"); expect(textbox.selectionStart).toBe(0); expect(textbox.selectionEnd).toBe(8); }); }); describe("multiline", () => { it("renders a