Skip to content

Commit

Permalink
Callout, Dialog, Modal: convert tests to use testing-library (#21664)
Browse files Browse the repository at this point in the history
  • Loading branch information
ecraig12345 authored Feb 10, 2022
1 parent e07cc82 commit c5e6c54
Show file tree
Hide file tree
Showing 8 changed files with 720 additions and 1,240 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Clear document.body after each test to avoid interference between tests",
"packageName": "@fluentui/react-conformance",
"email": "[email protected]",
"dependentChangeType": "patch"
}
5 changes: 5 additions & 0 deletions packages/react-conformance/src/isConformant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export function isConformant<TProps = {}>(...testInfo: Partial<IsConformantOptio
const { componentPath, displayName, disabledTests = [], extraTests, tsconfigDir } = mergedOptions;

describe('isConformant', () => {
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`);
}
Expand Down
243 changes: 82 additions & 161 deletions packages/react/src/components/Callout/Callout.test.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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();
});
Expand All @@ -29,8 +22,6 @@ describe('Callout', () => {
resetIds();
});

let realDom: HTMLDivElement;

isConformant({
Component: Callout,
displayName: 'Callout',
Expand All @@ -40,167 +31,116 @@ describe('Callout', () => {
});

it('renders Callout correctly', () => {
safeCreate(<CalloutContent>Content</CalloutContent>, component => {
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
const { container } = render(<CalloutContent>Content</CalloutContent>);
expect(container).toMatchSnapshot();
});

it('target id strings does not throw exception', () => {
let threwException = false;
try {
ReactTestUtils.renderIntoDocument<HTMLDivElement>(
it('does not throw with target id string', () => {
expect(() => {
render(
<div>
<button id="target" style={{ top: '10px', left: '10px', height: '0', width: '0px' }}>
{' '}
target{' '}
target
</button>
<Callout target="#target" directionalHint={DirectionalHint.topLeftEdge}>
<div>Content</div>
</Callout>
</div>,
);
} 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<HTMLDivElement>(
const mouseEvent = new MouseEvent('click', { relatedTarget: eventTarget });

expect(() => {
render(
<div>
<Callout target={eventTarget} directionalHint={DirectionalHint.topLeftEdge}>
<Callout target={mouseEvent} directionalHint={DirectionalHint.topLeftEdge}>
<div>Content</div>
</Callout>
</div>,
);
} 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<HTMLDivElement>(

expect(() => {
render(
<div>
<Callout target={targetElement} directionalHint={DirectionalHint.topLeftEdge}>
<div>Content</div>
</Callout>
</div>,
);
} catch (e) {
threwException = true;
}

expect(threwException).toEqual(false);
}).not.toThrow();
});

it('without target does not throw exception', () => {
let threwException = false;
try {
ReactTestUtils.renderIntoDocument<HTMLDivElement>(
it('does not throw without target', () => {
expect(() => {
render(
<div>
<Callout directionalHint={DirectionalHint.topLeftEdge}>
<div>Content</div>
</Callout>
</div>,
);
} 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<HTMLDivElement>(
<div>
<button id="focustarget"> button </button>
<button id="target" style={{ top: '10px', left: '10px', height: '0', width: '0px' }}>
{' '}
target{' '}
</button>
<Callout target="#target" directionalHint={DirectionalHint.topLeftEdge} onDismiss={onDismiss}>
<div>Content</div>
</Callout>
</div>,
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(
<div>
<button>button</button>
<button id="target" style={{ top: '10px', left: '10px', height: '0', width: '0px' }}>
target
</button>
<Callout target="#target" directionalHint={DirectionalHint.topLeftEdge} onDismiss={onDismiss}>
<div>Content</div>
</Callout>
</div>,
);

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<HTMLDivElement>(
<div>
<button id="focustarget"> button </button>
<Callout target="#target" preventDismissOnEvent={preventAllDismiss} onDismiss={onDismiss}>
<div>Content</div>
</Callout>
</div>,
realDom,
);
});
} catch (e) {
threwException = true;
}
expect(threwException).toEqual(false);

ReactTestUtils.act(() => {
const focusTarget = document.querySelector('#focustarget') as HTMLButtonElement;
const { getByText, queryByText } = render(
<div>
<button>button</button>
<Callout target="#target" preventDismissOnEvent={preventAllDismiss} onDismiss={onDismiss}>
<div>Content</div>
</Callout>
</div>,
);

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', () => {
Expand All @@ -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<HTMLDivElement>(
<div>
<button id="target" style={{ top: '10px', left: '10px', height: '0', width: '0px' }}>
target
</button>
<Callout target="#target" directionalHint={DirectionalHint.topLeftEdge} onRestoreFocus={onRestoreFocus}>
{/* must be a button to be focusable for the test*/}
<button id={'inner'}>Content</button>
</Callout>
</div>,
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(
<div>
<button id="target" style={{ top: '10px', left: '10px', height: '0', width: '0px' }}>
target
</button>
<Callout target="#target" directionalHint={DirectionalHint.topLeftEdge} onRestoreFocus={onRestoreFocus}>
{/* must be a button to be focusable for the test*/}
<button id="inner">Content</button>
</Callout>
</div>,
);

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<Partial<IPopupRestoreFocusParams>>({
originalElement: focusedElement,
containsFocus: true,
}),
);
});

// This behavior could be changed in the future
Expand Down
Loading

0 comments on commit c5e6c54

Please sign in to comment.