diff --git a/apps/reactotron-app/src/renderer/pages/customCommands/index.test.tsx b/apps/reactotron-app/src/renderer/pages/customCommands/index.test.tsx new file mode 100644 index 000000000..ec71fc17c --- /dev/null +++ b/apps/reactotron-app/src/renderer/pages/customCommands/index.test.tsx @@ -0,0 +1,216 @@ +import React from "react" +import { fireEvent, render, screen } from "@testing-library/react" +import { CustomCommandsContext } from "reactotron-core-ui" +import type { CustomCommand } from "reactotron-core-ui" +import { ThemeProvider } from "styled-components" + +import StandaloneContext from "../../contexts/Standalone" +import CustomCommands from "." + +jest.mock("reactotron-core-ui", () => { + const ReactActual = jest.requireActual("react") + + return { + CustomCommandsContext: ReactActual.createContext({ + customCommands: [], + sendCustomCommand: null, + }), + EmptyState: ({ title, children }) => + ReactActual.createElement( + "div", + null, + ReactActual.createElement("span", null, title), + children + ), + Header: ({ title, actions, children }) => + ReactActual.createElement( + "div", + null, + title, + actions.map((action) => + ReactActual.createElement( + "button", + { + "data-tip": action.tip, + key: action.tip, + onClick: action.onClick, + type: "button", + }, + action.tip + ) + ), + children + ), + } +}) + +jest.mock("../../contexts/Standalone", () => { + const ReactActual = jest.requireActual("react") + + return { + __esModule: true, + default: ReactActual.createContext(null), + } +}) + +const theme = { + backgroundLighter: "#333", + backgroundSubtleDark: "#111", + foreground: "#aaa", + foregroundDark: "#888", +} + +const connections = { + ios: { + id: 1, + clientId: "ios-client", + platform: "ios" as const, + commands: [], + connected: true, + }, + android: { + id: 2, + clientId: "android-client", + platform: "android" as const, + commands: [], + connected: true, + }, +} + +const commands: CustomCommand[] = [ + { + clientId: connections.ios.clientId, + id: "ios-command", + command: "reload-ios", + title: "Reload iOS", + description: "Reload the selected iOS app", + }, + { + clientId: connections.android.clientId, + id: "android-command", + command: "clear-android-cache", + title: "Clear Android Cache", + description: "Only available on the Android app", + }, +] + +interface ProvidersProps { + children: React.ReactNode + customCommands?: CustomCommand[] + selectedClientId: keyof typeof connections + sendCustomCommand?: jest.Mock +} + +function Providers({ + children, + customCommands = commands, + selectedClientId, + sendCustomCommand = jest.fn(), +}: ProvidersProps) { + return ( + + + + {children} + + + + ) +} + +function renderPage(props: Omit) { + return render( + + + + ) +} + +describe("CustomCommands", () => { + it("shows and sends only commands from the selected connection", () => { + const sendCustomCommand = jest.fn() + + renderPage({ selectedClientId: "ios", sendCustomCommand }) + + expect(screen.getByText("Reload iOS")).toBeTruthy() + expect(screen.queryByText("Clear Android Cache")).toBeNull() + expect(screen.getAllByText("Send Command")).toHaveLength(1) + + fireEvent.click(screen.getByText("Send Command")) + + expect(sendCustomCommand).toHaveBeenCalledWith("reload-ios", {}) + }) + + it("updates the command list when the selected connection changes", () => { + const view = renderPage({ selectedClientId: "ios" }) + + expect(screen.getByText("Reload iOS")).toBeTruthy() + expect(screen.queryByText("Clear Android Cache")).toBeNull() + + view.rerender( + + + + ) + + expect(screen.queryByText("Reload iOS")).toBeNull() + expect(screen.getByText("Clear Android Cache")).toBeTruthy() + }) + + it("shows the empty state when only another connection has commands", () => { + renderPage({ + selectedClientId: "android", + customCommands: [commands[0]], + }) + + expect(screen.getByText("No Custom Commands")).toBeTruthy() + expect(screen.queryByText("Reload iOS")).toBeNull() + }) + + it("does not reveal another connection's commands through search", () => { + const { container } = renderPage({ selectedClientId: "ios" }) + + fireEvent.click(container.querySelector('[data-tip="Search"]')) + fireEvent.change(screen.getByRole("textbox"), { target: { value: "android" } }) + + expect(screen.queryByText("Clear Android Cache")).toBeNull() + expect(screen.queryByText("Reload iOS")).toBeNull() + }) + + it("keeps single-connection search results working", () => { + const { container } = renderPage({ + selectedClientId: "ios", + customCommands: [commands[0]], + }) + + fireEvent.click(container.querySelector('[data-tip="Search"]')) + fireEvent.change(screen.getByRole("textbox"), { target: { value: "reload" } }) + + expect(screen.getByText("Reload iOS")).toBeTruthy() + expect(screen.queryByText("No Custom Commands")).toBeNull() + }) +}) diff --git a/apps/reactotron-app/src/renderer/pages/customCommands/index.tsx b/apps/reactotron-app/src/renderer/pages/customCommands/index.tsx index b297826ee..bb06574a2 100644 --- a/apps/reactotron-app/src/renderer/pages/customCommands/index.tsx +++ b/apps/reactotron-app/src/renderer/pages/customCommands/index.tsx @@ -6,6 +6,8 @@ import { MdSearch } from "react-icons/md" import { FaMagic } from "react-icons/fa" import { produce } from "immer" +import StandaloneContext from "../../contexts/Standalone" + const Container = styled.div` display: flex; flex-direction: column; @@ -170,17 +172,21 @@ function CustomCommands() { const [search, setSearch] = useState("") const { customCommands, sendCustomCommand } = useContext(CustomCommandsContext) + const { selectedConnection } = useContext(StandaloneContext) + const selectedCustomCommands = customCommands.filter( + (customCommand) => customCommand.clientId === selectedConnection?.clientId + ) const lowerSearch = search.toLowerCase() const filteredCustomCommands = search !== "" - ? customCommands.filter( + ? selectedCustomCommands.filter( (cc) => cc.command.toLowerCase().indexOf(lowerSearch) > -1 || (cc.title || "").toLowerCase().indexOf(lowerSearch) > -1 || (cc.description || "").toLowerCase().indexOf(lowerSearch) > -1 ) - : customCommands + : selectedCustomCommands return ( @@ -205,7 +211,7 @@ function CustomCommands() { )} - {customCommands.length === 0 ? ( + {selectedCustomCommands.length === 0 ? ( When your app registers a custom command it will show here!