/* * Copyright 2018-2022 Elyra Authors * * 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 userEvent from "@testing-library/user-event"; import { Provider } from "react-redux"; import { render, waitFor, screen, fireEvent } from "../test-utils"; import StringArrayControl, { reducer, ListItem } from "./StringArrayControl"; import { createPropertiesStore } from "./test-utils"; const propertyId = { name: "string-array" }; describe("reducer - DELETE_ITEM", () => { it("doesn't delete anything if the item doesn't exist", () => { const result = reducer(["one"], { type: "DELETE_ITEM", payload: { index: 1 }, }); expect(result).toHaveLength(1); expect(result[0]).toBe("one"); }); it("deletes an item", () => { const result = reducer(["one"], { type: "DELETE_ITEM", payload: { index: 0 }, }); expect(result).toHaveLength(0); }); }); describe("reducer - UPSERT_ITEM", () => { it("appends an item if it doesn't exist", () => { const result = reducer(["one"], { type: "UPSERT_ITEM", payload: { value: "two" }, }); expect(result).toHaveLength(2); expect(result[0]).toBe("one"); expect(result[1]).toBe("two"); }); it("updates an item if it exists", () => { const result = reducer(["one"], { type: "UPSERT_ITEM", payload: { value: "new one", index: 0 }, }); expect(result).toHaveLength(1); expect(result[0]).toBe("new one"); }); it("removes the item if value is empty", () => { const result = reducer(["one"], { type: "UPSERT_ITEM", payload: { value: "", index: 0 }, }); expect(result).toHaveLength(0); }); it("doesn't add the item if value is empty", () => { const result = reducer(["one"], { type: "UPSERT_ITEM", payload: { value: "" }, }); expect(result).toHaveLength(1); expect(result[0]).toBe("one"); }); }); describe("reducer - UPSERT_ITEMS", () => { it("appends a list of items", () => { const result = reducer([] as any[], { type: "UPSERT_ITEMS", payload: { index: undefined, items: ["one", "two"], }, }); expect(result).toHaveLength(2); expect(result[0]).toBe("one"); expect(result[1]).toBe("two"); }); it("inserts a list of items at id", () => { const result = reducer(["one", "two", "five", "six"], { type: "UPSERT_ITEMS", payload: { index: 1, items: ["two", "three", "four"], }, }); expect(result).toHaveLength(6); expect(result[0]).toBe("one"); expect(result[1]).toBe("two"); expect(result[2]).toBe("three"); expect(result[3]).toBe("four"); expect(result[4]).toBe("five"); expect(result[5]).toBe("six"); }); it("inserts nothing if items is empty", () => { const result = reducer(["one", "two", "five", "six"], { type: "UPSERT_ITEMS", payload: { index: 1, items: [], }, }); expect(result).toHaveLength(4); expect(result[0]).toBe("one"); expect(result[1]).toBe("two"); expect(result[2]).toBe("five"); expect(result[3]).toBe("six"); }); }); describe("ListItem", () => { it("renders", () => { const { container } = render(<ListItem value="Example list item" />); expect(container.firstChild).toHaveTextContent(/example list item/i); }); it("calls onEdit when double clicked", () => { const handleEdit = jest.fn(); const { rerender } = render( <ListItem value="Example list item" onEdit={handleEdit} /> ); const row = screen.getByTestId("list-row"); userEvent.dblClick(row); expect(handleEdit).toHaveBeenCalledTimes(1); rerender(<ListItem value="Example list item" />); userEvent.dblClick(row); expect(handleEdit).toHaveBeenCalledTimes(1); }); it("calls onEdit when edit button is clicked", () => { const handleEdit = jest.fn(); const { rerender } = render( <ListItem value="Example list item" onEdit={handleEdit} /> ); const editButton = screen.getByTitle(/edit/i); userEvent.click(editButton); expect(handleEdit).toHaveBeenCalledTimes(1); rerender(<ListItem value="Example list item" />); userEvent.click(editButton); expect(handleEdit).toHaveBeenCalledTimes(1); }); it("calls onChooseFiles when browse button is clicked", () => { const handleChooseFiles = jest.fn(); const { rerender } = render( <ListItem value="Example list item" onChooseFiles={handleChooseFiles} canBrowseFiles /> ); const browseButton = screen.getByTitle(/browse/i); userEvent.click(browseButton); expect(handleChooseFiles).toHaveBeenCalledTimes(1); rerender(<ListItem value="Example list item" canBrowseFiles />); userEvent.click(browseButton); expect(handleChooseFiles).toHaveBeenCalledTimes(1); }); it("calls onDelete when delete button is clicked", () => { const handleDelete = jest.fn(); const { rerender } = render( <ListItem value="Example list item" onDelete={handleDelete} /> ); const editButton = screen.getByTitle(/delete/i); userEvent.click(editButton); expect(handleDelete).toHaveBeenCalledTimes(1); rerender(<ListItem value="Example list item" />); userEvent.click(editButton); expect(handleDelete).toHaveBeenCalledTimes(1); }); it("should focus the input when isEditing is true", () => { render(<ListItem isEditing />); const input = screen.getByRole("textbox"); expect(input).toHaveFocus(); }); it("should not have an empty value if value is set", () => { render(<ListItem value="example value" isEditing />); const input = screen.getByRole("textbox"); expect(input).toHaveFocus(); expect(input).toHaveValue("example value"); }); it("call onSubmit with a string when 'ok' button is pressed but value is undefined", () => { const handleSubmit = jest.fn(); render(<ListItem onSubmit={handleSubmit} isEditing />); userEvent.click(screen.getByText(/ok/i)); expect(handleSubmit).toHaveBeenCalledTimes(1); expect(handleSubmit).toHaveBeenCalledWith(""); }); it("call onSubmit when 'ok' button is pressed", () => { const handleSubmit = jest.fn(); const { rerender } = render(<ListItem onSubmit={handleSubmit} isEditing />); userEvent.type(screen.getByRole("textbox"), "I am user entered text"); userEvent.click(screen.getByText(/ok/i)); expect(handleSubmit).toHaveBeenCalledTimes(1); expect(handleSubmit).toHaveBeenCalledWith("I am user entered text"); rerender(<ListItem isEditing />); userEvent.click(screen.getByText(/ok/i)); expect(handleSubmit).toHaveBeenCalledTimes(1); }); it("call onSubmit when 'enter' key is pressed", () => { const handleSubmit = jest.fn(); const { rerender } = render(<ListItem onSubmit={handleSubmit} isEditing />); userEvent.type(screen.getByRole("textbox"), "I am user entered text"); fireEvent.keyDown(screen.getByRole("textbox"), { key: "Enter", code: "Enter", }); expect(handleSubmit).toHaveBeenCalledTimes(1); expect(handleSubmit).toHaveBeenCalledWith("I am user entered text"); rerender(<ListItem isEditing />); fireEvent.keyDown(screen.getByRole("textbox"), { key: "Enter", code: "Enter", }); expect(handleSubmit).toHaveBeenCalledTimes(1); }); it("call onCancel when 'cancel' button is pressed", () => { const handleCancel = jest.fn(); const { rerender } = render(<ListItem onCancel={handleCancel} isEditing />); userEvent.type(screen.getByRole("textbox"), "I am user entered text"); userEvent.click(screen.getByText(/cancel/i)); expect(handleCancel).toHaveBeenCalledTimes(1); expect(handleCancel).toHaveBeenCalledWith(); rerender(<ListItem isEditing />); userEvent.click(screen.getByText(/cancel/i)); expect(handleCancel).toHaveBeenCalledTimes(1); }); it("call onCancel when 'escape' key is pressed", () => { const handleCancel = jest.fn(); const { rerender } = render(<ListItem onCancel={handleCancel} isEditing />); userEvent.type(screen.getByRole("textbox"), "I am user entered text"); fireEvent.keyDown(screen.getByRole("textbox"), { key: "Escape", code: "Escape", }); expect(handleCancel).toHaveBeenCalledTimes(1); expect(handleCancel).toHaveBeenCalledWith(); rerender(<ListItem isEditing />); fireEvent.keyDown(screen.getByRole("textbox"), { key: "Escape", code: "Escape", }); expect(handleCancel).toHaveBeenCalledTimes(1); }); }); it("has an id", () => { expect(StringArrayControl.id()).toBe("StringArrayControl"); }); it("renders only a button when items is undefined", () => { const store = createPropertiesStore(propertyId, undefined); const actionHandler = jest.fn().mockResolvedValue([]); const getHandlers = jest.fn(() => ({ actionHandler, })); const controller = { getHandlers, }; const data = { placeholder: undefined, format: undefined, }; const control = new StringArrayControl(propertyId, controller, data); render(<Provider store={store}>{control.renderControl()}</Provider>); const button = screen.getByRole("button"); expect(button).toHaveTextContent(/add item/i); }); it("renders only a button when items is empty", () => { const store = createPropertiesStore(propertyId, []); const actionHandler = jest.fn().mockResolvedValue([]); const getHandlers = jest.fn(() => ({ actionHandler, })); const controller = { getHandlers, }; const data = { placeholder: undefined, format: undefined, }; const control = new StringArrayControl(propertyId, controller, data); render(<Provider store={store}>{control.renderControl()}</Provider>); const button = screen.getByRole("button"); expect(button).toHaveTextContent(/add item/i); expect(screen.queryByRole("textbox")).not.toBeInTheDocument(); }); it("renders items", () => { const store = createPropertiesStore(propertyId, ["item one", "item two"]); const actionHandler = jest.fn().mockResolvedValue([]); const getHandlers = jest.fn(() => ({ actionHandler, })); const controller = { getHandlers, }; const data = { placeholder: undefined, format: undefined, }; const control = new StringArrayControl(propertyId, controller, data); const { container } = render( <Provider store={store}>{control.renderControl()}</Provider> ); expect(container).toHaveTextContent(/item one/i); expect(container).toHaveTextContent(/item two/i); }); it("renders as second button 'browse' when canBrowseFiles is true", () => { const store = createPropertiesStore(propertyId, []); const actionHandler = jest.fn().mockResolvedValue([]); const getHandlers = jest.fn(() => ({ actionHandler, })); const controller = { getHandlers, }; const data = { placeholder: undefined, format: "file", }; const control = new StringArrayControl(propertyId, controller, data); render(<Provider store={store}>{control.renderControl()}</Provider>); const buttons = screen.getAllByRole("button"); expect(buttons[1]).toHaveTextContent(/browse/i); }); it("shows an input with 'ok' and 'cancel' buttons when 'add item' is clicked", async () => { const store = createPropertiesStore(propertyId, []); const actionHandler = jest.fn().mockResolvedValue([]); const getHandlers = jest.fn(() => ({ actionHandler, })); const controller = { getHandlers, }; const data = { placeholder: undefined, format: undefined, }; const control = new StringArrayControl(propertyId, controller, data); render(<Provider store={store}>{control.renderControl()}</Provider>); userEvent.click(screen.getByText(/add item/i)); expect(screen.queryByText(/add item/i)).not.toBeInTheDocument(); expect(screen.getByRole("textbox")).toBeInTheDocument(); expect(screen.getByText(/ok/i)).toBeInTheDocument(); expect(screen.getByText(/cancel/i)).toBeInTheDocument(); userEvent.click(screen.getByText(/cancel/i)); expect(screen.queryByText(/add item/i)).toBeInTheDocument(); }); it("adds nothing to list if no files are chosen", async () => { const store = createPropertiesStore(propertyId, []); const updatePropertyValue = jest.fn(); const actionHandler = jest.fn().mockResolvedValue([]); const getHandlers = jest.fn(() => ({ actionHandler, })); const controller = { updatePropertyValue, getHandlers, }; const data = { placeholder: undefined, format: "file", }; const control = new StringArrayControl(propertyId, controller, data); render(<Provider store={store}>{control.renderControl()}</Provider>); userEvent.click(screen.getByText(/browse/i)); await waitFor(() => { expect(updatePropertyValue).toHaveBeenCalledTimes(1); expect(updatePropertyValue).toHaveBeenCalledWith(propertyId, []); }); }); it("adds appends items to list if files are chosen", async () => { const store = createPropertiesStore(propertyId, ["one", "two"]); const updatePropertyValue = jest.fn(); const actionHandler = jest.fn().mockResolvedValue(["three", "four"]); const getHandlers = jest.fn(() => ({ actionHandler, })); const controller = { updatePropertyValue, getHandlers, }; const data = { placeholder: undefined, format: "file", }; const control = new StringArrayControl(propertyId, controller, data); render(<Provider store={store}>{control.renderControl()}</Provider>); userEvent.click(screen.getByText(/browse/i)); await waitFor(() => { expect(updatePropertyValue).toHaveBeenCalledTimes(1); expect(updatePropertyValue).toHaveBeenCalledWith(propertyId, [ "one", "two", "three", "four", ]); }); }); it("calls updatePropertyValue with entered text", async () => { const store = createPropertiesStore(propertyId, []); const updatePropertyValue = jest.fn(); const actionHandler = jest.fn().mockResolvedValue([]); const getHandlers = jest.fn(() => ({ actionHandler, })); const controller = { updatePropertyValue, getHandlers, }; const data = { placeholder: undefined, format: undefined, }; const control = new StringArrayControl(propertyId, controller, data); render(<Provider store={store}>{control.renderControl()}</Provider>); userEvent.click(screen.getByText(/add item/i)); userEvent.type(screen.getByRole("textbox"), "I am user entered text"); userEvent.click(screen.getByText(/ok/i)); expect(updatePropertyValue).toHaveBeenCalledTimes(1); expect(updatePropertyValue).toHaveBeenCalledWith(propertyId, [ "I am user entered text", ]); }); it("can delete list item", async () => { const store = createPropertiesStore(propertyId, ["one"]); const updatePropertyValue = jest.fn(); const actionHandler = jest.fn().mockResolvedValue([]); const getHandlers = jest.fn(() => ({ actionHandler, })); const controller = { updatePropertyValue, getHandlers, }; const data = { placeholder: undefined, format: undefined, }; const control = new StringArrayControl(propertyId, controller, data); render(<Provider store={store}>{control.renderControl()}</Provider>); userEvent.click(screen.getByTitle(/delete/i)); expect(updatePropertyValue).toHaveBeenCalledTimes(1); expect(updatePropertyValue).toHaveBeenCalledWith(propertyId, []); }); it("can edit list item", async () => { const store = createPropertiesStore(propertyId, ["one"]); const updatePropertyValue = jest.fn(); const actionHandler = jest.fn().mockResolvedValue([]); const getHandlers = jest.fn(() => ({ actionHandler, })); const controller = { updatePropertyValue, getHandlers, }; const data = { placeholder: undefined, format: undefined, }; const control = new StringArrayControl(propertyId, controller, data); render(<Provider store={store}>{control.renderControl()}</Provider>); userEvent.click(screen.getByTitle(/edit/i)); userEvent.type(screen.getByRole("textbox"), "updated one"); userEvent.click(screen.getByText(/ok/i)); expect(updatePropertyValue).toHaveBeenCalledTimes(1); expect(updatePropertyValue).toHaveBeenCalledWith(propertyId, ["updated one"]); }); it("doesn't edit list item when canceled", async () => { const store = createPropertiesStore(propertyId, ["one"]); const updatePropertyValue = jest.fn(); const actionHandler = jest.fn().mockResolvedValue([]); const getHandlers = jest.fn(() => ({ actionHandler, })); const controller = { updatePropertyValue, getHandlers, }; const data = { placeholder: undefined, format: undefined, }; const control = new StringArrayControl(propertyId, controller, data); render(<Provider store={store}>{control.renderControl()}</Provider>); userEvent.click(screen.getByTitle(/edit/i)); userEvent.type(screen.getByRole("textbox"), "updated one"); userEvent.click(screen.getByText(/cancel/i)); expect(updatePropertyValue).toHaveBeenCalledTimes(0); userEvent.click(screen.getByTitle(/edit/i)); expect(screen.getByRole("textbox")).toHaveValue("one"); }); it("can browse for files from list item", async () => { const store = createPropertiesStore(propertyId, ["one"]); const updatePropertyValue = jest.fn(); const actionHandler = jest.fn().mockResolvedValue(["file.py"]); const getHandlers = jest.fn(() => ({ actionHandler, })); const controller = { updatePropertyValue, getHandlers, }; const data = { placeholder: undefined, format: "file", }; const control = new StringArrayControl(propertyId, controller, data); render(<Provider store={store}>{control.renderControl()}</Provider>); userEvent.click(screen.getByTitle(/browse/i)); await waitFor(() => { expect(updatePropertyValue).toHaveBeenCalledTimes(1); expect(updatePropertyValue).toHaveBeenCalledWith(propertyId, ["file.py"]); }); }); it("doesn't call updatePropertyValue when no files are retrieved", async () => { const store = createPropertiesStore(propertyId, ["one"]); const updatePropertyValue = jest.fn(); const getHandlers = jest.fn(() => ({})); const controller = { updatePropertyValue, getHandlers, }; const data = { placeholder: undefined, format: "file", }; const control = new StringArrayControl(propertyId, controller, data); render(<Provider store={store}>{control.renderControl()}</Provider>); userEvent.click(screen.getByTitle(/browse/i)); await waitFor(() => { expect(updatePropertyValue).toHaveBeenCalledTimes(0); }); });