diff --git a/cypress/integration/navigation.js b/cypress/integration/navigation.js new file mode 100644 index 00000000000..102ae673126 --- /dev/null +++ b/cypress/integration/navigation.js @@ -0,0 +1,32 @@ +import listPageFactory from '../support/ListPage'; + +describe('Navigation', () => { + const ListPage = listPageFactory('/#/posts'); + + describe('Sidebar', () => { + it('should have tabbable menu items', () => { + ListPage.navigate(); + + ListPage.waitUntilVisible(); + + cy.get('body').tab().tab().tab().tab(); + + cy.get(`${ListPage.elements.menuItems}:first-child`).should( + 'have.class', + 'Mui-focusVisible' + ); + }); + }); + + describe('Skip Navigation Button', () => { + it('should appear when a user immediately tabs on the homepage', () => { + ListPage.navigate(); + + ListPage.waitUntilVisible(); + + cy.get('body').tab(); + + cy.get(ListPage.elements.skipNavButton).should('exist'); + }); + }); +}); diff --git a/cypress/package.json b/cypress/package.json index 83f4cb7891d..ae0b49ee0df 100644 --- a/cypress/package.json +++ b/cypress/package.json @@ -9,6 +9,7 @@ "devDependencies": { "@cypress/webpack-preprocessor": "^5.4.5", "cypress": "^5.1.0", + "cypress-plugin-tab": "^1.0.5", "cypress-skip-and-only-ui": "^1.2.7" } } diff --git a/cypress/support/ListPage.js b/cypress/support/ListPage.js index e9f1eac04c0..4cbf5d27dd4 100644 --- a/cypress/support/ListPage.js +++ b/cypress/support/ListPage.js @@ -5,7 +5,7 @@ export default url => ({ displayedRecords: '.displayed-records', filter: name => `.filter-field[data-source='${name}'] input`, filterMenuItems: `.new-filter-item`, - menuItems: `[role=menuitem`, + menuItems: `[role=menuitem]`, filterMenuItem: source => `.new-filter-item[data-key="${source}"]`, hideFilterButton: source => `.filter-field[data-source="${source}"] .hide-filter`, @@ -31,6 +31,7 @@ export default url => ({ title: '#react-admin-title', headroomUnfixed: '.headroom--unfixed', headroomUnpinned: '.headroom--unpinned', + skipNavButton: '.skip-nav-button', }, navigate() { diff --git a/cypress/support/index.js b/cypress/support/index.js index 8c716ced73b..9083853f33c 100644 --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -1 +1,2 @@ require('cypress-skip-and-only-ui/support'); +require('cypress-plugin-tab'); diff --git a/docs/List.md b/docs/List.md index 3541759f312..8859a67d2e0 100644 --- a/docs/List.md +++ b/docs/List.md @@ -27,6 +27,7 @@ Here are all the props accepted by the `` component: * [`pagination`](#pagination) * [`aside`](#aside-component) * [`empty`](#empty-page) +- [`syncWithLocation`](#synchronize-with-url) Here is the minimal code necessary to display a list of posts: @@ -721,6 +722,29 @@ const PostList = props => ( The default value for the `component` prop is `Card`. +## Synchronize With URL + +When a List based component (eg: `PostList`) is passed to the `list` prop of a ``, it will automatically synchronize its parameters with the browser URL (using react-router location). However, when used anywhere outside of a ``, it won't synchronize, which can be useful when you have multiple lists on a single page for example. + +In order to enable the synchronization with the URL, you can set the `syncWithLocation` prop. For example, adding a `List` to an `Edit` page: + +```jsx +const TagsEdit = (props) => ( + <> + + // ... + + + + + + + + + +) +``` + ### CSS API The `List` component accepts the usual `className` prop but you can override many class names injected to the inner components by React-admin thanks to the `classes` property (as most Material UI components, see their [documentation about it](https://material-ui.com/customization/components/#overriding-styles-with-classes)). This property accepts the following keys: diff --git a/examples/demo/package.json b/examples/demo/package.json index b333c7a6969..59a32cfd797 100644 --- a/examples/demo/package.json +++ b/examples/demo/package.json @@ -46,7 +46,7 @@ "devDependencies": { "@types/fetch-mock": "^7.3.2", "@types/classnames": "^2.2.9", - "@types/jest": "^24.0.23", + "@types/jest": "^26.0.19", "@types/node": "^12.12.14", "@types/query-string": "5.1.0", "@types/react": "^16.9.13", diff --git a/examples/demo/src/layout/themes.ts b/examples/demo/src/layout/themes.ts index ca463a675a8..3540468b1a6 100644 --- a/examples/demo/src/layout/themes.ts +++ b/examples/demo/src/layout/themes.ts @@ -15,6 +15,32 @@ export const darkTheme = { backgroundColor: '#616161e6', }, }, + MuiButtonBase: { + root: { + '&:hover:active::after': { + // recreate a static ripple color + // use the currentColor to make it work both for outlined and contained buttons + // but to dim the background without dimming the text, + // put another element on top with a limited opacity + content: '""', + display: 'block', + width: '100%', + height: '100%', + position: 'absolute', + top: 0, + right: 0, + backgroundColor: 'currentColor', + opacity: 0.3, + borderRadius: 'inherit', + }, + }, + }, + }, + props: { + MuiButtonBase: { + // disable ripple for perf reasons + disableRipple: true, + }, }, }; @@ -62,6 +88,26 @@ export const lightTheme = { boxShadow: 'none', }, }, + MuiButtonBase: { + root: { + '&:hover:active::after': { + // recreate a static ripple color + // use the currentColor to make it work both for outlined and contained buttons + // but to dim the background without dimming the text, + // put another element on top with a limited opacity + content: '""', + display: 'block', + width: '100%', + height: '100%', + position: 'absolute', + top: 0, + right: 0, + backgroundColor: 'currentColor', + opacity: 0.3, + borderRadius: 'inherit', + }, + }, + }, MuiAppBar: { colorSecondary: { color: '#808080', @@ -85,4 +131,10 @@ export const lightTheme = { }, }, }, + props: { + MuiButtonBase: { + // disable ripple for perf reasons + disableRipple: true, + }, + }, }; diff --git a/examples/simple/src/tags/TagEdit.js b/examples/simple/src/tags/TagEdit.js index 9b622c7c7e3..1adf2848bf3 100644 --- a/examples/simple/src/tags/TagEdit.js +++ b/examples/simple/src/tags/TagEdit.js @@ -1,14 +1,43 @@ /* eslint react/jsx-key: off */ import * as React from 'react'; -import { Edit, SimpleForm, TextField, TextInput, required } from 'react-admin'; +import { + Edit, + SimpleForm, + TextField, + TextInput, + required, + List, + Datagrid, + ResourceContextProvider, + EditButton, +} from 'react-admin'; const TagEdit = props => ( - - - - - - + <> + + + + + + + + + + + + + + + + ); - export default TagEdit; diff --git a/package.json b/package.json index 7dd8f12d291..5087e79bc7d 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ } }, "devDependencies": { - "@types/jest": "^24.0.13", + "@types/jest": "^26.0.19", "@types/react": "^16.9.0", "@types/react-redux": "^7.1.1", "@typescript-eslint/eslint-plugin": "^3.10.1", @@ -63,14 +63,14 @@ "express": "~4.16.3", "full-icu": "~1.3.1", "husky": "^2.3.0", - "jest": "^24.7.0", + "jest": "^26.6.3", "lerna": "~2.9.1", "lint-staged": "^8.1.7", "lolex": "~2.3.2", "mutationobserver-shim": "^0.3.3", "prettier": "~2.1.1", "raf": "~3.4.1", - "ts-jest": "^24.0.0", + "ts-jest": "^26.4.4", "wait-on": "^3.2.0", "whatwg-fetch": "^3.0.0" }, @@ -82,4 +82,4 @@ "dependencies": { "typescript": "^4.0.2" } -} \ No newline at end of file +} diff --git a/packages/ra-core/package.json b/packages/ra-core/package.json index 883b2a9f685..5ee5fa70c73 100644 --- a/packages/ra-core/package.json +++ b/packages/ra-core/package.json @@ -65,7 +65,7 @@ "redux-saga": "^1.0.0" }, "dependencies": { - "@testing-library/react": "^8.0.7", + "@testing-library/react": "^11.2.2", "classnames": "~2.2.5", "date-fns": "^1.29.0", "eventemitter3": "^3.0.0", diff --git a/packages/ra-core/src/actions/notificationActions.ts b/packages/ra-core/src/actions/notificationActions.ts index 3a0917cfdd8..5dfb1e4bd91 100644 --- a/packages/ra-core/src/actions/notificationActions.ts +++ b/packages/ra-core/src/actions/notificationActions.ts @@ -1,6 +1,6 @@ export const SHOW_NOTIFICATION = 'RA/SHOW_NOTIFICATION'; -export type NotificationType = 'info' | 'warning' | 'error'; +export type NotificationType = 'success' | 'info' | 'warning' | 'error'; interface NotificationOptions { // The duration in milliseconds the notification is shown diff --git a/packages/ra-core/src/auth/Authenticated.spec.tsx b/packages/ra-core/src/auth/Authenticated.spec.tsx index 15ab116d66b..568db41d93c 100644 --- a/packages/ra-core/src/auth/Authenticated.spec.tsx +++ b/packages/ra-core/src/auth/Authenticated.spec.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import expect from 'expect'; -import { cleanup, wait } from '@testing-library/react'; +import { wait } from '@testing-library/react'; import Authenticated from './Authenticated'; import AuthContext from './AuthContext'; @@ -10,8 +10,6 @@ import { createMemoryHistory } from 'history'; import { Router } from 'react-router-dom'; describe('', () => { - afterEach(cleanup); - const Foo = () =>
Foo
; it('should render its child by default', async () => { diff --git a/packages/ra-core/src/auth/useAuthState.spec.tsx b/packages/ra-core/src/auth/useAuthState.spec.tsx index fa608c31c90..ca4d334a2f0 100644 --- a/packages/ra-core/src/auth/useAuthState.spec.tsx +++ b/packages/ra-core/src/auth/useAuthState.spec.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import expect from 'expect'; -import { cleanup, wait } from '@testing-library/react'; +import { wait } from '@testing-library/react'; import useAuthState from './useAuthState'; import AuthContext from './AuthContext'; @@ -20,8 +20,6 @@ const stateInpector = state => ( ); describe('useAuthState', () => { - afterEach(cleanup); - it('should return a loading state on mount', () => { const { queryByText } = renderWithRedux( {stateInpector} diff --git a/packages/ra-core/src/auth/useAuthenticated.spec.tsx b/packages/ra-core/src/auth/useAuthenticated.spec.tsx index 3ec31b5c199..4a0d4fd9ed4 100644 --- a/packages/ra-core/src/auth/useAuthenticated.spec.tsx +++ b/packages/ra-core/src/auth/useAuthenticated.spec.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import expect from 'expect'; -import { cleanup, wait } from '@testing-library/react'; +import { wait } from '@testing-library/react'; import Authenticated from './Authenticated'; import AuthContext from './AuthContext'; @@ -10,8 +10,6 @@ import { createMemoryHistory } from 'history'; import { Router } from 'react-router-dom'; describe('useAuthenticated', () => { - afterEach(cleanup); - const Foo = () =>
Foo
; it('should call authProvider on mount', () => { diff --git a/packages/ra-core/src/auth/useCheckAuth.spec.tsx b/packages/ra-core/src/auth/useCheckAuth.spec.tsx index 511e8cd4130..86407c8b7ed 100644 --- a/packages/ra-core/src/auth/useCheckAuth.spec.tsx +++ b/packages/ra-core/src/auth/useCheckAuth.spec.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { useState, useEffect } from 'react'; import expect from 'expect'; -import { render, cleanup, wait } from '@testing-library/react'; +import { render, wait } from '@testing-library/react'; import useCheckAuth from './useCheckAuth'; import AuthContext from './AuthContext'; @@ -60,7 +60,6 @@ describe('useCheckAuth', () => { afterEach(() => { logout.mockClear(); notify.mockClear(); - cleanup(); }); it('should not logout if has credentials', async () => { diff --git a/packages/ra-core/src/auth/useLogoutIfAccessDenied.spec.tsx b/packages/ra-core/src/auth/useLogoutIfAccessDenied.spec.tsx index d1a5495be01..982c119eba8 100644 --- a/packages/ra-core/src/auth/useLogoutIfAccessDenied.spec.tsx +++ b/packages/ra-core/src/auth/useLogoutIfAccessDenied.spec.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { useState, useEffect } from 'react'; import expect from 'expect'; -import { render, cleanup, wait } from '@testing-library/react'; +import { render, wait } from '@testing-library/react'; import useLogoutIfAccessDenied from './useLogoutIfAccessDenied'; import AuthContext from './AuthContext'; @@ -55,7 +55,6 @@ describe('useLogoutIfAccessDenied', () => { afterEach(() => { logout.mockClear(); notify.mockClear(); - cleanup(); }); it('should not logout if passed no error', async () => { diff --git a/packages/ra-core/src/auth/usePermissions.spec.tsx b/packages/ra-core/src/auth/usePermissions.spec.tsx index 978c491448f..9f3df596013 100644 --- a/packages/ra-core/src/auth/usePermissions.spec.tsx +++ b/packages/ra-core/src/auth/usePermissions.spec.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import expect from 'expect'; -import { cleanup, wait } from '@testing-library/react'; +import { wait } from '@testing-library/react'; import usePermissions from './usePermissions'; import AuthContext from './AuthContext'; @@ -21,8 +21,6 @@ const stateInpector = state => ( ); describe('usePermissions', () => { - afterEach(cleanup); - it('should return a loading state on mount', () => { const { queryByText } = renderWithRedux( {stateInpector} diff --git a/packages/ra-core/src/controller/ListBase.tsx b/packages/ra-core/src/controller/ListBase.tsx index 63ca5e8897a..02ffc3285d0 100644 --- a/packages/ra-core/src/controller/ListBase.tsx +++ b/packages/ra-core/src/controller/ListBase.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; -import useListController from './useListController'; +import { ReactNode } from 'react'; +import useListController, { ListProps } from './useListController'; import ListContextProvider from './ListContextProvider'; /** @@ -36,7 +37,10 @@ import ListContextProvider from './ListContextProvider'; * * ); */ -const ListBase = ({ children, ...props }) => ( +const ListBase = ({ + children, + ...props +}: ListProps & { children: ReactNode }) => ( {children} diff --git a/packages/ra-core/src/controller/details/useCreateController.spec.tsx b/packages/ra-core/src/controller/details/useCreateController.spec.tsx index fdb6ad6541f..b0860f62be1 100644 --- a/packages/ra-core/src/controller/details/useCreateController.spec.tsx +++ b/packages/ra-core/src/controller/details/useCreateController.spec.tsx @@ -1,6 +1,6 @@ import React from 'react'; import expect from 'expect'; -import { act, cleanup } from '@testing-library/react'; +import { act } from '@testing-library/react'; import { getRecord } from './useCreateController'; import { CreateController } from './CreateController'; @@ -9,8 +9,6 @@ import { DataProviderContext } from '../../dataProvider'; import { DataProvider } from '../../types'; describe('useCreateController', () => { - afterEach(cleanup); - describe('getRecord', () => { const location = { pathname: '/foo', diff --git a/packages/ra-core/src/controller/details/useEditController.spec.tsx b/packages/ra-core/src/controller/details/useEditController.spec.tsx index ae328c349c4..5277b50f1d5 100644 --- a/packages/ra-core/src/controller/details/useEditController.spec.tsx +++ b/packages/ra-core/src/controller/details/useEditController.spec.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import expect from 'expect'; -import { act, cleanup, wait } from '@testing-library/react'; +import { act, wait } from '@testing-library/react'; import { EditController } from './EditController'; import renderWithRedux from '../../util/renderWithRedux'; @@ -8,8 +8,6 @@ import { DataProviderContext } from '../../dataProvider'; import { DataProvider } from '../../types'; describe('useEditController', () => { - afterEach(cleanup); - const defaultProps = { basePath: '', hasCreate: true, diff --git a/packages/ra-core/src/controller/field/ReferenceArrayFieldController.spec.tsx b/packages/ra-core/src/controller/field/ReferenceArrayFieldController.spec.tsx index 83bf6d2b8dd..a736aeb0cc6 100644 --- a/packages/ra-core/src/controller/field/ReferenceArrayFieldController.spec.tsx +++ b/packages/ra-core/src/controller/field/ReferenceArrayFieldController.spec.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import { cleanup } from '@testing-library/react'; import expect from 'expect'; import ReferenceArrayFieldController from './ReferenceArrayFieldController'; @@ -7,7 +6,6 @@ import { DataProviderContext } from '../../dataProvider'; import renderWithRedux from '../../util/renderWithRedux'; describe('', () => { - afterEach(cleanup); it('should set the loaded prop to false when related records are not yet fetched', () => { const children = jest.fn().mockReturnValue('child'); diff --git a/packages/ra-core/src/controller/field/ReferenceFieldController.spec.tsx b/packages/ra-core/src/controller/field/ReferenceFieldController.spec.tsx index e4d0d953629..26fa29de60f 100644 --- a/packages/ra-core/src/controller/field/ReferenceFieldController.spec.tsx +++ b/packages/ra-core/src/controller/field/ReferenceFieldController.spec.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import { cleanup } from '@testing-library/react'; import expect from 'expect'; import ReferenceFieldController from './ReferenceFieldController'; @@ -13,7 +12,6 @@ const defaultState = { }; describe('', () => { - afterEach(cleanup); it('should call the CRUD_GET_MANY action on mount if reference source is defined', async () => { const dataProvider = { getMany: jest.fn(() => diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx index 444d7fface1..82e059f67a5 100644 --- a/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx +++ b/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx @@ -1,12 +1,11 @@ import * as React from 'react'; -import { cleanup, wait } from '@testing-library/react'; +import { wait } from '@testing-library/react'; import expect from 'expect'; import ReferenceManyFieldController from './ReferenceManyFieldController'; import renderWithRedux from '../../util/renderWithRedux'; describe('', () => { - afterEach(cleanup); it('should set loaded to false when related records are not yet fetched', async () => { const children = jest.fn().mockReturnValue('children'); const { dispatch } = renderWithRedux( diff --git a/packages/ra-core/src/controller/input/ReferenceArrayInputController.spec.tsx b/packages/ra-core/src/controller/input/ReferenceArrayInputController.spec.tsx index 3446c84aff4..29a401a74cd 100644 --- a/packages/ra-core/src/controller/input/ReferenceArrayInputController.spec.tsx +++ b/packages/ra-core/src/controller/input/ReferenceArrayInputController.spec.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import expect from 'expect'; -import { cleanup, wait, fireEvent } from '@testing-library/react'; +import { wait, fireEvent } from '@testing-library/react'; import ReferenceArrayInputController from './ReferenceArrayInputController'; import { renderWithRedux } from '../../util'; import { CRUD_GET_MATCHING, CRUD_GET_MANY } from '../../../lib'; @@ -15,8 +15,6 @@ describe('', () => { source: 'tag_ids', }; - afterEach(cleanup); - it('should set loading to true as long as there are no references fetched and no selected references', () => { const children = jest.fn(({ loading }) => (
{loading.toString()}
diff --git a/packages/ra-core/src/controller/input/ReferenceInputController.spec.tsx b/packages/ra-core/src/controller/input/ReferenceInputController.spec.tsx index c96ca02c6dc..b5becb2a427 100644 --- a/packages/ra-core/src/controller/input/ReferenceInputController.spec.tsx +++ b/packages/ra-core/src/controller/input/ReferenceInputController.spec.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { useState, useCallback } from 'react'; -import { cleanup, fireEvent, wait } from '@testing-library/react'; +import { fireEvent, wait } from '@testing-library/react'; import omit from 'lodash/omit'; import expect from 'expect'; @@ -19,8 +19,6 @@ describe('', () => { source: 'post_id', }; - afterEach(cleanup); - const dataProvider = { getMany: jest.fn(() => Promise.resolve({ data: [{ id: 1, title: 'foo' }] }) diff --git a/packages/ra-core/src/controller/input/useGetMatchingReferences.spec.tsx b/packages/ra-core/src/controller/input/useGetMatchingReferences.spec.tsx index 5be9fbde0fa..fea8362d060 100644 --- a/packages/ra-core/src/controller/input/useGetMatchingReferences.spec.tsx +++ b/packages/ra-core/src/controller/input/useGetMatchingReferences.spec.tsx @@ -1,6 +1,5 @@ import renderHook from '../../util/renderHook'; import useMatchingReferences from './useGetMatchingReferences'; -import { cleanup } from '@testing-library/react'; describe('useMatchingReferences', () => { const defaultProps = { @@ -16,8 +15,6 @@ describe('useMatchingReferences', () => { referenceSource: undefined, }; - afterEach(cleanup); - it('should fetch matchingReferences only on mount', () => { const { dispatch } = renderHook( () => { diff --git a/packages/ra-core/src/controller/useListContext.spec.tsx b/packages/ra-core/src/controller/useListContext.spec.tsx index eddf9c337cd..ff2b5767002 100644 --- a/packages/ra-core/src/controller/useListContext.spec.tsx +++ b/packages/ra-core/src/controller/useListContext.spec.tsx @@ -1,13 +1,11 @@ import * as React from 'react'; import expect from 'expect'; -import { render, cleanup } from '@testing-library/react'; +import { render } from '@testing-library/react'; import ListContext from './ListContext'; import useListContext from './useListContext'; describe('useListContext', () => { - afterEach(cleanup); - const NaiveList = props => { const { ids, data } = useListContext(props); return ( diff --git a/packages/ra-core/src/controller/useListController.spec.tsx b/packages/ra-core/src/controller/useListController.spec.tsx index 305ecf83061..801bd9fb37f 100644 --- a/packages/ra-core/src/controller/useListController.spec.tsx +++ b/packages/ra-core/src/controller/useListController.spec.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import expect from 'expect'; -import { fireEvent, wait, cleanup } from '@testing-library/react'; +import { fireEvent, wait } from '@testing-library/react'; import lolex from 'lolex'; import TextField from '@material-ui/core/TextField/TextField'; @@ -68,7 +68,7 @@ describe('useListController', () => { }; const { getByLabelText, dispatch, reduxStore } = renderWithRedux( - , + , { admin: { resources: { @@ -112,7 +112,7 @@ describe('useListController', () => { }; const { getByLabelText, dispatch, reduxStore } = renderWithRedux( - , + , { admin: { resources: { @@ -158,7 +158,11 @@ describe('useListController', () => { }; const { dispatch, rerender } = renderWithRedux( - , + , { admin: { resources: { @@ -186,7 +190,13 @@ describe('useListController', () => { // Check that the permanent filter is not included in the filterValues (passed to Filter form and button) expect(children.mock.calls[0][0].filterValues).toEqual({}); - rerender(); + rerender( + + ); const updatedCrudGetListCalls = dispatch.mock.calls.filter( call => call[0].type === 'RA/CRUD_GET_LIST' @@ -205,7 +215,6 @@ describe('useListController', () => { afterEach(() => { clock.uninstall(); - cleanup(); }); }); describe('showFilter', () => { diff --git a/packages/ra-core/src/controller/useListController.ts b/packages/ra-core/src/controller/useListController.ts index cebd36ea9bc..08cb9f610ce 100644 --- a/packages/ra-core/src/controller/useListController.ts +++ b/packages/ra-core/src/controller/useListController.ts @@ -43,6 +43,9 @@ export interface ListProps { location?: Location; path?: string; resource?: string; + // Wether to synchronize the list parameters with the current location (URL search parameters) + // This is set to true automatically when a List is used inside a Resource component + syncWithLocation: boolean; [key: string]: any; } @@ -117,6 +120,7 @@ const useListController = ( perPage = 10, filter, debounce = 500, + syncWithLocation, } = props; const resource = useResourceContext(props); @@ -126,17 +130,16 @@ const useListController = ( ); } - const location = useLocation(); const translate = useTranslate(); const notify = useNotify(); const [query, queryModifiers] = useListParams({ resource, - location, filterDefaultValues, sort, perPage, debounce, + syncWithLocation, }); const [selectedIds, selectionModifiers] = useRecordSelection(resource); diff --git a/packages/ra-core/src/controller/useListParams.spec.ts b/packages/ra-core/src/controller/useListParams.spec.tsx similarity index 71% rename from packages/ra-core/src/controller/useListParams.spec.ts rename to packages/ra-core/src/controller/useListParams.spec.tsx index e878d49b665..28b88f25090 100644 --- a/packages/ra-core/src/controller/useListParams.spec.ts +++ b/packages/ra-core/src/controller/useListParams.spec.tsx @@ -1,8 +1,12 @@ -import { getQuery, getNumberOrDefault } from './useListParams'; +import * as React from 'react'; +import useListParams, { getQuery, getNumberOrDefault } from './useListParams'; import { SORT_DESC, SORT_ASC, } from '../reducer/admin/resource/list/queryReducer'; +import { createMemoryHistory } from 'history'; +import { renderWithRedux, TestContext } from '../util'; +import { fireEvent, waitFor } from '@testing-library/react'; describe('useListParams', () => { describe('getQuery', () => { @@ -182,4 +186,61 @@ describe('useListParams', () => { expect(result).toEqual(0); }); }); + + describe('useListParams', () => { + const Component = ({ syncWithLocation = false }) => { + const [, { setPage }] = useListParams({ + resource: 'posts', + syncWithLocation, + }); + + const handleClick = () => { + setPage(10); + }; + + return ; + }; + + test('should synchronize parameters with location and redux state when sync is enabled', async () => { + const history = createMemoryHistory(); + jest.spyOn(history, 'push'); + let dispatch; + + const { getByText } = renderWithRedux( + + {({ store }) => { + dispatch = jest.spyOn(store, 'dispatch'); + return ; + }} + + ); + + fireEvent.click(getByText('update')); + + expect(history.push).toHaveBeenCalled(); + expect(dispatch).toHaveBeenCalled(); + }); + + test('should not synchronize parameters with location and redux state when sync is not enabled', async () => { + const history = createMemoryHistory(); + jest.spyOn(history, 'push'); + let dispatch; + + const { getByText } = renderWithRedux( + + {({ store }) => { + dispatch = jest.spyOn(store, 'dispatch'); + return ; + }} + + ); + + fireEvent.click(getByText('update')); + + await waitFor(() => { + expect(history.push).not.toHaveBeenCalled(); + expect(dispatch).not.toHaveBeenCalled(); + }); + }); + }); }); diff --git a/packages/ra-core/src/controller/useListParams.ts b/packages/ra-core/src/controller/useListParams.ts index 48a18febd68..007aaaaaa40 100644 --- a/packages/ra-core/src/controller/useListParams.ts +++ b/packages/ra-core/src/controller/useListParams.ts @@ -1,11 +1,10 @@ -import { useCallback, useMemo, useEffect } from 'react'; +import { useCallback, useMemo, useEffect, useState } from 'react'; import { useSelector, useDispatch, shallowEqual } from 'react-redux'; import { parse, stringify } from 'query-string'; import lodashDebounce from 'lodash/debounce'; import set from 'lodash/set'; import pickBy from 'lodash/pickBy'; -import { Location } from 'history'; -import { useHistory } from 'react-router-dom'; +import { useHistory, useLocation } from 'react-router-dom'; import queryReducer, { SET_FILTER, @@ -21,7 +20,6 @@ import removeKey from '../util/removeKey'; interface ListParamsOptions { resource: string; - location: Location; perPage?: number; sort?: SortPayload; // default value for a filter when displayed but not yet set @@ -29,6 +27,9 @@ interface ListParamsOptions { // permanent filter which always overrides the user entry filter?: FilterPayload; debounce?: number; + // Wether to synchronize the list parameters with the current location (URL search parameters) + // This is set to true automatically when a List is used inside a Resource component + syncWithLocation?: boolean; } interface Parameters extends ListParams { @@ -109,15 +110,17 @@ const defaultParams = {}; */ const useListParams = ({ resource, - location, filterDefaultValues, filter, // permanent filter sort = defaultSort, perPage = 10, debounce = 500, + syncWithLocation = false, }: ListParamsOptions): [Parameters, Modifiers] => { const dispatch = useDispatch(); + const location = useLocation(); const history = useHistory(); + const [localParams, setLocalParams] = useState(defaultParams); const params = useSelector( (reduxState: ReduxState) => reduxState.admin.resources[resource] @@ -129,19 +132,22 @@ const useListParams = ({ const requestSignature = [ location.search, resource, - params, + syncWithLocation ? params : localParams, filterDefaultValues, JSON.stringify(sort), perPage, + syncWithLocation, ]; - const queryFromLocation = parseQueryFromLocation(location); + const queryFromLocation = syncWithLocation + ? parseQueryFromLocation(location) + : {}; const query = useMemo( () => getQuery({ queryFromLocation, - params, + params: syncWithLocation ? params : localParams, filterDefaultValues, sort, perPage, @@ -161,14 +167,20 @@ const useListParams = ({ const changeParams = useCallback(action => { const newParams = queryReducer(query, action); - history.push({ - search: `?${stringify({ - ...newParams, - filter: JSON.stringify(newParams.filter), - displayedFilters: JSON.stringify(newParams.displayedFilters), - })}`, - }); - dispatch(changeListParams(resource, newParams)); + if (syncWithLocation) { + history.push({ + search: `?${stringify({ + ...newParams, + filter: JSON.stringify(newParams.filter), + displayedFilters: JSON.stringify( + newParams.displayedFilters + ), + })}`, + }); + dispatch(changeListParams(resource, newParams)); + } else { + setLocalParams(newParams); + } }, requestSignature); // eslint-disable-line react-hooks/exhaustive-deps const setSort = useCallback( diff --git a/packages/ra-core/src/controller/useReference.spec.tsx b/packages/ra-core/src/controller/useReference.spec.tsx index a61985005eb..f850fb9ee3c 100644 --- a/packages/ra-core/src/controller/useReference.spec.tsx +++ b/packages/ra-core/src/controller/useReference.spec.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import { cleanup } from '@testing-library/react'; import expect from 'expect'; import renderWithRedux from '../util/renderWithRedux'; @@ -18,8 +17,6 @@ describe('useReference', () => { reference: 'posts', }; - afterEach(cleanup); - it('should fetch reference on mount', async () => { const dataProvider = { getMany: jest.fn(() => diff --git a/packages/ra-core/src/core/CoreAdminRouter.spec.tsx b/packages/ra-core/src/core/CoreAdminRouter.spec.tsx index 9da840d60ca..cb82357a2c9 100644 --- a/packages/ra-core/src/core/CoreAdminRouter.spec.tsx +++ b/packages/ra-core/src/core/CoreAdminRouter.spec.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { cleanup, wait } from '@testing-library/react'; +import { wait } from '@testing-library/react'; import expect from 'expect'; import { Router, Route } from 'react-router-dom'; import { createMemoryHistory } from 'history'; @@ -12,8 +12,6 @@ import Resource from './Resource'; const Layout = ({ children }) =>
Layout {children}
; describe('', () => { - afterEach(cleanup); - const defaultProps = { customRoutes: [], }; diff --git a/packages/ra-core/src/core/Resource.spec.tsx b/packages/ra-core/src/core/Resource.spec.tsx index 7ff227cef8e..1c7ef8fb754 100644 --- a/packages/ra-core/src/core/Resource.spec.tsx +++ b/packages/ra-core/src/core/Resource.spec.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import expect from 'expect'; -import { cleanup, wait } from '@testing-library/react'; +import { wait } from '@testing-library/react'; import { Router } from 'react-router-dom'; import { createMemoryHistory } from 'history'; @@ -26,8 +26,6 @@ const resource = { }; describe('', () => { - afterEach(cleanup); - it(`registers its resource in redux on mount when context is 'registration'`, () => { const { dispatch } = renderWithRedux( diff --git a/packages/ra-core/src/core/Resource.tsx b/packages/ra-core/src/core/Resource.tsx index 47bfb7db76a..d1ccc879c6e 100644 --- a/packages/ra-core/src/core/Resource.tsx +++ b/packages/ra-core/src/core/Resource.tsx @@ -129,6 +129,7 @@ const ResourceRoutes: FunctionComponent = ({ basePath={basePath} {...routeProps} {...resourceData} + syncWithLocation /> )} /> diff --git a/packages/ra-core/src/core/RoutesWithLayout.spec.tsx b/packages/ra-core/src/core/RoutesWithLayout.spec.tsx index 91fa3f274f8..89f91fcc197 100644 --- a/packages/ra-core/src/core/RoutesWithLayout.spec.tsx +++ b/packages/ra-core/src/core/RoutesWithLayout.spec.tsx @@ -2,13 +2,11 @@ import * as React from 'react'; import { Route, MemoryRouter } from 'react-router-dom'; import { createStore } from 'redux'; import { Provider } from 'react-redux'; -import { render, cleanup } from '@testing-library/react'; +import { render } from '@testing-library/react'; import RoutesWithLayout from './RoutesWithLayout'; describe('', () => { - afterEach(cleanup); - const Dashboard = () =>
Dashboard
; const Custom = ({ name }) =>
Custom
; const FirstResource = ({ name }) =>
Default
; diff --git a/packages/ra-core/src/dataProvider/Mutation.spec.tsx b/packages/ra-core/src/dataProvider/Mutation.spec.tsx index 9d155021565..6e39e9be9ac 100644 --- a/packages/ra-core/src/dataProvider/Mutation.spec.tsx +++ b/packages/ra-core/src/dataProvider/Mutation.spec.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import { - cleanup, fireEvent, waitForDomChange, act, @@ -17,8 +16,6 @@ import { useNotify } from '../sideEffect'; import { History } from 'history'; describe('Mutation', () => { - afterEach(cleanup); - it('should render its child function', () => { const { getByTestId } = renderWithRedux( diff --git a/packages/ra-core/src/dataProvider/Query.spec.tsx b/packages/ra-core/src/dataProvider/Query.spec.tsx index 02030dcdd57..fcecd5dc7fc 100644 --- a/packages/ra-core/src/dataProvider/Query.spec.tsx +++ b/packages/ra-core/src/dataProvider/Query.spec.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { render, - cleanup, act, fireEvent, wait, @@ -20,8 +19,6 @@ import { useNotify, useRefresh } from '../sideEffect'; import { History } from 'history'; describe('Query', () => { - afterEach(cleanup); - it('should render its child', () => { const { getByTestId } = renderWithRedux( diff --git a/packages/ra-core/src/dataProvider/useDataProvider.spec.js b/packages/ra-core/src/dataProvider/useDataProvider.spec.js index 13786e5ac51..540e722a2e2 100644 --- a/packages/ra-core/src/dataProvider/useDataProvider.spec.js +++ b/packages/ra-core/src/dataProvider/useDataProvider.spec.js @@ -1,6 +1,6 @@ import * as React from 'react'; import { useState, useEffect } from 'react'; -import { cleanup, act, fireEvent } from '@testing-library/react'; +import { act, fireEvent } from '@testing-library/react'; import expect from 'expect'; import renderWithRedux from '../util/renderWithRedux'; @@ -55,8 +55,6 @@ const UseCustomVerbWithStandardSignature = ({ onSuccess }) => { }; describe('useDataProvider', () => { - afterEach(cleanup); - it('should return a way to call the dataProvider', async () => { const getOne = jest.fn(() => Promise.resolve({ data: { id: 1, title: 'foo' } }) diff --git a/packages/ra-core/src/dataProvider/useGetList.spec.tsx b/packages/ra-core/src/dataProvider/useGetList.spec.tsx index 5e882d8d277..617d369c9d4 100644 --- a/packages/ra-core/src/dataProvider/useGetList.spec.tsx +++ b/packages/ra-core/src/dataProvider/useGetList.spec.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { cleanup, wait } from '@testing-library/react'; +import { wait } from '@testing-library/react'; import expect from 'expect'; import renderWithRedux from '../util/renderWithRedux'; @@ -21,8 +21,6 @@ const UseGetList = ({ }; describe('useGetList', () => { - afterEach(cleanup); - it('should call dataProvider.getList() on mount', async () => { const dataProvider = { getList: jest.fn(() => diff --git a/packages/ra-core/src/dataProvider/useGetMany.spec.tsx b/packages/ra-core/src/dataProvider/useGetMany.spec.tsx index f57fec9cfe8..aa22fc784cb 100644 --- a/packages/ra-core/src/dataProvider/useGetMany.spec.tsx +++ b/packages/ra-core/src/dataProvider/useGetMany.spec.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import { cleanup } from '@testing-library/react'; import expect from 'expect'; import renderWithRedux from '../util/renderWithRedux'; @@ -19,8 +18,6 @@ const UseGetMany = ({ }; describe('useGetMany', () => { - afterEach(cleanup); - it('should call the dataProvider with a GET_MANY on mount', async () => { const dataProvider = { getMany: jest.fn(() => diff --git a/packages/ra-core/src/dataProvider/useMutation.spec.tsx b/packages/ra-core/src/dataProvider/useMutation.spec.tsx index e6877ec84e4..b8e9cd0626c 100644 --- a/packages/ra-core/src/dataProvider/useMutation.spec.tsx +++ b/packages/ra-core/src/dataProvider/useMutation.spec.tsx @@ -1,10 +1,5 @@ import * as React from 'react'; -import { - render, - cleanup, - fireEvent, - waitForDomChange, -} from '@testing-library/react'; +import { render, fireEvent, waitForDomChange } from '@testing-library/react'; import expect from 'expect'; import Mutation from './Mutation'; @@ -13,8 +8,6 @@ import renderWithRedux from '../util/renderWithRedux'; import { DataProviderContext } from '.'; describe('useMutation', () => { - afterEach(cleanup); - it('should pass a callback to trigger the mutation', () => { let callback = null; renderWithRedux( diff --git a/packages/ra-core/src/dataProvider/useQueryWithStore.spec.tsx b/packages/ra-core/src/dataProvider/useQueryWithStore.spec.tsx index d7303b1c3f9..318f15f589a 100644 --- a/packages/ra-core/src/dataProvider/useQueryWithStore.spec.tsx +++ b/packages/ra-core/src/dataProvider/useQueryWithStore.spec.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { cleanup, wait } from '@testing-library/react'; +import { wait } from '@testing-library/react'; import expect from 'expect'; import renderWithRedux from '../util/renderWithRedux'; @@ -25,8 +25,6 @@ const UseQueryWithStore = ({ }; describe('useQueryWithStore', () => { - afterEach(cleanup); - it('should return data from dataProvider', async () => { const dataProvider = { getOne: jest.fn(() => diff --git a/packages/ra-core/src/form/FormDataConsumer.spec.tsx b/packages/ra-core/src/form/FormDataConsumer.spec.tsx index 5437e33f648..991a45ee3b9 100644 --- a/packages/ra-core/src/form/FormDataConsumer.spec.tsx +++ b/packages/ra-core/src/form/FormDataConsumer.spec.tsx @@ -1,11 +1,9 @@ import * as React from 'react'; -import { render, cleanup } from '@testing-library/react'; +import { render } from '@testing-library/react'; import { FormDataConsumerView } from './FormDataConsumer'; describe('FormDataConsumerView', () => { - afterEach(cleanup); - it('does not call its children function with scopedFormData and getSource if it did not receive an index prop', () => { const children = jest.fn(); const formData = { id: 123, title: 'A title' }; diff --git a/packages/ra-core/src/form/FormField.spec.tsx b/packages/ra-core/src/form/FormField.spec.tsx index ce062dfe3bc..a6e75f74629 100644 --- a/packages/ra-core/src/form/FormField.spec.tsx +++ b/packages/ra-core/src/form/FormField.spec.tsx @@ -1,12 +1,10 @@ import * as React from 'react'; import expect from 'expect'; import { Form } from 'react-final-form'; -import { render, fireEvent, cleanup } from '@testing-library/react'; +import { render, fireEvent } from '@testing-library/react'; import FormField from './FormField'; describe('', () => { - afterEach(cleanup); - // disable deprecation warnings let consoleSpy; beforeAll(() => { diff --git a/packages/ra-core/src/form/FormWithRedirect.spec.tsx b/packages/ra-core/src/form/FormWithRedirect.spec.tsx index 3f4db1dadde..3d43a32ef58 100644 --- a/packages/ra-core/src/form/FormWithRedirect.spec.tsx +++ b/packages/ra-core/src/form/FormWithRedirect.spec.tsx @@ -1,12 +1,10 @@ import * as React from 'react'; -import { cleanup } from '@testing-library/react'; import { renderWithRedux } from '../util'; import FormWithRedirect from './FormWithRedirect'; import useInput from './useInput'; describe('FormWithRedirect', () => { - afterEach(cleanup); const Input = props => { const { input } = useInput(props); diff --git a/packages/ra-core/src/form/ValidationError.spec.tsx b/packages/ra-core/src/form/ValidationError.spec.tsx index 020f2848f38..f13ed7d6d6e 100644 --- a/packages/ra-core/src/form/ValidationError.spec.tsx +++ b/packages/ra-core/src/form/ValidationError.spec.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import polyglotI18nProvider from 'ra-i18n-polyglot'; -import { cleanup } from '@testing-library/react'; import ValidationError from './ValidationError'; import { TranslationProvider } from '../i18n'; @@ -33,7 +32,6 @@ const renderWithTranslations = content => describe('ValidationError', () => { afterEach(() => { - cleanup(); translate.mockClear(); }); diff --git a/packages/ra-core/src/form/useChoices.spec.tsx b/packages/ra-core/src/form/useChoices.spec.tsx index 89518ca15ee..a938c02235a 100644 --- a/packages/ra-core/src/form/useChoices.spec.tsx +++ b/packages/ra-core/src/form/useChoices.spec.tsx @@ -1,12 +1,11 @@ import * as React from 'react'; import expect from 'expect'; -import { render, cleanup } from '@testing-library/react'; +import { render } from '@testing-library/react'; import useChoices from './useChoices'; import { renderWithRedux } from '../util'; import { TestTranslationProvider } from '../i18n'; describe('useChoices hook', () => { - afterEach(cleanup); const defaultProps = { choice: { id: 42, name: 'test' }, optionValue: 'id', diff --git a/packages/ra-core/src/form/useWarnWhenUnsavedChanges.spec.tsx b/packages/ra-core/src/form/useWarnWhenUnsavedChanges.spec.tsx index 64169e461e8..c7ba28832d2 100644 --- a/packages/ra-core/src/form/useWarnWhenUnsavedChanges.spec.tsx +++ b/packages/ra-core/src/form/useWarnWhenUnsavedChanges.spec.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import expect from 'expect'; -import { render, cleanup, fireEvent } from '@testing-library/react'; +import { render, fireEvent } from '@testing-library/react'; import { Form, Field } from 'react-final-form'; import { Route, MemoryRouter, useHistory } from 'react-router-dom'; @@ -53,8 +53,6 @@ const App = () => ( ); describe('useWarnWhenUnsavedChanges', () => { - afterEach(cleanup); - it('should not warn when leaving form with no changes', () => { const { getByText } = render(); fireEvent.click(getByText('Submit')); diff --git a/packages/ra-core/src/i18n/useSetLocale.spec.js b/packages/ra-core/src/i18n/useSetLocale.spec.js index da248b86361..f49dbce050f 100644 --- a/packages/ra-core/src/i18n/useSetLocale.spec.js +++ b/packages/ra-core/src/i18n/useSetLocale.spec.js @@ -1,6 +1,6 @@ import * as React from 'react'; import expect from 'expect'; -import { fireEvent, cleanup, wait, act } from '@testing-library/react'; +import { fireEvent, wait, act } from '@testing-library/react'; import polyglotI18nProvider from 'ra-i18n-polyglot'; import useTranslate from './useTranslate'; @@ -9,8 +9,6 @@ import { TranslationContext, TranslationProvider } from './'; import { renderWithRedux } from '../util'; describe('useSetLocale', () => { - afterEach(cleanup); - const Component = () => { const translate = useTranslate(); const setLocale = useSetLocale(); diff --git a/packages/ra-core/src/i18n/useTranslate.spec.tsx b/packages/ra-core/src/i18n/useTranslate.spec.tsx index b32adef1c63..d36184ec826 100644 --- a/packages/ra-core/src/i18n/useTranslate.spec.tsx +++ b/packages/ra-core/src/i18n/useTranslate.spec.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import expect from 'expect'; -import { cleanup } from '@testing-library/react'; import useTranslate from './useTranslate'; import TranslationProvider from './TranslationProvider'; @@ -8,8 +7,6 @@ import { TranslationContext } from './TranslationContext'; import { renderWithRedux } from '../util'; describe('useTranslate', () => { - afterEach(cleanup); - const Component = () => { const translate = useTranslate(); return
{translate('hello')}
; diff --git a/packages/ra-core/src/util/FieldTitle.spec.tsx b/packages/ra-core/src/util/FieldTitle.spec.tsx index 1e0807cb46f..98510c10756 100644 --- a/packages/ra-core/src/util/FieldTitle.spec.tsx +++ b/packages/ra-core/src/util/FieldTitle.spec.tsx @@ -1,5 +1,5 @@ import expect from 'expect'; -import { render, cleanup } from '@testing-library/react'; +import { render } from '@testing-library/react'; import * as React from 'react'; import { FieldTitle } from './FieldTitle'; @@ -7,8 +7,6 @@ import TestTranslationProvider from '../i18n/TestTranslationProvider'; import renderWithRedux from './renderWithRedux'; describe('FieldTitle', () => { - afterEach(cleanup); - it('should return empty span by default', () => { const { container } = render(); expect(container.firstChild).toBeInstanceOf(HTMLSpanElement); diff --git a/packages/ra-core/src/util/TestContext.spec.tsx b/packages/ra-core/src/util/TestContext.spec.tsx index 2a59e8686d3..0f0c36cf14d 100644 --- a/packages/ra-core/src/util/TestContext.spec.tsx +++ b/packages/ra-core/src/util/TestContext.spec.tsx @@ -1,5 +1,5 @@ import expect from 'expect'; -import { render, cleanup } from '@testing-library/react'; +import { render } from '@testing-library/react'; import * as React from 'react'; import TestContext, { defaultStore } from './TestContext'; @@ -33,8 +33,6 @@ const primedStore = { }; describe('TestContext.js', () => { - afterEach(cleanup); - it('should render the given children', () => { const { queryAllByText } = render( diff --git a/packages/ra-core/src/util/TestContext.tsx b/packages/ra-core/src/util/TestContext.tsx index 2d611c95080..2ff32e123ad 100644 --- a/packages/ra-core/src/util/TestContext.tsx +++ b/packages/ra-core/src/util/TestContext.tsx @@ -29,6 +29,7 @@ type ChildrenFunction = ({ interface Props { initialState?: object; enableReducers?: boolean; + history?: History; children: ReactNode | ChildrenFunction; } diff --git a/packages/ra-input-rich-text/src/index.spec.js b/packages/ra-input-rich-text/src/index.spec.js index 4fdf0693f59..9322f29e830 100644 --- a/packages/ra-input-rich-text/src/index.spec.js +++ b/packages/ra-input-rich-text/src/index.spec.js @@ -1,6 +1,6 @@ import * as React from 'react'; import debounce from 'lodash/debounce'; -import { render, fireEvent, waitForElement } from '@testing-library/react'; +import { render, fireEvent, waitFor } from '@testing-library/react'; import { Form } from 'react-final-form'; import RichTextInput from './index'; @@ -8,6 +8,7 @@ import RichTextInput from './index'; let container; jest.mock('lodash/debounce'); + describe('RichTextInput', () => { beforeEach(() => { container = document.createElement('div'); @@ -39,7 +40,7 @@ describe('RichTextInput', () => { )} /> ); - const quillNode = await waitForElement(() => { + const quillNode = await waitFor(() => { return getByTestId('quill'); }); const node = quillNode.querySelector('.ql-editor'); diff --git a/packages/ra-input-rich-text/src/index.tsx b/packages/ra-input-rich-text/src/index.tsx index abdc7ef69c3..f134d8c0d73 100644 --- a/packages/ra-input-rich-text/src/index.tsx +++ b/packages/ra-input-rich-text/src/index.tsx @@ -107,7 +107,9 @@ const RichTextInput: FunctionComponent = props => { return () => { quillInstance.current.off('text-change', onTextChange); - onTextChange.cancel(); + if (onTextChange.cancel) { + onTextChange.cancel(); + } quillInstance.current = null; }; // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/packages/ra-language-english/src/index.ts b/packages/ra-language-english/src/index.ts index 08941697e42..233b5a70ea5 100644 --- a/packages/ra-language-english/src/index.ts +++ b/packages/ra-language-english/src/index.ts @@ -103,6 +103,7 @@ const englishMessages: TranslationMessages = { page_rows_per_page: 'Rows per page:', next: 'Next', prev: 'Prev', + skip_nav: 'Skip to content', }, sort: { sort_by: 'Sort by %{field} %{order}', diff --git a/packages/ra-ui-materialui/package.json b/packages/ra-ui-materialui/package.json index 1aa3119c3dd..ec67e18bf46 100644 --- a/packages/ra-ui-materialui/package.json +++ b/packages/ra-ui-materialui/package.json @@ -29,7 +29,7 @@ "@material-ui/core": "^4.10.0", "@material-ui/icons": "^4.9.1", "@material-ui/styles": "^4.10.0", - "@testing-library/react": "^8.0.7", + "@testing-library/react": "^11.2.2", "@types/query-string": "5.1.0", "cross-env": "^5.2.0", "enzyme": "~3.9.0", diff --git a/packages/ra-ui-materialui/src/button/Button.spec.tsx b/packages/ra-ui-materialui/src/button/Button.spec.tsx index f401353d6a0..3203ae1b226 100644 --- a/packages/ra-ui-materialui/src/button/Button.spec.tsx +++ b/packages/ra-ui-materialui/src/button/Button.spec.tsx @@ -1,4 +1,4 @@ -import { render, cleanup } from '@testing-library/react'; +import { render } from '@testing-library/react'; import * as React from 'react'; import expect from 'expect'; import { TestContext } from 'ra-core'; @@ -24,8 +24,6 @@ const invalidButtonDomProps = { }; describe('