mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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
This commit is contained in:
@@ -83,6 +83,7 @@ export const ConfirmDialog: React.FC<React.PropsWithChildren<ConfirmDialogProps>
|
||||
confirmLoading,
|
||||
confirmText,
|
||||
description,
|
||||
disabled = false,
|
||||
hideCancel,
|
||||
onClose,
|
||||
onConfirm,
|
||||
@@ -122,6 +123,7 @@ export const ConfirmDialog: React.FC<React.PropsWithChildren<ConfirmDialogProps>
|
||||
confirmDialog
|
||||
confirmLoading={confirmLoading}
|
||||
confirmText={confirmText || defaults.confirmText}
|
||||
disabled={disabled}
|
||||
onCancel={!hideCancel ? onClose : undefined}
|
||||
onConfirm={onConfirm || onClose}
|
||||
type={type}
|
||||
|
||||
@@ -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<typeof DeleteDialog>
|
||||
|
||||
@@ -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(
|
||||
<DeleteDialog
|
||||
isOpen
|
||||
onConfirm={jest.fn()}
|
||||
onCancel={jest.fn()}
|
||||
entity="template"
|
||||
name="MyTemplate"
|
||||
/>,
|
||||
)
|
||||
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(
|
||||
<DeleteDialog
|
||||
isOpen
|
||||
onConfirm={jest.fn()}
|
||||
onCancel={jest.fn()}
|
||||
entity="template"
|
||||
name="MyTemplate"
|
||||
/>,
|
||||
)
|
||||
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(
|
||||
<DeleteDialog
|
||||
isOpen
|
||||
onConfirm={jest.fn()}
|
||||
onCancel={jest.fn()}
|
||||
entity="template"
|
||||
name="MyTemplate"
|
||||
/>,
|
||||
)
|
||||
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()
|
||||
})
|
||||
})
|
||||
@@ -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<React.PropsWithChildren<DeleteDialogProps>>
|
||||
isOpen,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
title,
|
||||
description,
|
||||
entity,
|
||||
info,
|
||||
name,
|
||||
confirmLoading,
|
||||
}) => (
|
||||
<ConfirmDialog
|
||||
type="delete"
|
||||
hideCancel={false}
|
||||
open={isOpen}
|
||||
title={title}
|
||||
onConfirm={onConfirm}
|
||||
onClose={onCancel}
|
||||
description={description}
|
||||
confirmLoading={confirmLoading}
|
||||
/>
|
||||
)
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
const { t } = useTranslation("common")
|
||||
const [nameValue, setNameValue] = useState("")
|
||||
const confirmed = name === nameValue
|
||||
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setNameValue(event.target.value)
|
||||
}
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<Typography>{t("deleteDialog.intro", { entity })}</Typography>
|
||||
<Maybe condition={info !== undefined}>
|
||||
<Typography className={styles.warning}>{info}</Typography>
|
||||
</Maybe>
|
||||
<Typography>{t("deleteDialog.confirm", { entity })}</Typography>
|
||||
<Stack spacing={1}>
|
||||
<TextField
|
||||
name="confirmation"
|
||||
id="confirmation"
|
||||
placeholder={name}
|
||||
value={nameValue}
|
||||
onChange={handleChange}
|
||||
label={t("deleteDialog.confirmLabel", { entity })}
|
||||
/>
|
||||
<Maybe condition={nameValue.length > 0 && !confirmed}>
|
||||
<FormHelperText error>{t("deleteDialog.incorrectName", { entity })}</FormHelperText>
|
||||
</Maybe>
|
||||
</Stack>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<ConfirmDialog
|
||||
type="delete"
|
||||
hideCancel={false}
|
||||
open={isOpen}
|
||||
title={t("deleteDialog.title", { entity })}
|
||||
onConfirm={onConfirm}
|
||||
onClose={onCancel}
|
||||
description={content}
|
||||
confirmLoading={confirmLoading}
|
||||
disabled={!confirmed}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
warning: {
|
||||
color: theme.palette.warning.light,
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -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<DialogActionButtonsProps> = ({
|
||||
confirmText = "Confirm",
|
||||
confirmLoading = false,
|
||||
confirmDialog,
|
||||
disabled = false,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
type = "info",
|
||||
@@ -122,6 +125,7 @@ export const DialogActionButtons: React.FC<DialogActionButtonsProps> = ({
|
||||
onClick={onConfirm}
|
||||
color={typeToColor(type)}
|
||||
loading={confirmLoading}
|
||||
disabled={disabled}
|
||||
type="submit"
|
||||
className={combineClasses({
|
||||
[styles.dialogButton]: true,
|
||||
|
||||
@@ -45,7 +45,7 @@ export const EnterpriseSnackbar: FC<React.PropsWithChildren<EnterpriseSnackbarPr
|
||||
<div className={styles.actionWrapper}>
|
||||
{action}
|
||||
<IconButton onClick={onClose} className={styles.iconButton}>
|
||||
<CloseIcon className={styles.closeIcon} />
|
||||
<CloseIcon className={styles.closeIcon} aria-label="close" />
|
||||
</IconButton>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<React.PropsWithChildren<unknown>> = () => {
|
||||
const organizationId = useOrganizationId()
|
||||
const { t } = useTranslation("templatePage")
|
||||
const templateName = useTemplateName()
|
||||
const [templateState, templateSend] = useMachine(templateMachine, {
|
||||
context: {
|
||||
@@ -77,8 +75,8 @@ export const TemplatePage: FC<React.PropsWithChildren<unknown>> = () => {
|
||||
<DeleteDialog
|
||||
isOpen={templateState.matches("confirmingDelete")}
|
||||
confirmLoading={templateState.matches("deleting")}
|
||||
title={t("deleteDialog.title")}
|
||||
description={t("deleteDialog.description")}
|
||||
entity="template"
|
||||
name={template.name}
|
||||
onConfirm={() => {
|
||||
templateSend("CONFIRM_DELETE")
|
||||
}}
|
||||
|
||||
@@ -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(<UsersPage />)
|
||||
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(<UsersPage />)
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 }> = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
type="delete"
|
||||
hideCancel={false}
|
||||
open={usersState.matches("confirmUserDeletion")}
|
||||
confirmLoading={usersState.matches("deletingUser")}
|
||||
title={Language.deleteDialogTitle}
|
||||
confirmText={Language.deleteDialogAction}
|
||||
onConfirm={() => {
|
||||
usersSend("CONFIRM_USER_DELETE")
|
||||
}}
|
||||
onClose={() => {
|
||||
usersSend("CANCEL_USER_DELETE")
|
||||
}}
|
||||
description={
|
||||
<>
|
||||
{Language.deleteDialogMessagePrefix} <strong>{userToBeDeleted?.username}</strong>?
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{userToBeDeleted && (
|
||||
<DeleteDialog
|
||||
isOpen={usersState.matches("confirmUserDeletion")}
|
||||
confirmLoading={usersState.matches("deletingUser")}
|
||||
name={userToBeDeleted.username}
|
||||
entity="user"
|
||||
onConfirm={() => {
|
||||
usersSend("CONFIRM_USER_DELETE")
|
||||
}}
|
||||
onCancel={() => {
|
||||
usersSend("CANCEL_USER_DELETE")
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ConfirmDialog
|
||||
type="delete"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
import { fireEvent, screen, waitFor, within } from "@testing-library/react"
|
||||
import { fireEvent, screen, waitFor } from "@testing-library/react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import i18next from "i18next"
|
||||
import { rest } from "msw"
|
||||
import * as api from "../../api/api"
|
||||
@@ -75,13 +76,12 @@ beforeEach(() => {
|
||||
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()
|
||||
})
|
||||
|
||||
|
||||
@@ -140,8 +140,9 @@ export const WorkspacePage: FC = () => {
|
||||
buildInfo={buildInfoState.context.buildInfo}
|
||||
/>
|
||||
<DeleteDialog
|
||||
title={t("deleteDialog.title")}
|
||||
description={t("deleteDialog.description")}
|
||||
entity="workspace"
|
||||
name={workspace.name}
|
||||
info={t("deleteDialog.info", { timeAgo: dayjs(workspace.created_at).fromNow() })}
|
||||
isOpen={workspaceState.matches({ ready: { build: "askingDelete" } })}
|
||||
onCancel={() => workspaceSend("CANCEL_DELETE")}
|
||||
onConfirm={() => {
|
||||
|
||||
Reference in New Issue
Block a user