Skip to content

Commit ca7a6d3

Browse files
committed
test(linter/plugins): automate testing all fixtures (#15911)
Remove the need to list every fixture in `e2e.test.ts` file by automatically running tests on every fixture in `test/fixtures` directory. Running Oxlint with `--fix` and running fixtures with ESLint is now controlled by adding an `options.json` file to the fixture directory contining e.g. `{ "fix": true }` or `{ "eslint": true }`. Now we can add a new fixture just by creating a new directory in `fixtures`.
1 parent 3a6e8c7 commit ca7a6d3

File tree

9 files changed

+184
-307
lines changed

9 files changed

+184
-307
lines changed

apps/oxlint/test/e2e.test.ts

Lines changed: 45 additions & 251 deletions
Original file line numberDiff line numberDiff line change
@@ -1,265 +1,59 @@
1-
// oxlint-disable jest/expect-expect
2-
3-
import fs from 'node:fs/promises';
41
import { join as pathJoin } from 'node:path';
52
import { describe, it } from 'vitest';
6-
import { FIXTURES_DIR_PATH, PACKAGE_ROOT_PATH, testFixtureWithCommand } from './utils.js';
3+
import { PACKAGE_ROOT_PATH, getFixtures, testFixtureWithCommand } from './utils.js';
4+
5+
import type { Fixture } from './utils.ts';
76

87
const CLI_PATH = pathJoin(PACKAGE_ROOT_PATH, 'dist/cli.js');
98

10-
// Options to pass to `testFixture`.
11-
interface TestOptions {
12-
// Arguments to pass to the CLI.
13-
args?: string[];
14-
// Name of the snapshot file.
15-
// Defaults to `output`.
16-
// Supply a different name when there are multiple tests for a single fixture.
17-
snapshotName?: string;
18-
// Function to get extra data to include in the snapshot
19-
getExtraSnapshotData?: (dirPath: string) => Promise<Record<string, string>>;
20-
}
9+
// Use current NodeJS executable, rather than `node`, to avoid problems with a Node version manager
10+
// installed on system resulting in using wrong NodeJS version
11+
const NODE_BIN_PATH = process.execPath;
2112

2213
/**
23-
* Run a test fixture.
24-
* @param fixtureName - Name of the fixture directory within `test/fixtures`
25-
* @param options - Options to customize the test (optional)
14+
* Run Oxlint tests for all fixtures in `test/fixtures`.
15+
*
16+
* Oxlint is run with:
17+
* - CWD set to the fixture directory.
18+
* - `files` as the only argument (so only lints the files in the fixture's `files` directory).
19+
*
20+
* Fixtures with an `options.json` file containing `"fix": true` are also run with `--fix` CLI option.
21+
* The files' contents after fixes are recorded in the snapshot.
22+
*
23+
* Fixtures with an `options.json` file containing `"oxlint": false` are skipped.
2624
*/
27-
async function testFixture(fixtureName: string, options?: TestOptions): Promise<void> {
28-
const args = options?.args ?? [];
29-
30-
await testFixtureWithCommand({
31-
// Use current NodeJS executable, rather than `node`, to avoid problems with a Node version manager
32-
// installed on system resulting in using wrong NodeJS version
33-
command: process.execPath,
34-
args: [CLI_PATH, ...args, 'files'],
35-
fixtureName,
36-
snapshotName: options?.snapshotName ?? 'output',
37-
getExtraSnapshotData: options?.getExtraSnapshotData,
38-
isESLint: false,
39-
});
40-
}
41-
4225
describe('oxlint CLI', () => {
43-
it('should lint a directory without errors', async () => {
44-
await testFixture('built_in_no_errors');
45-
});
46-
47-
it('should lint a directory with errors', async () => {
48-
await testFixture('built_in_errors');
49-
});
50-
51-
it('should load a custom plugin', async () => {
52-
await testFixture('basic_custom_plugin');
53-
});
54-
55-
it('should support message placeholder interpolation', async () => {
56-
await testFixture('message_interpolation');
57-
});
58-
59-
it('should support messageId', async () => {
60-
await testFixture('message_id_plugin');
61-
});
62-
63-
it('should support messageId placeholder interpolation', async () => {
64-
await testFixture('message_id_interpolation');
65-
});
66-
67-
it('should report an error for unknown messageId', async () => {
68-
await testFixture('message_id_error');
69-
});
70-
71-
it('should load a custom plugin with various import styles', async () => {
72-
await testFixture('load_paths');
73-
});
74-
75-
it('should load a custom plugin with multiple files', async () => {
76-
await testFixture('basic_custom_plugin_many_files');
77-
});
78-
79-
it('should load a custom plugin correctly when extending in a nested config', async () => {
80-
await testFixture('custom_plugin_nested_config');
81-
});
82-
it('should do something', async () => {
83-
await testFixture('custom_plugin_nested_config_duplicate');
84-
});
85-
86-
it('should load a custom plugin when configured in overrides', async () => {
87-
await testFixture('custom_plugin_via_overrides');
88-
});
89-
90-
it('should report an error if a custom plugin is missing', async () => {
91-
await testFixture('missing_custom_plugin');
92-
});
93-
94-
it('should report an error if a custom plugin has a reserved name', async () => {
95-
await testFixture('reserved_name');
96-
});
97-
98-
it('should report an error if a custom plugin throws an error during import', async () => {
99-
await testFixture('custom_plugin_import_error');
100-
});
101-
102-
it('should report an error if a rule is not found within a custom plugin', async () => {
103-
await testFixture('custom_plugin_missing_rule');
104-
});
105-
106-
it('should report an error if a a rule is not found within a custom plugin (via overrides)', async () => {
107-
await testFixture('custom_plugin_via_overrides_missing_rule');
108-
});
109-
110-
describe('should report an error if a custom plugin throws an error during linting', () => {
111-
it('in `create` method', async () => {
112-
await testFixture('custom_plugin_lint_create_error');
113-
});
114-
115-
it('in `createOnce` method', async () => {
116-
await testFixture('custom_plugin_lint_createOnce_error');
117-
});
118-
119-
it('in visit function', async () => {
120-
await testFixture('custom_plugin_lint_visit_error');
121-
});
122-
123-
it('in `before` hook', async () => {
124-
await testFixture('custom_plugin_lint_before_hook_error');
125-
});
126-
127-
it('in `after` hook', async () => {
128-
await testFixture('custom_plugin_lint_after_hook_error');
129-
});
130-
131-
it('in `fix` function', async () => {
132-
await testFixture('custom_plugin_lint_fix_error');
133-
});
134-
});
135-
136-
it('should report the correct severity when using a custom plugin', async () => {
137-
await testFixture('basic_custom_plugin_warn_severity');
138-
});
26+
const fixtures = getFixtures();
27+
for (const fixture of fixtures) {
28+
if (!fixture.options.oxlint) continue;
13929

140-
it('should work with multiple rules', async () => {
141-
await testFixture('basic_custom_plugin_multiple_rules');
142-
});
143-
144-
it('should support reporting diagnostic with `loc`', async () => {
145-
await testFixture('diagnostic_loc');
146-
});
147-
148-
it('should receive ESTree-compatible AST', async () => {
149-
await testFixture('estree');
150-
});
151-
152-
it('should receive AST with all nodes having `parent` property', async () => {
153-
await testFixture('parent');
154-
});
155-
156-
it('should receive data via `context`', async () => {
157-
await testFixture('context_properties');
158-
});
159-
160-
it('should give access to source code via `context.sourceCode`', async () => {
161-
await testFixture('sourceCode');
162-
});
163-
164-
it('should give access to settings via `context.settings`', async () => {
165-
await testFixture('settings');
166-
});
167-
168-
it('should get source text and AST from `context.sourceCode` when accessed late', async () => {
169-
await testFixture('sourceCode_late_access');
170-
});
171-
172-
it('should get source text and AST from `context.sourceCode` when accessed in `after` hook only', async () => {
173-
await testFixture('sourceCode_late_access_after_only');
174-
});
175-
176-
it('should support scopeManager', async () => {
177-
await testFixture('scope_manager');
178-
});
179-
180-
it('should support scope helper methods in `context.sourceCode`', async () => {
181-
await testFixture('sourceCode_scope_methods');
182-
});
183-
184-
it('should support languageOptions', async () => {
185-
await testFixture('languageOptions');
186-
});
187-
188-
it('should support selectors', async () => {
189-
await testFixture('selector');
190-
});
191-
192-
it('should support `createOnce`', async () => {
193-
await testFixture('createOnce');
194-
});
195-
196-
it('should support `definePlugin`', async () => {
197-
await testFixture('definePlugin');
198-
});
199-
200-
it('should support `defineRule`', async () => {
201-
await testFixture('defineRule');
202-
});
203-
204-
it('should support `definePlugin` and `defineRule` together', async () => {
205-
await testFixture('definePlugin_and_defineRule');
206-
});
207-
208-
it('should have UTF-16 spans in AST', async () => {
209-
await testFixture('utf16_offsets');
210-
});
30+
// oxlint-disable-next-line jest/expect-expect
31+
it(`fixture: ${fixture.name}`, () => runFixture(fixture));
32+
}
33+
});
21134

212-
it('should respect disable directives for custom plugin rules', async () => {
213-
await testFixture('custom_plugin_disable_directives');
35+
/**
36+
* Run Oxlint on a test fixture.
37+
* @param fixture - Fixture object
38+
*/
39+
async function runFixture(fixture: Fixture): Promise<void> {
40+
// Run Oxlint without `--fix` option
41+
await testFixtureWithCommand({
42+
command: NODE_BIN_PATH,
43+
args: [CLI_PATH, 'files'],
44+
fixture,
45+
snapshotName: 'output',
46+
isESLint: false,
21447
});
21548

216-
it('should not apply fixes when `--fix` is disabled', async () => {
217-
await testFixture('fixes', {
218-
snapshotName: 'fixes_disabled',
219-
async getExtraSnapshotData(fixtureDirPath) {
220-
const fixtureFilePath = pathJoin(fixtureDirPath, 'files/index.js');
221-
const codeAfter = await fs.readFile(fixtureFilePath, 'utf8');
222-
return { 'Code after': codeAfter };
223-
},
49+
// Run Oxlint with `--fix` option
50+
if (fixture.options.fix) {
51+
await testFixtureWithCommand({
52+
command: NODE_BIN_PATH,
53+
args: [CLI_PATH, '--fix', 'files'],
54+
fixture,
55+
snapshotName: 'fix',
56+
isESLint: false,
22457
});
225-
});
226-
227-
it('should apply fixes when `--fix` is enabled', async () => {
228-
const fixtureFilePath = pathJoin(FIXTURES_DIR_PATH, 'fixes/files/index.js');
229-
const codeBefore = await fs.readFile(fixtureFilePath, 'utf8');
230-
231-
try {
232-
await testFixture('fixes', {
233-
args: ['--fix'],
234-
snapshotName: 'fixes_enabled',
235-
async getExtraSnapshotData() {
236-
const codeAfter = await fs.readFile(fixtureFilePath, 'utf8');
237-
return { 'Code after': codeAfter };
238-
},
239-
});
240-
} finally {
241-
// Revert fixture file code changes
242-
await fs.writeFile(fixtureFilePath, codeBefore);
243-
}
244-
});
245-
246-
it('should support comments-related APIs in `context.sourceCode`', async () => {
247-
await testFixture('comments');
248-
});
249-
250-
it('should support UTF16 characters in source code and comments with correct spans', async () => {
251-
await testFixture('unicode_comments');
252-
});
253-
254-
it('should return empty object for `parserServices` without throwing', async () => {
255-
await testFixture('parser_services');
256-
});
257-
258-
it('wrapping context should work', async () => {
259-
await testFixture('context_wrapping');
260-
});
261-
262-
it('should support `isSpaceBetween` in `context.sourceCode`', async () => {
263-
await testFixture('isSpaceBetween');
264-
});
265-
});
58+
}
59+
}

apps/oxlint/test/eslint-compat.test.ts

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,39 @@
1-
// oxlint-disable jest/expect-expect
2-
31
import { join as pathJoin } from 'node:path';
42
import { describe, it } from 'vitest';
5-
import { testFixtureWithCommand } from './utils.js';
3+
import { PACKAGE_ROOT_PATH, getFixtures, testFixtureWithCommand } from './utils.js';
4+
5+
import type { Fixture } from './utils.ts';
6+
7+
const ESLINT_PATH = pathJoin(PACKAGE_ROOT_PATH, 'node_modules/.bin/eslint');
8+
9+
/**
10+
* Run ESLint tests for all fixtures in `test/fixtures` which contain an `options.json` file
11+
* containing `"eslint": true`.
12+
*
13+
* ESLint is run with CWD set to the fixture directory.
14+
*/
15+
// These tests take longer than 5 seconds on CI, so increase timeout to 10 seconds
16+
// oxlint-disable-next-line jest/valid-describe-callback
17+
describe('ESLint compatibility', { timeout: 10_000 }, () => {
18+
const fixtures = getFixtures();
19+
for (const fixture of fixtures) {
20+
if (!fixture.options.eslint) continue;
621

7-
const ESLINT_PATH = pathJoin(import.meta.dirname, '../node_modules/.bin/eslint');
22+
// oxlint-disable-next-line jest/expect-expect
23+
it(`fixture: ${fixture.name}`, () => runFixture(fixture));
24+
}
25+
});
826

927
/**
1028
* Run ESLint on a test fixture.
11-
* @param fixtureName - Name of the fixture directory within `test/fixtures`
29+
* @param fixture - Fixture object
1230
*/
13-
async function testFixture(fixtureName: string): Promise<void> {
31+
async function runFixture(fixture: Fixture): Promise<void> {
1432
await testFixtureWithCommand({
1533
command: ESLINT_PATH,
1634
args: [],
17-
fixtureName,
35+
fixture,
1836
snapshotName: 'eslint',
1937
isESLint: true,
2038
});
2139
}
22-
23-
// These tests take longer than 5 seconds on CI, so increase timeout to 10 seconds
24-
// oxlint-disable-next-line jest/valid-describe-callback
25-
describe('ESLint compatibility', { timeout: 10_000 }, () => {
26-
it('`definePlugin` should work', async () => {
27-
await testFixture('definePlugin');
28-
});
29-
30-
it('`defineRule` should work', async () => {
31-
await testFixture('defineRule');
32-
});
33-
34-
it('`definePlugin` and `defineRule` together should work', async () => {
35-
await testFixture('definePlugin_and_defineRule');
36-
});
37-
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"eslint": true
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"eslint": true
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"eslint": true
3+
}

apps/oxlint/test/fixtures/fixes/fixes_enabled.snap.md renamed to apps/oxlint/test/fixtures/fixes/fix.snap.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ WARNING: JS plugins are experimental and not subject to semver.
1313
Breaking changes are possible while JS plugins support is under development.
1414
```
1515

16-
# Code after
16+
# File altered: files/index.js
1717
```
1818
1919
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"fix": true
3+
}

0 commit comments

Comments
 (0)