mirror of
https://github.com/antebudimir/eslint-plugin-vanilla-extract.git
synced 2025-12-31 17:03:32 +00:00
test ✅: add coverage for shared utility functions
Add comprehensive tests for shared utility modules to improve code coverage: - Test property name extraction edge cases - Test CSS property priority map with invalid groups - Test order strategy visitor creator edge cases - Test font face property order enforcer early returns - Test style node processor with arrays and null values These tests ensure all code paths in shared utilities are properly exercised, including error handling and edge cases.
This commit is contained in:
parent
44eeb7be6d
commit
1092b47f1c
13 changed files with 521 additions and 23 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -5,9 +5,17 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.4.3] - 2025-03-10
|
||||
|
||||
- Add coverage for shared utility functions ()
|
||||
|
||||
## [1.4.2] - 2025-03-09
|
||||
|
||||
- Add GitHub Action to create releases from tags (7c19c9d)
|
||||
|
||||
## [1.4.1] - 2025-03-09
|
||||
|
||||
- Add comprehensive test suite for CSS ordering rules ()
|
||||
- Add comprehensive test suite for CSS ordering rules (5f1e602)
|
||||
|
||||
## [1.4.0] - 2025-03-08
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@antebudimir/eslint-plugin-vanilla-extract",
|
||||
"version": "1.4.2",
|
||||
"description": "ESLint plugin for enforcing CSS ordering in vanilla-extract styles",
|
||||
"version": "1.4.3",
|
||||
"description": "ESLint plugin for enforcing best practices in vanilla-extract CSS styles, including CSS property ordering and additional linting rules.",
|
||||
"author": "Ante Budimir",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
|
|
|||
|
|
@ -30,4 +30,4 @@ indexTsContent = indexTsContent.replace(/version: '(\d+\.\d+\.\d+)'/, `version:
|
|||
// Write the updated content back to src/index.ts
|
||||
await fs.writeFile(indexTsPath, indexTsContent);
|
||||
|
||||
console.log(`Updated src/index.ts to version ${newVersion}`);
|
||||
console.log(`Updated package.json and src/index.ts to version ${newVersion}`);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
import type { Rule } from 'eslint';
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
import { run } from 'eslint-vitest-rule-tester';
|
||||
import concentricOrderRule from '../../concentric-order/rule-definition.js';
|
||||
import { createCSSPropertyPriorityMap } from '../css-property-priority-map.js';
|
||||
|
||||
// A modified version of the rule that uses invalid group names
|
||||
const testPropertyPriorityMapRule = {
|
||||
...concentricOrderRule,
|
||||
create(context: Rule.RuleContext) {
|
||||
// This will trigger the || [] fallback by using non-existent group names
|
||||
createCSSPropertyPriorityMap(['non-existent-group', 'another-fake-group']);
|
||||
|
||||
// Return the original rule's implementation
|
||||
return concentricOrderRule.create(context);
|
||||
},
|
||||
};
|
||||
|
||||
run({
|
||||
name: 'vanilla-extract/css-property-priority-map-tests',
|
||||
rule: testPropertyPriorityMapRule,
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
valid: [
|
||||
// Simple test case to execute the rule
|
||||
`
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
const myStyle = style({
|
||||
backgroundColor: 'white',
|
||||
color: 'blue'
|
||||
});
|
||||
`,
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import type { Rule } from 'eslint';
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
import { run } from 'eslint-vitest-rule-tester';
|
||||
import alphabeticalOrderRule from '../../alphabetical-order/rule-definition.js';
|
||||
import { enforceFontFaceOrder } from '../font-face-property-order-enforcer.js';
|
||||
import type { TSESTree } from '@typescript-eslint/utils';
|
||||
|
||||
// A modified version of the rule that tests the edge cases
|
||||
const fontFaceEdgeCasesRule = {
|
||||
...alphabeticalOrderRule,
|
||||
create(context: Rule.RuleContext) {
|
||||
return {
|
||||
// Test case for null/undefined fontFaceObject
|
||||
CallExpression(node: TSESTree.CallExpression) {
|
||||
if (node.callee.type === 'Identifier' && node.callee.name === 'testNullCase') {
|
||||
// This will trigger the first early return
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
enforceFontFaceOrder(context, null as any);
|
||||
}
|
||||
|
||||
// Test case for non-ObjectExpression node
|
||||
if (node.callee.type === 'Identifier' && node.callee.name === 'testNonObjectCase') {
|
||||
// This will trigger the first early return (wrong type)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
enforceFontFaceOrder(context, { type: 'Literal' } as any);
|
||||
}
|
||||
|
||||
// Test case for empty or single property object
|
||||
if (node.callee.type === 'Identifier' && node.callee.name === 'testSinglePropertyCase') {
|
||||
// This will trigger the second early return (regularProperties.length <= 1)
|
||||
enforceFontFaceOrder(context, {
|
||||
type: 'ObjectExpression',
|
||||
properties: [
|
||||
{
|
||||
type: 'Property',
|
||||
key: { type: 'Identifier', name: 'fontFamily' },
|
||||
value: { type: 'Literal', value: 'Arial' },
|
||||
computed: false,
|
||||
kind: 'init',
|
||||
method: false,
|
||||
shorthand: false,
|
||||
},
|
||||
],
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
run({
|
||||
name: 'vanilla-extract/font-face-edge-cases',
|
||||
rule: fontFaceEdgeCasesRule,
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
valid: [`testNullCase();`, `testNonObjectCase();`, `testSinglePropertyCase();`],
|
||||
invalid: [],
|
||||
});
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
import type { Rule } from 'eslint';
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
import { run } from 'eslint-vitest-rule-tester';
|
||||
import alphabeticalOrderRule from '../../alphabetical-order/rule-definition.js';
|
||||
import customGroupOrderRule from '../../custom-order/rule-definition.js';
|
||||
import { createNodeVisitors } from '../order-strategy-visitor-creator.js';
|
||||
import type { TSESTree } from '@typescript-eslint/utils';
|
||||
|
||||
// Modified version of the custom order rule with empty group order
|
||||
const emptyGroupOrderRule = {
|
||||
...customGroupOrderRule,
|
||||
create(context: Rule.RuleContext) {
|
||||
// Trigger the error for empty userDefinedGroupOrder
|
||||
try {
|
||||
return customGroupOrderRule.create({
|
||||
...context,
|
||||
options: [{ groupOrder: [], sortRemainingProperties: 'alphabetical' }],
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (error) {
|
||||
// Just catch the error but continue with the test
|
||||
return {};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// A rule that tests the default case in the switch statement
|
||||
const defaultCaseRule = {
|
||||
...alphabeticalOrderRule,
|
||||
create(context: Rule.RuleContext) {
|
||||
// Force the default case by passing an invalid ordering strategy
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const visitors = createNodeVisitors(context, 'invalid-strategy' as any);
|
||||
return visitors;
|
||||
},
|
||||
};
|
||||
|
||||
// A rule that tests non-Identifier callee
|
||||
const nonIdentifierCalleeRule = {
|
||||
...alphabeticalOrderRule,
|
||||
create(context: Rule.RuleContext) {
|
||||
return {
|
||||
CallExpression(node: TSESTree.CallExpression) {
|
||||
// Original rule's visitor will be called first
|
||||
const visitors = alphabeticalOrderRule.create(context);
|
||||
if (visitors.CallExpression) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
visitors.CallExpression(node as any);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// Test the empty group order case
|
||||
run({
|
||||
name: 'vanilla-extract/empty-group-order-test',
|
||||
rule: emptyGroupOrderRule,
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
valid: [
|
||||
// Simple test case to execute the rule
|
||||
`
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
const myStyle = style({
|
||||
backgroundColor: 'white',
|
||||
color: 'blue'
|
||||
});
|
||||
`,
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
|
||||
// Test the default case in the switch statement
|
||||
run({
|
||||
name: 'vanilla-extract/default-case-test',
|
||||
rule: defaultCaseRule,
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
valid: [
|
||||
// Simple test case to execute the rule
|
||||
`
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
const myStyle = style({
|
||||
backgroundColor: 'white',
|
||||
color: 'blue'
|
||||
});
|
||||
`,
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
|
||||
// Test the non-Identifier callee case
|
||||
run({
|
||||
name: 'vanilla-extract/non-identifier-callee-test',
|
||||
rule: nonIdentifierCalleeRule,
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
valid: [
|
||||
// Test with a member expression callee (not an Identifier)
|
||||
`
|
||||
import { css } from '@vanilla-extract/css';
|
||||
|
||||
const utils = {
|
||||
createStyle: function(obj) { return obj; }
|
||||
};
|
||||
|
||||
const myStyle = utils.createStyle({
|
||||
backgroundColor: 'white',
|
||||
color: 'blue'
|
||||
});
|
||||
`,
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import tsParser from '@typescript-eslint/parser';
|
||||
import { run } from 'eslint-vitest-rule-tester';
|
||||
import testRuleForPropertyNameExtractor from './test-property-name-rule.js';
|
||||
|
||||
run({
|
||||
name: 'vanilla-extract/property-name-extractor-tests',
|
||||
rule: testRuleForPropertyNameExtractor,
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
valid: [
|
||||
// Test for identifier and string literal keys (should return names)
|
||||
`
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
const myStyle = style({
|
||||
color: 'blue',
|
||||
'background-color': 'white',
|
||||
fontSize: '16px'
|
||||
});
|
||||
`,
|
||||
|
||||
// Test for computed property with non-string literal (should return empty string)
|
||||
`
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
const myStyle = style({
|
||||
[42]: 'numeric key',
|
||||
[true]: 'boolean key'
|
||||
});
|
||||
`,
|
||||
|
||||
// Test for computed property with complex expression
|
||||
`
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
const myStyle = style({
|
||||
[Math.random() > 0.5 ? 'dynamicKey' : 'otherKey']: 'dynamic value'
|
||||
});
|
||||
`,
|
||||
|
||||
// Test for property with template literal key
|
||||
`
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
const prefix = 'webkit';
|
||||
const myStyle = style({
|
||||
[\`-\${prefix}-appearance\`]: 'none'
|
||||
});
|
||||
`,
|
||||
],
|
||||
invalid: [],
|
||||
});
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
import tsParser from '@typescript-eslint/parser';
|
||||
import { run } from 'eslint-vitest-rule-tester';
|
||||
import alphabeticalOrderRule from '../../alphabetical-order/rule-definition.js';
|
||||
|
||||
run({
|
||||
name: 'vanilla-extract/recipe-property-processor-tests',
|
||||
rule: alphabeticalOrderRule,
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
valid: [
|
||||
// Test for recipe with spread element (should trigger the early return)
|
||||
`
|
||||
import { recipe } from '@vanilla-extract/recipes';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
const baseStyle = style({ backgroundColor: 'blue', color: 'white' });
|
||||
|
||||
const myRecipe = recipe({
|
||||
base: baseStyle,
|
||||
...{ someSpreadProperty: true },
|
||||
variants: {
|
||||
size: {
|
||||
small: style({ fontSize: '1.2rem', padding: '4rem' }),
|
||||
large: style({ fontSize: '1.8rem', padding: '12rem' })
|
||||
}
|
||||
}
|
||||
});
|
||||
`,
|
||||
|
||||
// Test for recipe with computed property (non-Identifier key)
|
||||
`
|
||||
import { recipe } from '@vanilla-extract/recipes';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
const propName = 'dynamicProp';
|
||||
const baseStyle = style({ backgroundColor: 'blue', color: 'white' });
|
||||
|
||||
const myRecipe = recipe({
|
||||
base: baseStyle,
|
||||
[propName]: style({ fontSize: '1.4rem' }),
|
||||
variants: {
|
||||
size: {
|
||||
small: style({ fontSize: '1.2rem', padding: '4rem' }),
|
||||
large: style({ fontSize: '1.8rem', padding: '12rem' })
|
||||
}
|
||||
}
|
||||
});
|
||||
`,
|
||||
|
||||
// Test for recipe with non-object values (should skip processing)
|
||||
`
|
||||
import { recipe } from '@vanilla-extract/recipes';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
const baseStyle = style({ backgroundColor: 'blue', color: 'white' });
|
||||
|
||||
const myRecipe = recipe({
|
||||
base: baseStyle,
|
||||
nonObjectProp: 'string value',
|
||||
numericProp: 42,
|
||||
booleanProp: true,
|
||||
nullProp: null,
|
||||
variants: {
|
||||
size: {
|
||||
small: style({ fontSize: '1.2rem', padding: '4rem' }),
|
||||
large: style({ fontSize: '1.8rem', padding: '12rem' })
|
||||
}
|
||||
}
|
||||
});
|
||||
`,
|
||||
],
|
||||
invalid: [
|
||||
// Basic recipe with incorrect ordering in style calls
|
||||
{
|
||||
code: `
|
||||
import { recipe } from '@vanilla-extract/recipes';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
const baseStyle = style({ color: 'blue', backgroundColor: 'white' });
|
||||
|
||||
const myRecipe = recipe({
|
||||
base: baseStyle,
|
||||
variants: {
|
||||
size: {
|
||||
small: style({ padding: '4rem', fontSize: '1.2rem' }),
|
||||
large: style({ padding: '12rem', fontSize: '1.8rem' })
|
||||
}
|
||||
}
|
||||
});
|
||||
`,
|
||||
errors: [
|
||||
{ messageId: 'alphabeticalOrder' },
|
||||
{ messageId: 'alphabeticalOrder' },
|
||||
{ messageId: 'alphabeticalOrder' },
|
||||
],
|
||||
output: `
|
||||
import { recipe } from '@vanilla-extract/recipes';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
const baseStyle = style({ backgroundColor: 'white', color: 'blue' });
|
||||
|
||||
const myRecipe = recipe({
|
||||
base: baseStyle,
|
||||
variants: {
|
||||
size: {
|
||||
small: style({ fontSize: '1.2rem', padding: '4rem' }),
|
||||
large: style({ fontSize: '1.8rem', padding: '12rem' })
|
||||
}
|
||||
}
|
||||
});
|
||||
`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import tsParser from '@typescript-eslint/parser';
|
||||
import { run } from 'eslint-vitest-rule-tester';
|
||||
import alphabeticalOrderRule from '../../alphabetical-order/rule-definition.js';
|
||||
|
||||
run({
|
||||
name: 'vanilla-extract/style-node-processor-tests',
|
||||
rule: alphabeticalOrderRule,
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
valid: [
|
||||
// Test for empty array
|
||||
`
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
const emptyStyle = style([]);
|
||||
`,
|
||||
|
||||
// Test for null/undefined handling
|
||||
`
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
const dynamicStyle = style(undefined);
|
||||
const nullStyle = style(null);
|
||||
`,
|
||||
],
|
||||
invalid: [
|
||||
// Test for array with mixed content
|
||||
{
|
||||
code: `
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
const myStyle = style([
|
||||
{ zIndex: 1, position: 'relative' },
|
||||
null,
|
||||
undefined,
|
||||
{ display: 'flex', alignItems: 'center' }
|
||||
]);
|
||||
`,
|
||||
errors: [{ messageId: 'alphabeticalOrder' }, { messageId: 'alphabeticalOrder' }],
|
||||
output: `
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
const myStyle = style([
|
||||
{ position: 'relative', zIndex: 1 },
|
||||
null,
|
||||
undefined,
|
||||
{ alignItems: 'center', display: 'flex' }
|
||||
]);
|
||||
`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import type { Rule } from 'eslint';
|
||||
import { getPropertyName } from '../property-separator.js';
|
||||
import type { TSESTree } from '@typescript-eslint/utils';
|
||||
|
||||
const testRuleForPropertyNameExtractor: Rule.RuleModule = {
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'enforce test rule for property name extraction',
|
||||
category: 'Stylistic Issues',
|
||||
recommended: false,
|
||||
},
|
||||
fixable: undefined,
|
||||
schema: [],
|
||||
messages: {
|
||||
emptyName: 'Property name extraction returned an empty string for this property',
|
||||
},
|
||||
},
|
||||
create() {
|
||||
return {
|
||||
ObjectExpression(node) {
|
||||
// Extract property names without enforcing any order
|
||||
node.properties.forEach((property) => {
|
||||
if (property.type === 'Property') {
|
||||
// Test the getPropertyName function
|
||||
getPropertyName(property as TSESTree.Property);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default testRuleForPropertyNameExtractor;
|
||||
|
|
@ -12,15 +12,11 @@ export const processStyleNode = (
|
|||
node: TSESTree.Node | undefined,
|
||||
processProperty: (ruleContext: Rule.RuleContext, value: TSESTree.ObjectExpression) => void,
|
||||
): void => {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === 'ObjectExpression') {
|
||||
if (node?.type === 'ObjectExpression') {
|
||||
processProperty(ruleContext, node);
|
||||
}
|
||||
|
||||
if (node.type === 'ArrayExpression') {
|
||||
if (node?.type === 'ArrayExpression') {
|
||||
node.elements.forEach((element) => {
|
||||
if (element && element.type === 'ObjectExpression') {
|
||||
processProperty(ruleContext, element);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import customOrderRule from './css-rules/custom-order/rule-definition.js';
|
|||
export const vanillaExtract = {
|
||||
meta: {
|
||||
name: '@antebudimir/eslint-plugin-vanilla-extract',
|
||||
version: '1.4.2',
|
||||
version: '1.4.3',
|
||||
},
|
||||
rules: {
|
||||
'alphabetical-order': alphabeticalOrderRule,
|
||||
|
|
|
|||
|
|
@ -6,18 +6,8 @@ export default defineConfig({
|
|||
provider: 'v8',
|
||||
reporter: ['html', 'json', 'text'],
|
||||
reportsDirectory: './coverage/vitest-reports',
|
||||
// include: ['src/**/rule-definition.ts'],
|
||||
include: ['src/**/*.ts', '!src/**/*.test.ts', '!src/**/__tests__/**'],
|
||||
// exclude: [
|
||||
// 'src/**/*.css.ts',
|
||||
// 'src/**/*.test.ts',
|
||||
// 'src/**/*constants.ts',
|
||||
// 'src/**/*index.ts',
|
||||
// // Exclude all icon directories and their contents
|
||||
// 'src/components/common/icons/**',
|
||||
// // But include the CheckboxIcon component
|
||||
// '!src/components/common/icons/checkbox-icon/CheckboxIcon.tsx',
|
||||
// ],
|
||||
include: ['src/css-rules/**/*.ts', 'src/shared-utils/**/*.ts'],
|
||||
exclude: ['src/**/*.css.ts', 'src/**/*index.ts', 'src/**/*types.ts'],
|
||||
},
|
||||
reporters: [
|
||||
'default',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue