From 1092b47f1c71714cdf601e24575623f0b8bc537e Mon Sep 17 00:00:00 2001 From: Ante Budimir Date: Mon, 10 Mar 2025 09:28:51 +0200 Subject: [PATCH] =?UTF-8?q?test=20=E2=9C=85:=20add=20coverage=20for=20shar?= =?UTF-8?q?ed=20utility=20functions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CHANGELOG.md | 10 +- package.json | 4 +- scripts/update-version.mjs | 2 +- .../css-property-priority-map.test.ts | 41 ++++++ .../font-face-property-order-enforcer.test.ts | 64 +++++++++ .../order-strategy-visitor-creator.test.ts | 132 ++++++++++++++++++ .../__tests__/property-name-extractor.test.ts | 57 ++++++++ .../recipe-property-processor.test.ts | 119 ++++++++++++++++ .../__tests__/style-node-processor.test.ts | 57 ++++++++ .../__tests__/test-property-name-rule.ts | 34 +++++ .../shared-utils/style-node-processor.ts | 8 +- src/index.ts | 2 +- vitest.config.mjs | 14 +- 13 files changed, 521 insertions(+), 23 deletions(-) create mode 100644 src/css-rules/shared-utils/__tests__/css-property-priority-map.test.ts create mode 100644 src/css-rules/shared-utils/__tests__/font-face-property-order-enforcer.test.ts create mode 100644 src/css-rules/shared-utils/__tests__/order-strategy-visitor-creator.test.ts create mode 100644 src/css-rules/shared-utils/__tests__/property-name-extractor.test.ts create mode 100644 src/css-rules/shared-utils/__tests__/recipe-property-processor.test.ts create mode 100644 src/css-rules/shared-utils/__tests__/style-node-processor.test.ts create mode 100644 src/css-rules/shared-utils/__tests__/test-property-name-rule.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 271fd8c..17b39c5 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/package.json b/package.json index 6d42034..50dd720 100644 --- a/package.json +++ b/package.json @@ -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": [ diff --git a/scripts/update-version.mjs b/scripts/update-version.mjs index 8de9074..0c726cc 100644 --- a/scripts/update-version.mjs +++ b/scripts/update-version.mjs @@ -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}`); diff --git a/src/css-rules/shared-utils/__tests__/css-property-priority-map.test.ts b/src/css-rules/shared-utils/__tests__/css-property-priority-map.test.ts new file mode 100644 index 0000000..1cd02a1 --- /dev/null +++ b/src/css-rules/shared-utils/__tests__/css-property-priority-map.test.ts @@ -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: [], +}); diff --git a/src/css-rules/shared-utils/__tests__/font-face-property-order-enforcer.test.ts b/src/css-rules/shared-utils/__tests__/font-face-property-order-enforcer.test.ts new file mode 100644 index 0000000..c4002dc --- /dev/null +++ b/src/css-rules/shared-utils/__tests__/font-face-property-order-enforcer.test.ts @@ -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: [], +}); diff --git a/src/css-rules/shared-utils/__tests__/order-strategy-visitor-creator.test.ts b/src/css-rules/shared-utils/__tests__/order-strategy-visitor-creator.test.ts new file mode 100644 index 0000000..00a110b --- /dev/null +++ b/src/css-rules/shared-utils/__tests__/order-strategy-visitor-creator.test.ts @@ -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: [], +}); diff --git a/src/css-rules/shared-utils/__tests__/property-name-extractor.test.ts b/src/css-rules/shared-utils/__tests__/property-name-extractor.test.ts new file mode 100644 index 0000000..2e0b714 --- /dev/null +++ b/src/css-rules/shared-utils/__tests__/property-name-extractor.test.ts @@ -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: [], +}); diff --git a/src/css-rules/shared-utils/__tests__/recipe-property-processor.test.ts b/src/css-rules/shared-utils/__tests__/recipe-property-processor.test.ts new file mode 100644 index 0000000..ba5e9ec --- /dev/null +++ b/src/css-rules/shared-utils/__tests__/recipe-property-processor.test.ts @@ -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' }) + } + } + }); + `, + }, + ], +}); diff --git a/src/css-rules/shared-utils/__tests__/style-node-processor.test.ts b/src/css-rules/shared-utils/__tests__/style-node-processor.test.ts new file mode 100644 index 0000000..000d9eb --- /dev/null +++ b/src/css-rules/shared-utils/__tests__/style-node-processor.test.ts @@ -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' } + ]); + `, + }, + ], +}); diff --git a/src/css-rules/shared-utils/__tests__/test-property-name-rule.ts b/src/css-rules/shared-utils/__tests__/test-property-name-rule.ts new file mode 100644 index 0000000..be24ccb --- /dev/null +++ b/src/css-rules/shared-utils/__tests__/test-property-name-rule.ts @@ -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; diff --git a/src/css-rules/shared-utils/style-node-processor.ts b/src/css-rules/shared-utils/style-node-processor.ts index 92b3d79..607186d 100644 --- a/src/css-rules/shared-utils/style-node-processor.ts +++ b/src/css-rules/shared-utils/style-node-processor.ts @@ -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); diff --git a/src/index.ts b/src/index.ts index ed9607d..e12c0c8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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, diff --git a/vitest.config.mjs b/vitest.config.mjs index 90151e4..8615dc7 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -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',