diff --git a/change/@fluentui-react-conformance-8f7a5ab5-539c-4cbb-be77-0b1e9e42a5fa.json b/change/@fluentui-react-conformance-8f7a5ab5-539c-4cbb-be77-0b1e9e42a5fa.json new file mode 100644 index 00000000000000..8c940e8753c761 --- /dev/null +++ b/change/@fluentui-react-conformance-8f7a5ab5-539c-4cbb-be77-0b1e9e42a5fa.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Clear document.body after each test to avoid interference between tests", + "packageName": "@fluentui/react-conformance", + "email": "elcraig@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-conformance/src/isConformant.ts b/packages/react-conformance/src/isConformant.ts index 734e1cb0090828..41abe227afbbd6 100644 --- a/packages/react-conformance/src/isConformant.ts +++ b/packages/react-conformance/src/isConformant.ts @@ -11,6 +11,11 @@ export function isConformant(...testInfo: Partial { + afterEach(() => { + // TODO: remove this once cleanup is properly implemented or after moving to testing-library + document.body.innerHTML = ''; + }); + if (!fs.existsSync(componentPath)) { throw new Error(`Path ${componentPath} does not exist`); } diff --git a/packages/react/src/components/Callout/Callout.test.tsx b/packages/react/src/components/Callout/Callout.test.tsx index d6721c2caffb58..d7f31077f2eb90 100644 --- a/packages/react/src/components/Callout/Callout.test.tsx +++ b/packages/react/src/components/Callout/Callout.test.tsx @@ -1,8 +1,5 @@ import * as React from 'react'; -import * as ReactDOM from 'react-dom'; -import * as ReactTestUtils from 'react-dom/test-utils'; -import { render } from '@testing-library/react'; -import { safeCreate } from '@fluentui/test-utilities'; +import { render, act } from '@testing-library/react'; import { isConformant } from '../../common/isConformant'; import { DirectionalHint } from '../../common/DirectionalHint'; import { resetIds } from '../../Utilities'; @@ -13,14 +10,10 @@ import { expectNoHiddenParents } from '../../common/testUtilities'; describe('Callout', () => { beforeEach(() => { - realDom = document.createElement('div'); - document.body.appendChild(realDom); resetIds(); }); afterEach(() => { - ReactDOM.unmountComponentAtNode(realDom); - document.body.removeChild(realDom); jest.useRealTimers(); jest.resetAllMocks(); }); @@ -29,8 +22,6 @@ describe('Callout', () => { resetIds(); }); - let realDom: HTMLDivElement; - isConformant({ Component: Callout, displayName: 'Callout', @@ -40,167 +31,116 @@ describe('Callout', () => { }); it('renders Callout correctly', () => { - safeCreate(Content, component => { - const tree = component.toJSON(); - expect(tree).toMatchSnapshot(); - }); + const { container } = render(Content); + expect(container).toMatchSnapshot(); }); - it('target id strings does not throw exception', () => { - let threwException = false; - try { - ReactTestUtils.renderIntoDocument( + it('does not throw with target id string', () => { + expect(() => { + render(
Content
, ); - } catch (e) { - threwException = true; - } - - expect(threwException).toEqual(false); + }).not.toThrow(); }); - it('target MouseEvents does not throw exception', () => { - const mouseEvent = document.createEvent('MouseEvent'); + it('does not throw with target MouseEvent', () => { const eventTarget = document.createElement('div'); - mouseEvent.initMouseEvent('click', false, false, window, 0, 0, 0, 0, 0, false, false, false, false, 1, eventTarget); - let threwException = false; - try { - ReactTestUtils.renderIntoDocument( + const mouseEvent = new MouseEvent('click', { relatedTarget: eventTarget }); + + expect(() => { + render(
- +
Content
, ); - } catch (e) { - threwException = true; - } - - expect(threwException).toEqual(false); + }).not.toThrow(); }); - it('target Elements does not throw exception', () => { + it('does not throw with target Element', () => { const targetElement = document.createElement('div'); document.body.appendChild(targetElement); - let threwException = false; - try { - ReactTestUtils.renderIntoDocument( + + expect(() => { + render(
Content
, ); - } catch (e) { - threwException = true; - } - - expect(threwException).toEqual(false); + }).not.toThrow(); }); - it('without target does not throw exception', () => { - let threwException = false; - try { - ReactTestUtils.renderIntoDocument( + it('does not throw without target', () => { + expect(() => { + render(
Content
, ); - } catch (e) { - threwException = true; - } - expect(threwException).toEqual(false); + }).not.toThrow(); }); it('passes event to onDismiss prop', () => { jest.useFakeTimers(); - let threwException = false; - let gotEvent = false; - const onDismiss = (ev?: unknown) => { - if (ev) { - gotEvent = true; - } - }; - - // In order to have eventlisteners that have been added to the window to be called the JSX needs - // to be rendered into the real dom rather than the testutil simulated dom. - - try { - ReactTestUtils.act(() => { - ReactDOM.render( -
- - - -
Content
-
-
, - realDom, - ); - }); - } catch (e) { - threwException = true; - } - expect(threwException).toEqual(false); + const onDismiss = jest.fn(); - ReactTestUtils.act(() => { - const focusTarget = document.querySelector('#focustarget') as HTMLButtonElement; + const { getByText } = render( +
+ + + +
Content
+
+
, + ); + act(() => { // Move focus jest.runAllTimers(); + }); - focusTarget.focus(); + getByText('button').focus(); - expect(gotEvent).toEqual(true); - }); + // ensure event is passed to callback + expect(onDismiss).toHaveBeenCalledWith(expect.objectContaining({ type: 'focus' })); }); it('prevents dismiss when preventDismissOnEvent is passed', () => { jest.useFakeTimers(); - let threwException = false; const onDismiss = jest.fn(); const preventAllDismiss = () => true; - try { - ReactTestUtils.act(() => { - ReactDOM.render( -
- - -
Content
-
-
, - realDom, - ); - }); - } catch (e) { - threwException = true; - } - expect(threwException).toEqual(false); - - ReactTestUtils.act(() => { - const focusTarget = document.querySelector('#focustarget') as HTMLButtonElement; + const { getByText, queryByText } = render( +
+ + +
Content
+
+
, + ); + act(() => { // Move focus jest.runAllTimers(); - - focusTarget.focus(); - - expect(onDismiss.mock.calls.length).toEqual(0); }); + getByText('button').focus(); + + expect(queryByText('Content')).toBeTruthy(); + expect(onDismiss).not.toHaveBeenCalled(); }); it('will correctly return focus to element that spawned it', () => { @@ -211,59 +151,40 @@ describe('Callout', () => { // Callout/popup checks active element to get what currently has focus // to know what to return focus to. By mocking the return value we can be sure // that it will have something "focused" when mounted - const b = jest.spyOn(window.document, 'activeElement', 'get'); - b.mockReturnValue(focusedElement as Element); - - let threwException = false; - let previousFocusElement; - let isFocused; - let restoreCalled = false; - const onRestoreFocus = (options: IPopupRestoreFocusParams) => { - previousFocusElement = options.originalElement; - isFocused = options.containsFocus; - restoreCalled = true; - }; - // In order to have eventlisteners that have been added to the window to be called the JSX needs - // to be rendered into the real dom rather than the testutil simulated dom. - try { - ReactTestUtils.act(() => { - ReactDOM.render( -
- - - {/* must be a button to be focusable for the test*/} - - -
, - realDom, - ); - }); - } catch (e) { - threwException = true; - } - expect(threwException).toEqual(false); - - ReactTestUtils.act(() => { - const focusTarget = document.querySelector('#inner') as HTMLDivElement; + jest.spyOn(window.document, 'activeElement', 'get').mockReturnValue(focusedElement as Element); + + const onRestoreFocus = jest.fn(); + + const { getByText, unmount } = render( +
+ + + {/* must be a button to be focusable for the test*/} + + +
, + ); + act(() => { jest.runAllTimers(); - // Make sure that focus is in the callout - focusTarget.focus(); }); - // Unmounting everything is the same as dismissing the Callout. As - // the tree is unmounted, popup will get unmounted first and the - // onRestoreFocus method will get called - ReactDOM.unmountComponentAtNode(realDom); + // Make sure that focus is in the callout + getByText('Content').focus(); - expect(restoreCalled).toEqual(true); - expect(isFocused).toEqual(true); + // Unmounting everything is the same as dismissing the Callout. + // As the tree is unmounted, popup will get unmounted first and onRestoreFocus will get called. + unmount(); - // Just to make sure that both elements are not undefined - expect(previousFocusElement).not.toBeFalsy(); - expect(previousFocusElement).toEqual(focusedElement); + expect(onRestoreFocus).toHaveBeenCalledTimes(1); + expect(onRestoreFocus).toHaveBeenLastCalledWith( + expect.objectContaining>({ + originalElement: focusedElement, + containsFocus: true, + }), + ); }); // This behavior could be changed in the future diff --git a/packages/react/src/components/Callout/__snapshots__/Callout.test.tsx.snap b/packages/react/src/components/Callout/__snapshots__/Callout.test.tsx.snap index 1dedf07c7231bb..ddd8e7fdb7e3ee 100644 --- a/packages/react/src/components/Callout/__snapshots__/Callout.test.tsx.snap +++ b/packages/react/src/components/Callout/__snapshots__/Callout.test.tsx.snap @@ -1,69 +1,56 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Callout renders Callout correctly 1`] = ` -
+
- Content +
+ Content +
diff --git a/packages/react/src/components/Dialog/Dialog.test.tsx b/packages/react/src/components/Dialog/Dialog.test.tsx index 1686478ee6730b..1d5b969ac78d7f 100644 --- a/packages/react/src/components/Dialog/Dialog.test.tsx +++ b/packages/react/src/components/Dialog/Dialog.test.tsx @@ -1,14 +1,11 @@ import * as React from 'react'; -import * as renderer from 'react-test-renderer'; -import { render } from '@testing-library/react'; +import { render, act } from '@testing-library/react'; -import { mount } from 'enzyme'; import { Dialog } from './Dialog'; import { DialogBase } from './Dialog.base'; import { DialogContent } from './DialogContent'; import { DialogType } from './DialogContent.types'; // for express fluent assertions import { resetIds, setWarningCallback } from '@fluentui/utilities'; -import { act } from 'react-dom/test-utils'; import { isConformant } from '../../common/isConformant'; import { expectNoHiddenParents } from '../../common/testUtilities'; @@ -17,22 +14,28 @@ describe('Dialog', () => { resetIds(); }); + afterEach(() => { + setWarningCallback(); + jest.useRealTimers(); + }); + afterAll(() => { resetIds(); }); - afterEach(() => { - jest.useRealTimers(); + isConformant({ + Component: Dialog, + displayName: 'Dialog', + disabledTests: ['component-handles-ref', 'component-has-root-ref', 'component-handles-classname'], }); - it('renders Dialog correctly', () => { - const component = renderer.create(); - const tree = component.toJSON(); - expect(tree).toMatchSnapshot(); + it('renders DialogContent correctly', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); }); - it('renders Dialog with a jsx title', () => { - const component = renderer.create( + it('respects a jsx title', () => { + const { queryByText } = render( { } />, ); - const tree = component.toJSON(); - expect(tree).toMatchSnapshot(); + + expect(queryByText('I am span 1')).toBeTruthy(); + expect(queryByText('I am span 2')).toBeTruthy(); }); - it('renders DialogContent with titleProps', () => { - const component = renderer.create( + it('respects titleProps', () => { + const { getByTitle } = render( { }} />, ); - const tree = component.toJSON(); - expect(tree).toMatchSnapshot(); - }); - isConformant({ - Component: Dialog, - displayName: 'Dialog', - disabledTests: ['component-handles-ref', 'component-has-root-ref', 'component-handles-classname'], + const title = getByTitle('tooltip'); + expect(title.getAttribute('aria-level')).toBe('3'); + expect(title.className).toMatch(/\btitle_class\b/); }); it('Fires dismissed after closing', () => { - act(() => { - jest.useFakeTimers(); - }); - let dismissedCalled = false; + jest.useFakeTimers(); + const onDismissed = jest.fn(); - const handleDismissed = () => { - dismissedCalled = true; - }; + const { queryByRole, rerender } = render(