Skip to content

Commit

Permalink
chore(tests): create shared delete button and add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
huwshimi committed Jan 7, 2025
1 parent ba0ebe6 commit 9b1cc5b
Show file tree
Hide file tree
Showing 13 changed files with 504 additions and 190 deletions.
224 changes: 224 additions & 0 deletions ui/src/components/DeletePanelButton/DeletePanelButton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import { screen, waitFor } from "@testing-library/dom";
import * as reactQuery from "@tanstack/react-query";
import userEvent from "@testing-library/user-event";
import {
NotificationConsumer,
NotificationProvider,
} from "@canonical/react-components";

import { renderComponent } from "test/utils";

import DeletePanelButton from "./DeletePanelButton";
import { Label } from "./types";
import { Location } from "react-router-dom";

vi.mock("@tanstack/react-query", async () => {
const actual = await vi.importActual("@tanstack/react-query");
return {
...actual,
useQueryClient: vi.fn(),
};
});

beforeEach(() => {
vi.spyOn(reactQuery, "useQueryClient").mockReturnValue({
invalidateQueries: vi.fn(),
} as unknown as reactQuery.QueryClient);
});

test("displays the delete button", () => {
renderComponent(
<DeletePanelButton
confirmButtonLabel="Confirm"
confirmContent="Content"
entityName="Nebulous"
invalidateQuery="nebulous"
onDelete={() => Promise.resolve()}
successPath="/nebulous"
successMessage="successfully formed"
/>,
);
expect(
screen.getByRole("button", { name: Label.DELETE }),
).toBeInTheDocument();
});

test("displays a confirmation", async () => {
renderComponent(
<DeletePanelButton
confirmButtonLabel="Confirm"
confirmContent="Content"
confirmTitle="Define"
entityName="Nebulous"
invalidateQuery="nebulous"
onDelete={() => Promise.resolve()}
successPath="/nebulous"
successMessage="successfully formed"
/>,
);
await userEvent.click(screen.getByRole("button", { name: Label.DELETE }));
expect(screen.getByRole("dialog", { name: "Define" })).toBeInTheDocument();
});

test("can disable the confirm button", async () => {
renderComponent(
<DeletePanelButton
confirmButtonLabel="Confirm"
confirmButtonDisabled
confirmContent="Content"
entityName="Nebulous"
invalidateQuery="nebulous"
onDelete={() => Promise.resolve()}
successPath="/nebulous"
successMessage="successfully formed"
/>,
);
await userEvent.click(screen.getByRole("button", { name: Label.DELETE }));
expect(screen.getByRole("button", { name: "Confirm" })).toBeDisabled();
});

test("starts deletion", async () => {
renderComponent(
<DeletePanelButton
confirmButtonLabel="Confirm"
confirmContent="Content"
entityName="Nebulous"
invalidateQuery="nebulous"
onDelete={() => Promise.resolve()}
successPath="/nebulous"
successMessage="successfully formed"
/>,
);
await userEvent.click(screen.getByRole("button", { name: Label.DELETE }));
await userEvent.click(screen.getByRole("button", { name: "Confirm" }));
expect(document.querySelector(".u-animation--spin")).toBeInTheDocument();
});

test("calls the delete method", async () => {
const onDelete = vi.fn().mockImplementation(() => Promise.resolve());
renderComponent(
<DeletePanelButton
confirmButtonLabel="Confirm"
confirmContent="Content"
entityName="Nebulous"
invalidateQuery="nebulous"
onDelete={onDelete}
successPath="/nebulous"
successMessage="successfully formed"
/>,
);
await userEvent.click(screen.getByRole("button", { name: Label.DELETE }));
await userEvent.click(screen.getByRole("button", { name: "Confirm" }));
expect(onDelete).toHaveBeenCalled();
});

test("handles a successful delete call", async () => {
let location: Location | null = null;
renderComponent(
<NotificationProvider>
<NotificationConsumer />
<DeletePanelButton
confirmButtonLabel="Confirm"
confirmContent="Content"
entityName="Nebulous"
invalidateQuery="nebulous"
onDelete={() => Promise.resolve()}
successPath="/nebulous"
successMessage="successfully formed"
/>
</NotificationProvider>,
{
setLocation: (newLocation) => {
location = newLocation;
},
},
);
await userEvent.click(screen.getByRole("button", { name: Label.DELETE }));
await userEvent.click(screen.getByRole("button", { name: "Confirm" }));
expect(
screen
.getByText("successfully formed")
.closest(".p-notification--positive"),
).toBeInTheDocument();
expect((location as Location | null)?.pathname).toBe("/nebulous");
});

test("notifies on error", async () => {
renderComponent(
<NotificationProvider>
<NotificationConsumer />
<DeletePanelButton
confirmButtonLabel="Confirm"
confirmContent="Content"
entityName="Nebulous"
invalidateQuery="nebulous"
onDelete={() => Promise.reject("Oops")}
successPath="/nebulous"
successMessage="successfully formed"
/>
</NotificationProvider>,
);
await userEvent.click(screen.getByRole("button", { name: Label.DELETE }));
await userEvent.click(screen.getByRole("button", { name: "Confirm" }));
expect(
screen
.getByText("Nebulous deletion failed")
.closest(".p-notification--negative"),
).toBeInTheDocument();
expect(screen.getByText("Oops")).toHaveClass("p-notification__message");
});

test("notifies on error object", async () => {
renderComponent(
<NotificationProvider>
<NotificationConsumer />
<DeletePanelButton
confirmButtonLabel="Confirm"
confirmContent="Content"
entityName="Nebulous"
invalidateQuery="nebulous"
onDelete={() => Promise.reject(new Error("Oops"))}
successPath="/nebulous"
successMessage="successfully formed"
/>
</NotificationProvider>,
);
await userEvent.click(screen.getByRole("button", { name: Label.DELETE }));
await userEvent.click(screen.getByRole("button", { name: "Confirm" }));
expect(
screen
.getByText("Nebulous deletion failed")
.closest(".p-notification--negative"),
).toBeInTheDocument();
expect(screen.getByText("Oops")).toHaveClass("p-notification__message");
});

test("invlidates queries and hides the spinner on success", async () => {
const invalidateQueries = vi.fn();
vi.spyOn(reactQuery, "useQueryClient").mockReturnValue({
invalidateQueries,
} as unknown as reactQuery.QueryClient);
renderComponent(
<DeletePanelButton
confirmButtonLabel="Confirm"
confirmContent="Content"
entityName="Nebulous"
invalidateQuery="nebulous"
onDelete={() => Promise.resolve()}
successPath="/nebulous"
successMessage="successfully formed"
/>,
);
await userEvent.click(screen.getByRole("button", { name: Label.DELETE }));
await userEvent.click(screen.getByRole("button", { name: "Confirm" }));
await waitFor(() =>
expect(invalidateQueries).toHaveBeenCalledWith({
queryKey: ["nebulous"],
}),
);
await waitFor(() =>
expect(
document.querySelector(".u-animation--spin"),
).not.toBeInTheDocument(),
);
});
62 changes: 62 additions & 0 deletions ui/src/components/DeletePanelButton/DeletePanelButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { FC, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useQueryClient } from "@tanstack/react-query";
import { ConfirmationButton, useNotify } from "@canonical/react-components";
import { Label, Props } from "./types";

const DeletePanelButton: FC<Props> = ({
confirmButtonDisabled,
confirmButtonLabel,
confirmContent,
confirmTitle = "Confirm delete",
invalidateQuery,
entityName,
onDelete,
successMessage,
successPath,
}) => {
const notify = useNotify();
const queryClient = useQueryClient();
const [isLoading, setLoading] = useState(false);
const navigate = useNavigate();

const handleDelete = () => {
setLoading(true);
onDelete()
.then(() => {
navigate(successPath, notify.queue(notify.success(successMessage)));
})
.catch((error: unknown) => {
notify.failure(
`${entityName} deletion failed`,
error instanceof Error ? error : null,
typeof error === "string" ? error : null,
);
})
.finally(() => {
setLoading(false);
void queryClient.invalidateQueries({
queryKey: [invalidateQuery],
});
});
};

return (
<ConfirmationButton
className="u-no-margin--bottom"
loading={isLoading}
confirmationModalProps={{
children: confirmContent,
confirmButtonDisabled,
confirmButtonLabel,
onConfirm: handleDelete,
title: confirmTitle,
}}
title={confirmTitle}
>
{Label.DELETE}
</ConfirmationButton>
);
};

export default DeletePanelButton;
2 changes: 2 additions & 0 deletions ui/src/components/DeletePanelButton/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from "./DeletePanelButton";
export { Label as DeletePanelButtonLabel } from "./types";
17 changes: 17 additions & 0 deletions ui/src/components/DeletePanelButton/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ReactNode } from "react";

export type Props = {
confirmButtonLabel: string;
confirmButtonDisabled?: boolean;
confirmTitle?: string;
confirmContent: ReactNode;
entityName: string;
onDelete: () => Promise<unknown>;
successMessage: string;
successPath: string;
invalidateQuery: string;
};

export enum Label {
DELETE = "Delete",
}
27 changes: 27 additions & 0 deletions ui/src/pages/clients/DeleteClientBtn/DeleteClientBtn.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { screen } from "@testing-library/dom";
import userEvent from "@testing-library/user-event";

import { renderComponent } from "test/utils";

import DeleteClientBtn from "./DeleteClientBtn";
import { mockClient } from "test/mocks/clients";
import { DeletePanelButtonLabel } from "components/DeletePanelButton";
import MockAdapter from "axios-mock-adapter";
import { axiosInstance } from "api/axios";
import { Label } from "./types";

const mock = new MockAdapter(axiosInstance);

beforeEach(() => {
mock.reset();
});

test("deletes the client", async () => {
const client = mockClient();
renderComponent(<DeleteClientBtn client={client} />);
await userEvent.click(
screen.getByRole("button", { name: DeletePanelButtonLabel.DELETE }),
);
await userEvent.click(screen.getByRole("button", { name: Label.CONFIRM }));
expect(mock.history.delete[0].url).toBe(`/clients/${client.client_id}`);
});
Loading

0 comments on commit 9b1cc5b

Please sign in to comment.