diff --git a/app/layouts/helpers.tsx b/app/layouts/helpers.tsx index e47e505d7..7624ac961 100644 --- a/app/layouts/helpers.tsx +++ b/app/layouts/helpers.tsx @@ -1,5 +1,5 @@ import './helpers.css' -import { classed } from '@oxide/ui' +import { classed } from '@oxide/util' export const PageContainer = classed.div`ox-page-container` export const Sidebar = classed.div`ox-sidebar` diff --git a/app/pages/ToastTestPage.tsx b/app/pages/ToastTestPage.tsx index 9aa366b3e..99cad8231 100644 --- a/app/pages/ToastTestPage.tsx +++ b/app/pages/ToastTestPage.tsx @@ -1,6 +1,7 @@ -import { Button, classed, Comment16Icon, Success16Icon } from '@oxide/ui' +import { Button, Comment16Icon, Success16Icon } from '@oxide/ui' import React, { useState } from 'react' import { useToast } from '../hooks' +import { classed } from '@oxide/util' const useCounter = (initialValue: number): [number, () => void] => { const [value, setValue] = useState(initialValue) diff --git a/app/pages/project/instances/InstancesPage.tsx b/app/pages/project/instances/InstancesPage.tsx index 63f89e3ed..4593c4964 100644 --- a/app/pages/project/instances/InstancesPage.tsx +++ b/app/pages/project/instances/InstancesPage.tsx @@ -46,7 +46,7 @@ export const InstancesPage = () => { to={`/orgs/${orgName}/projects/${projectName}/instances/new`} className={buttonStyle({ size: 'xs', variant: 'dim' })} > - new instance + New Instance diff --git a/app/pages/project/instances/create/InstancesCreatePage.tsx b/app/pages/project/instances/create/InstancesCreatePage.tsx index ff761cb37..71d2d4334 100644 --- a/app/pages/project/instances/create/InstancesCreatePage.tsx +++ b/app/pages/project/instances/create/InstancesCreatePage.tsx @@ -4,7 +4,6 @@ import cn from 'classnames' import { Formik, Form } from 'formik' import { - classed, Button, PageHeader, PageTitle, @@ -19,6 +18,7 @@ import { FieldTitle, Badge, } from '@oxide/ui' +import { classed } from '@oxide/util' import { useApiMutation } from '@oxide/api' import { getServerError } from '../../../../util/errors' import { INSTANCE_SIZES } from './instance-types' diff --git a/app/pages/project/networking/VpcPage/VpcPage.spec.ts b/app/pages/project/networking/VpcPage/VpcPage.spec.ts new file mode 100644 index 000000000..93e78d9e2 --- /dev/null +++ b/app/pages/project/networking/VpcPage/VpcPage.spec.ts @@ -0,0 +1,85 @@ +import { + fireEvent, + lastPostBody, + renderAppAt, + screen, + userEvent, + waitForElementToBeRemoved, +} from '../../../../test-utils' +import fetchMock from 'fetch-mock' +import { + org, + project, + vpc, + vpcSubnet, + vpcSubnet2, + vpcSubnets, +} from '@oxide/api-mocks' + +const vpcUrl = `/api/organizations/${org.name}/projects/${project.name}/vpcs/default` +const subnetsUrl = `${vpcUrl}/subnets` +const getSubnetsUrl = `${subnetsUrl}?limit=10` + +describe('VpcPage', () => { + describe('subnets tab', () => { + it('creating a subnet works', async () => { + fetchMock.get(vpcUrl, { status: 200, body: vpc }) + fetchMock.getOnce(getSubnetsUrl, { status: 200, body: vpcSubnets }) + const postMock = fetchMock.postOnce(subnetsUrl, { + status: 201, + body: vpcSubnet2, + }) + + renderAppAt('/orgs/mock-org/projects/mock-project/vpcs/default') + screen.getByText('Subnets') + + // wait for subnet to show up in the table + await screen.findByRole('cell', { name: vpcSubnet.identity.name }) + + // modal is not already open + expect(screen.queryByRole('dialog', { name: 'Create subnet' })).toBeNull() + + // click button to open modal + fireEvent.click(screen.getByRole('button', { name: 'New subnet' })) + + // modal is open + screen.getByRole('dialog', { name: 'Create subnet' }) + + const ipv4 = screen.getByRole('textbox', { name: 'IPv4 block' }) + userEvent.type(ipv4, '1.1.1.2/24') + + const name = screen.getByRole('textbox', { name: 'Name' }) + userEvent.type(name, 'mock-subnet-2') + + // override the subnets GET to include both subnets + fetchMock.getOnce( + getSubnetsUrl, + { + status: 200, + body: { items: [vpcSubnet, vpcSubnet2] }, + }, + { overwriteRoutes: true } + ) + + // submit the form + fireEvent.click(screen.getByRole('button', { name: 'Create subnet' })) + + // wait for modal to close + await waitForElementToBeRemoved(() => + screen.queryByRole('dialog', { name: 'Create subnet' }) + ) + + // it posted the form + expect(lastPostBody(postMock)).toEqual({ + ipv4Block: '1.1.1.2/24', + ipv6Block: null, + name: 'mock-subnet-2', + description: '', + }) + + // table should refetch and now include second subnet + screen.getByRole('cell', { name: vpcSubnet.identity.name }) + screen.getByRole('cell', { name: vpcSubnet2.identity.name }) + }) + }) +}) diff --git a/app/pages/project/networking/VpcPage/VpcPage.tsx b/app/pages/project/networking/VpcPage/VpcPage.tsx index 5fd8b40e9..dddd2d1a0 100644 --- a/app/pages/project/networking/VpcPage/VpcPage.tsx +++ b/app/pages/project/networking/VpcPage/VpcPage.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { format } from 'date-fns' import { Networking24Icon, PageHeader, @@ -11,32 +12,37 @@ import { VpcSystemRoutesTab } from './tabs/VpcSystemRoutesTab' import { VpcRoutersTab } from './tabs/VpcRoutersTab' import { useParams } from '../../../../hooks' import { VpcFirewallRulesTab } from './tabs/VpcFirewallRulesTab' +import { useApiQuery } from '@oxide/api' + +const formatDateTime = (s: string) => format(new Date(s), 'MMM d, yyyy H:mm aa') export const VpcPage = () => { - const { vpcName } = useParams('vpcName') + const vpcParams = useParams('orgName', 'projectName', 'vpcName') + const { data: vpc } = useApiQuery('projectVpcsGetVpc', vpcParams) + return ( <> }> - {vpcName} + {vpcParams.vpcName} - Default network for the project + {vpc?.description} - frontend-production-vpc + {vpc?.dnsName} - Default network for the project + {vpc?.timeCreated && formatDateTime(vpc.timeCreated)} - Default network for the project + {vpc?.timeModified && formatDateTime(vpc.timeModified)} diff --git a/app/pages/project/networking/VpcPage/modals/vpc-routers.tsx b/app/pages/project/networking/VpcPage/modals/vpc-routers.tsx new file mode 100644 index 000000000..035732654 --- /dev/null +++ b/app/pages/project/networking/VpcPage/modals/vpc-routers.tsx @@ -0,0 +1,170 @@ +import React from 'react' +import { Formik, Form } from 'formik' + +import { Button, FieldTitle, SideModal, TextField } from '@oxide/ui' +import type { VpcRouter, ErrorResponse } from '@oxide/api' +import { useApiMutation, useApiQueryClient } from '@oxide/api' +import { getServerError } from '../../../../../util/errors' + +// this will get a lot more interesting once the API is updated to allow us to +// put rules in the router, which is the whole point of a router + +type FormProps = { + error: ErrorResponse | null + id: string +} + +// the moment the two forms diverge, inline them rather than introducing BS +// props here +const CommonForm = ({ error, id }: FormProps) => ( + + +
+ + Name + + +
+
+ + Description {/* TODO: indicate optional */} + + +
+
+ +
{getServerError(error)}
+
+ +) + +type CreateProps = { + isOpen: boolean + onDismiss: () => void + orgName: string + projectName: string + vpcName: string +} + +export function CreateVpcRouterModal({ + isOpen, + onDismiss, + orgName, + projectName, + vpcName, +}: CreateProps) { + const parentIds = { orgName, projectName, vpcName } + const queryClient = useApiQueryClient() + + function dismiss() { + createRouter.reset() + onDismiss() + } + + const createRouter = useApiMutation('vpcRoutersPost', { + onSuccess() { + queryClient.invalidateQueries('vpcRoutersGet', parentIds) + dismiss() + }, + }) + + const formId = 'create-vpc-router-form' + + return ( + + { + createRouter.mutate({ + ...parentIds, + body: { name, description }, + }) + }} + > + + + + + + + + ) +} + +type EditProps = { + onDismiss: () => void + orgName: string + projectName: string + vpcName: string + originalRouter: VpcRouter | null +} + +export function EditVpcRouterModal({ + onDismiss, + orgName, + projectName, + vpcName, + originalRouter, +}: EditProps) { + const parentIds = { orgName, projectName, vpcName } + const queryClient = useApiQueryClient() + + function dismiss() { + updateRouter.reset() + onDismiss() + } + + const updateRouter = useApiMutation('vpcRoutersPutRouter', { + onSuccess() { + queryClient.invalidateQueries('vpcRoutersGet', parentIds) + dismiss() + }, + }) + + if (!originalRouter) return null + + const formId = 'edit-vpc-router-form' + return ( + + { + updateRouter.mutate({ + ...parentIds, + routerName: originalRouter.identity.name, + body: { name, description }, + }) + }} + > + + + + + + + + ) +} diff --git a/app/pages/project/networking/VpcPage/modals/vpc-subnets.tsx b/app/pages/project/networking/VpcPage/modals/vpc-subnets.tsx new file mode 100644 index 000000000..e917f6e9a --- /dev/null +++ b/app/pages/project/networking/VpcPage/modals/vpc-subnets.tsx @@ -0,0 +1,201 @@ +import React from 'react' +import { Formik, Form } from 'formik' + +import { Button, FieldTitle, SideModal, TextField } from '@oxide/ui' +import type { VpcSubnet, ErrorResponse } from '@oxide/api' +import { useApiMutation, useApiQueryClient } from '@oxide/api' +import { getServerError } from '../../../../../util/errors' + +type FormProps = { + error: ErrorResponse | null + id: string +} + +// the moment the two forms diverge, inline them rather than introducing BS +// props here +const CommonForm = ({ id, error }: FormProps) => ( + + +
+ + IPv4 block + + +
+
+ + IPv6 block + + +
+
+ +
+ + Name + + +
+
+ + Description {/* TODO: indicate optional */} + + +
+
+ +
{getServerError(error)}
+
+ +) + +type CreateProps = { + isOpen: boolean + onDismiss: () => void + orgName: string + projectName: string + vpcName: string +} + +export function CreateVpcSubnetModal({ + isOpen, + onDismiss, + orgName, + projectName, + vpcName, +}: CreateProps) { + const parentIds = { orgName, projectName, vpcName } + const queryClient = useApiQueryClient() + + function dismiss() { + createSubnet.reset() + onDismiss() + } + + const createSubnet = useApiMutation('vpcSubnetsPost', { + onSuccess() { + queryClient.invalidateQueries('vpcSubnetsGet', parentIds) + dismiss() + }, + }) + + const formId = 'create-vpc-subnet-form' + + return ( + + { + createSubnet.mutate({ + ...parentIds, + // XXX body is optional. useApiMutation should be smarter and require body when it's required + body: { + name, + description, + // TODO: validate these client-side using the patterns. sadly non-trivial + ipv4Block: ipv4Block || null, + ipv6Block: ipv6Block || null, + }, + }) + }} + > + + + + + + + + ) +} + +type EditProps = { + onDismiss: () => void + orgName: string + projectName: string + vpcName: string + originalSubnet: VpcSubnet | null +} + +export function EditVpcSubnetModal({ + onDismiss, + orgName, + projectName, + vpcName, + originalSubnet, +}: EditProps) { + const parentIds = { orgName, projectName, vpcName } + const queryClient = useApiQueryClient() + + function dismiss() { + updateSubnet.reset() + onDismiss() + } + + const updateSubnet = useApiMutation('vpcSubnetsPutSubnet', { + onSuccess() { + queryClient.invalidateQueries('vpcSubnetsGet', parentIds) + dismiss() + }, + }) + + if (!originalSubnet) return null + + const formId = 'edit-vpc-subnet-form' + return ( + + { + updateSubnet.mutate({ + ...parentIds, + subnetName: originalSubnet.identity.name, + body: { + name, + description, + // TODO: validate these client-side using the patterns. sadly non-trivial + ipv4Block: ipv4Block || null, + ipv6Block: ipv6Block || null, + }, + }) + }} + > + + + + + + + + ) +} diff --git a/app/pages/project/networking/VpcPage/tabs/VpcRoutersTab.tsx b/app/pages/project/networking/VpcPage/tabs/VpcRoutersTab.tsx index 63ecb6039..e2eca6c21 100644 --- a/app/pages/project/networking/VpcPage/tabs/VpcRoutersTab.tsx +++ b/app/pages/project/networking/VpcPage/tabs/VpcRoutersTab.tsx @@ -1,22 +1,57 @@ -import React from 'react' +import React, { useState } from 'react' import { useParams } from '../../../../../hooks' +import { Button } from '@oxide/ui' +import type { MenuAction } from '@oxide/table' import { useQueryTable, DateCell, LabelCell } from '@oxide/table' +import type { VpcRouter } from '@oxide/api' +import { CreateVpcRouterModal, EditVpcRouterModal } from '../modals/vpc-routers' export const VpcRoutersTab = () => { const vpcParams = useParams('orgName', 'projectName', 'vpcName') const { Table, Column } = useQueryTable('vpcRoutersGet', vpcParams) + const [createModalOpen, setCreateModalOpen] = useState(false) + const [editing, setEditing] = useState(null) + + const actions = (router: VpcRouter): MenuAction[] => [ + { + label: 'Edit', + onActivate: () => setEditing(router), + }, + ] + return ( -
- - - -
+ <> +
+ + setCreateModalOpen(false)} + /> + setEditing(null)} + /> +
+ + + + +
+ ) } diff --git a/app/pages/project/networking/VpcPage/tabs/VpcSubnetsTab.tsx b/app/pages/project/networking/VpcPage/tabs/VpcSubnetsTab.tsx index de5395889..45fb2ea7f 100644 --- a/app/pages/project/networking/VpcPage/tabs/VpcSubnetsTab.tsx +++ b/app/pages/project/networking/VpcPage/tabs/VpcSubnetsTab.tsx @@ -1,22 +1,57 @@ -import React from 'react' +import React, { useState } from 'react' import { useParams } from '../../../../../hooks' +import type { MenuAction } from '@oxide/table' import { useQueryTable, TwoLineCell, DateCell } from '@oxide/table' +import { Button } from '@oxide/ui' +import { CreateVpcSubnetModal, EditVpcSubnetModal } from '../modals/vpc-subnets' +import type { VpcSubnet } from '@oxide/api' export const VpcSubnetsTab = () => { const vpcParams = useParams('orgName', 'projectName', 'vpcName') const { Table, Column } = useQueryTable('vpcSubnetsGet', vpcParams) + const [createModalOpen, setCreateModalOpen] = useState(false) + const [editing, setEditing] = useState(null) + + const actions = (subnet: VpcSubnet): MenuAction[] => [ + { + label: 'Edit', + onActivate: () => setEditing(subnet), + }, + ] + return ( - - - [vpc.ipv4_block, vpc.ipv6_block]} - cell={TwoLineCell} - /> - -
+ <> +
+ + setCreateModalOpen(false)} + /> + setEditing(null)} + /> +
+ + + [vpc.ipv4_block, vpc.ipv6_block]} + cell={TwoLineCell} + /> + +
+ ) } diff --git a/app/test-utils.tsx b/app/test-utils.tsx index a19fa573b..2da931a4f 100644 --- a/app/test-utils.tsx +++ b/app/test-utils.tsx @@ -38,4 +38,5 @@ export const lastPostBody = (mock: FetchMockStatic): any => JSON.parse(mock.lastOptions(undefined, 'POST')?.body as unknown as string) export * from '@testing-library/react' +export { default as userEvent } from '@testing-library/user-event' export { customRender as render } diff --git a/libs/api-mocks/index.ts b/libs/api-mocks/index.ts index 3a64ad4f9..2bfdd793c 100644 --- a/libs/api-mocks/index.ts +++ b/libs/api-mocks/index.ts @@ -2,3 +2,4 @@ export * from './instance' export * from './org' export * from './project' export * from './users' +export * from './vpc' diff --git a/libs/api-mocks/vpc.ts b/libs/api-mocks/vpc.ts new file mode 100644 index 000000000..5321c6891 --- /dev/null +++ b/libs/api-mocks/vpc.ts @@ -0,0 +1,41 @@ +import { project } from './project' +import type { Vpc, VpcSubnet, VpcSubnetResultsPage } from '@oxide/api' + +export const vpc: Vpc = { + id: 'vpc-id', + name: 'mock-vpc', + description: 'a fake vpc', + dnsName: 'mock-vpc', + timeCreated: new Date(2021, 0, 1).toISOString(), + timeModified: new Date(2021, 0, 2).toISOString(), + projectId: project.id, + systemRouterId: 'router-id', // ??? +} + +export const vpcSubnet: VpcSubnet = { + // this is supposed to be flattened into the top level. will fix in API + identity: { + id: 'vpc-subnet-id', + name: 'mock-subnet', + description: 'a fake subnet', + timeCreated: new Date(2021, 0, 1).toISOString(), + timeModified: new Date(2021, 0, 2).toISOString(), + }, + // supposed to be camelcase, will fix in API + vpc_id: vpc.id, + ipv4_block: '1.1.1.1/24', +} + +export const vpcSubnet2: VpcSubnet = { + identity: { + ...vpcSubnet.identity, + id: 'vpc-subnet-id-2', + name: 'mock-subnet-2', + }, + vpc_id: vpc.id, + ipv4_block: '1.1.1.2/24', +} + +export const vpcSubnets: VpcSubnetResultsPage = { + items: [vpcSubnet], +} diff --git a/libs/api/index.ts b/libs/api/index.ts index a08f96337..814521256 100644 --- a/libs/api/index.ts +++ b/libs/api/index.ts @@ -11,7 +11,14 @@ export type { ErrorResponse, Params, Result, ResultItem } from './hooks' const basePath = process.env.API_URL ?? '/api' -const api = new Api({ baseUrl: basePath }) +const api = new Api({ + baseUrl: basePath, + baseApiParams: { + // default format so that the client parses JSON error responses for endpoints + // where the success response is 204 No Content + format: 'json', + }, +}) type A = typeof api.methods diff --git a/libs/ui/index.ts b/libs/ui/index.ts index 60f78a635..23a847929 100644 --- a/libs/ui/index.ts +++ b/libs/ui/index.ts @@ -22,5 +22,3 @@ export * from './lib/tabs/Tabs' export * from './lib/table/Table' export * from './lib/tooltip/Tooltip' export * from './lib/toast/Toast' - -export * from './util/classed' diff --git a/libs/ui/lib/PageHeader.tsx b/libs/ui/lib/PageHeader.tsx index 12615226f..32ac158a2 100644 --- a/libs/ui/lib/PageHeader.tsx +++ b/libs/ui/lib/PageHeader.tsx @@ -2,7 +2,7 @@ import type { ReactElement } from 'react' import { cloneElement } from 'react' import React from 'react' -import { classed } from '../util/classed' +import { classed } from '@oxide/util' export const PageHeader = classed.header`flex items-center justify-between mb-16 mt-4` diff --git a/libs/ui/lib/checkbox/Checkbox.tsx b/libs/ui/lib/checkbox/Checkbox.tsx index 902bf9ffd..df492f2f1 100644 --- a/libs/ui/lib/checkbox/Checkbox.tsx +++ b/libs/ui/lib/checkbox/Checkbox.tsx @@ -1,7 +1,7 @@ import { Checkmark12Icon } from '@oxide/ui' import React from 'react' -import { classed } from '../../util/classed' +import { classed } from '@oxide/util' const Check = () => ( diff --git a/libs/ui/lib/radio-group/RadioGroup.tsx b/libs/ui/lib/radio-group/RadioGroup.tsx index fbdddce48..53273e6df 100644 --- a/libs/ui/lib/radio-group/RadioGroup.tsx +++ b/libs/ui/lib/radio-group/RadioGroup.tsx @@ -43,7 +43,7 @@ import React from 'react' import cn from 'classnames' -import { classed } from '../../util/classed' +import { classed } from '@oxide/util' export const RadioGroupHint = classed.p`text-base text-gray-100 font-sans font-light max-w-3xl` diff --git a/libs/ui/lib/side-modal/SideModal.tsx b/libs/ui/lib/side-modal/SideModal.tsx index 8a3d7a793..04e25e32f 100644 --- a/libs/ui/lib/side-modal/SideModal.tsx +++ b/libs/ui/lib/side-modal/SideModal.tsx @@ -2,7 +2,7 @@ import React from 'react' import type { DialogProps } from '@reach/dialog' import Dialog from '@reach/dialog' import { Button } from '../button/Button' -import { pluckFirstOfType } from '@oxide/util' +import { classed, pluckFirstOfType } from '@oxide/util' import type { ChildrenProp } from '@oxide/util' import { Close12Icon } from '../icons' @@ -27,7 +27,7 @@ export function SideModal({ id={id} onDismiss={onDismiss} {...dialogProps} - className="absolute right-0 top-0 bottom-0 w-[32rem] p-0 m-0 flex flex-col justify-between bg-gray-500 border-l border-gray-400" + className="absolute right-0 top-0 bottom-0 w-[32rem] p-0 m-0 flex flex-col justify-between bg-black border-l border-gray-400" aria-labelledby={titleId} >
( -
{children}
-) +SideModal.Section = classed.div`p-8 space-y-6 border-gray-400` SideModal.Docs = ({ children }: ChildrenProp) => ( diff --git a/libs/ui/util/classed.ts b/libs/util/classed.ts similarity index 100% rename from libs/ui/util/classed.ts rename to libs/util/classed.ts diff --git a/libs/util/index.ts b/libs/util/index.ts index 985449f48..a937e615f 100644 --- a/libs/util/index.ts +++ b/libs/util/index.ts @@ -1,3 +1,4 @@ +export * from './classed' export * from './str' export * from './invariant' export * from './object' diff --git a/package.json b/package.json index 4959c38f5..359a69e2f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "build": "vite build", "build-for-nexus": "yarn install && API_URL='' vite build", "ci": "yarn tsc && yarn lint && yarn test", - "test": "jest", + "test": "DEBUG_PRINT_LIMIT=0 jest", "lint": "eslint --ext .js,.ts,.tsx,.json .", "fmt": "prettier --write", "gen": "plop", @@ -70,9 +70,11 @@ "@storybook/manager-webpack5": "^6.4.0-rc.1", "@storybook/react": "^6.4.0-rc.1", "@storybook/theming": "^6.4.0-rc.1", + "@testing-library/dom": "^8.11.1", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^12.0.0", "@testing-library/react-hooks": "^7.0.1", + "@testing-library/user-event": "^13.5.0", "@types/jest": "^26.0.23", "@types/jscodeshift": "^0.11.2", "@types/mousetrap": "^1.6.8", diff --git a/yarn.lock b/yarn.lock index ef4d5719c..b98894fdb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3201,6 +3201,20 @@ lz-string "^1.4.4" pretty-format "^27.0.2" +"@testing-library/dom@^8.11.1": + version "8.11.1" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.11.1.tgz#03fa2684aa09ade589b460db46b4c7be9fc69753" + integrity sha512-3KQDyx9r0RKYailW2MiYrSSKEfH0GTkI51UGEvJenvcoDoeRYs0PZpi2SXqtnMClQvCqdtTTpOfFETDTVADpAg== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^4.2.0" + aria-query "^5.0.0" + chalk "^4.1.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.4.4" + pretty-format "^27.0.2" + "@testing-library/jest-dom@^5.14.1": version "5.14.1" resolved "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.14.1.tgz" @@ -3235,6 +3249,13 @@ "@babel/runtime" "^7.12.5" "@testing-library/dom" "^8.0.0" +"@testing-library/user-event@^13.5.0": + version "13.5.0" + resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-13.5.0.tgz#69d77007f1e124d55314a2b73fd204b333b13295" + integrity sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg== + dependencies: + "@babel/runtime" "^7.12.5" + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz" @@ -4360,6 +4381,11 @@ aria-query@^4.2.2: "@babel/runtime" "^7.10.2" "@babel/runtime-corejs3" "^7.10.2" +aria-query@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.0.0.tgz#210c21aaf469613ee8c9a62c7f86525e058db52c" + integrity sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg== + arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz" @@ -6384,6 +6410,11 @@ dom-accessibility-api@^0.5.6: resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.6.tgz" integrity sha512-DplGLZd8L1lN64jlT27N9TVSESFR5STaEJvX+thCby7fuCHonfPpAlodYc3vuUYbDuDec5w8AMP7oCM5TWFsqw== +dom-accessibility-api@^0.5.9: + version "0.5.10" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.10.tgz#caa6d08f60388d0bb4539dd75fe458a9a1d0014c" + integrity sha512-Xu9mD0UjrJisTmv7lmVSDMagQcU9R5hwAbxsaAE/35XPnPLJobbuREfV/rraiSaEj/UOvgrzQs66zyTWTlyd+g== + dom-converter@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz"