Skip to content

Commit

Permalink
fix: updated linter (#80)
Browse files Browse the repository at this point in the history
  • Loading branch information
veritem authored Mar 2, 2023
1 parent 2078fc3 commit 2014484
Show file tree
Hide file tree
Showing 5 changed files with 335 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ To use the all configuration, extend it in your `.eslintrc` file:
| [no-hooks](docs/rules/no-hooks.md) | Disallow setup and teardown hooks | 🌐 | | |
| [no-identical-title](docs/rules/no-identical-title.md) | Disallow identical titles || 🔧 | |
| [no-interpolation-in-snapshots](docs/rules/no-interpolation-in-snapshots.md) | Disallow string interpolation in snapshots | 🌐 | 🔧 | |
| [no-large-snapshots](docs/rules/no-large-snapshots.md) | Disallow large snapshots | 🌐 | | |
| [no-restricted-vi-methods](docs/rules/no-restricted-vi-methods.md) | Disallow specific `vi.` methods | 🌐 | | |
| [no-skipped-tests](docs/rules/no-skipped-tests.md) | Disallow skipped tests | 🌐 | | |
| [prefer-lowercase-title](docs/rules/prefer-lowercase-title.md) | Enforce lowercase titles | 🌐 | 🔧 | |
Expand Down
54 changes: 54 additions & 0 deletions docs/rules/no-large-snapshots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Disallow large snapshots (`vitest/no-large-snapshots`)

⚠️ This rule _warns_ in the 🌐 `all` config.

<!-- end auto-generated rule header -->

## Rule Details

This rule aims to prevent large snapshots.


### Options

This rule accepts an object with the following properties:

* `maxSize` (default: `50`): The maximum size of a snapshot.
* `inlineMaxSize` (default: `0`): The maximum size of a snapshot when it is inline.
* `allowedSnapshots` (default: `[]`): The list of allowed snapshots.

### For example:

```json
{
"vitest/no-large-snapshots": [
"error",
{
"maxSize": 50,
"inlineMaxSize": 0,
"allowedSnapshots": []
}
]
}
```

Examples of **incorrect** code for this rule with the above configuration:

```js
test('large snapshot', () => {
expect('a'.repeat(100)).toMatchSnapshot()
})
```

Examples of **correct** code for this rule with the above configuration:

```js
test('large snapshot', () => {
expect('a'.repeat(50)).toMatchSnapshot()
})
```

## When Not To Use It

If you don't want to limit the size of your snapshots, you can disable this rule.

3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import noConditionalInTest, { RULE_NAME as noConditionalInTestName } from './rul
import noDisabledTests, { RULE_NAME as noDisabledTestsName } from './rules/no-disabled-tests'
import noDoneCallback, { RULE_NAME as noDoneCallbackName } from './rules/no-done-callback'
import noDuplicateHooks, { RULE_NAME as noDuplicateHooksName } from './rules/no-duplicate-hooks'
import noLargeSnapshots, { RULE_NAME as noLargeSnapShotsName } from './rules/no-large-snapshots'
import nonInterpolationInSnapShots, { RULE_NAME as noInterpolationInSnapshotsName } from './rules/no-interpolation-in-snapshots'

const createConfig = (rules: Record<string, string>) => ({
Expand Down Expand Up @@ -47,6 +48,7 @@ const allRules = {
[noDisabledTestsName]: 'warn',
[noDoneCallbackName]: 'warn',
[noDuplicateHooksName]: 'warn',
[noLargeSnapShotsName]: 'warn',
[noInterpolationInSnapshotsName]: 'warn'
}

Expand Down Expand Up @@ -79,6 +81,7 @@ export default {
[noDisabledTestsName]: noDisabledTests,
[noDoneCallbackName]: noDoneCallback,
[noDuplicateHooksName]: noDuplicateHooks,
[noLargeSnapShotsName]: noLargeSnapshots,
[noInterpolationInSnapshotsName]: nonInterpolationInSnapShots
},
configs: {
Expand Down
155 changes: 155 additions & 0 deletions src/rules/no-large-snapshots.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { TSESLint } from '@typescript-eslint/utils'
import { RuleTester } from '@typescript-eslint/utils/dist/ts-eslint'
import { describe, expect, it } from 'vitest'
import rule, { RULE_NAME } from './no-large-snapshots'

const generateSnaShotLines = (lines: number) => `\`\n${'line\n'.repeat(lines)}\``

const generateExportsSnapshotString = (
lines: number,
title = 'a big component 1'
) => `exports[\`${title}\`] = ${generateSnaShotLines(lines - 1)};`

const generateExpectInlineSnapsCode = (
lines: number,
matcher: 'toMatchInlineSnapshot' | 'toThrowErrorMatchingInlineSnapshot'
) => `expect(something).${matcher}(${generateSnaShotLines(lines)});`

describe(RULE_NAME, () => {
const ruleTester = new RuleTester({
parser: require.resolve('@typescript-eslint/parser')
})

it(RULE_NAME, () => {
ruleTester.run(RULE_NAME, rule, {
valid: [
'expect(something)',
'expect(something).toBe(1)',
'expect(something).toMatchInlineSnapshot',
'expect(something).toMatchInlineSnapshot()',
{
filename: 'mock.js',
code: generateExpectInlineSnapsCode(2, 'toMatchInlineSnapshot')
},
{
filename: 'mock.jsx',
code: generateExpectInlineSnapsCode(20, 'toMatchInlineSnapshot'),
options: [
{
maxSize: 19,
inlineMaxSize: 21
}
]
},
{
filename: 'mock.jsx',
code: generateExpectInlineSnapsCode(60, 'toMatchInlineSnapshot'),
options: [
{
maxSize: 61
}
]
},
{
// "it should not report snapshots that are allowed to be large"
filename: '/mock-component.jsx.snap',
code: generateExportsSnapshotString(58),
options: [
{
allowedSnapshots: {
'/mock-component.jsx.snap': ['a big component 1']
}
}
]
}
],
invalid: [
{
filename: 'mock.js',
code: generateExpectInlineSnapsCode(50, 'toMatchInlineSnapshot'),
errors: [
{
messageId: 'tooLongSnapShot',
data: { lineLimit: 50, lineCount: 51 }
}
]
},
{
filename: 'mock.js',
code: generateExpectInlineSnapsCode(
50,
'toThrowErrorMatchingInlineSnapshot'
),
errors: [
{
messageId: 'tooLongSnapShot',
data: { lineLimit: 50, lineCount: 51 }
}
]
},
{
filename: 'mock.js',
code: generateExpectInlineSnapsCode(
50,
'toThrowErrorMatchingInlineSnapshot'
),
options: [{ maxSize: 51, inlineMaxSize: 50 }],
errors: [
{
messageId: 'tooLongSnapShot',
data: { lineLimit: 50, lineCount: 51 }
}
]
},
{
// "should not report allowed large snapshots based on regexp"
filename: '/mock-component.jsx.snap',
code: [
generateExportsSnapshotString(58, 'a big component w/ text'),
generateExportsSnapshotString(58, 'a big component 2')
].join('\n\n'),
options: [
{
allowedSnapshots: {
'/mock-component.jsx.snap': [/a big component \d+/u]
}
}
],
errors: [
{
messageId: 'tooLongSnapShot',
data: { lineLimit: 50, lineCount: 58 }
}
]
}
]
})
})
})

describe(RULE_NAME, () => {
describe('when "allowedSnapshots" options contains relative paths', () => {
it('should throw an exception', () => {
expect(() => {
const linter = new TSESLint.Linter()

linter.defineRule(RULE_NAME, rule)

linter.verify('console.log()', {
rules: {
'no-large-snapshots': [
'error',
{
allowedSnapshots: {
'./mock-component.jsx.snap': [/a big component \d+/u]
}
}
]
}
},
'mock-component.jsx.snap'
)
}).toThrow('All paths for allowedSnapshots must be absolute. You can use JS config and `path.resolve`')
})
})
})
122 changes: 122 additions & 0 deletions src/rules/no-large-snapshots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { isAbsolute } from 'node:path'
import { AST_NODE_TYPES, TSESLint, TSESTree } from '@typescript-eslint/utils'
import { createEslintRule, getAccessorValue, isSupportedAccessor } from '../utils'
import { parseVitestFnCall } from '../utils/parseVitestFnCall'

export const RULE_NAME = 'no-large-snapshots'
type MESSAGE_IDS = 'noSnapShot' | 'tooLongSnapShot';

type RuleOptions = {
maxSize?: number;
inlineMaxSize?: number;
allowedSnapshots?: Record<string, Array<string | RegExp>>
}

const reportOnViolation = (
context: TSESLint.RuleContext<MESSAGE_IDS, [RuleOptions]>,
node: TSESTree.CallExpressionArgument | TSESTree.ExpressionStatement,
{ maxSize: lineLimit = 50, allowedSnapshots = {} }: RuleOptions
) => {
const startLine = node.loc.start.line
const endLine = node.loc.end.line
const lineCount = endLine - startLine

const allPathsAreAbsolute = Object.keys(allowedSnapshots).every(isAbsolute)

if (!allPathsAreAbsolute)
throw new Error('All paths for allowedSnapshots must be absolute. You can use JS config and `path.resolve`')

let isAllowed = false

if (node.type === AST_NODE_TYPES.ExpressionStatement &&
'left' in node.expression &&
node.expression.left.type === AST_NODE_TYPES.MemberExpression &&
isSupportedAccessor(node.expression.left.property)
) {
const fileName = context.getFilename()
const allowedSnapshotsInFile = allowedSnapshots[fileName]

if (allowedSnapshotsInFile) {
const snapshotName = getAccessorValue(node.expression.left.property)

isAllowed = allowedSnapshotsInFile.some(name => {
if (name instanceof RegExp)
return name.test(snapshotName)

return snapshotName === name
})
}
}

if (!isAllowed && lineCount > lineLimit) {
context.report({
node,
messageId: lineLimit === 0 ? 'noSnapShot' : 'tooLongSnapShot',
data: {
lineCount,
lineLimit
}
})
}
}

export default createEslintRule<[RuleOptions], MESSAGE_IDS>({
name: RULE_NAME,
meta: {
docs: {
description: 'Disallow large snapshots',
recommended: 'warn'
},
messages: {
noSnapShot: '`{{ lineCount }}`s should begin with lowercase',
tooLongSnapShot: 'Expected Jest snapshot to be smaller than {{ lineLimit }} lines but was {{ lineCount }} lines long'
},
type: 'suggestion',
schema: [
{
type: 'object',
properties: {
maxSize: {
type: 'number'
},
inlineMaxSize: {
type: 'number'
},
allowedSnapshots: {
type: 'object',
additionalProperties: { type: 'array' }
}
},
additionalProperties: false
}
]
},
defaultOptions: [{}],
create(context, [options]) {
if (context.getFilename().endsWith('.snap')) {
return {
ExpressionStatement(node) {
reportOnViolation(context, node, options)
}
}
}

return {
CallExpression(node) {
const vitestFnCall = parseVitestFnCall(node, context)

if (vitestFnCall?.type !== 'expect')
return

if (['toMatchInlineSnapshot',
'toThrowErrorMatchingInlineSnapshot'
].includes(getAccessorValue(vitestFnCall.matcher)) && vitestFnCall.args.length) {
reportOnViolation(context, vitestFnCall.args[0], {
...options,
maxSize: options.inlineMaxSize ?? options.maxSize
})
}
}
}
}
})

0 comments on commit 2014484

Please sign in to comment.