UNPKG

react-tabs

Version:

An accessible and easy tab component for ReactJS

572 lines (496 loc) 14.9 kB
/* eslint-env jest */ /* eslint-disable react/no-multi-comp */ import React from 'react'; import Enzyme, { shallow, mount } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import renderer from 'react-test-renderer'; import Tab from '../Tab'; import TabList from '../TabList'; import TabPanel from '../TabPanel'; import Tabs from '../Tabs'; import { reset as resetIdCounter } from '../../helpers/uuid'; import { TabListWrapper, TabWrapper, TabPanelWrapper, } from './helpers/higherOrder'; Enzyme.configure({ adapter: new Adapter() }); function expectToMatchSnapshot(component) { expect(renderer.create(component).toJSON()).toMatchSnapshot(); } function createTabs(props = {}) { return ( <Tabs {...props}> <TabList> <Tab>Foo</Tab> <Tab>Bar</Tab> <Tab> <a href="a">Baz</a> </Tab> <Tab disabled>Qux</Tab> </TabList> <TabPanel>Hello Foo</TabPanel> <TabPanel>Hello Bar</TabPanel> <TabPanel>Hello Baz</TabPanel> <TabPanel>Hello Qux</TabPanel> </Tabs> ); } function assertTabSelected(wrapper, index) { const tab = wrapper.find(Tab).at(index); const panel = wrapper.find(TabPanel).at(index); expect(tab.prop('selected')).toBe(true); expect(panel.prop('selected')).toBe(true); } describe('<Tabs />', () => { beforeEach(() => resetIdCounter()); beforeAll(() => { // eslint-disable-next-line no-console console.error = error => { throw new Error(error); }; }); describe('props', () => { test('should have sane defaults', () => { expectToMatchSnapshot(createTabs()); }); test('should honor positive defaultIndex prop', () => { expectToMatchSnapshot(createTabs({ defaultIndex: 1 })); }); test('should honor negative defaultIndex prop', () => { expectToMatchSnapshot(createTabs({ defaultIndex: -1 })); }); test('should call onSelect when selection changes', () => { const called = { index: -1, last: -1 }; const wrapper = mount( createTabs({ onSelect(index, last) { called.index = index; called.last = last; }, }), ); wrapper .find(Tab) .at(1) .simulate('click'); expect(called.index).toBe(1); expect(called.last).toBe(0); }); test('should accept className', () => { expectToMatchSnapshot(createTabs({ className: 'foobar' })); }); test('should accept domRef', () => { let domNode; mount( createTabs({ domRef: node => { domNode = node; }, }), ); expect(domNode).not.toBeUndefined(); expect(domNode.className).toBe('react-tabs'); }); }); describe('child props', () => { test('should reset ids correctly', () => { expectToMatchSnapshot(createTabs()); resetIdCounter(); expectToMatchSnapshot(createTabs()); }); }); describe('interaction', () => { test('should update selectedIndex when clicked', () => { const wrapper = mount(createTabs()); wrapper .find(Tab) .at(1) .simulate('click'); assertTabSelected(wrapper, 1); }); test('should update selectedIndex when tab child is clicked', () => { const wrapper = mount(createTabs()); wrapper .find(Tab) .at(2) .childAt(0) .simulate('click'); assertTabSelected(wrapper, 2); }); test('should not change selectedIndex when clicking a disabled tab', () => { const wrapper = mount(createTabs({ defaultIndex: 0 })); wrapper .find(Tab) .at(3) .simulate('click'); assertTabSelected(wrapper, 0); }); }); describe('performance', () => { test('should only render the selected tab panel', () => { const wrapper = mount(createTabs()); const tabPanels = wrapper.find(TabPanel); expect(tabPanels.at(0).text()).toBe('Hello Foo'); expect(tabPanels.at(1).text()).toBe(''); expect(tabPanels.at(2).text()).toBe(''); wrapper .find(Tab) .at(1) .simulate('click'); expect(tabPanels.at(0).text()).toBe(''); expect(tabPanels.at(1).text()).toBe('Hello Bar'); expect(tabPanels.at(2).text()).toBe(''); wrapper .find(Tab) .at(2) .simulate('click'); expect(tabPanels.at(0).text()).toBe(''); expect(tabPanels.at(1).text()).toBe(''); expect(tabPanels.at(2).text()).toBe('Hello Baz'); }); test('should render all tabs if forceRenderTabPanel is true', () => { expectToMatchSnapshot(createTabs({ forceRenderTabPanel: true })); }); }); describe('validation', () => { test('should result with warning when tabs/panels are imbalanced', () => { const oldConsoleError = console.error; // eslint-disable-line no-console console.error = () => {}; // eslint-disable-line no-console const wrapper = shallow( <Tabs> <TabList> <Tab>Foo</Tab> </TabList> </Tabs>, ); console.error = oldConsoleError; // eslint-disable-line no-console // eslint-disable-next-line react/forbid-foreign-prop-types const result = Tabs.propTypes.children( wrapper.props(), 'children', 'Tabs', ); expect(result).toBeInstanceOf(Error); }); test('should result with warning when tab outside of tablist', () => { const oldConsoleError = console.error; // eslint-disable-line no-console console.error = () => {}; // eslint-disable-line no-console const wrapper = shallow( <Tabs> <TabList> <Tab>Foo</Tab> </TabList> <Tab>Foo</Tab> <TabPanel /> <TabPanel /> </Tabs>, ); console.error = oldConsoleError; // eslint-disable-line no-console // eslint-disable-next-line react/forbid-foreign-prop-types const result = Tabs.propTypes.children( wrapper.props(), 'children', 'Tabs', ); expect(result).toBeInstanceOf(Error); }); test('should result with warning when multiple tablist components exist', () => { const oldConsoleError = console.error; // eslint-disable-line no-console console.error = () => {}; // eslint-disable-line no-console const wrapper = shallow( <Tabs> <TabList> <Tab>Foo</Tab> </TabList> <TabList> <Tab>Foo</Tab> </TabList> <TabPanel /> <TabPanel /> </Tabs>, ); console.error = oldConsoleError; // eslint-disable-line no-console // eslint-disable-next-line react/forbid-foreign-prop-types const result = Tabs.propTypes.children( wrapper.props(), 'children', 'Tabs', ); expect(result).toBeInstanceOf(Error); }); test('should result with warning when onSelect missing when selectedIndex set', () => { const oldConsoleError = console.error; // eslint-disable-line no-console let catchedError; // eslint-disable-next-line no-console console.error = error => { catchedError = error; }; shallow( <Tabs selectedIndex={1}> <TabList> <Tab>Foo</Tab> </TabList> <TabPanel>Foo</TabPanel> </Tabs>, ); console.error = oldConsoleError; // eslint-disable-line no-console const expectedMessage = 'The prop `onSelect` is marked as required in `Tabs`, but its value is `undefined` or `null`.'; expect(catchedError).toMatch(expectedMessage); }); test('should result with warning when defaultIndex and selectedIndex set', () => { const oldConsoleError = console.error; // eslint-disable-line no-console let catchedError; // eslint-disable-next-line no-console console.error = error => { catchedError = error; }; shallow( <Tabs selectedIndex={1} defaultIndex={1}> <TabList> <Tab>Foo</Tab> </TabList> <TabPanel>Foo</TabPanel> </Tabs>, ); console.error = oldConsoleError; // eslint-disable-line no-console const expectedMessage = 'The prop `selectedIndex` cannot be used together with `defaultIndex` in `Tabs`.'; expect(catchedError).toMatch(expectedMessage); }); test('should result with warning when tabs/panels are imbalanced and it should ignore non tab children', () => { const oldConsoleError = console.error; // eslint-disable-line no-console console.error = () => {}; // eslint-disable-line no-console const wrapper = shallow( <Tabs> <TabList> <Tab>Foo</Tab> <div>+</div> </TabList> <TabPanel>Hello Foo</TabPanel> <TabPanel>Hello Bar</TabPanel> </Tabs>, ); console.error = oldConsoleError; // eslint-disable-line no-console // eslint-disable-next-line react/forbid-foreign-prop-types const result = Tabs.propTypes.children( wrapper.props(), 'children', 'Tabs', ); expect(result).toBeInstanceOf(Error); }); test('should allow random order for elements', () => { expectToMatchSnapshot( <Tabs forceRenderTabPanel> <TabPanel>Hello Foo</TabPanel> <TabList> <Tab>Foo</Tab> <Tab>Bar</Tab> </TabList> <TabPanel>Hello Bar</TabPanel> </Tabs>, ); }); test('should not throw a warning when wrong element is found', () => { const wrapper = shallow( <Tabs> <TabList> <Tab /> <div /> </TabList> <TabPanel /> </Tabs>, ); // eslint-disable-next-line react/forbid-foreign-prop-types const result = Tabs.propTypes.children( wrapper.props(), 'children', 'Tabs', ); expect(result instanceof Error).toBe(false); }); test('should be okay with rendering without any children', () => { expect(() => shallow(<Tabs />)).not.toThrow(); }); test('should be okay with rendering just TabList', () => { expect(() => shallow( <Tabs> <TabList /> </Tabs>, ), ).not.toThrow(); }); test('should gracefully render null', () => { expect(() => shallow( <Tabs> <TabList> <Tab>Tab A</Tab> {false && <Tab>Tab B</Tab>} </TabList> <TabPanel>Content A</TabPanel> {false && <TabPanel>Content B</TabPanel>} </Tabs>, ), ).not.toThrow(); }); test('should support nested tabs', () => { const wrapper = mount( <Tabs className="first"> <TabList> <Tab /> <Tab /> </TabList> <TabPanel> <Tabs className="second"> <TabList> <Tab /> <Tab /> </TabList> <TabPanel /> <TabPanel /> </Tabs> </TabPanel> <TabPanel /> </Tabs>, ); wrapper .find('Tabs.second') .find(Tab) .at(1) .simulate('click'); assertTabSelected(wrapper, 0); assertTabSelected(wrapper.find('Tabs.second'), 1); }); test('should allow other DOM nodes', () => { expectToMatchSnapshot( <Tabs> <div id="tabs-nav-wrapper"> <button type="button">Left</button> <div className="tabs-container"> <TabList> <Tab /> <Tab /> </TabList> </div> <button type="button">Right</button> </div> <div className="tab-panels"> <TabPanel /> <TabPanel /> </div> </Tabs>, ); }); }); test('should pass through custom properties', () => { expectToMatchSnapshot(<Tabs data-tooltip="Tooltip contents" />); }); test('should not add known props to dom', () => { expectToMatchSnapshot(<Tabs defaultIndex={3} />); }); test('should cancel if event handler returns false', () => { const wrapper = mount(createTabs({ onSelect: () => false })); assertTabSelected(wrapper, 0); wrapper .find(Tab) .at(1) .simulate('click'); assertTabSelected(wrapper, 0); wrapper .find(Tab) .at(2) .simulate('click'); assertTabSelected(wrapper, 0); }); test('should trigger onSelect handler when clicking', () => { let wasClicked = false; const wrapper = mount( createTabs({ onSelect: () => { wasClicked = true; }, }), ); assertTabSelected(wrapper, 0); wrapper .find(Tab) .at(1) .simulate('click'); assertTabSelected(wrapper, 1); expect(wasClicked).toBe(true); }); test('should trigger onSelect handler when clicking on open tab', () => { let wasClicked = false; const wrapper = mount( createTabs({ onSelect: () => { wasClicked = true; }, }), ); assertTabSelected(wrapper, 0); wrapper .find(Tab) .at(0) .simulate('click'); assertTabSelected(wrapper, 0); expect(wasClicked).toBe(true); }); test('should switch tabs if setState is called within onSelect', () => { class Wrap extends React.Component { state = {}; handleSelect = () => this.setState({ foo: 'bar' }); render() { const { foo } = this.state; return createTabs({ onSelect: this.handleSelect, className: foo, }); } } const wrapper = mount(<Wrap />); wrapper .find(Tab) .at(1) .simulate('click'); assertTabSelected(wrapper, 1); wrapper .find(Tab) .at(2) .simulate('click'); assertTabSelected(wrapper, 2); }); test('should allow for higher order components', () => { expectToMatchSnapshot( <Tabs> <TabListWrapper> <TabWrapper>Foo</TabWrapper> <TabWrapper>Bar</TabWrapper> </TabListWrapper> <TabPanelWrapper>Foo</TabPanelWrapper> <TabPanelWrapper>Bar</TabPanelWrapper> </Tabs>, ); }); test('should allow string children', () => { expectToMatchSnapshot( <Tabs> Foo <TabList> Foo <Tab>Foo</Tab> Foo <Tab>Bar</Tab> Foo </TabList> <TabPanel>Bar</TabPanel> <TabPanel>Foo</TabPanel> Foo </Tabs>, ); }); });