Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Callout, Dialog, Modal: convert tests to use testing-library #21664

Merged
merged 4 commits into from
Feb 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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