mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
feat: Add suspend user action (#1275)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import "@testing-library/jest-dom"
|
||||
import { server } from "./src/testHelpers/server"
|
||||
|
||||
// Establish API mocking before all tests through MSW.
|
||||
|
||||
Generated
+23318
File diff suppressed because it is too large
Load Diff
@@ -58,6 +58,7 @@
|
||||
"@storybook/addon-essentials": "6.4.22",
|
||||
"@storybook/addon-links": "6.4.22",
|
||||
"@storybook/react": "6.4.22",
|
||||
"@testing-library/jest-dom": "5.16.4",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"@testing-library/user-event": "14.1.1",
|
||||
"@types/express": "4.17.13",
|
||||
|
||||
@@ -76,7 +76,7 @@ export const getApiKey = async (): Promise<Types.APIKeyResponse> => {
|
||||
}
|
||||
|
||||
export const getUsers = async (): Promise<TypesGen.User[]> => {
|
||||
const response = await axios.get<TypesGen.User[]>("/api/v2/users?offset=0&limit=1000")
|
||||
const response = await axios.get<TypesGen.User[]>("/api/v2/users?status=active")
|
||||
return response.data
|
||||
}
|
||||
|
||||
@@ -135,3 +135,8 @@ export const updateProfile = async (userId: string, data: Types.UpdateProfileReq
|
||||
const response = await axios.put(`/api/v2/users/${userId}/profile`, data)
|
||||
return response.data
|
||||
}
|
||||
|
||||
export const suspendUser = async (userId: TypesGen.User["id"]): Promise<TypesGen.User> => {
|
||||
const response = await axios.put<TypesGen.User>(`/api/v2/users/${userId}/suspend`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { displaySuccess, isNotificationTextPrefixed, MsgType, NotificationMsg } from "./utils"
|
||||
import {
|
||||
displayError,
|
||||
displaySuccess,
|
||||
isNotificationTextPrefixed,
|
||||
MsgType,
|
||||
NotificationMsg,
|
||||
SnackbarEventType,
|
||||
} from "./utils"
|
||||
|
||||
describe("Snackbar", () => {
|
||||
describe("isNotificationTextPrefixed", () => {
|
||||
@@ -76,4 +83,18 @@ describe("Snackbar", () => {
|
||||
expect(extractNotificationEvent(dispatchEventMock)).toStrictEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe("displayError", () => {
|
||||
it("shows the title and the message", (done) => {
|
||||
const message = "Some error happened"
|
||||
|
||||
window.addEventListener(SnackbarEventType, (event) => {
|
||||
const notificationEvent = event as CustomEvent<NotificationMsg>
|
||||
expect(notificationEvent.detail.msg).toEqual(message)
|
||||
done()
|
||||
})
|
||||
|
||||
displayError(message)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -60,3 +60,7 @@ export const displayMsg = (msg: string, additionalMsg?: string): void => {
|
||||
export const displaySuccess = (msg: string, additionalMsg?: string): void => {
|
||||
dispatchNotificationEvent(MsgType.Success, msg, additionalMsg ? [additionalMsg] : undefined)
|
||||
}
|
||||
|
||||
export const displayError = (msg: string, additionalMsg?: string): void => {
|
||||
dispatchNotificationEvent(MsgType.Error, msg, additionalMsg ? [additionalMsg] : undefined)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Column, Table } from "../Table/Table"
|
||||
import { TableRowMenu } from "../TableRowMenu/TableRowMenu"
|
||||
import { UserCell } from "../UserCell/UserCell"
|
||||
|
||||
const Language = {
|
||||
export const Language = {
|
||||
pageTitle: "Users",
|
||||
usersTitle: "All users",
|
||||
emptyMessage: "No users found",
|
||||
@@ -27,9 +27,10 @@ const columns: Column<UserResponse>[] = [
|
||||
|
||||
export interface UsersTableProps {
|
||||
users: UserResponse[]
|
||||
onSuspendUser: (user: UserResponse) => void
|
||||
}
|
||||
|
||||
export const UsersTable: React.FC<UsersTableProps> = ({ users }) => {
|
||||
export const UsersTable: React.FC<UsersTableProps> = ({ users, onSuspendUser }) => {
|
||||
return (
|
||||
<Table
|
||||
columns={columns}
|
||||
@@ -42,9 +43,7 @@ export const UsersTable: React.FC<UsersTableProps> = ({ users }) => {
|
||||
menuItems={[
|
||||
{
|
||||
label: Language.suspendMenuItem,
|
||||
onClick: () => {
|
||||
// TO-DO: Add suspend action here
|
||||
},
|
||||
onClick: onSuspendUser,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,38 @@
|
||||
import { screen } from "@testing-library/react"
|
||||
import { fireEvent, screen, waitFor, within } from "@testing-library/react"
|
||||
import React from "react"
|
||||
import { render } from "../../testHelpers"
|
||||
import { UsersPage } from "./UsersPage"
|
||||
import * as API from "../../api"
|
||||
import { GlobalSnackbar } from "../../components/GlobalSnackbar/GlobalSnackbar"
|
||||
import { Language as UsersTableLanguage } from "../../components/UsersTable/UsersTable"
|
||||
import { MockUser, MockUser2, render } from "../../testHelpers"
|
||||
import { Language as usersXServiceLanguage } from "../../xServices/users/usersXService"
|
||||
import { Language as UsersPageLanguage, UsersPage } from "./UsersPage"
|
||||
|
||||
const suspendUser = 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")
|
||||
}
|
||||
|
||||
// Click on the "more" button to display the "Suspend" option
|
||||
const moreButton = within(firstUserRow).getByLabelText("more")
|
||||
fireEvent.click(moreButton)
|
||||
const menu = screen.getByRole("menu")
|
||||
const suspendButton = within(menu).getByText(UsersTableLanguage.suspendMenuItem)
|
||||
fireEvent.click(suspendButton)
|
||||
|
||||
// Check if the confirm message is displayed
|
||||
const confirmDialog = screen.getByRole("dialog")
|
||||
expect(confirmDialog).toHaveTextContent(`${UsersPageLanguage.suspendDialogMessagePrefix} ${MockUser.username}?`)
|
||||
|
||||
// Setup spies to check the actions after
|
||||
setupActionSpies()
|
||||
|
||||
// Click on the "Confirm" button
|
||||
const confirmButton = within(confirmDialog).getByText(UsersPageLanguage.suspendDialogAction)
|
||||
fireEvent.click(confirmButton)
|
||||
}
|
||||
|
||||
describe("Users Page", () => {
|
||||
it("shows users", async () => {
|
||||
@@ -9,4 +40,54 @@ describe("Users Page", () => {
|
||||
const users = await screen.findAllByText(/.*@coder.com/)
|
||||
expect(users.length).toEqual(2)
|
||||
})
|
||||
|
||||
describe("suspend user", () => {
|
||||
describe("when it is success", () => {
|
||||
it("shows a success message and refresh the page", async () => {
|
||||
render(
|
||||
<>
|
||||
<UsersPage />
|
||||
<GlobalSnackbar />
|
||||
</>,
|
||||
)
|
||||
|
||||
await suspendUser(() => {
|
||||
jest.spyOn(API, "suspendUser").mockResolvedValueOnce(MockUser)
|
||||
jest.spyOn(API, "getUsers").mockImplementationOnce(() => Promise.resolve([MockUser, MockUser2]))
|
||||
})
|
||||
|
||||
// Check if the success message is displayed
|
||||
await screen.findByText(usersXServiceLanguage.suspendUserSuccess)
|
||||
|
||||
// Check if the API was called correctly
|
||||
expect(API.suspendUser).toBeCalledTimes(1)
|
||||
expect(API.suspendUser).toBeCalledWith(MockUser.id)
|
||||
|
||||
// Check if the users list was reload
|
||||
await waitFor(() => expect(API.getUsers).toBeCalledTimes(1))
|
||||
})
|
||||
})
|
||||
|
||||
describe("when it fails", () => {
|
||||
it("shows an error message", async () => {
|
||||
render(
|
||||
<>
|
||||
<UsersPage />
|
||||
<GlobalSnackbar />
|
||||
</>,
|
||||
)
|
||||
|
||||
await suspendUser(() => {
|
||||
jest.spyOn(API, "suspendUser").mockRejectedValueOnce({})
|
||||
})
|
||||
|
||||
// Check if the success message is displayed
|
||||
await screen.findByText(usersXServiceLanguage.suspendUserError)
|
||||
|
||||
// Check if the API was called correctly
|
||||
expect(API.suspendUser).toBeCalledTimes(1)
|
||||
expect(API.suspendUser).toBeCalledWith(MockUser.id)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import { useActor } from "@xstate/react"
|
||||
import React, { useContext, useEffect } from "react"
|
||||
import { useNavigate } from "react-router"
|
||||
import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary"
|
||||
import { ConfirmDialog } from "../../components/ConfirmDialog/ConfirmDialog"
|
||||
import { FullScreenLoader } from "../../components/Loader/FullScreenLoader"
|
||||
import { XServiceContext } from "../../xServices/StateContext"
|
||||
import { UsersPageView } from "./UsersPageView"
|
||||
|
||||
export const Language = {
|
||||
suspendDialogTitle: "Suspend user",
|
||||
suspendDialogAction: "Suspend",
|
||||
suspendDialogMessagePrefix: "Do you want to suspend the user",
|
||||
}
|
||||
|
||||
export const UsersPage: React.FC = () => {
|
||||
const xServices = useContext(XServiceContext)
|
||||
const [usersState, usersSend] = useActor(xServices.usersXService)
|
||||
const { users, getUsersError } = usersState.context
|
||||
const { users, getUsersError, userIdToSuspend } = usersState.context
|
||||
const navigate = useNavigate()
|
||||
const userToBeSuspended = users?.find((u) => u.id === userIdToSuspend)
|
||||
|
||||
/**
|
||||
* Fetch users on component mount
|
||||
@@ -19,20 +26,42 @@ export const UsersPage: React.FC = () => {
|
||||
usersSend("GET_USERS")
|
||||
}, [usersSend])
|
||||
|
||||
if (usersState.matches("error")) {
|
||||
return <ErrorSummary error={getUsersError} />
|
||||
}
|
||||
|
||||
if (!users) {
|
||||
return <FullScreenLoader />
|
||||
} else {
|
||||
return (
|
||||
<UsersPageView
|
||||
users={users}
|
||||
openUserCreationDialog={() => {
|
||||
navigate("/users/create")
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<UsersPageView
|
||||
users={users}
|
||||
openUserCreationDialog={() => {
|
||||
navigate("/users/create")
|
||||
}}
|
||||
onSuspendUser={(user) => {
|
||||
usersSend({ type: "SUSPEND_USER", userId: user.id })
|
||||
}}
|
||||
error={getUsersError}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
type="delete"
|
||||
hideCancel={false}
|
||||
open={usersState.matches("confirmUserSuspension")}
|
||||
confirmLoading={usersState.matches("suspendingUser")}
|
||||
title={Language.suspendDialogTitle}
|
||||
confirmText={Language.suspendDialogAction}
|
||||
onConfirm={() => {
|
||||
usersSend("CONFIRM_USER_SUSPENSION")
|
||||
}}
|
||||
onClose={() => {
|
||||
usersSend("CANCEL_USER_SUSPENSION")
|
||||
}}
|
||||
description={
|
||||
<>
|
||||
{Language.suspendDialogMessagePrefix} <strong>{userToBeSuspended?.username}</strong>?
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react"
|
||||
import { UserResponse } from "../../api/types"
|
||||
import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary"
|
||||
import { Header } from "../../components/Header/Header"
|
||||
import { Margins } from "../../components/Margins/Margins"
|
||||
import { Stack } from "../../components/Stack/Stack"
|
||||
@@ -13,14 +14,21 @@ export const Language = {
|
||||
export interface UsersPageViewProps {
|
||||
users: UserResponse[]
|
||||
openUserCreationDialog: () => void
|
||||
onSuspendUser: (user: UserResponse) => void
|
||||
error?: unknown
|
||||
}
|
||||
|
||||
export const UsersPageView: React.FC<UsersPageViewProps> = ({ users, openUserCreationDialog }) => {
|
||||
export const UsersPageView: React.FC<UsersPageViewProps> = ({
|
||||
users,
|
||||
openUserCreationDialog,
|
||||
onSuspendUser,
|
||||
error,
|
||||
}) => {
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Header title={Language.pageTitle} action={{ text: Language.newUserButton, onClick: openUserCreationDialog }} />
|
||||
<Margins>
|
||||
<UsersTable users={users} />
|
||||
{error ? <ErrorSummary error={error} /> : <UsersTable users={users} onSuspendUser={onSuspendUser} />}
|
||||
</Margins>
|
||||
</Stack>
|
||||
)
|
||||
|
||||
@@ -30,9 +30,11 @@ export function renderWithAuth(ui: JSX.Element, { route = "/" }: { route?: strin
|
||||
const renderResult = wrappedRender(
|
||||
<MemoryRouter initialEntries={[route]}>
|
||||
<XServiceProvider>
|
||||
<Routes>
|
||||
<Route path={route} element={<RequireAuth>{ui}</RequireAuth>} />
|
||||
</Routes>
|
||||
<ThemeProvider theme={dark}>
|
||||
<Routes>
|
||||
<Route path={route} element={<RequireAuth>{ui}</RequireAuth>} />
|
||||
</Routes>
|
||||
</ThemeProvider>
|
||||
</XServiceProvider>
|
||||
</MemoryRouter>,
|
||||
)
|
||||
|
||||
@@ -3,20 +3,29 @@ import * as API from "../../api"
|
||||
import { ApiError, FieldErrors, isApiError, mapApiErrorToFieldErrors } from "../../api/errors"
|
||||
import * as Types from "../../api/types"
|
||||
import * as TypesGen from "../../api/typesGenerated"
|
||||
import { displaySuccess } from "../../components/GlobalSnackbar/utils"
|
||||
import { displayError, displaySuccess } from "../../components/GlobalSnackbar/utils"
|
||||
|
||||
export const Language = {
|
||||
createUserSuccess: "Successfully created user.",
|
||||
suspendUserSuccess: "Successfully suspended the user.",
|
||||
suspendUserError: "Error on suspend the user",
|
||||
}
|
||||
|
||||
export interface UsersContext {
|
||||
users?: TypesGen.User[]
|
||||
userIdToSuspend?: TypesGen.User["id"]
|
||||
getUsersError?: Error | unknown
|
||||
createUserError?: Error | unknown
|
||||
createUserFormErrors?: FieldErrors
|
||||
suspendUserError?: Error | unknown
|
||||
}
|
||||
|
||||
export type UsersEvent = { type: "GET_USERS" } | { type: "CREATE"; user: Types.CreateUserRequest }
|
||||
export type UsersEvent =
|
||||
| { type: "GET_USERS" }
|
||||
| { type: "CREATE"; user: Types.CreateUserRequest }
|
||||
| { type: "SUSPEND_USER"; userId: TypesGen.User["id"] }
|
||||
| { type: "CONFIRM_USER_SUSPENSION" }
|
||||
| { type: "CANCEL_USER_SUSPENSION" }
|
||||
|
||||
export const usersMachine = createMachine(
|
||||
{
|
||||
@@ -31,18 +40,25 @@ export const usersMachine = createMachine(
|
||||
createUser: {
|
||||
data: TypesGen.User
|
||||
}
|
||||
suspendUser: {
|
||||
data: TypesGen.User
|
||||
}
|
||||
},
|
||||
},
|
||||
id: "usersState",
|
||||
initial: "idle",
|
||||
context: {
|
||||
users: [],
|
||||
},
|
||||
initial: "idle",
|
||||
states: {
|
||||
idle: {
|
||||
on: {
|
||||
GET_USERS: "gettingUsers",
|
||||
CREATE: "creatingUser",
|
||||
SUSPEND_USER: {
|
||||
target: "confirmUserSuspension",
|
||||
actions: ["assignUserIdToSuspend"],
|
||||
},
|
||||
},
|
||||
},
|
||||
gettingUsers: {
|
||||
@@ -86,6 +102,28 @@ export const usersMachine = createMachine(
|
||||
},
|
||||
tags: "loading",
|
||||
},
|
||||
confirmUserSuspension: {
|
||||
on: {
|
||||
CONFIRM_USER_SUSPENSION: "suspendingUser",
|
||||
CANCEL_USER_SUSPENSION: "idle",
|
||||
},
|
||||
},
|
||||
suspendingUser: {
|
||||
entry: "clearSuspendUserError",
|
||||
invoke: {
|
||||
src: "suspendUser",
|
||||
id: "suspendUser",
|
||||
onDone: {
|
||||
// Update users list
|
||||
target: "gettingUsers",
|
||||
actions: ["displaySuspendSuccess"],
|
||||
},
|
||||
onError: {
|
||||
target: "idle",
|
||||
actions: ["assignSuspendUserError", "displaySuspendedErrorMessage"],
|
||||
},
|
||||
},
|
||||
},
|
||||
error: {
|
||||
on: {
|
||||
GET_USERS: "gettingUsers",
|
||||
@@ -95,8 +133,18 @@ export const usersMachine = createMachine(
|
||||
},
|
||||
{
|
||||
services: {
|
||||
getUsers: API.getUsers,
|
||||
// Passing API.getUsers directly does not invoke the function properly
|
||||
// when it is mocked. This happen in the UsersPage tests inside of the
|
||||
// "shows a success message and refresh the page" test case.
|
||||
getUsers: () => API.getUsers(),
|
||||
createUser: (_, event) => API.createUser(event.user),
|
||||
suspendUser: (context) => {
|
||||
if (!context.userIdToSuspend) {
|
||||
throw new Error("userIdToSuspend is undefined")
|
||||
}
|
||||
|
||||
return API.suspendUser(context.userIdToSuspend)
|
||||
},
|
||||
},
|
||||
guards: {
|
||||
isFormError: (_, event) => isApiError(event.data),
|
||||
@@ -108,6 +156,9 @@ export const usersMachine = createMachine(
|
||||
assignGetUsersError: assign({
|
||||
getUsersError: (_, event) => event.data,
|
||||
}),
|
||||
assignUserIdToSuspend: assign({
|
||||
userIdToSuspend: (_, event) => event.userId,
|
||||
}),
|
||||
clearGetUsersError: assign((context: UsersContext) => ({
|
||||
...context,
|
||||
getUsersError: undefined,
|
||||
@@ -119,13 +170,25 @@ export const usersMachine = createMachine(
|
||||
// the guard ensures it is ApiError
|
||||
createUserFormErrors: (_, event) => mapApiErrorToFieldErrors((event.data as ApiError).response.data),
|
||||
}),
|
||||
assignSuspendUserError: assign({
|
||||
suspendUserError: (_, event) => event.data,
|
||||
}),
|
||||
clearCreateUserError: assign((context: UsersContext) => ({
|
||||
...context,
|
||||
createUserError: undefined,
|
||||
})),
|
||||
clearSuspendUserError: assign({
|
||||
suspendUserError: (_) => undefined,
|
||||
}),
|
||||
displayCreateUserSuccess: () => {
|
||||
displaySuccess(Language.createUserSuccess)
|
||||
},
|
||||
displaySuspendSuccess: () => {
|
||||
displaySuccess(Language.suspendUserSuccess)
|
||||
},
|
||||
displaySuspendedErrorMessage: () => {
|
||||
displayError(Language.suspendUserError)
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1077,6 +1077,13 @@
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.9.2":
|
||||
version "7.17.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72"
|
||||
integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/template@^7.12.7", "@babel/template@^7.16.7", "@babel/template@^7.3.3":
|
||||
version "7.16.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155"
|
||||
@@ -2727,6 +2734,21 @@
|
||||
lz-string "^1.4.4"
|
||||
pretty-format "^27.0.2"
|
||||
|
||||
"@testing-library/jest-dom@5.16.4":
|
||||
version "5.16.4"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.16.4.tgz#938302d7b8b483963a3ae821f1c0808f872245cd"
|
||||
integrity sha512-Gy+IoFutbMQcky0k+bqqumXZ1cTGswLsFqmNLzNdSKkU9KGV2u9oXhukCbbJ9/LRPKiqwxEE8VpV/+YZlfkPUA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.9.2"
|
||||
"@types/testing-library__jest-dom" "^5.9.1"
|
||||
aria-query "^5.0.0"
|
||||
chalk "^3.0.0"
|
||||
css "^3.0.0"
|
||||
css.escape "^1.5.1"
|
||||
dom-accessibility-api "^0.5.6"
|
||||
lodash "^4.17.15"
|
||||
redent "^3.0.0"
|
||||
|
||||
"@testing-library/react-hooks@8.0.0":
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-8.0.0.tgz#7d0164bffce4647f506039de0a97f6fcbd20f4bf"
|
||||
@@ -2972,6 +2994,14 @@
|
||||
dependencies:
|
||||
"@types/istanbul-lib-report" "*"
|
||||
|
||||
"@types/jest@*":
|
||||
version "27.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.5.0.tgz#e04ed1824ca6b1dd0438997ba60f99a7405d4c7b"
|
||||
integrity sha512-9RBFx7r4k+msyj/arpfaa0WOOEcaAZNmN+j80KFbFCoSqCJGHTz7YMAMGQW9Xmqm5w6l5c25vbSjMwlikJi5+g==
|
||||
dependencies:
|
||||
jest-matcher-utils "^27.0.0"
|
||||
pretty-format "^27.0.0"
|
||||
|
||||
"@types/jest@27.4.1":
|
||||
version "27.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.4.1.tgz#185cbe2926eaaf9662d340cc02e548ce9e11ab6d"
|
||||
@@ -3182,6 +3212,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.8.tgz#b94a4391c85666c7b73299fd3ad79d4faa435310"
|
||||
integrity sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==
|
||||
|
||||
"@types/testing-library__jest-dom@^5.9.1":
|
||||
version "5.14.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.3.tgz#ee6c7ffe9f8595882ee7bda8af33ae7b8789ef17"
|
||||
integrity sha512-oKZe+Mf4ioWlMuzVBaXQ9WDnEm1+umLx0InILg+yvZVBBDmzV5KfZyLrCvadtWcx8+916jLmHafcmqqffl+iIw==
|
||||
dependencies:
|
||||
"@types/jest" "*"
|
||||
|
||||
"@types/uglify-js@*":
|
||||
version "3.13.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.13.1.tgz#5e889e9e81e94245c75b6450600e1c5ea2878aea"
|
||||
@@ -5586,6 +5623,20 @@ css-what@^5.1.0:
|
||||
resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.1.0.tgz#3f7b707aadf633baf62c2ceb8579b545bb40f7fe"
|
||||
integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==
|
||||
|
||||
css.escape@^1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb"
|
||||
integrity sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=
|
||||
|
||||
css@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/css/-/css-3.0.0.tgz#4447a4d58fdd03367c516ca9f64ae365cee4aa5d"
|
||||
integrity sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==
|
||||
dependencies:
|
||||
inherits "^2.0.4"
|
||||
source-map "^0.6.1"
|
||||
source-map-resolve "^0.6.0"
|
||||
|
||||
cssesc@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
|
||||
@@ -5942,6 +5993,11 @@ doctrine@^3.0.0:
|
||||
dependencies:
|
||||
esutils "^2.0.2"
|
||||
|
||||
dom-accessibility-api@^0.5.6:
|
||||
version "0.5.14"
|
||||
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.14.tgz#56082f71b1dc7aac69d83c4285eef39c15d93f56"
|
||||
integrity sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg==
|
||||
|
||||
dom-accessibility-api@^0.5.9:
|
||||
version "0.5.11"
|
||||
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.11.tgz#79d5846c4f90eba3e617d9031e921de9324f84ed"
|
||||
@@ -11694,6 +11750,14 @@ rechoir@^0.7.0:
|
||||
dependencies:
|
||||
resolve "^1.9.0"
|
||||
|
||||
redent@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
|
||||
integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==
|
||||
dependencies:
|
||||
indent-string "^4.0.0"
|
||||
strip-indent "^3.0.0"
|
||||
|
||||
refractor@^3.1.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.5.0.tgz#334586f352dda4beaf354099b48c2d18e0819aec"
|
||||
@@ -12459,6 +12523,14 @@ source-map-resolve@^0.5.0:
|
||||
source-map-url "^0.4.0"
|
||||
urix "^0.1.0"
|
||||
|
||||
source-map-resolve@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.6.0.tgz#3d9df87e236b53f16d01e58150fc7711138e5ed2"
|
||||
integrity sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==
|
||||
dependencies:
|
||||
atob "^2.1.2"
|
||||
decode-uri-component "^0.2.0"
|
||||
|
||||
source-map-support@0.4.18:
|
||||
version "0.4.18"
|
||||
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f"
|
||||
|
||||
Reference in New Issue
Block a user