diff --git a/__test__/containers/CampaignList.test.js b/__test__/containers/CampaignList.test.js index a81702f13..069f7d710 100644 --- a/__test__/containers/CampaignList.test.js +++ b/__test__/containers/CampaignList.test.js @@ -3,14 +3,31 @@ */ import React from "react"; import { mount } from "enzyme"; -import { AdminCampaignList } from "../../src/containers/AdminCampaignList"; -import { TIMEZONE_SORT } from "../../src/components/AdminCampaignList/SortBy"; +import { act } from "react-dom/test-utils"; +import { + render, + screen, + waitFor, + cleanup +} from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { StyleSheetTestUtils } from "aphrodite"; +import { AdminCampaignList } from "../../src/containers/AdminCampaignList"; + +// https://github.com/Khan/aphrodite/issues/62#issuecomment-267026726 +beforeEach(() => { + StyleSheetTestUtils.suppressStyleInjection(); +}); +afterEach(() => { + StyleSheetTestUtils.clearBufferAndResumeStyleInjection(); + cleanup(); +}); + describe("CampaignList", () => { const params = { adminPerms: true, - organizationId: 77 + organizationId: "77" }; const mutations = { @@ -110,19 +127,21 @@ describe("CampaignList", () => { describe("Campaign list sorting", () => { const campaignWithCreator = { - id: 1, + id: "1", + title: "test", creator: { displayName: "Lorem Ipsum" }, completionStats: {}, organization: { id: 1 - } + }, + timezone: "US/Eastern" }; const data = { organization: { - id: 1, + id: "1", cacheable: 2, campaigns: { campaigns: [campaignWithCreator], @@ -132,26 +151,58 @@ describe("CampaignList", () => { total: 1 } } - } + }, + refetch: () => {} }; - test("Timezone column is displayed when timezone is current sort", () => { + test("Timezone column is displayed when timezone is current sort", async () => { StyleSheetTestUtils.suppressStyleInjection(); - const wrapper = mount( - - ); - wrapper.setState({ - sortBy: TIMEZONE_SORT.value + act(() => { + render( + + ); + }); + + act(() => { + userEvent.click( + screen.getByRole("button", { name: /sort: created, newest/i }), + { skipHover: true } + ); }); - expect(wrapper.containsMatchingElement("Timezone")).toBeTruthy(); + + act(() => { + userEvent.click( + screen.getByRole("option", { name: /sort: timezone/i }), + { skipHover: true } + ); + }); + + await waitFor(() => + expect( + screen.getByRole("columnheader", { name: /timezone/i }) + ).toBeTruthy() + ); }); test("Timezone column is hidden when it isn't the current sort", () => { StyleSheetTestUtils.suppressStyleInjection(); - const wrapper = mount( - - ); - expect(wrapper.containsMatchingElement("Timezone")).toBeFalsy(); + act(() => { + render( + + ); + }); + const timezoneButton = screen.queryByText("columnheader", { + name: /timezone/i + }); + expect(timezoneButton).toBeNull(); }); }); }); diff --git a/package.json b/package.json index 4b6fcebae..574fe5ce1 100644 --- a/package.json +++ b/package.json @@ -182,6 +182,9 @@ }, "devDependencies": { "@babel/eslint-parser": "^7.24.7", + "@testing-library/dom": "^10.4.0", + "@testing-library/react": "^12.1.5", + "@testing-library/user-event": "^12.1.5", "babel-jest": "^29.3.1", "babel-preset-es2017": "^6.24.1", "cypress": "^13.11.0", diff --git a/src/components/AdminCampaignList/CampaignTable.jsx b/src/components/AdminCampaignList/CampaignTable.jsx index aa44a3e92..a86e0ffbd 100644 --- a/src/components/AdminCampaignList/CampaignTable.jsx +++ b/src/components/AdminCampaignList/CampaignTable.jsx @@ -1,5 +1,5 @@ import PropTypes from "prop-types"; -import React from "react"; +import React, { useState } from "react"; import { Link as RouterLink } from "react-router"; import moment from "moment"; @@ -20,35 +20,39 @@ const inlineStyles = { whiteSpace: "nowrap" } }; + +const CampaignTable = ({ + data, + campaignsToArchive, + campaignsWithChangingStatus, + currentSortBy, + onNextPageClick, + onPreviousPageClick, + onRowSizeChange, + adminPerms, + selectMultiple, + organizationId, + handleChecked, + archiveCampaign, + unarchiveCampaign + }) => { -export class CampaignTable extends React.Component { - static propTypes = { - adminPerms: PropTypes.bool, - selectMultiple: PropTypes.bool, - organizationId: PropTypes.string, - data: PropTypes.object, - handleChecked: PropTypes.func, - archiveCampaign: PropTypes.func, - unarchiveCampaign: PropTypes.func, - onNextPageClick: PropTypes.func, - onPreviousPageClick: PropTypes.func, - onRowSizeChange: PropTypes.func, - campaignsToArchive: PropTypes.array, - campaignsWithChangingStatus: PropTypes.array, - currentSortBy: PropTypes.oneOf(SORTS.map(s => s.value)) - }; + const [campaigns, setCampaigns] = useState(data.organization.campaigns.campaigns.map((campaign) => ({...campaign}))); - state = { - dataTableKey: "initial", - campaigns: [...this.props.data.organization.campaigns.campaigns] - }; + const [state, setState] = useState({ + dataTableKey: "initial" + }); - statusIsChanging = campaign => { - return this.props.campaignsWithChangingStatus.includes(campaign.id); + const { limit, offset, total } = data.organization.campaigns.pageInfo; + const displayPage = Math.floor(offset / limit) + 1; + let rowSizeList = [10, 20, 50, 100]; + + const statusIsChanging = campaign => { + return campaignsWithChangingStatus.includes(campaign.id); }; - renderArchiveIcon(campaign) { - if (this.statusIsChanging(campaign)) { + const renderArchiveIcon = campaign => { + if (statusIsChanging(campaign)) { return ; } if (campaign.isArchived) { @@ -58,7 +62,10 @@ export class CampaignTable extends React.Component { return ( await this.props.unarchiveCampaign(campaign.id)} + onClick={async () => { + await unarchiveCampaign(campaign.id); + setCampaigns(campaigns.filter(e => e.id != campaign.id)); + }} > @@ -67,14 +74,17 @@ export class CampaignTable extends React.Component { return ( await this.props.archiveCampaign(campaign.id)} + onClick={async () => { + await archiveCampaign(campaign.id); + setCampaigns(campaigns.filter(e => e.id != campaign.id)); + }} > ); } - sortFunc(key) { + const sortFunc = key => { const sorts = { id: (a, b) => b.id - a.id, title: (a, b) => (b.title > a.title ? 1 : -1), @@ -92,7 +102,7 @@ export class CampaignTable extends React.Component { return sorts[key]; } - prepareTableColumns(organization, campaigns) { + const prepareTableColumns = (organization, campaigns) => { const extraRows = []; const needsResponseCol = campaigns.some( c => c.completionStats.needsResponseCount @@ -112,14 +122,14 @@ export class CampaignTable extends React.Component { } }); } - if (this.props.adminPerms) { + if (adminPerms) { extraRows.push({ label: "Archive", name: "archive", options: { customBodyRender: (value, tableMeta) => { const campaign = campaigns.find(c => c.id === tableMeta.rowData[0]); - return this.renderArchiveIcon(campaign); + return renderArchiveIcon(campaign); }, sort: false }, @@ -131,7 +141,7 @@ export class CampaignTable extends React.Component { const timezoneColumn = []; // only show the timezone column when we're currently sorting by timezone - if (this.props.currentSortBy === TIMEZONE_SORT.value) { + if (currentSortBy === TIMEZONE_SORT.value) { timezoneColumn.push({ key: "timezone", name: "timezone", @@ -157,7 +167,7 @@ export class CampaignTable extends React.Component { customBodyRender: (value, tableMeta) => { const campaign = campaigns.find(c => c.id === tableMeta.rowData[0]); let org = ""; - if (this.props.organizationId != campaign.organization.id) { + if (organizationId != campaign.organization.id) { org = ` (${campaign.organization.id})`; } return `${campaign.id}${org}`; @@ -280,11 +290,11 @@ export class CampaignTable extends React.Component { ]; } - getSelectedRowIndexes = () => { - const campaignIds = this.props.data.organization.campaigns.campaigns.map( + const getSelectedRowIndexes = () => { + const campaignIds = campaigns.map( c => c.id ); - const indexes = this.props.campaignsToArchive.map(campaignId => + const indexes = campaignsToArchive.map(campaignId => campaignIds.indexOf(campaignId) ); if (indexes.includes(-1)) { @@ -297,96 +307,106 @@ export class CampaignTable extends React.Component { return indexes; }; - clearCampaignSelection = () => { - this.props.handleChecked([]); + const clearCampaignSelection = () => { + handleChecked([]); // Terrible hack around buggy DataTables: we have to force the component // to remount if we want clear the "select all" status - this.setState({ + setState({ dataTableKey: new Date().getTime() }); }; - render() { - const { limit, offset, total } = this.props.data.organization.campaigns.pageInfo; - const displayPage = Math.floor(offset / limit) + 1; - let rowSizeList = [10, 20, 50, 100]; - - const options = { - filterType: "checkbox", - selectableRows: "multiple", // this.props.selectMultiple ? "multiple" : "none", - elevation: 0, - download: false, - print: false, - searchable: false, - filter: false, - sort: true, - search: false, - viewColumns: false, - page: displayPage - 1, - count: total, - rowsPerPage: limit, - rowsPerPageOptions: rowSizeList, - serverSide: true, - rowsSelected: this.getSelectedRowIndexes(), - customToolbarSelect: () => null, - onTableChange: (action, tableState) => { - switch (action) { - case "changePage": - if (tableState.page > displayPage - 1) { - this.clearCampaignSelection(); - this.props.onNextPageClick(); - } else { - this.clearCampaignSelection(); - this.props.onPreviousPageClick(); - } - break; - case "changeRowsPerPage": - this.clearCampaignSelection(); - const _ = undefined; - this.props.onRowSizeChange(_, tableState.rowsPerPage); - break; - case "sort": - this.clearCampaignSelection(); - this.state.campaigns.sort(this.sortFunc(tableState.sortOrder.name)); - if (tableState.sortOrder.direction === "desc") { - this.state.campaigns.reverse() - } - break; - case "rowSelectionChange": - const ids = tableState.selectedRows.data.map(({ index }) => { - return this.state.campaigns[index].id; - }); - this.props.handleChecked(ids); - break; - case "propsUpdate": - break; - default: - break; - } + const options = { + filterType: "checkbox", + selectableRows: "multiple", // selectMultiple ? "multiple" : "none", + elevation: 0, + download: false, + print: false, + searchable: false, + filter: false, + sort: true, + search: false, + viewColumns: false, + page: displayPage - 1, + count: total, + rowsPerPage: limit, + rowsPerPageOptions: rowSizeList, + serverSide: true, + rowsSelected: getSelectedRowIndexes(), + customToolbarSelect: () => null, + onTableChange: (action, tableState) => { + switch (action) { + case "changePage": + if (tableState.page > displayPage - 1) { + clearCampaignSelection(); + onNextPageClick(); + } else { + clearCampaignSelection(); + onPreviousPageClick(); + } + break; + case "changeRowsPerPage": + clearCampaignSelection(); + const _ = undefined; + onRowSizeChange(_, tableState.rowsPerPage); + break; + case "sort": + clearCampaignSelection(); + campaigns.sort(sortFunc(tableState.sortOrder.name)); + if (tableState.sortOrder.direction === "desc") { + campaigns.reverse() + } + break; + case "rowSelectionChange": + const ids = tableState.selectedRows.data.map(({ index }) => { + return campaigns[index].id; + }); + handleChecked(ids); + break; + case "propsUpdate": + break; + default: + break; } - }; + } + }; - return this.state.campaigns.length === 0 ? ( - } /> - ) : ( -
-
-
- - {/* make space for Floating Action Button */} -
-
-
-
- ); - } + return campaigns.length === 0 ? ( + } /> + ) : ( +
+
+
+ + {/* make space for Floating Action Button */} +
+
+
+
+ ); +} + +CampaignTable.propTypes = { + adminPerms: PropTypes.bool, + selectMultiple: PropTypes.bool, + organizationId: PropTypes.string, + data: PropTypes.object, + handleChecked: PropTypes.func, + archiveCampaign: PropTypes.func, + unarchiveCampaign: PropTypes.func, + onNextPageClick: PropTypes.func, + onPreviousPageClick: PropTypes.func, + onRowSizeChange: PropTypes.func, + campaignsToArchive: PropTypes.array, + campaignsWithChangingStatus: PropTypes.array, + currentSortBy: PropTypes.oneOf(SORTS.map(s => s.value)) } export default CampaignTable; diff --git a/src/containers/AdminCampaignList.jsx b/src/containers/AdminCampaignList.jsx index 8ed20f5aa..80c986ba6 100644 --- a/src/containers/AdminCampaignList.jsx +++ b/src/containers/AdminCampaignList.jsx @@ -1,5 +1,5 @@ import PropTypes from "prop-types"; -import React from "react"; +import React, { useState } from "react"; import { StyleSheet, css } from "aphrodite"; import { withRouter } from "react-router"; import { gql } from "@apollo/client"; @@ -18,9 +18,7 @@ import LoadingIndicator from "../components/LoadingIndicator"; import { dataTest } from "../lib/attributes"; import loadData from "./hoc/load-data"; import theme from "../styles/theme"; -import SortBy, { - ID_DESC_SORT -} from "../components/AdminCampaignList/SortBy"; +import SortBy, { ID_DESC_SORT } from "../components/AdminCampaignList/SortBy"; import Search from "../components/Search"; import CampaignTable from "../components/AdminCampaignList/CampaignTable"; @@ -33,41 +31,32 @@ const styles = StyleSheet.create({ } }); -const INITIAL_ROW_SIZE = 50; -const INITIAL_FILTER = { - isArchived: false, - searchString: "" -}; const INITIAL_SORT_BY = ID_DESC_SORT.value; -export class AdminCampaignList extends React.Component { - static propTypes = { - params: PropTypes.object, - mutations: PropTypes.exact({ - createCampaign: PropTypes.func, - archiveCampaigns: PropTypes.func, - unarchiveCampaign: PropTypes.func - }), - data: PropTypes.object, - router: PropTypes.object - }; - - state = { - pageSize: INITIAL_ROW_SIZE, +// Exported for testing +export const AdminCampaignList = ({ params, mutations, router, data }) => { + const [state, setState] = useState({ + pageSize: 50, page: 0, isLoading: false, - campaignsFilter: INITIAL_FILTER, + campaignsFilter: { + isArchived: false, + searchString: "" + }, archiveMultiple: false, campaignsToArchive: [], campaignsWithChangingStatus: [], sortBy: INITIAL_SORT_BY, archiveMultipleMenu: false - }; + }); - handleClickNewButton = async () => { - const { organizationId } = this.props.params; - this.setState({ isLoading: true }); - const newCampaign = await this.props.mutations.createCampaign({ + const handleClickNewButton = async () => { + const { organizationId } = params; + setState({ + ...state, + isLoading: true + }); + const newCampaign = await mutations.createCampaign({ title: "New Campaign", description: "", dueBy: null, @@ -83,17 +72,21 @@ export class AdminCampaignList extends React.Component { throw new Error(newCampaign.errors); } - await this.props.router.push( + await router.push( `/admin/${organizationId}/campaigns/${newCampaign.data.createCampaign.id}/edit?new=true` ); }; - handleClickArchiveMultipleButton = async keys => { + const handleClickArchiveMultipleButton = async keys => { if (keys.length) { - this.setState({ isLoading: true }); - await this.props.mutations.archiveCampaigns(keys); - await this.props.data.refetch(); - this.setState({ + setState({ + ...state, + isLoading: true + }); + await mutations.archiveCampaigns(keys); + await data.refetch(); + setState({ + ...state, archiveMultiple: false, isLoading: false, campaignsToArchive: [] @@ -101,90 +94,98 @@ export class AdminCampaignList extends React.Component { } }; - handleArchiveFilterChange = async event => { - this.changeFilter({ isArchived: event.target.value }); + const handleArchiveFilterChange = async event => { + changeFilter({ isArchived: event.target.value }); }; - handleChecked = campaignIds => { - this.setState({ + const handleChecked = campaignIds => { + setState({ + ...state, campaignsToArchive: [...campaignIds] }); }; - handleSearchRequested = searchString => { + const handleSearchRequested = searchString => { const campaignsFilter = { - ...this.state.campaignsFilter, + ...state.campaignsFilter, searchString }; - this.changeFilter(campaignsFilter); + changeFilter(campaignsFilter); }; - handleCancelSearch = () => { + const handleCancelSearch = () => { const campaignsFilter = { - ...this.state.campaignsFilter, + ...state.campaignsFilter, searchString: "" }; - this.changeFilter(campaignsFilter); + changeFilter(campaignsFilter); }; - renderArchivedAndSortBy = () => { + const renderArchivedAndSortBy = () => { return ( - !this.state.archiveMultiple && ( + !state.archiveMultiple && ( - + ) ); }; - renderSearch = () => { + const renderSearch = () => { return ( - !this.state.archiveMultiple && ( + !state.archiveMultiple && (
) ); }; - renderFilters = () => ( + const renderFilters = () => (
- {this.props.params.adminPerms && this.renderArchiveMultiple()} - {this.renderArchivedAndSortBy()} + {params.adminPerms && renderArchiveMultiple()} + {renderArchivedAndSortBy()}
- {this.renderSearch()} + {renderSearch()}
); - handleMenuClick = event => { + const handleMenuClick = event => { console.log("event.target", event.target); - this.setState({ menuAnchorEl: event.target }); + setState({ + ...state, + menuAnchorEl: event.target + }); }; - handleMenuClose = () => { - this.setState({ menuAnchorEl: null }); + const handleMenuClose = () => { + setState({ + ...state, + menuAnchorEl: null + }); }; - renderArchiveMultiple() { + const renderArchiveMultiple = () => { const iconButton = ( { - this.handleMenuClick(event); - this.setState({ - archiveMultipleMenu: !this.state.archiveMultipleMenu + handleMenuClick(event); + setState({ + ...state, + archiveMultipleMenu: !state.archiveMultipleMenu }); }} > @@ -192,23 +193,21 @@ export class AdminCampaignList extends React.Component { ); - if (this.state.campaignsFilter.isArchived) { + if (state.campaignsFilter.isArchived) { return iconButton; } return ( {iconButton} - - {this.state.archiveMultiple ? ( + + {state.archiveMultiple ? ( { - this.setState({ - archiveMultipleMenu: !this.state.archiveMultipleMenu, - archiveMultiple: !this.state.archiveMultiple + setState({ + ...state, + archiveMultipleMenu: !state.archiveMultipleMenu, + archiveMultiple: !state.archiveMultiple }); }} > @@ -217,9 +216,10 @@ export class AdminCampaignList extends React.Component { ) : ( { - this.setState({ - archiveMultipleMenu: !this.state.archiveMultipleMenu, - archiveMultiple: !this.state.archiveMultiple + setState({ + ...state, + archiveMultipleMenu: !state.archiveMultipleMenu, + archiveMultiple: !state.archiveMultiple }); }} > @@ -229,21 +229,17 @@ export class AdminCampaignList extends React.Component { ); - } + }; - changePage = (pageDelta, pageSize) => { - const { - limit, - offset, - total - } = this.props.data.organization.campaigns.pageInfo; + const changePage = (pageDelta, pageSize) => { + const { limit, offset, total } = data.organization.campaigns.pageInfo; const currentPage = Math.floor(offset / limit); const pageSizeAdjustedCurrentPage = Math.floor( (currentPage * limit) / pageSize ); const maxPage = Math.floor(total / pageSize); const newPage = Math.min(maxPage, pageSizeAdjustedCurrentPage + pageDelta); - this.props.data.fetchMore({ + data.fetchMore({ variables: { cursor: { offset: newPage * pageSize, @@ -259,81 +255,91 @@ export class AdminCampaignList extends React.Component { }); }; - changeFilter = async newFilter => { - this.setState({ - isLoading: true, - campaignsFilter: newFilter + const changeFilter = async newFilter => { + setState({ + ...state, + campaignsFilter: newFilter, + isLoading: true }); - await this.props.data.refetch({ + await data.refetch({ campaignsFilter: newFilter }); - this.setState({ isLoading: false }); + setState({ + ...state, + campaignsFilter: newFilter, + isLoading: false + }); }; - changeSortBy = async newSort => { - this.setState({ + const changeSortBy = async newSort => { + setState({ + ...state, isLoading: true, sortBy: newSort }); - await this.props.data.refetch({ + await data.refetch({ + sortBy: newSort + }); + setState({ + ...state, + isLoading: false, sortBy: newSort }); - this.setState({ isLoading: false }); }; - handleNextPageClick = () => { - this.changePage(1, this.state.pageSize); + const handleNextPageClick = () => { + changePage(1, state.pageSize); }; - handlePreviousPageClick = () => { - this.changePage(-1, this.state.pageSize); + const handlePreviousPageClick = () => { + changePage(-1, state.pageSize); }; - handleRowSizeChanged = (index, value) => { + const handleRowSizeChanged = (index, value) => { console.log("rowsizechanged", index, value); // eslint-disable-line no-console - this.changePage(0, value); - this.setState({ + changePage(0, value); + setState({ + ...state, pageSize: value }); }; - changeCampaignStatus = async (campaignId, changeFn) => { - this.setState({ - campaignsWithChangingStatus: this.state.campaignsWithChangingStatus.concat( - [campaignId] - ) + const changeCampaignStatus = async (campaignId, changeFn) => { + setState({ + ...state, + campaignsWithChangingStatus: state.campaignsWithChangingStatus.concat([ + campaignId + ]) }); await changeFn(campaignId); - await this.props.data.refetch(); - this.setState({ - campaignsWithChangingStatus: this.state.campaignsWithChangingStatus.filter( + await data.refetch(); + setState({ + ...state, + campaignsWithChangingStatus: state.campaignsWithChangingStatus.filter( id => id !== campaignId ) }); }; - handleArchiveCampaign = async campaignId => { - await this.changeCampaignStatus(campaignId, async id => { - await this.props.mutations.archiveCampaigns([id]); + const handleArchiveCampaign = async campaignId => { + await changeCampaignStatus(campaignId, async id => { + await mutations.archiveCampaigns([id]); }); }; - handleUnarchiveCampaign = async campaignId => { - await this.changeCampaignStatus( - campaignId, - this.props.mutations.unarchiveCampaign - ); + const handleUnarchiveCampaign = async campaignId => { + await changeCampaignStatus(campaignId, mutations.unarchiveCampaign); }; - renderActionButton() { - if (this.state.archiveMultiple) { - const keys = this.state.campaignsToArchive; + const renderActionButton = () => { + if (state.archiveMultiple) { + const keys = state.campaignsToArchive; return ( this.handleClickArchiveMultipleButton(keys)} + onClick={() => handleClickArchiveMultipleButton(keys)} disabled={!keys.length} > @@ -345,43 +351,40 @@ export class AdminCampaignList extends React.Component { color="primary" {...dataTest("addCampaign")} style={theme.components.floatingButton} - onClick={this.handleClickNewButton} + onClick={handleClickNewButton} > ); - } - - render() { - const { adminPerms } = this.props.params; - return ( -
- {this.renderFilters()} - {this.state.isLoading ? ( - - ) : ( - - )} - - {adminPerms && this.renderActionButton()} -
- ); - } -} + }; + // don't think this is right + return ( +
+ {renderFilters()} + {state.isLoading ? ( + + ) : ( + + )} + + {params.adminPerms && renderActionButton()} +
+ ); +}; const campaignInfoFragment = ` id @@ -478,7 +481,10 @@ const queries = { variables: { cursor: { offset: 0, limit: 50 }, organizationId: ownProps.params.organizationId, - campaignsFilter: INITIAL_FILTER, + campaignsFilter: { + isArchived: false, + searchString: "" + }, sortBy: INITIAL_SORT_BY }, fetchPolicy: "network-only" @@ -517,4 +523,15 @@ const mutations = { }) }; +AdminCampaignList.propTypes = { + params: PropTypes.object, + mutations: PropTypes.exact({ + createCampaign: PropTypes.func, + archiveCampaigns: PropTypes.func, + unarchiveCampaign: PropTypes.func + }), + data: PropTypes.object, + router: PropTypes.object +}; + export default loadData({ queries, mutations })(withRouter(AdminCampaignList)); diff --git a/yarn.lock b/yarn.lock index 00fb26e8a..5d12e2647 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5051,6 +5051,50 @@ "@svgr/plugin-svgo" "^5.5.0" loader-utils "^2.0.0" +"@testing-library/dom@^10.4.0": + version "10.4.0" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.4.0.tgz#82a9d9462f11d240ecadbf406607c6ceeeff43a8" + integrity sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^5.0.1" + aria-query "5.3.0" + chalk "^4.1.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.5.0" + pretty-format "^27.0.2" + +"@testing-library/dom@^8.0.0": + version "8.20.1" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.20.1.tgz#2e52a32e46fc88369eef7eef634ac2a192decd9f" + integrity sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^5.0.1" + aria-query "5.1.3" + chalk "^4.1.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.5.0" + pretty-format "^27.0.2" + +"@testing-library/react@^12.1.5": + version "12.1.5" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.1.5.tgz#bb248f72f02a5ac9d949dea07279095fa577963b" + integrity sha512-OfTXCJUFgjd/digLUuPxa0+/3ZxsQmE7ub9kcbW/wi96Bh3o/p5vrETcBGfP17NWPGqeYYl5LTRpwyGoMC4ysg== + dependencies: + "@babel/runtime" "^7.12.5" + "@testing-library/dom" "^8.0.0" + "@types/react-dom" "<18.0.0" + +"@testing-library/user-event@^12.1.5": + version "12.8.3" + resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-12.8.3.tgz#1aa3ed4b9f79340a1e1836bc7f57c501e838704a" + integrity sha512-IR0iWbFkgd56Bu5ZI/ej8yQwrkCv8Qydx6RzwbKz9faXazR/+5tvYKsZQgyXJiwgpcva127YO6JcWy7YlCfofQ== + dependencies: + "@babel/runtime" "^7.12.5" + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" @@ -5071,6 +5115,11 @@ resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== +"@types/aria-query@^5.0.1": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708" + integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== + "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": version "7.20.5" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" @@ -5335,6 +5384,13 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== +"@types/react-dom@<18.0.0": + version "17.0.25" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.25.tgz#e0e5b3571e1069625b3a3da2b279379aa33a0cb5" + integrity sha512-urx7A7UxkZQmThYA4So0NelOVjx3V4rNFVJwp0WZlbIK5eM4rNJDiN3R/E9ix0MBh6kAEojk/9YL+Te6D9zHNA== + dependencies: + "@types/react" "^17" + "@types/react-transition-group@^4.2.0": version "4.4.10" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.10.tgz#6ee71127bdab1f18f11ad8fb3322c6da27c327ac" @@ -5351,6 +5407,15 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@^17": + version "17.0.82" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.82.tgz#eb84c38ee1023cd61be1b909fde083ac83fc163f" + integrity sha512-wTW8Lu/PARGPFE8tOZqCvprOKg5sen/2uS03yKn2xbCDFP9oLncm7vMDQ2+dEQXHVIXrOpW6u72xUXEXO0ypSw== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "^0.16" + csstype "^3.0.2" + "@types/resolve@1.17.1": version "1.17.1" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" @@ -5363,7 +5428,7 @@ resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== -"@types/scheduler@*": +"@types/scheduler@*", "@types/scheduler@^0.16": version "0.16.8" resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff" integrity sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A== @@ -6130,20 +6195,20 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -aria-query@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" - integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== - dependencies: - dequal "^2.0.3" - -aria-query@~5.1.3: +aria-query@5.1.3, aria-query@~5.1.3: version "5.1.3" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== dependencies: deep-equal "^2.0.5" +aria-query@5.3.0, aria-query@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" + integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== + dependencies: + dequal "^2.0.3" + arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" @@ -9003,6 +9068,11 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-accessibility-api@^0.5.9: + version "0.5.16" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" + integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== + dom-converter@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" @@ -14041,6 +14111,11 @@ lru-cache@~2.2.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.2.4.tgz#6c658619becf14031d0d0b594b16042ce4dc063d" integrity sha512-Q5pAgXs+WEAfoEdw2qKQhNFFhMoFMTYqRVKKUMnzuiR7oKFHS7fWo848cPcTKw+4j/IdN17NyzdhVKgabFV0EA== +lz-string@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" + integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== + magic-string@^0.25.0, magic-string@^0.25.7: version "0.25.9" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" @@ -16380,7 +16455,7 @@ pretty-error@^4.0.0: lodash "^4.17.20" renderkid "^3.0.0" -pretty-format@^27.5.1: +pretty-format@^27.0.2, pretty-format@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==