Skip to content

Commit c91c6ad

Browse files
fiskersindresorhus
andauthored
Add test.only helper to test a single case (#2236)
Co-authored-by: Sindre Sorhus <[email protected]>
1 parent 9d7048c commit c91c6ad

File tree

4 files changed

+120
-20
lines changed

4 files changed

+120
-20
lines changed

scripts/internal-rules/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const RULES_DIRECTORIES = [
1313
const rules = [
1414
{id: 'fix-snapshot-test', directories: TEST_DIRECTORIES},
1515
{id: 'prefer-negative-boolean-attribute', directories: RULES_DIRECTORIES},
16+
{id: 'no-test-only', directories: TEST_DIRECTORIES},
1617
];
1718

1819
const isFileInsideDirectory = (filename, directory) => filename.startsWith(directory + path.sep);
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
'use strict';
2+
const path = require('node:path');
3+
4+
const messageId = path.basename(__filename, '.js');
5+
6+
module.exports = {
7+
create(context) {
8+
if (path.basename(context.physicalFilename) === 'snapshot-rule-tester.mjs') {
9+
return {};
10+
}
11+
12+
return {
13+
MemberExpression(node) {
14+
if (
15+
!(
16+
!node.computed
17+
&& !node.optional
18+
&& node.object.type === 'Identifier'
19+
&& node.object.name === 'test'
20+
&& node.property.type === 'Identifier'
21+
&& node.property.name === 'only'
22+
)
23+
) {
24+
return;
25+
}
26+
27+
const isTaggedTemplateExpression = node.parent.type === 'TaggedTemplateExpression' && node.parent.tag === node;
28+
const isCallee = !isTaggedTemplateExpression
29+
&& node.parent.type === 'CallExpression'
30+
&& node.parent.callee === node
31+
&& !node.parent.optional
32+
&& node.parent.arguments.length === 1;
33+
34+
const problem = {node, messageId};
35+
36+
if (isTaggedTemplateExpression) {
37+
problem.fix = fixer => fixer.remove(node);
38+
}
39+
40+
if (isCallee) {
41+
problem.fix = function * (fixer) {
42+
const {sourceCode} = context;
43+
const openingParenToken = sourceCode.getTokenAfter(node);
44+
const closingParenToken = sourceCode.getLastToken(node.parent);
45+
if (openingParenToken.value !== '(' || closingParenToken.value !== ')') {
46+
return;
47+
}
48+
49+
yield fixer.remove(node);
50+
yield fixer.remove(openingParenToken);
51+
yield fixer.remove(closingParenToken);
52+
53+
// Trailing comma
54+
const tokenBefore = sourceCode.getTokenBefore(closingParenToken);
55+
56+
if (tokenBefore.value !== ',') {
57+
return;
58+
}
59+
60+
yield fixer.remove(tokenBefore);
61+
};
62+
}
63+
64+
context.report(problem);
65+
},
66+
};
67+
},
68+
meta: {
69+
fixable: 'code',
70+
messages: {
71+
[messageId]: '`test.only` should only be used for debugging purposes. Please remove it before committing.',
72+
},
73+
},
74+
};

test/utils/snapshot-rule-tester.mjs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ function normalizeTests(tests) {
6060

6161
const additionalProperties = getAdditionalProperties(
6262
testCase,
63-
['code', 'options', 'filename', 'parserOptions', 'parser', 'globals'],
63+
['code', 'options', 'filename', 'parserOptions', 'parser', 'globals', 'only'],
6464
);
6565

6666
if (additionalProperties.length > 0) {
@@ -154,11 +154,11 @@ class SnapshotRuleTester {
154154
const {valid, invalid} = normalizeTests(tests);
155155

156156
for (const [index, testCase] of valid.entries()) {
157-
const {code, filename} = testCase;
157+
const {code, filename, only} = testCase;
158158
const verifyConfig = getVerifyConfig(ruleId, config, testCase);
159159
defineParser(linter, verifyConfig.parser);
160160

161-
test(
161+
(only ? test.only : test)(
162162
`valid(${index + 1}): ${code}`,
163163
t => {
164164
const messages = verify(linter, code, verifyConfig, {filename});
@@ -168,12 +168,12 @@ class SnapshotRuleTester {
168168
}
169169

170170
for (const [index, testCase] of invalid.entries()) {
171-
const {code, options, filename} = testCase;
171+
const {code, options, filename, only} = testCase;
172172
const verifyConfig = getVerifyConfig(ruleId, config, testCase);
173173
defineParser(linter, verifyConfig.parser);
174174
const runVerify = code => verify(linter, code, verifyConfig, {filename});
175175

176-
test(
176+
(only ? test.only : test)(
177177
`invalid(${index + 1}): ${code}`,
178178
t => {
179179
const messages = runVerify(code);

test/utils/test.mjs

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import SnapshotRuleTester from './snapshot-rule-tester.mjs';
77
import defaultOptions from './default-options.mjs';
88
import parsers from './parsers.mjs';
99

10-
function normalizeTests(tests) {
11-
return tests.map(test => typeof test === 'string' ? {code: test} : test);
10+
function normalizeTestCase(testCase) {
11+
return typeof testCase === 'string' ? {code: testCase} : {...testCase};
1212
}
1313

1414
function normalizeInvalidTest(test, rule) {
@@ -34,22 +34,45 @@ function normalizeInvalidTest(test, rule) {
3434
}
3535

3636
function normalizeParser(options) {
37-
const {
37+
let {
3838
parser,
3939
parserOptions,
4040
} = options;
4141

4242
if (parser) {
43-
if (parser.name) {
44-
options.parser = parser.name;
43+
if (parser.mergeParserOptions) {
44+
parserOptions = parser.mergeParserOptions(parserOptions);
4545
}
4646

47-
if (parser.mergeParserOptions) {
48-
options.parserOptions = parser.mergeParserOptions(parserOptions);
47+
if (parser.name) {
48+
parser = parser.name;
4949
}
5050
}
5151

52-
return options;
52+
return {...options, parser, parserOptions};
53+
}
54+
55+
// https://github.com/tc39/proposal-array-is-template-object
56+
const isTemplateObject = value => Array.isArray(value?.raw);
57+
// https://github.com/tc39/proposal-string-cooked
58+
const cooked = (raw, ...substitutions) => String.raw({raw}, ...substitutions);
59+
60+
function only(...arguments_) {
61+
/*
62+
```js
63+
only`code`;
64+
```
65+
*/
66+
if (isTemplateObject(arguments_[0])) {
67+
return {code: cooked(...arguments_), only: true};
68+
}
69+
70+
/*
71+
```js
72+
only('code');
73+
only({code: 'code'});
74+
*/
75+
return {...normalizeTestCase(arguments_[0]), only: true};
5376
}
5477

5578
class Tester {
@@ -98,8 +121,8 @@ class Tester {
98121
} = tests;
99122

100123
testerOptions = normalizeParser(testerOptions);
101-
valid = normalizeTests(valid).map(test => normalizeParser(test));
102-
invalid = normalizeTests(invalid).map(test => normalizeParser(test));
124+
valid = valid.map(testCase => normalizeParser(normalizeTestCase(testCase)));
125+
invalid = invalid.map(testCase => normalizeParser(normalizeTestCase(testCase)));
103126

104127
const tester = new SnapshotRuleTester(test, {
105128
...testerOptions,
@@ -126,6 +149,7 @@ function getTester(importMeta) {
126149
const tester = new Tester(ruleId);
127150
const runTest = Tester.prototype.runTest.bind(tester);
128151
runTest.snapshot = Tester.prototype.snapshot.bind(tester);
152+
runTest.only = only;
129153

130154
for (const [parserName, parserSettings] of Object.entries(parsers)) {
131155
Reflect.defineProperty(runTest, parserName, {
@@ -152,10 +176,11 @@ function getTester(importMeta) {
152176
};
153177
}
154178

155-
const addComment = (test, comment) => {
156-
const {code, output} = test;
179+
const addComment = (testCase, comment) => {
180+
testCase = normalizeTestCase(testCase);
181+
const {code, output} = testCase;
157182
const fixedTest = {
158-
...test,
183+
...testCase,
159184
code: `${code}\n/* ${comment} */`,
160185
};
161186
if (Object.prototype.hasOwnProperty.call(fixedTest, 'output') && typeof output === 'string') {
@@ -169,8 +194,8 @@ const avoidTestTitleConflict = (tests, comment) => {
169194
const {valid, invalid} = tests;
170195
return {
171196
...tests,
172-
valid: normalizeTests(valid).map(test => addComment(test, comment)),
173-
invalid: normalizeTests(invalid).map(test => addComment(test, comment)),
197+
valid: valid.map(testCase => addComment(testCase, comment)),
198+
invalid: invalid.map(testCase => addComment(testCase, comment)),
174199
};
175200
};
176201

0 commit comments

Comments
 (0)