@instructure/ui-test-utils
Version:
A UI testing library made by Instructure Inc.
225 lines (221 loc) • 7.71 kB
JavaScript
import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray";
/*
* The MIT License (MIT)
*
* Copyright (c) 2015 - present Instructure, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { nanoid } from 'nanoid';
import { generatePropCombinations } from './generatePropCombinations';
import React from 'react';
/**
* Generates examples for the given component based on the given configuration.
* @param Component A React component
* @param config A configuration object (stored in xy.examples.jsx files in InstUI)
* @returns Array of examples broken into sections and pages if configured to do so.
* @module generateComponentExamples
* @private
*
*/
export function generateComponentExamples(Component, config) {
const sectionProp = config.sectionProp,
excludeProps = config.excludeProps,
filter = config.filter;
const PROPS_CACHE = [];
const sections = [];
const maxExamples = config.maxExamples ? config.maxExamples : 500;
let exampleCount = 0;
let propValues = {};
const getParameters = page => {
const examples = page.examples;
const index = page.index;
let parameters = {};
if (typeof config.getParameters === 'function') {
parameters = {
...config.getParameters({
examples,
index
})
};
}
return parameters;
};
/**
* Merges the auto-generated props with ones in the examples files specified
* by the `getComponentProps()` method; props from the example files have
* priority
*/
const mergeComponentPropsFromConfig = props => {
let componentProps = props;
// TODO this code is so complicated because getComponentProps(props) can return
// different values based on its props parameter.
// If it would always return the same thing then we could reduce the
// number of combinations generated by generatePropCombinations() by
// getComponentProps() reducing some to 1 value, it would also remove the
// need of PROPS_CACHE and duplicate checks.
// InstUI is not using the 'props' param of getComponentProps(), but others are
if (typeof config.getComponentProps === 'function') {
componentProps = {
...componentProps,
...config.getComponentProps(props)
};
}
return componentProps;
};
const getExampleProps = props => {
let exampleProps = {};
if (typeof config.getExampleProps === 'function') {
exampleProps = {
...config.getExampleProps(props)
};
}
return exampleProps;
};
const addPage = section => {
const page = {
examples: [],
index: section.pages.length
};
section.pages.push(page);
return page;
};
const addExample = (sectionName, example) => {
let section = sections.find(section => section.sectionName === sectionName);
if (!section) {
section = {
sectionName: sectionName,
propName: sectionProp,
propValue: sectionName,
pages: []
};
sections.push(section);
}
let page = section.pages[section.pages.length - 1];
let maxExamplesPerPage = config.maxExamplesPerPage;
if (typeof maxExamplesPerPage === 'function') {
maxExamplesPerPage = maxExamplesPerPage(sectionName);
}
if (!page) {
page = addPage(section);
} else if (maxExamplesPerPage && page.examples.length % maxExamplesPerPage === 0 && page.examples.length > 0) {
page = addPage(section);
}
page.examples.push(example);
};
// Serializes the given recursively, faster than JSON.stringify()
const fastSerialize = props => {
const strArr = [];
objToString(props, strArr);
return strArr.join('');
};
const objToString = (currObject, currString) => {
if (!currObject) {
return;
}
if (/*#__PURE__*/React.isValidElement(currObject)) {
currString.push(JSON.stringify(currObject));
} else if (typeof currObject === 'object') {
for (const _ref of Object.entries(currObject)) {
var _ref2 = _slicedToArray(_ref, 2);
const key = _ref2[0];
const value = _ref2[1];
currString.push(key);
objToString(value, currString);
}
} else {
currString.push(currObject);
}
};
const maybeAddExample = props => {
const componentProps = mergeComponentPropsFromConfig(props);
const ignore = typeof filter === 'function' ? filter(componentProps) : false;
if (ignore) {
return;
}
const propsString = fastSerialize(componentProps);
if (!PROPS_CACHE.includes(propsString)) {
const key = nanoid();
const exampleProps = getExampleProps(props);
exampleCount++;
if (exampleCount < maxExamples) {
PROPS_CACHE.push(propsString);
let sectionName = 'Examples';
if (sectionProp && componentProps[sectionProp]) {
sectionName = componentProps[sectionProp];
}
addExample(sectionName, {
Component,
componentProps,
exampleProps,
key
});
}
}
};
if (isEmpty(config.propValues)) {
maybeAddExample({});
} else {
if (Array.isArray(excludeProps)) {
;
Object.keys(config.propValues).forEach(propName => {
if (!excludeProps.includes(propName)) {
propValues[propName] = config.propValues[propName];
}
});
} else {
propValues = config.propValues;
}
// eslint-disable-next-line no-console
console.info(`Generating examples for ${Component.displayName} (${Object.keys(propValues).length} props):`, propValues);
// TODO reconcile the differences between these files
// generatePropCombinations should call getComponentProps and not do anything?
const combos = generatePropCombinations(propValues).filter(Boolean);
let index = 0;
while (index < combos.length && exampleCount < maxExamples) {
const combo = combos[index];
if (combo) {
maybeAddExample(combo);
index++;
}
}
}
if (exampleCount >= maxExamples) {
console.error(`Too many examples for ${Component.displayName}! Add a filter to the config.`);
}
// eslint-disable-next-line no-console
console.info(`Generated ${exampleCount} examples for ${Component.displayName}`);
sections.forEach(({
pages
}) => {
pages.forEach(page => {
// eslint-disable-next-line no-param-reassign
page.parameters = getParameters(page);
});
});
return sections;
}
function isEmpty(obj) {
if (typeof obj !== 'object') return true;
for (const key in obj) {
if (Object.hasOwnProperty.call(obj, key)) return false;
}
return true;
}
export default generateComponentExamples;