-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathgenerateTests.js
204 lines (182 loc) · 6.87 KB
/
generateTests.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
/* eslint no-console: 0 */
const fs = require('fs');
const { logInfo, logWarning, logError, isComponentDirectory } = require('./scriptUtils');
const COMPONENTS_PATHS = ['./src/components', './src/charts'];
const isComponentsDir = dir => COMPONENTS_PATHS.some(path => isComponentDirectory((path, dir)));
/**
* Creates a part of regex for an optional custom require, which can be used to substitute
* an actual component with a special styleguide component for demo purposes.
* E.g. you don't want to use a plain actual modal, because it will block the whole Styleguide UI.
*
* The resulting part of regex, given componentName is "Button" and this require is optional is:
*
* (const\s+ButtonStyleguide\s+=\srequire.*;)?
*
* Where:
* ()? - the whole thing is optional,
* const - "const" literally,
* \s+ - whitespace characters,
* ButtonStyleguide - name of the custom component used in the story,
* require - "require" literally,
* .*; - any characters, ending with a ";".
*
* An example of the string falling under this regex is:
*
* const ButtonStyleguide = require('./ButtonStyleguide').default;
*/
const getCustomRequireRegex = (componentName, optional = false) =>
`(const\\s+${componentName}Styleguide\\s+=\\s+require.*;\\n)${optional ? '?' : ''}`;
// Place any folders inside `/src/compoentns' you want to be excluded from test generation here.
// e.g const excludedFolders = ['Badge']; will exclude the Badge component.
const excludedFolders = [];
const notExcluded = name => excludedFolders.indexOf(name) === -1;
const allComponents = COMPONENTS_PATHS.reduce((acc, path) => {
const components = fs
.readdirSync(path)
.filter(isComponentsDir)
.filter(notExcluded)
.map(name => ({ name, path }));
return [...acc, ...components];
}, []);
const boilerPlate = `/* eslint-disable */
// This file was automatically generated
import React from 'react';
import 'jest-styled-components';
import ~COMPONENT_NAME~ from './~COMPONENT_NAME~';
~OPTIONAL_IMPORTS~
jest.mock('../../charts/HighChart', () => () => <high-charts />);
describe('~COMPONENT_NAME~ Component', () => {
describe('snapshots', () => {
it('should match snapshot', () => {
~YIELD~
});
});
});
`;
const writeTests = tests => {
tests.forEach(test => {
const path = `${test.path}/${test.name}/${test.name}_snapshot.spec.js`;
fs.writeFile(path, test.code, err => {
if (err) {
logError(`Error writing file ${test.name}`);
throw err;
}
logInfo(`Writing file to ${test.path}/${test.name}/${test.name}_snapshot.spec.js`);
});
});
};
const finishedGeneration = progress =>
Object.keys(progress).filter(key => progress[key] === false).length === 0;
const cleanAndGetImport = componentName => {
if (componentName.indexOf('.') > -1) {
return '';
}
const cleanComponent = componentName
.replace(/</g, '')
.replace(/>/g, '')
.replace(/\s/g, '');
return `import ${cleanComponent} from '../${cleanComponent}'`;
};
const injectSnapshotCode = (code, componentName) => {
const otherComponentRegexString = `(?!<${componentName}( |>))<([A-Z].+?)( |>)`;
const otherComponentRegex = new RegExp(otherComponentRegexString, 'g');
const otherComponents = code.match(otherComponentRegex);
let optionalImports = [];
if (otherComponents && otherComponents.length) {
optionalImports = otherComponents
.map(cleanAndGetImport)
.filter((elem, pos, arr) => arr.indexOf(elem) === pos);
}
return boilerPlate
.replace('~YIELD~', code)
.replace(/~COMPONENT_NAME~/g, componentName)
.replace('~OPTIONAL_IMPORTS~', optionalImports.join('\n'));
};
const tabWidth = ' ';
// removes the extra characters from the matched strings (jsx & ```).
const cleanComponentCode = (code, componentName) =>
code.map(component =>
component
.replace(new RegExp(getCustomRequireRegex(componentName), 'g'), '')
.replace(new RegExp(`${componentName}Styleguide`, 'g'), componentName) // replace custom styleguide component name to a normal component name
.replace(/```/g, '') // remove all occurences of the string "```".
.replace(/jsx/g, '') // remove all occurences of the string "jsx".
// indent the component by 4 tabs (8 spaces)
.replace(/\n/g, `\n${tabWidth.repeat(4)}`)
);
const readMarkdownFile = (path, fileName, callBack, errorCallBack) => {
fs.readFile(`${path}/${fileName}/${fileName}.md`, (err, out) => {
if (err) {
logWarning(`Could not find markdown file for ${fileName}`);
errorCallBack();
} else {
const file = out.toString();
callBack(file);
}
});
};
const getMatchingComponents = (file, componentName) => {
// To check which components will match the regex use this link https://regex101.com/r/7BlXLf/2
// and replace 'Badge' with your component name.
const customRequire = getCustomRequireRegex(componentName, true);
const regexString = `\`\`\`jsx\\n${customRequire}<${componentName}(Styleguide)?[\\s\\S]+?(\\/>|<\\/${componentName}(Styleguide)?>)`;
// Result regex is:
//
// ```jsx\n(const\\s+ButtonStyleguide\\s+=\\srequire.*;\n)?
// <Button(Styleguide)?[\s\S]+?(\/>|<\/Button(Styleguide)?>)
//
// Where:
// ```jsx\n (starts matching when it finds a code block that begins with this string)
// (const\\s+ButtonStyleguide\\s+=\\srequire.*;\n)? (optional custom component require,
// e.g. see Modal.md)
// <${componentName} (Ensures that the string begins with the component we are currently on)
// [\s\S]+? (matches any white space character as few times as possible)
// (\/>|<\/${componentName}>) until it reaches "/>" or "</ComponentName>"
const regex = new RegExp(regexString, 'g');
const matches = file.match(regex);
if (!matches) {
return null;
}
const components = cleanComponentCode(matches, componentName);
return components;
};
const getSnapshotCode = (component, index) => `
const wrapper${index + 1} = mount(${component}
);
expect(wrapper${index + 1}).toMatchSnapshot();
`;
const generateTests = () => {
const tests = [];
const progress = allComponents.reduce((acc, { name }) => {
acc[name] = false;
return acc;
}, {});
allComponents.forEach(({ name, path }) => {
readMarkdownFile(
path,
name,
file => {
const components = getMatchingComponents(file, name);
if (components && components.length) {
const snapshotCode = components.map(getSnapshotCode).join('');
const result = injectSnapshotCode(snapshotCode, name);
tests.push({ name, code: result, path });
} else {
logWarning(`No matching components found for ${name}`);
}
progress[name] = true;
if (finishedGeneration(progress)) {
writeTests(tests);
}
},
() => {
// error callback
progress[name] = true;
if (finishedGeneration(progress)) {
writeTests(tests);
}
}
);
});
};
generateTests();