diff --git a/docs/requirements.txt b/docs/requirements.txt index 3a0f71bb1..999250766 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ -dash>=2.0.0 -dash_bootstrap_components==ci-fixes +dash==3.0.0rc1 +dash_bootstrap_components==1.7.1-dev gunicorn markdown pandas diff --git a/src/components/accordion/__tests__/Accordion.test.js b/src/components/accordion/__tests__/Accordion.test.js index bd785ff0d..2e978e379 100644 --- a/src/components/accordion/__tests__/Accordion.test.js +++ b/src/components/accordion/__tests__/Accordion.test.js @@ -3,7 +3,7 @@ */ import React from 'react'; -import {render} from '@testing-library/react'; +import {act, render, screen, waitFor} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import Accordion from '../Accordion'; import AccordionItem from '../AccordionItem'; @@ -91,7 +91,8 @@ describe('Accordion', () => { ).not.toHaveClass('show'); }); - test('tracks most recently clicked item with "active_item" prop', () => { + test('tracks most recently clicked item with "active_item" prop', async () => { + const user = userEvent.setup({advanceTimers: jest.advanceTimersByTime}); const mockSetProps = jest.fn(); const {container, rerender} = render( @@ -109,9 +110,7 @@ describe('Accordion', () => { accordionItems.children[1].querySelector('div.accordion-collapse') ).not.toHaveClass('show'); - userEvent.click( - accordionItems.children[1].querySelector('h2.accordion-header > button') - ); + await user.click(await screen.findByText('item-title-2')); expect(mockSetProps.mock.calls).toHaveLength(1); rerender( @@ -120,7 +119,7 @@ describe('Accordion', () => { item-content-2 ); - jest.runAllTimers(); + act(() => jest.runAllTimers()); expect( accordionItems.children[0].querySelector('div.accordion-collapse') @@ -130,9 +129,7 @@ describe('Accordion', () => { ).toHaveClass('show'); // clicking on an open item closes it - userEvent.click( - accordionItems.children[1].querySelector('h2.accordion-header > button') - ); + await user.click(await screen.findByText('item-title-2')); expect(mockSetProps.mock.calls).toHaveLength(2); rerender( @@ -141,7 +138,7 @@ describe('Accordion', () => { item-content-2 ); - jest.runAllTimers(); + act(() => jest.runAllTimers()); expect( accordionItems.children[0].querySelector('div.accordion-collapse') @@ -151,7 +148,8 @@ describe('Accordion', () => { ).not.toHaveClass('show'); }); - test('keeps item open with "always_open" prop', () => { + test('keeps item open with "always_open" prop', async () => { + const user = userEvent.setup({advanceTimers: jest.advanceTimersByTime}); const mockSetProps = jest.fn(); const {container} = render( @@ -172,34 +170,31 @@ describe('Accordion', () => { ).not.toHaveClass('show'); // Click on the second item - userEvent.click( - accordionItems.children[1].querySelector('h2.accordion-header > button') - ); + await user.click(await screen.findByText('item-title-2')); expect(mockSetProps.mock.calls).toHaveLength(1); - // Allow the click to take effect - jest.runAllTimers(); + // wait for transition to complete + await waitFor(() => + expect( + accordionItems.children[1].querySelector('div.accordion-collapse') + ).toHaveClass('show') + ); // Check just the second item is open expect( accordionItems.children[0].querySelector('div.accordion-collapse') ).not.toHaveClass('show'); - expect( - accordionItems.children[1].querySelector('div.accordion-collapse') - ).toHaveClass('show'); // Click on the first item - userEvent.click( - accordionItems.children[0].querySelector('h2.accordion-header > button') - ); + await user.click(await screen.findByText('item-title-1')); expect(mockSetProps.mock.calls).toHaveLength(2); - // Allow the click to take effect - jest.runAllTimers(); // Check that the first child is now open, and the second remains open - expect( - accordionItems.children[0].querySelector('div.accordion-collapse') - ).toHaveClass('show'); + await waitFor(() => + expect( + accordionItems.children[0].querySelector('div.accordion-collapse') + ).toHaveClass('show') + ); expect( accordionItems.children[1].querySelector('div.accordion-collapse') ).toHaveClass('show'); @@ -224,7 +219,8 @@ describe('Accordion', () => { ).toHaveClass('show'); }); - test('tracks most recently clicked item with "active_item" prop when always_open', () => { + test('tracks most recently clicked item with "active_item" prop when always_open', async () => { + const user = userEvent.setup({advanceTimers: jest.advanceTimersByTime}); const mockSetProps = jest.fn(); const {container, rerender} = render( @@ -248,9 +244,7 @@ describe('Accordion', () => { ).not.toHaveClass('show'); // Click the middle option - userEvent.click( - accordionItems.children[1].querySelector('h2.accordion-header > button') - ); + await user.click(await screen.getByText('item-title-2')); expect(mockSetProps.mock.calls).toHaveLength(1); rerender( @@ -264,7 +258,7 @@ describe('Accordion', () => { item-content-3 ); - jest.runAllTimers(); + act(() => jest.runAllTimers()); // Check first option stayed open, middle option now open but third still // closed @@ -279,9 +273,7 @@ describe('Accordion', () => { ).not.toHaveClass('show'); // clicking on 1st item closes it, but keeps second item open and 3rd closed - userEvent.click( - accordionItems.children[0].querySelector('h2.accordion-header > button') - ); + await user.click(await screen.getByText('item-title-1')); expect(mockSetProps.mock.calls).toHaveLength(2); rerender( @@ -295,7 +287,7 @@ describe('Accordion', () => { item-content-3 ); - jest.runAllTimers(); + act(() => jest.runAllTimers()); // Check that 1 and 3 now closed, and 2 is open expect( diff --git a/src/components/badge/__tests__/Badge.test.js b/src/components/badge/__tests__/Badge.test.js index 3ddfa7a6b..6bb1ba1d5 100644 --- a/src/components/badge/__tests__/Badge.test.js +++ b/src/components/badge/__tests__/Badge.test.js @@ -56,19 +56,21 @@ describe('Badge', () => { expect(badge.getAttribute('href')).toBe(href); }); - test('tracks clicks with n_clicks', () => { + test('tracks clicks with n_clicks', async () => { + const user = userEvent.setup(); const mockSetProps = jest.fn(); const badge = render(Clickable); expect(mockSetProps.mock.calls).toHaveLength(0); - userEvent.click(badge.getByText('Clickable')); + await user.click(badge.getByText('Clickable')); expect(mockSetProps.mock.calls).toHaveLength(1); expect(mockSetProps.mock.calls[0][0].n_clicks).toBe(1); }); - test('relative links are internal by default', () => { + test('relative links are internal by default', async () => { + const user = userEvent.setup(); const badge = render(Clickable); const mockEventListener = jest.fn(); @@ -76,11 +78,12 @@ describe('Badge', () => { window.scrollTo = jest.fn(); expect(mockEventListener.mock.calls).toHaveLength(0); - userEvent.click(badge.getByText('Clickable')); + await user.click(badge.getByText('Clickable')); expect(mockEventListener.mock.calls).toHaveLength(1); }); - test('relative links are external with external_link=true', () => { + test('relative links are external with external_link=true', async () => { + const user = userEvent.setup(); const badge = render( Clickable @@ -92,7 +95,7 @@ describe('Badge', () => { window.scrollTo = jest.fn(); expect(mockEventListener.mock.calls).toHaveLength(0); - userEvent.click(badge.getByText('Clickable')); + await user.click(badge.getByText('Clickable')); expect(mockEventListener.mock.calls).toHaveLength(0); }); }); diff --git a/src/components/button/Button.js b/src/components/button/Button.js index cc052f76d..0993aad44 100644 --- a/src/components/button/Button.js +++ b/src/components/button/Button.js @@ -12,28 +12,26 @@ import Link from '../../private/Link'; * Use the `n_clicks` prop to trigger callbacks when the button has been * clicked. */ -const Button = props => { - const { - children, - disabled, - href, - loading_state, - setProps, - n_clicks, - target, - type, - download, - name, - value, - className, - class_name, - color, - outline, - onClick, - rel, - ...otherProps - } = props; - +const Button = ({ + children, + disabled, + href, + loading_state, + setProps, + target, + type, + download, + name, + value, + className, + class_name, + color, + outline, + onClick, + rel, + n_clicks = 0, + ...otherProps +}) => { const incrementClicks = () => { if (!disabled && setProps) { setProps({ @@ -73,11 +71,6 @@ const Button = props => { ); }; -Button.dashPersistence = { - n_clicks: 0, - n_clicks_timestamp: -1 -}; - Button.propTypes = { /** * The ID of this component, used to identify dash components diff --git a/src/components/button/__tests__/Button.test.js b/src/components/button/__tests__/Button.test.js index d7cd61e95..386b07374 100644 --- a/src/components/button/__tests__/Button.test.js +++ b/src/components/button/__tests__/Button.test.js @@ -84,19 +84,21 @@ describe('Button', () => { ); }); - test('tracks clicks with n_clicks', () => { + test('tracks clicks with n_clicks', async () => { + const user = userEvent.setup(); const mockSetProps = jest.fn(); const button = render(); expect(mockSetProps.mock.calls).toHaveLength(0); - userEvent.click(button.getByText('Clickable')); + await user.click(button.getByText('Clickable')); expect(mockSetProps.mock.calls).toHaveLength(1); expect(mockSetProps.mock.calls[0][0].n_clicks).toBe(1); }); - test("doesn't track clicks if disabled", () => { + test("doesn't track clicks if disabled", async () => { + const user = userEvent.setup(); const mockSetProps = jest.fn(); const button = render( ); const mockEventListener = jest.fn(); @@ -119,11 +122,12 @@ describe('Button', () => { window.scrollTo = jest.fn(); expect(mockEventListener.mock.calls).toHaveLength(0); - userEvent.click(button.getByText('Clickable')); + await user.click(button.getByText('Clickable')); expect(mockEventListener.mock.calls).toHaveLength(1); }); - test('relative links are external with external_link=true', () => { + test('relative links are external with external_link=true', async () => { + const user = userEvent.setup(); const button = render(