@lapidist/design-lint-testing 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,69 @@
1
+ # @lapidist/design-lint-testing
2
+
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 5c9a371: v8: DSR kernel integration — remove v7 token fallback, require tokenProvider
8
+
9
+ `createLinter` and the `Linter` constructor now throw when `Environment.tokenProvider` is absent. The v7 inline-config fallback (`ConfigTokenProvider` loaded silently from `config.tokens`) has been removed from both `setup.ts` and `linter.ts`. Callers must supply a `tokenProvider` — the DSR kernel is the authoritative source.
10
+
11
+ `DsrOptions.beforeConnect` is a new optional async hook that programmatic callers (LSP, MCP, scripts) can use to auto-start the kernel without depending on the CLI's `prepareEnvironment`.
12
+
13
+ - 5c9a371: Initial release of eight new design-lint v8 packages: config-recommended, config-strict, config-ai-agent, testing (RuleTester), telemetry (DLTS v1 SDK + OTel), mcp (AEP v1 types), lsp (LSP capability types), rdk (Rule Development Kit)
14
+
15
+ ### Minor Changes
16
+
17
+ - 5c9a371: feat!: v8 release — DSR kernel integration, MCP/LSP/telemetry packages, RuleTester, config presets, and RDK; the linter now delegates token resolution to the DSR kernel when running, removing internal token/rule state ownership
18
+ - 5c9a371: feat(testing): complete RuleTester coverage for all 30 built-in rules; add DtifFlattenedToken injection to ValidCase/InvalidCase so token-based rules can exercise real validation logic; fix TSX/JSX docType normalisation through FILE_TYPE_MAP so tsx snippets resolve to the TypeScript parser
19
+
20
+ ### Patch Changes
21
+
22
+ - 5c9a371: feat(testing): export SnippetLinter from package index
23
+ test(config): add tests for config-recommended, config-strict, and config-ai-agent presets
24
+ - Updated dependencies [5c9a371]
25
+ - Updated dependencies [5c9a371]
26
+ - Updated dependencies [5c9a371]
27
+ - Updated dependencies [3a76856]
28
+ - Updated dependencies [5c9a371]
29
+ - Updated dependencies [5c9a371]
30
+ - Updated dependencies [5c9a371]
31
+ - Updated dependencies [5c9a371]
32
+ - Updated dependencies [5c9a371]
33
+ - Updated dependencies [5c9a371]
34
+ - Updated dependencies [5c9a371]
35
+ - Updated dependencies [5c9a371]
36
+ - Updated dependencies [5c9a371]
37
+ - Updated dependencies [5c9a371]
38
+ - Updated dependencies [5c9a371]
39
+ - Updated dependencies [5c9a371]
40
+ - Updated dependencies [5c9a371]
41
+ - Updated dependencies [5c9a371]
42
+ - Updated dependencies [5c9a371]
43
+ - Updated dependencies [5c9a371]
44
+ - Updated dependencies [5c9a371]
45
+ - Updated dependencies [5c9a371]
46
+ - Updated dependencies [5c9a371]
47
+ - Updated dependencies [5c9a371]
48
+ - Updated dependencies [5c9a371]
49
+ - Updated dependencies [5c9a371]
50
+ - Updated dependencies [5c9a371]
51
+ - Updated dependencies [5c9a371]
52
+ - Updated dependencies [5c9a371]
53
+ - Updated dependencies [5c9a371]
54
+ - Updated dependencies [5c9a371]
55
+ - Updated dependencies [5c9a371]
56
+ - Updated dependencies [5c9a371]
57
+ - Updated dependencies [5c9a371]
58
+ - Updated dependencies [5c9a371]
59
+ - Updated dependencies [5c9a371]
60
+ - Updated dependencies [5c9a371]
61
+ - Updated dependencies [5c9a371]
62
+ - Updated dependencies [5c9a371]
63
+ - Updated dependencies [5c9a371]
64
+ - Updated dependencies [5c9a371]
65
+ - Updated dependencies [5c9a371]
66
+ - Updated dependencies [5c9a371]
67
+ - Updated dependencies [5c9a371]
68
+ - Updated dependencies [5c9a371]
69
+ - @lapidist/design-lint@8.0.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Brett Dorrans and Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@lapidist/design-lint-testing",
3
+ "version": "1.0.0",
4
+ "description": "Testing utilities and RuleTester for @lapidist/design-lint rule authors.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/bylapidist/design-lint.git",
12
+ "directory": "packages/testing"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/bylapidist/design-lint/issues"
16
+ },
17
+ "keywords": [
18
+ "dtif",
19
+ "design tokens",
20
+ "lint",
21
+ "testing",
22
+ "rule-tester"
23
+ ],
24
+ "exports": {
25
+ ".": {
26
+ "source": "./src/index.ts",
27
+ "import": "./dist/index.js",
28
+ "types": "./dist/index.d.ts"
29
+ }
30
+ },
31
+ "engines": {
32
+ "node": ">=22"
33
+ },
34
+ "peerDependencies": {
35
+ "@lapidist/design-lint": ">=8.0.0"
36
+ },
37
+ "devDependencies": {
38
+ "@lapidist/design-lint": "8.0.0"
39
+ },
40
+ "scripts": {
41
+ "build": "tsc -p tsconfig.json",
42
+ "format:check": "prettier --check 'src/**/*.ts'"
43
+ }
44
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { RuleTester } from './rule-tester.js';
2
+ export type { RuleTesterConfig, RuleTesterTests, ValidCase, InvalidCase, ExpectedError } from './rule-tester.js';
3
+ export { SnippetLinter } from './snippet-linter.js';
@@ -0,0 +1,192 @@
1
+ import type { LintMessage, RuleModule, DtifFlattenedToken } from '@lapidist/design-lint';
2
+ import assert from 'node:assert/strict';
3
+ import { SnippetLinter } from './snippet-linter.js';
4
+
5
+ /** A file type accepted by the linter. */
6
+ export type FileType = 'css' | 'ts' | 'tsx' | 'vue' | 'svelte';
7
+
8
+ /** A valid (no-violation) test case. */
9
+ export interface ValidCase {
10
+ /** Source code snippet to lint. */
11
+ code: string;
12
+ /** The file type to lint as. */
13
+ fileType: FileType;
14
+ /** Optional rule-specific options. */
15
+ options?: unknown;
16
+ /**
17
+ * Flattened DTIF tokens to inject into the linter for this case.
18
+ * Required when testing token-based rules that need a non-empty token set
19
+ * to exercise real validation (rather than the "configure tokens" message).
20
+ */
21
+ tokens?: DtifFlattenedToken[];
22
+ }
23
+
24
+ /** An expected diagnostic produced by an invalid case. */
25
+ export interface ExpectedError {
26
+ /** The rule id that produced this diagnostic (e.g. `design-token/colors`). */
27
+ ruleId?: string;
28
+ /** Substring that the diagnostic message must contain. */
29
+ message?: string;
30
+ /** Expected 1-based source line. */
31
+ line?: number;
32
+ /** Expected 1-based source column. */
33
+ column?: number;
34
+ }
35
+
36
+ /** An invalid (must-produce-violations) test case. */
37
+ export interface InvalidCase {
38
+ /** Source code snippet to lint. */
39
+ code: string;
40
+ /** The file type to lint as. */
41
+ fileType: FileType;
42
+ /** Optional rule-specific options. */
43
+ options?: unknown;
44
+ /**
45
+ * Flattened DTIF tokens to inject into the linter for this case.
46
+ * Required when testing token-based rules that need a non-empty token set
47
+ * to exercise real validation (rather than the "configure tokens" message).
48
+ */
49
+ tokens?: DtifFlattenedToken[];
50
+ /** The diagnostics that must be reported — at least one required. */
51
+ errors: [ExpectedError, ...ExpectedError[]];
52
+ /** Expected auto-fixed output, if the rule is fixable. */
53
+ output?: string;
54
+ }
55
+
56
+ /** Top-level test collection passed to {@link RuleTester.run}. */
57
+ export interface RuleTesterTests {
58
+ valid: ValidCase[];
59
+ invalid: InvalidCase[];
60
+ }
61
+
62
+ /** Construction options for {@link RuleTester}. */
63
+ export interface RuleTesterConfig {
64
+ /** Default file type to use when a case does not specify one. */
65
+ defaultFileType?: FileType;
66
+ }
67
+
68
+ /**
69
+ * Utility for testing individual `@lapidist/design-lint` rules.
70
+ *
71
+ * Mirrors the ESLint `RuleTester` API so rule authors have a familiar
72
+ * testing surface. Each `invalid` case must produce at least the number of
73
+ * diagnostics listed in `errors`; each `valid` case must produce none.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * const tester = new RuleTester({ defaultFileType: 'css' });
78
+ * await tester.run('design-token/colors', colorsRule, {
79
+ * valid: [{ code: 'a { color: var(--color-brand-primary); }', fileType: 'css' }],
80
+ * invalid: [{ code: 'a { color: #3B82F6; }', fileType: 'css', errors: [{ ruleId: 'design-token/colors' }] }],
81
+ * });
82
+ * ```
83
+ */
84
+ export class RuleTester {
85
+ readonly #config: Required<RuleTesterConfig>;
86
+
87
+ constructor(config: RuleTesterConfig = {}) {
88
+ this.#config = {
89
+ defaultFileType: config.defaultFileType ?? 'css',
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Runs the test suite for a single rule.
95
+ *
96
+ * @param {string} ruleName - The rule's canonical name (e.g. `design-token/colors`).
97
+ * @param {RuleModule} rule - The rule module under test.
98
+ * @param {RuleTesterTests} tests - Valid and invalid test cases.
99
+ * @returns {Promise<void>} Resolves when all assertions pass.
100
+ */
101
+ async run(
102
+ ruleName: string,
103
+ rule: RuleModule,
104
+ tests: RuleTesterTests,
105
+ ): Promise<void> {
106
+ for (const testCase of tests.valid) {
107
+ const fileType = testCase.fileType ?? this.#config.defaultFileType;
108
+ const diagnostics = await this.#lint(
109
+ rule,
110
+ testCase.code,
111
+ fileType,
112
+ testCase.options,
113
+ testCase.tokens,
114
+ );
115
+ assert.equal(
116
+ diagnostics.length,
117
+ 0,
118
+ `Rule "${ruleName}" reported unexpected diagnostics for valid case:\n${testCase.code}\n\nDiagnostics:\n${JSON.stringify(diagnostics, null, 2)}`,
119
+ );
120
+ }
121
+
122
+ for (const testCase of tests.invalid) {
123
+ const fileType = testCase.fileType ?? this.#config.defaultFileType;
124
+ const diagnostics = await this.#lint(
125
+ rule,
126
+ testCase.code,
127
+ fileType,
128
+ testCase.options,
129
+ testCase.tokens,
130
+ );
131
+ assert.ok(
132
+ diagnostics.length >= testCase.errors.length,
133
+ `Rule "${ruleName}" expected at least ${String(testCase.errors.length)} diagnostic(s) but got ${String(diagnostics.length)}:\n${testCase.code}`,
134
+ );
135
+
136
+ for (let i = 0; i < testCase.errors.length; i += 1) {
137
+ const expected = testCase.errors[i];
138
+ const actual = diagnostics[i];
139
+ if (expected === undefined || actual === undefined) continue;
140
+
141
+ if (expected.ruleId !== undefined) {
142
+ assert.equal(
143
+ actual.ruleId,
144
+ expected.ruleId,
145
+ `Diagnostic ${String(i)} ruleId mismatch`,
146
+ );
147
+ }
148
+ if (expected.message !== undefined) {
149
+ assert.ok(
150
+ actual.message.includes(expected.message),
151
+ `Diagnostic ${String(i)} message "${actual.message}" does not include "${expected.message}"`,
152
+ );
153
+ }
154
+ if (expected.line !== undefined) {
155
+ assert.equal(
156
+ actual.line,
157
+ expected.line,
158
+ `Diagnostic ${String(i)} line mismatch`,
159
+ );
160
+ }
161
+ if (expected.column !== undefined) {
162
+ assert.equal(
163
+ actual.column,
164
+ expected.column,
165
+ `Diagnostic ${String(i)} column mismatch`,
166
+ );
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Runs the linter against a code snippet using a `SnippetLinter` that
174
+ * injects the rule under test directly, bypassing the rule registry.
175
+ *
176
+ * @param {RuleModule} rule - The rule module to test.
177
+ * @param {string} code - Source code to lint.
178
+ * @param {FileType} fileType - The file type to lint as.
179
+ * @param {unknown} options - Rule-specific options.
180
+ * @returns {Promise<LintMessage[]>} Diagnostics produced by the rule.
181
+ */
182
+ async #lint(
183
+ rule: RuleModule,
184
+ code: string,
185
+ fileType: FileType,
186
+ options?: unknown,
187
+ tokens?: DtifFlattenedToken[],
188
+ ): Promise<LintMessage[]> {
189
+ const linter = new SnippetLinter(rule, options, tokens);
190
+ return linter.lintSnippet(code, fileType);
191
+ }
192
+ }
@@ -0,0 +1,88 @@
1
+ import { Linter, TokenRegistry } from '@lapidist/design-lint';
2
+ import type {
3
+ RuleModule,
4
+ LintMessage,
5
+ LintDocument,
6
+ LintResult,
7
+ DtifFlattenedToken,
8
+ } from '@lapidist/design-lint';
9
+
10
+ /** Minimal enabled-rule shape (mirrors the internal RuleRegistry return type). */
11
+ interface EnabledRule {
12
+ rule: RuleModule;
13
+ options: unknown;
14
+ severity: 'error' | 'warn';
15
+ }
16
+
17
+ /** Stub DocumentSource — never actually scanned; only lintDocument is used. */
18
+ const stubSource = {
19
+ scan: async () => ({ documents: [], ignoreFiles: [] }),
20
+ };
21
+
22
+ /** No-op TokenProvider — SnippetLinter injects tokens via TokenRegistry directly. */
23
+ const stubTokenProvider = {
24
+ load: () => Promise.resolve({}),
25
+ };
26
+
27
+ /**
28
+ * A stripped-down `Linter` subclass used internally by `RuleTester`.
29
+ *
30
+ * Overrides `buildRuleContexts` to inject a single rule under test, bypassing
31
+ * the `RuleRegistry` entirely. This allows testing rules that are not in the
32
+ * built-in rule set.
33
+ *
34
+ * Optionally accepts a list of {@link DtifFlattenedToken}s to inject into the
35
+ * token registry, enabling `RuleTester` coverage of token-based rules that
36
+ * need a non-empty token set to exercise their real validation logic (rather
37
+ * than the "configure tokens" early-exit path).
38
+ */
39
+ export class SnippetLinter extends Linter {
40
+ readonly #injected: EnabledRule;
41
+
42
+ constructor(
43
+ rule: RuleModule,
44
+ options: unknown = undefined,
45
+ tokens: DtifFlattenedToken[] = [],
46
+ ) {
47
+ super({ rules: {} }, { documentSource: stubSource, tokenProvider: stubTokenProvider });
48
+ this.#injected = { rule, options, severity: 'error' };
49
+ if (tokens.length > 0) {
50
+ // The base class's tokensReady promise resolves by setting this.tokenRegistry
51
+ // from the (empty) config tokens. We chain an additional .then() here so
52
+ // our injected registry is applied AFTER the base-class resolution, winning
53
+ // the assignment race.
54
+ const registry = new TokenRegistry({ default: tokens });
55
+ this.tokensReady = this.tokensReady.then(() => {
56
+ this.tokenRegistry = registry;
57
+ });
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Overrides the base `buildRuleContexts` to always use the injected rule
63
+ * instead of whatever the registry resolves.
64
+ */
65
+ protected override buildRuleContexts(
66
+ _enabled: EnabledRule[],
67
+ sourceId: string,
68
+ metadata?: Record<string, unknown>,
69
+ ): ReturnType<Linter['buildRuleContexts']> {
70
+ return super.buildRuleContexts([this.#injected], sourceId, metadata);
71
+ }
72
+
73
+ /**
74
+ * Lints a code snippet and returns the diagnostics produced by the injected rule.
75
+ *
76
+ * @param code - Source code to lint.
77
+ * @param ext - File extension used to select the correct parser (e.g. `"css"`, `"tsx"`).
78
+ */
79
+ async lintSnippet(code: string, ext: string): Promise<LintMessage[]> {
80
+ const doc: LintDocument = {
81
+ id: `snippet.${ext}`,
82
+ type: ext,
83
+ getText: async () => code,
84
+ };
85
+ const result: LintResult = await this.lintDocument(doc);
86
+ return result.messages;
87
+ }
88
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "declaration": true,
12
+ "declarationMap": true,
13
+ "types": ["node"]
14
+ },
15
+ "include": ["src/**/*"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }