UNPKG

lucid-ui

Version:

A UI component library from AppNexus.

206 lines 11 kB
import sinon from 'sinon'; import { parse } from 'path'; import React from 'react'; import PropTypes from 'prop-types'; import { mount, shallow } from 'enzyme'; import assert from 'assert'; import _, { each, omit } from 'lodash'; import glob from 'glob'; import * as lucid from '../index'; // Common tests for all our components export function common(Component, { getDefaultProps = _.constant({}), exemptFunctionProps = [], exemptChildComponents = [], selectRoot = _.identity, noExport = false, } = {}) { function generateDefaultProps(props = {}) { return _.assign({}, getDefaultProps(), props); } describe('[common]', () => { if (!Component) { throw new Error('An undefined component was passed to generic tests.'); } if (Component._isLucidHybridComponent) { throw new Error(`You're trying to run generic tests on a hybrid component which is bad and won't work and will make you cry. Check your spec files for ${Component.displayName} and import the raw component instead of the hybrid version.`); } it('should have a `displayName` defined', () => { assert(Component.displayName); }); it('should pass through styles to the root element', () => { const style = { backgroundColor: '#f0f', }; const wrapper = shallow(React.createElement(Component, Object.assign({}, generateDefaultProps(), { style: style })), { disableLifecycleMethods: true }); const rootWrapper = selectRoot(wrapper).first(); const rootStyle = rootWrapper.prop('style'); assert(_.every(style, (val, key) => val === rootStyle[key]), 'root style must contain passed styles'); }); it('should pass through `className`', () => { const expectedClass = 'rAnDoM'; const wrapper = shallow(React.createElement(Component, Object.assign({}, generateDefaultProps(), { className: expectedClass })), { disableLifecycleMethods: true }); const rootWrapper = selectRoot(wrapper).first(); const classNames = rootWrapper.prop('className').split(' '); assert(_.includes(classNames, expectedClass), `'${classNames}' should include '${expectedClass}'`); }); it('should have an application scoped base class', () => { const expectedClass = 'lucid-' + Component.displayName; const wrapper = shallow(React.createElement(Component, Object.assign({}, generateDefaultProps())), { disableLifecycleMethods: true, }); const rootWrapper = selectRoot(wrapper).first(); const classNames = rootWrapper.prop('className').split(' '); assert(_.includes(classNames, expectedClass), `'${classNames}' should include '${Component.displayName}'`); }); it('should have only application scoped classes', () => { const wrapper = shallow(React.createElement(Component, Object.assign({}, generateDefaultProps())), { disableLifecycleMethods: true, }); const rootWrapper = selectRoot(wrapper).first(); const parentClasses = rootWrapper.prop('className').split(' '); const childrenClasses = rootWrapper.children().reduce((acc, node) => { if (!node.prop('className')) { return acc; } return acc.concat(node.prop('className').split(' ')); }, []); const allClasses = parentClasses.concat(childrenClasses); _.forEach(allClasses, (className) => { assert(_.includes(className, `lucid-${Component.displayName}`), `${className} must be scoped`); }); }); describe('function propTypes', () => { const funcProps = _.pickBy(Component.propTypes, (propType) => propType === PropTypes.func); _.forEach(funcProps, (propType, propName) => { it(`${propName} should only use onX convention for function proptypes`, () => { assert(_.startsWith(propName, 'on') || _.includes(exemptFunctionProps, propName), `${propName} must follow onX convention`); }); }); }); describe('child components', () => { // Child components are all function types which start with a capital letter const childComponents = _.pickBy(Component, (value, key) => { return /^[A-Z]/.test(key) && _.isFunction(value); }); describe('propNames in propTypes', () => { _.flow((x) => _.map(x, 'propName'), (x) => _.compact(x), (x) => _.flatMap(x, _.castArray), (x) => _.reject(x, (propName) => _.includes(exemptChildComponents, propName)), (x) => _.forEach(x, (propName) => { it(`should include ${propName} in propTypes`, () => { assert(Component.propTypes[propName], `must include ${propName} in propTypes`); }); }))(childComponents); }); }); describe('example testing', () => { const fileNames = glob.sync(`./src/components/**/${Component.displayName}/*.stories.@(j|t)sx`); each(fileNames, (path) => { const lib = require('../../' + path.replace('.tsx', '')); each(omit(lib, ['default']), (Story, name) => { it(`should match snapshot(s) for ${name}`, () => { expect(shallow(React.createElement(Story, null), { disableLifecycleMethods: true, })).toMatchSnapshot(); }); }); }); // Support for older examples const exampleFileNames = glob.sync(`./src/components/**/${Component.displayName}/examples/*.@(j|t)sx`); _.each(exampleFileNames, (path) => { const lib = require('../../' + path.replace('.tsx', '')); const Example = lib.default; const title = parse(path).name; it(`should match snapshot(s) for ${title}`, () => { const shallowExample = shallow(React.createElement(Example, null), { disableLifecycleMethods: true, }); // If the root of the example is an instance of the Component under test, snapshot it. // Otherwise, look under the root for instances of the Component and snapshot those. if (shallowExample.is(Component.displayName)) { expect(shallow(React.createElement(Component, Object.assign({}, shallowExample.props())), { disableLifecycleMethods: true, })).toMatchSnapshot(); } else { shallowExample.find(Component.displayName).forEach((example) => { expect(shallow(React.createElement(Component, Object.assign({}, example.props())), { disableLifecycleMethods: true, })).toMatchSnapshot(); }); } }); }); }); // Only run this test if it's a public component if (!Component._isPrivate && !noExport) { it('should be available as an exported module from index.ts', () => { assert(lucid[Component.displayName]); }); } }); } // Common tests for all our icon components export function icons(Component) { describe('[icon]', () => { it('should add the correct class for isClickable', () => { const wrapper = mount(React.createElement(Component, { isClickable: true })); const targetClassName = 'lucid-Icon-is-clickable'; assert(wrapper.find('svg').hasClass(targetClassName), `Missing '${targetClassName}' class`); }); }); } // Common tests for all control components export function controls(Component, { callbackName, controlSelector, eventType, additionalProps = {} }) { // Use DOM tests here since some of our controls use dom events under the hood describe('[control]', () => { it('should callback with `event` and `props`', () => { const expectedSpecialProp = 32; const props = { specialProp: expectedSpecialProp, [callbackName]: sinon.spy(), ...additionalProps, }; const wrapper = mount(React.createElement(Component, Object.assign({}, props))); wrapper.find(controlSelector).first().simulate(eventType); // Last argument should be an object with `uniqueId` and `event` const { props: { specialProp }, event, } = _.last(props[callbackName].args[0]); assert(event, 'missing event'); assert.equal(specialProp, expectedSpecialProp, 'incorrect or missing specialProp'); }); }); } // Common tests for all Functional Components // // These tests are intended to help us make sure our FCs are shaped corrected. // They are necessary because there isn't a perfect way to get the defaultProps // to be factored in correctly yet with React/TypeScript: // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30695#issuecomment-474780159 export function functionalComponents(FC) { // Use DOM tests here since some of our controls use dom events under the hood describe('[functionalComponent]', () => { it('should have the correct `peek` properties', () => { expect(FC.propName === undefined || typeof FC.propName === 'string').toBe(true); expect(FC._isPrivate === undefined || typeof FC._isPrivate === 'boolean').toBe(true); expect(typeof FC.peek).toBe('object'); expect(typeof FC.peek.description).toBe('string'); expect(FC.peek.extend === undefined || typeof FC.peek.extend === 'string').toBe(true); expect(FC.peek.extend === undefined || typeof FC.peek.extend === 'string').toBe(true); expect(FC.peek.categories === undefined || Array.isArray(FC.peek.categories)).toBe(true); expect(FC.peek.madeFrom === undefined || Array.isArray(FC.peek.madeFrom)).toBe(true); }); }); } const NativeDate = global.Date; const createMockDateClass = (...args) => _.assign(function MockDate() { // @ts-ignore return new NativeDate(...args); }, { UTC: NativeDate.UTC, parse: NativeDate.parse, // @ts-ignore now: () => new NativeDate(...args).getTime(), prototype: NativeDate.prototype, }); export const mockDate = _.assign((...args) => { // @ts-ignore global.Date = createMockDateClass(...args); }, { restore() { global.Date = NativeDate; }, }); //# sourceMappingURL=generic-tests.js.map