@thetimes/jest-lint
Advanced tools
Comparing version 1.1.0 to 1.2.0
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const fs_extra_1 = require("fs-extra"); | ||
const path = require("path"); | ||
const parse_1 = require("./parse"); | ||
@@ -15,3 +16,3 @@ const sanitiseValue = (v) => { | ||
}); | ||
const extractElement = (element) => { | ||
const extractElement = (depth) => (element) => { | ||
if (element.type === "Identifier") { | ||
@@ -26,24 +27,41 @@ return element.name; | ||
} | ||
else if (element.type === "JSXElement") { | ||
return traverse(depth + 1)(element); | ||
} | ||
else if (element.type === "ArrayExpression") { | ||
return element.elements.map(extractElement(depth)); | ||
} | ||
return element.properties.map(extractProp); | ||
}; | ||
const extractProps = (depth) => ({ name, value }) => { | ||
let type; | ||
let propValue; | ||
if (value.type === "Literal") { | ||
type = value.type; | ||
propValue = value.value; | ||
} | ||
else if (value.expression.type === "Literal") { | ||
type = value.expression.type; | ||
propValue = value.expression.value; | ||
} | ||
else if (value.expression.type === "ObjectExpression") { | ||
type = value.expression.type; | ||
propValue = value.expression.properties.map(extractProp); | ||
} | ||
else if (value.expression.type === "ArrayExpression") { | ||
propValue = value.expression.elements.map(extractElement); | ||
type = value.expression.type; | ||
propValue = value.expression.elements.map(extractElement(depth)); | ||
} | ||
else if (value.expression.type === "Identifier") { | ||
type = value.expression.type; | ||
propValue = value.expression.name; | ||
} | ||
else if (value.expression.type === "UnaryExpression") { | ||
type = value.expression.type; | ||
propValue = value.expression.operator; | ||
} | ||
else { | ||
return { | ||
key: name.name, | ||
type: value.expression.type, | ||
value: traverse(depth + 1)(value.expression) | ||
@@ -54,2 +72,3 @@ }; | ||
key: name.name, | ||
type, | ||
value: sanitiseValue(propValue) | ||
@@ -63,3 +82,5 @@ }; | ||
const oe = x.openingElement; | ||
const elementName = oe.name.name; | ||
const elementName = oe.name.type === "JSXIdentifier" | ||
? oe.name.name | ||
: `${oe.name.object.name}.${oe.name.property.name}`; | ||
const props = oe.attributes.map(extractProps(depth)); | ||
@@ -94,3 +115,30 @@ return [ | ||
} | ||
const [{ expression }] = snapshot.value.body; | ||
if (snapshot.value.body.length === 0) { | ||
return { | ||
key: snapshot.key, | ||
lines: snapshotLength, | ||
elements: [] | ||
}; | ||
} | ||
const [body] = snapshot.value.body; | ||
if (body.type !== "ExpressionStatement") { | ||
return { | ||
key: snapshot.key, | ||
lines: snapshotLength, | ||
elements: [] | ||
}; | ||
} | ||
const { expression } = body; | ||
if (expression.type === "ArrayExpression") { | ||
return { | ||
key: snapshot.key, | ||
lines: snapshotLength, | ||
elements: expression.elements.reduce((es, element) => { | ||
if (element.type !== "JSXElement") { | ||
return es; | ||
} | ||
return [...es, ...traverse(0)(element)]; | ||
}, []) | ||
}; | ||
} | ||
if (expression.type !== "JSXElement") { | ||
@@ -110,3 +158,3 @@ return { | ||
exports.default = async (snapshotPath) => ({ | ||
path: snapshotPath, | ||
path: path.parse(snapshotPath).base, | ||
fileSize: (await fs_extra_1.stat(snapshotPath)).size, | ||
@@ -113,0 +161,0 @@ analyses: parse_1.default(await fs_extra_1.readFile(snapshotPath, "utf8")).map(analyseSnapshot) |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const chalk_1 = require("chalk"); | ||
const logger_1 = require("./logger"); | ||
const sortByType = (a, b) => { | ||
@@ -13,10 +13,14 @@ if (a.type < b.type) { | ||
}; | ||
exports.default = (sas) => { | ||
exports.default = (criteria, sas, opts = {}) => { | ||
const logger = new logger_1.default({ | ||
errorsOnly: opts.errorsOnly, | ||
isVerbose: opts.isVerbose | ||
}); | ||
sas.forEach(sa => { | ||
console.log(chalk_1.default.white(sa.path)); | ||
logger.log(sa.path); | ||
if (sa.errors.length > 0) { | ||
console.log(chalk_1.default.red("Snapshot errors:")); | ||
logger.error("Snapshot errors:"); | ||
} | ||
sa.errors.forEach(e => { | ||
console.log(chalk_1.default.red(`• ${e.size} bytes is too large, consider breaking your tests into separate snapshot files`)); | ||
logger.error(`• ${e.size} bytes is larger than ${criteria.maxFileSize}, consider breaking your tests into separate snapshot files`); | ||
}); | ||
@@ -27,7 +31,7 @@ let hasErrors = false; | ||
if (l.error || l.errors.length > 0 || l.warnings.length > 0) { | ||
console.log(chalk_1.default.white(l.key)); | ||
logger.log(l.key); | ||
} | ||
if (l.error) { | ||
hasErrors = true; | ||
console.log(chalk_1.default.red(`Snapshot could not be parsed: ${l.error}`)); | ||
logger.error(`Snapshot could not be parsed: ${l.error}`); | ||
return; | ||
@@ -41,18 +45,21 @@ } | ||
if (e.type === "GENERIC_ATTR") { | ||
console.log(chalk_1.default.red(`Generic Attributes: ${e.elementName} has ${e.attributes}`)); | ||
logger.error(`Generic Attributes: ${e.elementName} has ${e.attributes}, max (${criteria.genericAttrs})`); | ||
} | ||
if (e.type === "GENERIC_VALUE") { | ||
console.log(chalk_1.default.red(`Generic Values: ${e.elementName} has ${e.values}`)); | ||
if (e.type === "GENERIC_VALUE" && criteria.genericValues) { | ||
logger.error(`Generic Values: ${e.elementName} has ${e.values}, disallowed [${criteria.genericValues.join(", ")}]`); | ||
} | ||
if (e.type === "MAX_ATTR") { | ||
console.log(chalk_1.default.red(`Maximum Attributes: ${e.elementName} has ${e.count} attributes`)); | ||
logger.error(`Maximum Attributes: ${e.elementName} has ${e.count} attributes, max (${criteria.maxAttr})`); | ||
} | ||
if (e.type === "MAX_ATTR_LENGTH") { | ||
console.log(chalk_1.default.red(`Maximum Attribute Length: ${e.elementName} ${e.attributeName} has a length of ${e.attributeLength}`)); | ||
if (e.type === "MAX_ATTR_ARR_LENGTH") { | ||
logger.error(`Maximum Attribute Array Length: ${e.elementName} ${e.attributeName} has a length of ${e.attributeLength}, max (${criteria.maxAttrArrayLength})`); | ||
} | ||
if (e.type === "MAX_ATTR_STR_LENGTH") { | ||
logger.error(`Maximum Attribute String Length: ${e.elementName} ${e.attributeName} has a length of ${e.attributeLength}, max (${criteria.maxAttrStringLength})`); | ||
} | ||
if (e.type === "MAX_DEPTH") { | ||
console.log(chalk_1.default.red(`Maximum Depth: ${e.leafElementName} has a depth of ${e.depth}`)); | ||
logger.error(`Maximum Depth: ${e.leafElementName} has a depth of ${e.depth}, max (${criteria.maxDepth})`); | ||
} | ||
if (e.type === "MAX_LINES") { | ||
console.log(chalk_1.default.red(`Maximum Lines: ${e.count} lines is too long, consider breaking this snapshot down`)); | ||
logger.error(`Maximum Lines: ${e.count} lines is longer than ${criteria.maxLines}, consider breaking this snapshot down`); | ||
} | ||
@@ -64,3 +71,8 @@ }); | ||
l.warnings.forEach(w => { | ||
console.log(chalk_1.default.yellow(`Max Generic Elements: Too many (${w.count}) generic elements (${w.elementName}) reduce the clarity of a snapshot`)); | ||
if (w.type === "NO_ELEMENTS_FOUND") { | ||
logger.warn("No JSX found"); | ||
} | ||
else { | ||
logger.warn(`Max Generic Elements: Too many (${w.count}) generic elements (${w.elementName}) reduce the clarity of a snapshot, max(${criteria.maxGenericElement})`); | ||
} | ||
}); | ||
@@ -72,3 +84,3 @@ }); | ||
!hasWarnings) { | ||
console.log(chalk_1.default.green("No issues ✔️")); | ||
logger.success("No issues ✔️"); | ||
} | ||
@@ -75,0 +87,0 @@ }); |
@@ -14,7 +14,9 @@ #!/usr/bin/env node | ||
.option("-v --verbose", "whether to log out everything or not") | ||
.option("--ci --ci", "apply ci type behaviours such as silence informative logging and exit with a non zero code") | ||
.parse(process.argv); | ||
const { verbose = false } = program; | ||
const { verbose = false, ci = false } = program; | ||
fs_extra_1.readFile(path.join(process.cwd(), ".jestlint"), "utf8") | ||
.then(contents => main_1.default(process.cwd(), { | ||
...JSON.parse(contents), | ||
usingCI: ci, | ||
isVerbose: verbose | ||
@@ -27,2 +29,3 @@ })) | ||
main_1.default(process.cwd(), { | ||
usingCI: ci, | ||
isVerbose: verbose | ||
@@ -29,0 +32,0 @@ }); |
17
main.js
@@ -22,10 +22,12 @@ "use strict"; | ||
}); | ||
const reportHasError = (r) => r.errors.length > 0; | ||
exports.default = async (cwd, opts) => { | ||
const snapshots = await getSnapshots(cwd, opts.snapPattern); | ||
const results = await Promise.all(snapshots.map(analyse_1.default)); | ||
const criteria = report_1.default({ | ||
const criteria = { | ||
genericAttrs: opts.genericAttributes || [], | ||
genericValues: opts.genericValues || ["[Function]"], | ||
maxAttr: opts.maxAttributes || 5, | ||
maxAttrLength: opts.maxAttributeLength || 30, | ||
maxAttrArrayLength: opts.maxAttributeArrayLength || 5, | ||
maxAttrStringLength: opts.maxAttributeStringLength || 30, | ||
maxDepth: opts.maxDepth || 10, | ||
@@ -35,6 +37,13 @@ maxFileSize: opts.maxFileSize || 10000, | ||
maxLines: opts.maxLines || 300 | ||
}; | ||
const reporter = report_1.default(criteria); | ||
const output = results.map(r => reporter(r)); | ||
console_1.default(criteria, output, { | ||
errorsOnly: opts.usingCI, | ||
isVerbose: opts.isVerbose | ||
}); | ||
const output = results.map(r => criteria(r)); | ||
console_1.default(output); | ||
if (output.some(reportHasError)) { | ||
process.exit(1); | ||
} | ||
}; | ||
//# sourceMappingURL=main.js.map |
{ | ||
"name": "@thetimes/jest-lint", | ||
"version": "1.1.0", | ||
"version": "1.2.0", | ||
"description": "A tool to analyse/validate jest snapshots", | ||
"main": "index.js", | ||
"scripts": { | ||
"coveralls": "nyc report --reporter=text-lcov | coveralls", | ||
"coverage:report": "nyc report --reporter=html", | ||
@@ -31,6 +32,12 @@ "generate-snaps": "BABEL_ENV=fg jest", | ||
"testMatch": [ | ||
"**/generate-snapshots.js" | ||
"**/generate-snapshots.js", | ||
"**/generate-style-snapshots.js" | ||
], | ||
"testEnvironment": "jsdom", | ||
"snapshotSerializers": [ | ||
"enzyme-to-json/serializer" | ||
] | ||
}, | ||
"devDependencies": { | ||
"@times-components/jest-serializer": "1.0.1", | ||
"@types/fs-extra": "5.0.2", | ||
@@ -42,8 +49,17 @@ "@types/glob": "5.0.35", | ||
"babel-preset-react-native": "4.0.0", | ||
"coveralls": "^3.0.1", | ||
"enzyme": "3.3.0", | ||
"enzyme-adapter-react-16": "1.1.1", | ||
"enzyme-to-json": "3.3.4", | ||
"jest": "23.0.0", | ||
"jest-styled-components": "5.0.1", | ||
"nyc": "11.8.0", | ||
"prettier": "1.12.1", | ||
"react": "16.4.0", | ||
"react-art": "16.4.1", | ||
"react-dom": "16.4.1", | ||
"react-native": "0.55.4", | ||
"react-native-web": "0.8.4", | ||
"react-test-renderer": "16.4.0", | ||
"styled-components": "3.3.2", | ||
"typescript": "2.8.3" | ||
@@ -50,0 +66,0 @@ }, |
48
parse.js
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const acorn = require("acorn-jsx"); | ||
const stripStyleBlock = (s) => s.replace(/<style>.*<\/style>/gs, ""); | ||
const stripJestStructures = (s) => s | ||
@@ -8,5 +9,24 @@ .replace(/Array\s\[/g, "[") | ||
.replace(/>[\s]*{.*}[\s]*</gs, ">[Replaced Object]<"); | ||
const stripStyledComponentStyles = (s) => s.replace(/\.c\d {.*}\s*(?=<|Array \[)/gs, ""); | ||
const tryJson = (key, rawValue, value) => { | ||
try { | ||
JSON.parse(JSON.stringify(value)); | ||
return { | ||
key, | ||
rawValue, | ||
value: null | ||
}; | ||
} | ||
catch (e) { | ||
return { | ||
key, | ||
rawValue, | ||
value: null, | ||
error: e.message | ||
}; | ||
} | ||
}; | ||
const parseVal = (snapObj) => (key) => { | ||
let value; | ||
const sanitised = stripJestStructures(snapObj[key]); | ||
const sanitised = stripStyleBlock(stripJestStructures(snapObj[key])); | ||
try { | ||
@@ -18,17 +38,15 @@ value = acorn.parse(sanitised, { | ||
catch (e) { | ||
try { | ||
JSON.parse(JSON.stringify(sanitised)); | ||
return { | ||
key, | ||
rawValue: snapObj[key], | ||
value: null | ||
}; | ||
const styledComponentsStyleRegex = /\.c\d {.*}\s*(?=<|Array \[)/gs; | ||
const noStyleBlock = stripStyleBlock(snapObj[key]); | ||
const hasStyledComponentStyles = styledComponentsStyleRegex.test(noStyleBlock); | ||
if (hasStyledComponentStyles) { | ||
try { | ||
value = acorn.parse(stripJestStructures(stripStyledComponentStyles(noStyleBlock)), { | ||
plugins: { jsx: true } | ||
}); | ||
} | ||
catch (e) { } | ||
} | ||
catch (e) { | ||
return { | ||
key, | ||
rawValue: snapObj[key], | ||
value: null, | ||
error: e.message | ||
}; | ||
if (!value) { | ||
return tryJson(key, snapObj[key], sanitised); | ||
} | ||
@@ -35,0 +53,0 @@ } |
@@ -0,1 +1,4 @@ | ||
[![Coverage Status](https://coveralls.io/repos/github/newsuk/jest-lint/badge.svg?branch=master)](https://coveralls.io/github/newsuk/jest-lint?branch=master) | ||
[![Build Status](https://travis-ci.org/newsuk/jest-lint.svg?branch=master)](https://travis-ci.org/newsuk/jest-lint) | ||
# jest-lint | ||
@@ -11,3 +14,3 @@ | ||
``` | ||
yarn @thetimes/jest-lint --dev | ||
yarn add --dev @thetimes/jest-lint | ||
``` | ||
@@ -106,10 +109,19 @@ | ||
### MaxAttrLengthError | ||
### MaxAttrArrayLengthError | ||
**config option**: `maxAttributeLength: number` | ||
**config option**: `maxAttributeArrayLength: number` | ||
**default**: `5` | ||
If a prop has a value that is very long it is unlikely the whole value is of | ||
interest to the test and can be shortened for easier diffing, certainly for lists | ||
### MaxAttrStringLengthError | ||
**config option**: `maxAttributeStringLength: number` | ||
**default**: `30` | ||
If a prop has a value that is very long it is unlikely the whole value is of | ||
interest to the test and can be shortened for easier diffing | ||
interest to the test and can be shortened to an example for easier diffing | ||
@@ -116,0 +128,0 @@ ### MaxDepthError |
@@ -34,12 +34,23 @@ "use strict"; | ||
} | ||
if (criteria.maxAttrLength && | ||
p.value && | ||
p.value.toString().length > criteria.maxAttrLength) { | ||
if (criteria.maxAttrArrayLength !== undefined && | ||
p.type === "ArrayExpression" && | ||
Array.isArray(p.value) && | ||
p.value.length > criteria.maxAttrArrayLength) { | ||
errors.push({ | ||
type: "MAX_ATTR_LENGTH", | ||
type: "MAX_ATTR_ARR_LENGTH", | ||
elementName: e.elementName, | ||
attributeName: p.key, | ||
attributeLength: p.value.toString().length | ||
attributeLength: p.value.length | ||
}); | ||
} | ||
if (criteria.maxAttrStringLength !== undefined && | ||
typeof p.value === "string" && | ||
p.value.length > criteria.maxAttrStringLength) { | ||
errors.push({ | ||
type: "MAX_ATTR_STR_LENGTH", | ||
elementName: e.elementName, | ||
attributeName: p.key, | ||
attributeLength: p.value.length | ||
}); | ||
} | ||
}); | ||
@@ -90,2 +101,7 @@ if (e.props.length === 0) { | ||
}); | ||
if (analysis.elements.length === 0) { | ||
warnings.push({ | ||
type: "NO_ELEMENTS_FOUND" | ||
}); | ||
} | ||
return { | ||
@@ -92,0 +108,0 @@ key: analysis.key, |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
27584
11
613
165
23