From 0899548208576c21212332fec560a5ed0f3c1090 Mon Sep 17 00:00:00 2001 From: Presley Pizzo <1290996+presleyp@users.noreply.github.com> Date: Tue, 20 Sep 2022 17:13:48 -0400 Subject: [PATCH] feat: have user type name of thing to delete for extra safety (#4080) * Add info and text field to delete dialog * Format * Use DeleteDialog for Users, nix info except for Workspaces * Format * Update storybook * Add and update tests * Fix the worst of the UsersPage test bugs * Fix users page tests * Fix workspace tests * Format --- .../Dialogs/ConfirmDialog/ConfirmDialog.tsx | 2 + .../DeleteDialog/DeleteDialog.stories.tsx | 12 +- .../DeleteDialog/DeleteDialog.test.tsx | 57 ++++++++ .../Dialogs/DeleteDialog/DeleteDialog.tsx | 83 ++++++++--- site/src/components/Dialogs/Dialog.tsx | 4 + .../EnterpriseSnackbar/EnterpriseSnackbar.tsx | 2 +- site/src/i18n/en/common.json | 7 + site/src/i18n/en/templatePage.json | 4 - site/src/i18n/en/workspacePage.json | 3 +- site/src/pages/TemplatePage/TemplatePage.tsx | 6 +- site/src/pages/UsersPage/UsersPage.test.tsx | 135 +++++++++--------- site/src/pages/UsersPage/UsersPage.tsx | 37 ++--- .../WorkspacePage/WorkspacePage.test.tsx | 26 ++-- .../src/pages/WorkspacePage/WorkspacePage.tsx | 5 +- 14 files changed, 243 insertions(+), 140 deletions(-) create mode 100644 site/src/components/Dialogs/DeleteDialog/DeleteDialog.test.tsx diff --git a/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx b/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx index 268199b8e4..93439c1ede 100644 --- a/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx +++ b/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx @@ -83,6 +83,7 @@ export const ConfirmDialog: React.FC confirmLoading, confirmText, description, + disabled = false, hideCancel, onClose, onConfirm, @@ -122,6 +123,7 @@ export const ConfirmDialog: React.FC confirmDialog confirmLoading={confirmLoading} confirmText={confirmText || defaults.confirmText} + disabled={disabled} onCancel={!hideCancel ? onClose : undefined} onConfirm={onConfirm || onClose} type={type} diff --git a/site/src/components/Dialogs/DeleteDialog/DeleteDialog.stories.tsx b/site/src/components/Dialogs/DeleteDialog/DeleteDialog.stories.tsx index 0e2190a9c2..58fb268519 100644 --- a/site/src/components/Dialogs/DeleteDialog/DeleteDialog.stories.tsx +++ b/site/src/components/Dialogs/DeleteDialog/DeleteDialog.stories.tsx @@ -15,12 +15,14 @@ export default { control: "boolean", defaultValue: true, }, - title: { - defaultValue: "Delete Something", + entity: { + defaultValue: "foo", }, - description: { - defaultValue: - "This is irreversible. To confirm, type the name of the thing you want to delete.", + name: { + defaultValue: "MyFoo", + }, + info: { + defaultValue: "Here's some info about the foo so you know you're deleting the right one.", }, }, } as ComponentMeta diff --git a/site/src/components/Dialogs/DeleteDialog/DeleteDialog.test.tsx b/site/src/components/Dialogs/DeleteDialog/DeleteDialog.test.tsx new file mode 100644 index 0000000000..098bd95ce1 --- /dev/null +++ b/site/src/components/Dialogs/DeleteDialog/DeleteDialog.test.tsx @@ -0,0 +1,57 @@ +import { screen } from "@testing-library/react" +import userEvent from "@testing-library/user-event" +import i18next from "i18next" +import { render } from "testHelpers/renderHelpers" +import { DeleteDialog } from "./DeleteDialog" + +describe("DeleteDialog", () => { + it("disables confirm button when the text field is empty", () => { + render( + , + ) + const confirmButton = screen.getByRole("button", { name: "Delete" }) + expect(confirmButton).toBeDisabled() + }) + + it("disables confirm button when the text field is filled incorrectly", async () => { + const { t } = i18next + render( + , + ) + const labelText = t("deleteDialog.confirmLabel", { ns: "common", entity: "template" }) + const textField = screen.getByLabelText(labelText) + await userEvent.type(textField, "MyTemplateWrong") + const confirmButton = screen.getByRole("button", { name: "Delete" }) + expect(confirmButton).toBeDisabled() + }) + + it("enables confirm button when the text field is filled correctly", async () => { + const { t } = i18next + render( + , + ) + const labelText = t("deleteDialog.confirmLabel", { ns: "common", entity: "template" }) + const textField = screen.getByLabelText(labelText) + await userEvent.type(textField, "MyTemplate") + const confirmButton = screen.getByRole("button", { name: "Delete" }) + expect(confirmButton).not.toBeDisabled() + }) +}) diff --git a/site/src/components/Dialogs/DeleteDialog/DeleteDialog.tsx b/site/src/components/Dialogs/DeleteDialog/DeleteDialog.tsx index 49184dff35..c49eaffe99 100644 --- a/site/src/components/Dialogs/DeleteDialog/DeleteDialog.tsx +++ b/site/src/components/Dialogs/DeleteDialog/DeleteDialog.tsx @@ -1,12 +1,20 @@ -import React, { ReactNode } from "react" +import FormHelperText from "@material-ui/core/FormHelperText" +import makeStyles from "@material-ui/core/styles/makeStyles" +import TextField from "@material-ui/core/TextField" +import Typography from "@material-ui/core/Typography" +import { Maybe } from "components/Conditionals/Maybe" +import { Stack } from "components/Stack/Stack" +import React, { ChangeEvent, useState } from "react" +import { useTranslation } from "react-i18next" import { ConfirmDialog } from "../ConfirmDialog/ConfirmDialog" export interface DeleteDialogProps { isOpen: boolean onConfirm: () => void onCancel: () => void - title: string - description: string | ReactNode + entity: string + name: string + info?: string confirmLoading?: boolean } @@ -14,18 +22,59 @@ export const DeleteDialog: React.FC> isOpen, onCancel, onConfirm, - title, - description, + entity, + info, + name, confirmLoading, -}) => ( - -) +}) => { + const styles = useStyles() + const { t } = useTranslation("common") + const [nameValue, setNameValue] = useState("") + const confirmed = name === nameValue + const handleChange = (event: ChangeEvent) => { + setNameValue(event.target.value) + } + + const content = ( + <> + {t("deleteDialog.intro", { entity })} + + {info} + + {t("deleteDialog.confirm", { entity })} + + + 0 && !confirmed}> + {t("deleteDialog.incorrectName", { entity })} + + + + ) + + return ( + + ) +} + +const useStyles = makeStyles((theme) => ({ + warning: { + color: theme.palette.warning.light, + }, +})) diff --git a/site/src/components/Dialogs/Dialog.tsx b/site/src/components/Dialogs/Dialog.tsx index fd7d70fbe6..0d53f462f8 100644 --- a/site/src/components/Dialogs/Dialog.tsx +++ b/site/src/components/Dialogs/Dialog.tsx @@ -72,6 +72,8 @@ export interface DialogActionButtonsProps { confirmLoading?: boolean /** Whether or not this is a confirm dialog */ confirmDialog?: boolean + /** Whether or not the submit button is disabled */ + disabled?: boolean /** Called when cancel is clicked */ onCancel?: () => void /** Called when confirm is clicked */ @@ -94,6 +96,7 @@ export const DialogActionButtons: React.FC = ({ confirmText = "Confirm", confirmLoading = false, confirmDialog, + disabled = false, onCancel, onConfirm, type = "info", @@ -122,6 +125,7 @@ export const DialogActionButtons: React.FC = ({ onClick={onConfirm} color={typeToColor(type)} loading={confirmLoading} + disabled={disabled} type="submit" className={combineClasses({ [styles.dialogButton]: true, diff --git a/site/src/components/EnterpriseSnackbar/EnterpriseSnackbar.tsx b/site/src/components/EnterpriseSnackbar/EnterpriseSnackbar.tsx index 4e7d4e1fc3..475d61ad3f 100644 --- a/site/src/components/EnterpriseSnackbar/EnterpriseSnackbar.tsx +++ b/site/src/components/EnterpriseSnackbar/EnterpriseSnackbar.tsx @@ -45,7 +45,7 @@ export const EnterpriseSnackbar: FC {action} - + } diff --git a/site/src/i18n/en/common.json b/site/src/i18n/en/common.json index 2630903370..514205e9da 100644 --- a/site/src/i18n/en/common.json +++ b/site/src/i18n/en/common.json @@ -11,5 +11,12 @@ "canceled": "Canceled action", "failed": "Failed", "queued": "Queued" + }, + "deleteDialog": { + "title": "Delete {{entity}}", + "intro": "Deleting this {{entity}} is irreversible!", + "confirm": "Are you sure you want to proceed? Type the name of this {{entity}} below to confirm.", + "confirmLabel": "Name of {{entity}} to delete", + "incorrectName": "Incorrect {{entity}} name." } } diff --git a/site/src/i18n/en/templatePage.json b/site/src/i18n/en/templatePage.json index 0e735c4bc3..9116f007b8 100644 --- a/site/src/i18n/en/templatePage.json +++ b/site/src/i18n/en/templatePage.json @@ -1,7 +1,3 @@ { - "deleteDialog": { - "title": "Delete template", - "description": "Deleting a template is irreversible. Are you sure you want to proceed?" - }, "deleteSuccess": "Template successfully deleted." } diff --git a/site/src/i18n/en/workspacePage.json b/site/src/i18n/en/workspacePage.json index f7469fa3af..9094ce9f7d 100644 --- a/site/src/i18n/en/workspacePage.json +++ b/site/src/i18n/en/workspacePage.json @@ -1,7 +1,6 @@ { "deleteDialog": { - "title": "Delete workspace", - "description": "Deleting a workspace is irreversible. Are you sure you want to proceed?" + "info": "This workspace was created {{timeAgo}}." }, "workspaceScheduleButton": { "schedule": "Schedule", diff --git a/site/src/pages/TemplatePage/TemplatePage.tsx b/site/src/pages/TemplatePage/TemplatePage.tsx index 95141c5384..129a63cef0 100644 --- a/site/src/pages/TemplatePage/TemplatePage.tsx +++ b/site/src/pages/TemplatePage/TemplatePage.tsx @@ -2,7 +2,6 @@ import { useMachine, useSelector } from "@xstate/react" import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog" import { FC, useContext } from "react" import { Helmet } from "react-helmet-async" -import { useTranslation } from "react-i18next" import { Navigate, useParams } from "react-router-dom" import { selectPermissions } from "xServices/auth/authSelectors" import { XServiceContext } from "xServices/StateContext" @@ -24,7 +23,6 @@ const useTemplateName = () => { export const TemplatePage: FC> = () => { const organizationId = useOrganizationId() - const { t } = useTranslation("templatePage") const templateName = useTemplateName() const [templateState, templateSend] = useMachine(templateMachine, { context: { @@ -77,8 +75,8 @@ export const TemplatePage: FC> = () => { { templateSend("CONFIRM_DELETE") }} diff --git a/site/src/pages/UsersPage/UsersPage.test.tsx b/site/src/pages/UsersPage/UsersPage.test.tsx index 6f39b558d1..bda2fa2794 100644 --- a/site/src/pages/UsersPage/UsersPage.test.tsx +++ b/site/src/pages/UsersPage/UsersPage.test.tsx @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/no-floating-promises */ import { fireEvent, screen, waitFor, within } from "@testing-library/react" +import userEvent from "@testing-library/user-event" +import { i18n } from "i18n" import { rest } from "msw" import { Language as usersXServiceLanguage } from "xServices/users/usersXService" import * as API from "../../api/api" @@ -20,23 +22,20 @@ import { permissionsToCheck } from "../../xServices/auth/authXService" import { Language as UsersPageLanguage, UsersPage } from "./UsersPage" import { Language as UsersViewLanguage } from "./UsersPageView" +const { t } = i18n + const suspendUser = async (setupActionSpies: () => void) => { + const user = userEvent.setup() // Get the first user in the table - const users = await screen.findAllByText(/.*@coder.com/) - const firstUserRow = users[0].closest("tr") - if (!firstUserRow) { - throw new Error("Error on get the first user row") - } + const moreButtons = await screen.findAllByLabelText("more") + const firstMoreButton = moreButtons[0] - // Click on the "more" button to display the "Suspend" option - const moreButton = within(firstUserRow).getByLabelText("more") - - fireEvent.click(moreButton) + await user.click(firstMoreButton) const menu = await screen.findByRole("menu") const suspendButton = within(menu).getByText(UsersTableBodyLanguage.suspendMenuItem) - fireEvent.click(suspendButton) + await user.click(suspendButton) // Check if the confirm message is displayed const confirmDialog = await screen.findByRole("dialog") @@ -48,53 +47,51 @@ const suspendUser = async (setupActionSpies: () => void) => { setupActionSpies() // Click on the "Confirm" button - const confirmButton = within(confirmDialog).getByText(UsersPageLanguage.suspendDialogAction) - fireEvent.click(confirmButton) + const confirmButton = await within(confirmDialog).findByText( + UsersPageLanguage.suspendDialogAction, + ) + await user.click(confirmButton) } const deleteUser = async (setupActionSpies: () => void) => { - // Get the first user in the table - const users = await screen.findAllByText(/.*@coder.com/) - const firstUserRow = users[0].closest("tr") - if (!firstUserRow) { - throw new Error("Error on get the first user row") - } + const user = userEvent.setup() + // Click on the "more" button to display the "Delete" option + // Needs to await fetching users and fetching permissions, because they're needed to see the more button + const moreButtons = await screen.findAllByLabelText("more") + // get MockUser2 + const selectedMoreButton = moreButtons[1] - // Click on the "more" button to display the "Suspend" option - const moreButton = within(firstUserRow).getByLabelText("more") - - fireEvent.click(moreButton) + await user.click(selectedMoreButton) const menu = await screen.findByRole("menu") - const suspendButton = within(menu).getByText(UsersTableBodyLanguage.deleteMenuItem) + const deleteButton = within(menu).getByText(UsersTableBodyLanguage.deleteMenuItem) - fireEvent.click(suspendButton) + await user.click(deleteButton) // Check if the confirm message is displayed const confirmDialog = await screen.findByRole("dialog") expect(confirmDialog).toHaveTextContent( - `${UsersPageLanguage.deleteDialogMessagePrefix} ${MockUser.username}?`, + t("deleteDialog.confirm", { ns: "common", entity: "user" }), ) + // Confirm with text input + const labelText = t("deleteDialog.confirmLabel", { ns: "common", entity: "user" }) + const textField = screen.getByLabelText(labelText) + const dialog = screen.getByRole("dialog") + await user.type(textField, MockUser2.username) + // Setup spies to check the actions after setupActionSpies() // Click on the "Confirm" button - const confirmButton = within(confirmDialog).getByText(UsersPageLanguage.deleteDialogAction) - fireEvent.click(confirmButton) + const confirmButton = within(dialog).getByRole("button", { name: "Delete" }) + await user.click(confirmButton) } const activateUser = async (setupActionSpies: () => void) => { - // Get the first user in the table - const users = await screen.findAllByText(/.*@coder.com/) - const firstUserRow = users[2].closest("tr") - if (!firstUserRow) { - throw new Error("Error on get the first user row") - } - - // Click on the "more" button to display the "Activate" option - const moreButton = within(firstUserRow).getByLabelText("more") - fireEvent.click(moreButton) + const moreButtons = await screen.findAllByLabelText("more") + const suspendedMoreButton = moreButtons[2] + fireEvent.click(suspendedMoreButton) const menu = screen.getByRole("menu") const activateButton = within(menu).getByText(UsersTableBodyLanguage.activateMenuItem) @@ -115,17 +112,10 @@ const activateUser = async (setupActionSpies: () => void) => { } const resetUserPassword = async (setupActionSpies: () => void) => { - // Get the first user in the table - const users = await screen.findAllByText(/.*@coder.com/) - const firstUserRow = users[0].closest("tr") - if (!firstUserRow) { - throw new Error("Error on get the first user row") - } + const moreButtons = await screen.findAllByLabelText("more") + const firstMoreButton = moreButtons[0] - // Click on the "more" button to display the "Suspend" option - const moreButton = within(firstUserRow).getByLabelText("more") - - fireEvent.click(moreButton) + fireEvent.click(firstMoreButton) const menu = screen.getByRole("menu") const resetPasswordButton = within(menu).getByText(UsersTableBodyLanguage.resetPasswordMenuItem) @@ -184,13 +174,15 @@ describe("UsersPage", () => { expect(users.length).toEqual(3) }) - it("shows 'Create user' button to an authorized user", () => { + it("shows 'Create user' button to an authorized user", async () => { render() - const createUserButton = screen.queryByText(UsersViewLanguage.createButton) + const createUserButton = await screen.findByText(UsersViewLanguage.createButton) + // wait for users page to finish loading + await screen.findAllByLabelText("more") expect(createUserButton).toBeDefined() }) - it("does not show 'Create user' button to unauthorized user", () => { + it("does not show 'Create user' button to unauthorized user", async () => { server.use( rest.post("/api/v2/users/:userId/authorization", async (req, res, ctx) => { const permissions = Object.keys(permissionsToCheck) @@ -207,6 +199,8 @@ describe("UsersPage", () => { ) render() const createUserButton = screen.queryByText(UsersViewLanguage.createButton) + // wait for users page to finish loading + await screen.findAllByLabelText("more") expect(createUserButton).toBeNull() }) @@ -222,13 +216,11 @@ describe("UsersPage", () => { await suspendUser(() => { jest.spyOn(API, "suspendUser").mockResolvedValueOnce(MockUser) - jest - .spyOn(API, "getUsers") - .mockImplementationOnce(() => Promise.resolve([MockUser, MockUser2])) + jest.spyOn(API, "getUsers").mockResolvedValueOnce([SuspendedMockUser, MockUser2]) }) // Check if the success message is displayed - screen.findByText(usersXServiceLanguage.suspendUserSuccess) + await screen.findByText(usersXServiceLanguage.suspendUserSuccess) // Check if the API was called correctly expect(API.suspendUser).toBeCalledTimes(1) @@ -252,7 +244,7 @@ describe("UsersPage", () => { }) // Check if the error message is displayed - screen.findByText(usersXServiceLanguage.suspendUserError) + await screen.findByText(usersXServiceLanguage.suspendUserError) // Check if the API was called correctly expect(API.suspendUser).toBeCalledTimes(1) @@ -273,20 +265,21 @@ describe("UsersPage", () => { await deleteUser(() => { jest.spyOn(API, "deleteUser").mockResolvedValueOnce(undefined) - jest - .spyOn(API, "getUsers") - .mockImplementationOnce(() => Promise.resolve([MockUser, MockUser2])) + jest.spyOn(API, "getUsers").mockResolvedValueOnce([MockUser, SuspendedMockUser]) }) // Check if the success message is displayed - screen.findByText(usersXServiceLanguage.deleteUserSuccess) + await screen.findByText(usersXServiceLanguage.deleteUserSuccess) // Check if the API was called correctly expect(API.deleteUser).toBeCalledTimes(1) - expect(API.deleteUser).toBeCalledWith(MockUser.id) + expect(API.deleteUser).toBeCalledWith(MockUser2.id) - // Check if the users list was reload - await waitFor(() => expect(API.getUsers).toBeCalledTimes(1)) + // Check if the users list was reloaded + await waitFor(() => { + const users = screen.getAllByLabelText("more") + expect(users.length).toEqual(2) + }) }) }) describe("when it fails", () => { @@ -303,11 +296,11 @@ describe("UsersPage", () => { }) // Check if the error message is displayed - screen.findByText(usersXServiceLanguage.deleteUserError) + await screen.findByText(usersXServiceLanguage.deleteUserError) // Check if the API was called correctly expect(API.deleteUser).toBeCalledTimes(1) - expect(API.deleteUser).toBeCalledWith(MockUser.id) + expect(API.deleteUser).toBeCalledWith(MockUser2.id) }) }) }) @@ -330,7 +323,7 @@ describe("UsersPage", () => { }) // Check if the success message is displayed - screen.findByText(usersXServiceLanguage.activateUserSuccess) + await screen.findByText(usersXServiceLanguage.activateUserSuccess) // Check if the API was called correctly expect(API.activateUser).toBeCalledTimes(1) @@ -351,7 +344,7 @@ describe("UsersPage", () => { }) // Check if the error message is displayed - screen.findByText(usersXServiceLanguage.activateUserError) + await screen.findByText(usersXServiceLanguage.activateUserError) // Check if the API was called correctly expect(API.activateUser).toBeCalledTimes(1) @@ -375,7 +368,7 @@ describe("UsersPage", () => { }) // Check if the success message is displayed - screen.findByText(usersXServiceLanguage.resetUserPasswordSuccess) + await screen.findByText(usersXServiceLanguage.resetUserPasswordSuccess) // Check if the API was called correctly expect(API.updateUserPassword).toBeCalledTimes(1) @@ -399,7 +392,7 @@ describe("UsersPage", () => { }) // Check if the error message is displayed - screen.findByText(usersXServiceLanguage.resetUserPasswordError) + await screen.findByText(usersXServiceLanguage.resetUserPasswordError) // Check if the API was called correctly expect(API.updateUserPassword).toBeCalledTimes(1) @@ -455,7 +448,7 @@ describe("UsersPage", () => { }, MockAuditorRole) // Check if the error message is displayed - const errorMessage = screen.findByText(usersXServiceLanguage.updateUserRolesError) + const errorMessage = await screen.findByText(usersXServiceLanguage.updateUserRolesError) await waitFor(() => expect(errorMessage).toBeDefined()) // Check if the API was called correctly @@ -485,8 +478,8 @@ describe("UsersPage", () => { await updateUserRole(() => {}, MockAuditorRole) // Check if the error message is displayed - const errorMessage = screen.findByText("message from the backend") - await waitFor(() => expect(errorMessage).toBeDefined()) + const errorMessage = await screen.findByText("message from the backend") + expect(errorMessage).toBeDefined() }) }) }) diff --git a/site/src/pages/UsersPage/UsersPage.tsx b/site/src/pages/UsersPage/UsersPage.tsx index 6a6b022050..0e61325594 100644 --- a/site/src/pages/UsersPage/UsersPage.tsx +++ b/site/src/pages/UsersPage/UsersPage.tsx @@ -1,4 +1,5 @@ import { useActor, useMachine } from "@xstate/react" +import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog" import { FC, ReactNode, useContext, useEffect } from "react" import { Helmet } from "react-helmet-async" import { useNavigate } from "react-router" @@ -11,9 +12,6 @@ import { XServiceContext } from "../../xServices/StateContext" import { UsersPageView } from "./UsersPageView" export const Language = { - deleteDialogTitle: "Delete user", - deleteDialogAction: "Delete", - deleteDialogMessagePrefix: "Do you want to delete the user", suspendDialogTitle: "Suspend user", suspendDialogAction: "Suspend", suspendDialogMessagePrefix: "Do you want to suspend the user", @@ -127,25 +125,20 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => { }} /> - { - usersSend("CONFIRM_USER_DELETE") - }} - onClose={() => { - usersSend("CANCEL_USER_DELETE") - }} - description={ - <> - {Language.deleteDialogMessagePrefix} {userToBeDeleted?.username}? - - } - /> + {userToBeDeleted && ( + { + usersSend("CONFIRM_USER_DELETE") + }} + onCancel={() => { + usersSend("CANCEL_USER_DELETE") + }} + /> + )} { describe("WorkspacePage", () => { it("shows a workspace", async () => { await renderWorkspacePage() - const workspaceName = screen.getByText(MockWorkspace.name) + const workspaceName = await screen.findByText(MockWorkspace.name) expect(workspaceName).toBeDefined() - }) - it("shows the status of the workspace", async () => { - await renderWorkspacePage() - const status = screen.getByRole("status") + const status = await screen.findByRole("status") expect(status).toHaveTextContent("Running") + // wait for workspace page to finish loading + await screen.findByText("stop") }) it("requests a stop job when the user presses Stop", async () => { const stopWorkspaceMock = jest @@ -91,6 +91,7 @@ describe("WorkspacePage", () => { }) it("requests a delete job when the user presses Delete and confirms", async () => { + const user = userEvent.setup() const deleteWorkspaceMock = jest .spyOn(api, "deleteWorkspace") .mockResolvedValueOnce(MockWorkspaceBuild) @@ -98,15 +99,16 @@ describe("WorkspacePage", () => { // open the workspace action popover so we have access to all available ctas const trigger = await screen.findByTestId("workspace-actions-button") - fireEvent.click(trigger) + await user.click(trigger) const button = await screen.findByText(Language.delete) - fireEvent.click(button) + await user.click(button) - const confirmDialog = await screen.findByRole("dialog") - const confirmButton = within(confirmDialog).getByText("Delete") - - fireEvent.click(confirmButton) + const labelText = t("deleteDialog.confirmLabel", { ns: "common", entity: "workspace" }) + const textField = await screen.findByLabelText(labelText) + await user.type(textField, MockWorkspace.name) + const confirmButton = await screen.findByRole("button", { name: "Delete" }) + await user.click(confirmButton) expect(deleteWorkspaceMock).toBeCalled() }) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index eaf9ce1117..17e25a3e20 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -140,8 +140,9 @@ export const WorkspacePage: FC = () => { buildInfo={buildInfoState.context.buildInfo} /> workspaceSend("CANCEL_DELETE")} onConfirm={() => {