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:
Ante Budimir 2025-03-10 09:28:51 +02:00
parent 44eeb7be6d
commit 1092b47f1c
13 changed files with 521 additions and 23 deletions

View file

@ -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/), 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). 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 ## [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.4.0] - 2025-03-08

View file

@ -1,7 +1,7 @@
{ {
"name": "@antebudimir/eslint-plugin-vanilla-extract", "name": "@antebudimir/eslint-plugin-vanilla-extract",
"version": "1.4.2", "version": "1.4.3",
"description": "ESLint plugin for enforcing CSS ordering in vanilla-extract styles", "description": "ESLint plugin for enforcing best practices in vanilla-extract CSS styles, including CSS property ordering and additional linting rules.",
"author": "Ante Budimir", "author": "Ante Budimir",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [

View file

@ -30,4 +30,4 @@ indexTsContent = indexTsContent.replace(/version: '(\d+\.\d+\.\d+)'/, `version:
// Write the updated content back to src/index.ts // Write the updated content back to src/index.ts
await fs.writeFile(indexTsPath, indexTsContent); 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}`);

View file

@ -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: [],
});

View file

@ -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: [],
});

View file

@ -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: [],
});

View file

@ -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: [],
});

View file

@ -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' })
}
}
});
`,
},
],
});

View file

@ -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' }
]);
`,
},
],
});

View file

@ -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;

View file

@ -12,15 +12,11 @@ export const processStyleNode = (
node: TSESTree.Node | undefined, node: TSESTree.Node | undefined,
processProperty: (ruleContext: Rule.RuleContext, value: TSESTree.ObjectExpression) => void, processProperty: (ruleContext: Rule.RuleContext, value: TSESTree.ObjectExpression) => void,
): void => { ): void => {
if (!node) { if (node?.type === 'ObjectExpression') {
return;
}
if (node.type === 'ObjectExpression') {
processProperty(ruleContext, node); processProperty(ruleContext, node);
} }
if (node.type === 'ArrayExpression') { if (node?.type === 'ArrayExpression') {
node.elements.forEach((element) => { node.elements.forEach((element) => {
if (element && element.type === 'ObjectExpression') { if (element && element.type === 'ObjectExpression') {
processProperty(ruleContext, element); processProperty(ruleContext, element);

View file

@ -5,7 +5,7 @@ import customOrderRule from './css-rules/custom-order/rule-definition.js';
export const vanillaExtract = { export const vanillaExtract = {
meta: { meta: {
name: '@antebudimir/eslint-plugin-vanilla-extract', name: '@antebudimir/eslint-plugin-vanilla-extract',
version: '1.4.2', version: '1.4.3',
}, },
rules: { rules: {
'alphabetical-order': alphabeticalOrderRule, 'alphabetical-order': alphabeticalOrderRule,

View file

@ -6,18 +6,8 @@ export default defineConfig({
provider: 'v8', provider: 'v8',
reporter: ['html', 'json', 'text'], reporter: ['html', 'json', 'text'],
reportsDirectory: './coverage/vitest-reports', reportsDirectory: './coverage/vitest-reports',
// include: ['src/**/rule-definition.ts'], include: ['src/css-rules/**/*.ts', 'src/shared-utils/**/*.ts'],
include: ['src/**/*.ts', '!src/**/*.test.ts', '!src/**/__tests__/**'], exclude: ['src/**/*.css.ts', 'src/**/*index.ts', 'src/**/*types.ts'],
// 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',
// ],
}, },
reporters: [ reporters: [
'default', 'default',