lucid-ui
Version:
A UI component library from Xandr.
228 lines • 12.5 kB
JavaScript
import sinon from 'sinon';
import { parse } from 'path';
import React from 'react';
import PropTypes from 'prop-types';
import { mount, shallow, render } from 'enzyme';
import assert from 'assert';
import _, { each, omit, keys, includes, forEach } from 'lodash';
import glob from 'glob';
import { addons, mockChannel } from '@storybook/addons';
import timekeeper from 'timekeeper';
addons.setChannel(mockChannel());
// Common tests for all our components
export function common(Component, config = {}) {
const { getDefaultProps = _.constant({}), exemptFunctionProps = [], exemptChildComponents = [], selectRoot = _.identity, noExport = false, } = config;
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, { ...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, { ...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, { ...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, { ...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}`, () => {
let result;
try {
result = render(React.createElement(Story, { ...Story.args }), {
disableLifecycleMethods: true,
});
expect(result).toMatchSnapshot();
}
catch (err) {
expect(err).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, { ...shallowExample.props() }), {
disableLifecycleMethods: true,
})).toMatchSnapshot();
}
else {
shallowExample.find(Component.displayName).forEach((example) => {
expect(shallow(React.createElement(Component, { ...example.props() }), {
disableLifecycleMethods: true,
})).toMatchSnapshot();
});
}
});
});
});
});
}
export function icons(Component, config = {}) {
// The default expectation is for every Icon to omit `initailState`,
// if it is passed through to the underlying element
const { includeInitialState = false } = config;
describe('[icon]', () => {
it('should almost always omit the `initialState` key', () => {
const wrapper = shallow(React.createElement(Component, { initialState: { testState: true } }));
const rootProps = keys(wrapper.first().props());
expect(includes(rootProps, 'initialState')).toBe(includeInitialState);
});
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`);
});
});
}
export function passThroughs(Component, config = {}) {
// The default expectation is for every Component to omit `initialState`,
// if it is passed through to the underlying element
const { includeInitialState = false } = config;
describe('pass throughs', () => {
it('should almost always omit the `initialState` key', () => {
const wrapper = shallow(React.createElement(Component, { initialState: { testState: true } }));
const rootElementProps = keys(wrapper.first().props());
expect(includes(rootElementProps, 'initialState')).toBe(includeInitialState);
});
it('should pass through all props not defined in `propTypes` to the root element', () => {
const wrapper = shallow(React.createElement(Component, { ...{
foo: 1,
bar: 2,
baz: 3,
qux: 4,
quux: 5,
} }));
const rootElementProps = keys(wrapper.first().props());
// It should pass `foo`, `bar`, `baz`, `qux`, and `quux`
// to the root element.
forEach(['foo', 'bar', 'baz', 'qux', 'quux'], (prop) => {
expect(includes(rootElementProps, prop)).toBe(true);
});
});
});
}
// 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, { ...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);
});
});
}
export const mockDate = (dateString) => {
timekeeper.freeze(new Date(dateString));
};
//# sourceMappingURL=generic-tests.js.map