Skip to content

Port code from jest-preset-stylelint #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Dec 8, 2023
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Changelog

## 0.1.0

Initial release.
160 changes: 160 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,163 @@
[![CI](https://github.com/stylelint/stylelint-test-rule-node/actions/workflows/ci.yml/badge.svg)](https://github.com/stylelint/stylelint-test-rule-node/actions/workflows/ci.yml)

A Stylelint rule tester using [Node.js built-in test runner](https://nodejs.org/api/test.html) (`node:test`).

## Installation

Install the tester package alongside Stylelint:

```shell
npm install stylelint-test-rule-node stylelint --save-dev
```

## Usage

First, write a test file for rules you want to test. For example:

```js
// block-no-empty.test.js
import { testRule } from "stylelint-test-rule-node";

testRule({
ruleName: "block-no-empty",
config: true,

accept: [
{
code: "a { color: red }"
}
],

reject: [
{
code: "a {}",
message: "Unexpected empty block (block-no-empty)"
}
]
});
```

Then, run the test via `node --test`:

```sh-session
$ node --test block-no-empty.test.js
...
▶ block-no-empty (28.773291ms)

ℹ tests 2
ℹ suites 7
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 158.194084
```

See also the [type definitions](lib/index.d.ts) for more details.

### `testRule`

The `testRule` function enables you to efficiently test your plugin using a schema.

For example, we can test a plugin that enforces and autofixes kebab-case class selectors:

```js
// my-plugin.test.js
import { testRule } from "stylelint-test-rule-node";

import myPlugin from "./my-plugin.js";

const plugins = [myPlugin];
const {
ruleName,
rule: { messages }
} = myPlugin;

testRule({
plugins,
ruleName,
config: [true, { type: "kebab" }],
fix: true,

accept: [
{
code: ".class {}",
description: "simple class selector"
},
{
code: ".my-class {}",
description: "kebab class selector"
}
],

reject: [
{
code: ".myClass {}",
fixed: ".my-class {}",
description: "camel case class selector",
message: messages.expected(),
line: 1,
column: 1,
endLine: 1,
endColumn: 8
},
{
code: ".MyClass,\n.MyOtherClass {}",
fixed: ".my-class,\n.my-other-class {}",
description: "two pascal class selectors in a selector list",
warnings: [
{
message: messages.expected(),
line: 1,
column: 1,
endLine: 1,
endColumn: 8
},
{
message: messages.expected(),
line: 2,
column: 1,
endLine: 2,
endColumn: 13
}
]
}
]
});
```

### `testRuleConfigs`

The `testRuleConfigs` function enables you to test invalid configs for a rule.

For example:

```js
import { testRuleConfigs } from "stylelint-test-rule-node";

testRuleConfigs({
plugins,
ruleName,

accept: [
{
config: "valid"
}
],

reject: [
{
config: "invalid"
},
{
config: [/invalid/],
description: "regex is not allowed"
}
]
});
```

## [Changelog](CHANGELOG.md)

## [License](LICENSE)
56 changes: 56 additions & 0 deletions lib/__tests__/fixtures/plugin-foo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import stylelint from 'stylelint';

const {
// @ts-expect-error -- TS2339: Property 'createPlugin' does not exist on type 'typeof import("...").'
createPlugin,
// @ts-expect-error -- TS2339: Property 'utils' does not exist on type 'typeof import("...")'.
utils: { report, ruleMessages, validateOptions },
} = stylelint;

const ruleName = 'plugin/foo';

const messages = ruleMessages(ruleName, {
rejected: (/** @type {string} */ selector) => `No "${selector}" selector`,
});

/** @type {(value: unknown) => boolean} */
const isString = (value) => typeof value === 'string';

/** @type {import('stylelint').Rule} */
const ruleFunction = (primary, _secondaryOptions, { fix }) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: [isString],
});

if (!validOptions) {
return;
}

root.walkRules((rule) => {
const { selector } = rule;

if (primary === selector) return;

if (fix) {
rule.selector = primary;

return;
}

report({
result,
ruleName,
message: messages.rejected(selector),
node: rule,
word: selector,
});
});
};
};

ruleFunction.ruleName = ruleName;
ruleFunction.messages = messages;

export default createPlugin(ruleName, ruleFunction);
82 changes: 82 additions & 0 deletions lib/__tests__/testRule.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { testRule } from '../index.js';

import plugin from './fixtures/plugin-foo.js';

const plugins = [plugin];
const {
ruleName,
rule: { messages },
} = plugin;

testRule({
plugins,
ruleName,
config: ['.a'],

accept: [
{
code: '.a {}',
},
{
code: '.a {}',
description: 'with description',
},
],

reject: [
{
code: '#a {}',
message: messages.rejected('#a'),
},
{
code: '#a {}',
message: messages.rejected('#a'),
description: 'with description',
},
{
code: '#a {}',
message: messages.rejected('#a'),
description: 'with location',
line: 1,
column: 1,
endLine: 1,
endColumn: 3,
},
{
code: '#a {} #b {}',
description: 'multiple warnings',
warnings: [
{
message: messages.rejected('#a'),
},
{
message: messages.rejected('#b'),
line: 1,
column: 7,
endLine: 1,
endColumn: 9,
},
],
},
{
code: '#a {',
message: 'Unclosed block (CssSyntaxError)',
description: 'syntax error',
},
],
});

testRule({
plugins,
ruleName,
config: ['.a'],
fix: true,

reject: [
{
code: '#a {}',
fixed: '.a {}',
message: messages.rejected('#a'),
},
],
});
31 changes: 31 additions & 0 deletions lib/__tests__/testRuleConfigs.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { testRuleConfigs } from '../index.js';

import plugin from './fixtures/plugin-foo.js';

const plugins = [plugin];
const { ruleName } = plugin;

testRuleConfigs({
plugins,
ruleName,

accept: [
{
config: 'a',
},
{
config: ['b'],
description: 'string is allowed',
},
],

reject: [
{
config: 123,
},
{
config: [/foo/],
description: 'regex is not allowed',
},
],
});
5 changes: 0 additions & 5 deletions lib/__tests__/types.test.ts

This file was deleted.

Loading