diff --git a/docs/src/integrate/nodejs-api.md b/docs/src/integrate/nodejs-api.md index ebe08109269..ce0890c79b6 100644 --- a/docs/src/integrate/nodejs-api.md +++ b/docs/src/integrate/nodejs-api.md @@ -735,19 +735,19 @@ A test case is an object with the following properties: In addition to the properties above, invalid test cases can also have the following properties: -* `errors` (number or array, required): Asserts some properties of the errors that the rule is expected to produce when run on this code. If this is a number, asserts the number of errors produced. Otherwise, this should be a list of objects, each containing information about a single reported error. The following properties can be used for an error (all are optional): - * `message` (string/regexp): The message for the error - * `messageId` (string): The Id for the error. See [testing errors with messageId](#testing-errors-with-messageid) for details +* `errors` (number or array, required): Asserts some properties of the errors that the rule is expected to produce when run on this code. If this is a number, asserts the number of errors produced. Otherwise, this should be a list of objects, each containing information about a single reported error. The following properties can be used for an error (all are optional unless otherwise noted): + * `message` (string/regexp): The message for the error. Must provide this or `messageId` + * `messageId` (string): The Id for the error. Must provide this or `message`. See [testing errors with messageId](#testing-errors-with-messageid) for details * `data` (object): Placeholder data which can be used in combination with `messageId` * `type` (string): The type of the reported AST node * `line` (number): The 1-based line number of the reported location * `column` (number): The 1-based column number of the reported location * `endLine` (number): The 1-based line number of the end of the reported location * `endColumn` (number): The 1-based column number of the end of the reported location - * `suggestions` (array): An array of objects with suggestion details to check. See [Testing Suggestions](#testing-suggestions) for details + * `suggestions` (array): An array of objects with suggestion details to check. Required if the rule produces suggestions. See [Testing Suggestions](#testing-suggestions) for details If a string is provided as an error instead of an object, the string is used to assert the `message` of the error. -* `output` (string, required if the rule fixes code): Asserts the output that will be produced when using this rule for a single pass of autofixing (e.g. with the `--fix` command line flag). If this is `null`, asserts that none of the reported problems suggest autofixes. +* `output` (string, required if the rule fixes code): Asserts the output that will be produced when using this rule for a single pass of autofixing (e.g. with the `--fix` command line flag). If this is `null` or omitted, asserts that none of the reported problems suggest autofixes. Any additional properties of a test case will be passed directly to the linter as config options. For example, a test case can have a `languageOptions` property to configure parser behavior: @@ -784,12 +784,12 @@ Please note that `data` in a test case does not assert `data` passed to `context ### Testing Suggestions -Suggestions can be tested by defining a `suggestions` key on an errors object. The options to check for the suggestions are the following (all are optional): +Suggestions can be tested by defining a `suggestions` key on an errors object. If this is a number, it asserts the number of suggestions provided for the error. Otherwise, this should be an array of objects, each containing information about a single provided suggestion. The following properties can be used: -* `desc` (string): The suggestion `desc` value -* `messageId` (string): The suggestion `messageId` value for suggestions that use `messageId`s -* `data` (object): Placeholder data which can be used in combination with `messageId` -* `output` (string): A code string representing the result of applying the suggestion fix to the input code +* `desc` (string): The suggestion `desc` value. Must provide this or `messageId` +* `messageId` (string): The suggestion `messageId` value for suggestions that use `messageId`s. Must provide this or `desc` +* `data` (object): Placeholder data which can be used in combination with `messageId`. +* `output` (string, required): A code string representing the result of applying the suggestion fix to the input code Example: diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index 97e4ef32b7a..bc728159f03 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -645,7 +645,11 @@ class RuleTester { configs.push(itemConfig); } - if (item.filename) { + if (hasOwnProperty(item, "only")) { + assert.ok(typeof item.only === "boolean", "Optional test case property 'only' must be a boolean"); + } + if (hasOwnProperty(item, "filename")) { + assert.ok(typeof item.filename === "string", "Optional test case property 'filename' must be a string"); filename = item.filename; } @@ -960,6 +964,7 @@ class RuleTester { // Just an error message. assertMessageMatches(message.message, error); + assert.ok(message.suggestions === void 0, `Error at index ${i} has suggestions. Please convert the test error into an object and specify 'suggestions' property on it to test suggestions.`); } else if (typeof error === "object" && error !== null) { /* @@ -1008,13 +1013,10 @@ class RuleTester { `Hydrated message "${rehydratedMessage}" does not match "${message.message}"` ); } + } else { + assert.fail("Test error must specify either a 'messageId' or 'message'."); } - assert.ok( - hasOwnProperty(error, "data") ? hasOwnProperty(error, "messageId") : true, - "Error must specify 'messageId' if 'data' is used." - ); - if (error.type) { assert.strictEqual(message.nodeType, error.type, `Error type should be ${error.type}, found ${message.nodeType}`); } @@ -1035,81 +1037,91 @@ class RuleTester { assert.strictEqual(message.endColumn, error.endColumn, `Error endColumn should be ${error.endColumn}`); } + assert.ok(!message.suggestions || hasOwnProperty(error, "suggestions"), `Error at index ${i} has suggestions. Please specify 'suggestions' property on the test error object.`); if (hasOwnProperty(error, "suggestions")) { // Support asserting there are no suggestions - if (!error.suggestions || (Array.isArray(error.suggestions) && error.suggestions.length === 0)) { - if (Array.isArray(message.suggestions) && message.suggestions.length > 0) { - assert.fail(`Error should have no suggestions on error with message: "${message.message}"`); - } - } else { - assert.strictEqual(Array.isArray(message.suggestions), true, `Error should have an array of suggestions. Instead received "${message.suggestions}" on error with message: "${message.message}"`); - assert.strictEqual(message.suggestions.length, error.suggestions.length, `Error should have ${error.suggestions.length} suggestions. Instead found ${message.suggestions.length} suggestions`); - - error.suggestions.forEach((expectedSuggestion, index) => { - assert.ok( - typeof expectedSuggestion === "object" && expectedSuggestion !== null, - "Test suggestion in 'suggestions' array must be an object." - ); - Object.keys(expectedSuggestion).forEach(propertyName => { + const expectsSuggestions = Array.isArray(error.suggestions) ? error.suggestions.length > 0 : Boolean(error.suggestions); + const hasSuggestions = message.suggestions !== void 0; + + if (!hasSuggestions && expectsSuggestions) { + assert.ok(!error.suggestions, `Error should have suggestions on error with message: "${message.message}"`); + } else if (hasSuggestions) { + assert.ok(expectsSuggestions, `Error should have no suggestions on error with message: "${message.message}"`); + if (typeof error.suggestions === "number") { + assert.strictEqual(message.suggestions.length, error.suggestions, `Error should have ${error.suggestions} suggestions. Instead found ${message.suggestions.length} suggestions`); + } else if (Array.isArray(error.suggestions)) { + assert.strictEqual(message.suggestions.length, error.suggestions.length, `Error should have ${error.suggestions.length} suggestions. Instead found ${message.suggestions.length} suggestions`); + + error.suggestions.forEach((expectedSuggestion, index) => { assert.ok( - suggestionObjectParameters.has(propertyName), - `Invalid suggestion property name '${propertyName}'. Expected one of ${friendlySuggestionObjectParameterList}.` + typeof expectedSuggestion === "object" && expectedSuggestion !== null, + "Test suggestion in 'suggestions' array must be an object." ); - }); + Object.keys(expectedSuggestion).forEach(propertyName => { + assert.ok( + suggestionObjectParameters.has(propertyName), + `Invalid suggestion property name '${propertyName}'. Expected one of ${friendlySuggestionObjectParameterList}.` + ); + }); - const actualSuggestion = message.suggestions[index]; - const suggestionPrefix = `Error Suggestion at index ${index} :`; - - if (hasOwnProperty(expectedSuggestion, "desc")) { - assert.ok( - !hasOwnProperty(expectedSuggestion, "data"), - `${suggestionPrefix} Test should not specify both 'desc' and 'data'.` - ); - assert.strictEqual( - actualSuggestion.desc, - expectedSuggestion.desc, - `${suggestionPrefix} desc should be "${expectedSuggestion.desc}" but got "${actualSuggestion.desc}" instead.` - ); - } - - if (hasOwnProperty(expectedSuggestion, "messageId")) { - assert.ok( - ruleHasMetaMessages, - `${suggestionPrefix} Test can not use 'messageId' if rule under test doesn't define 'meta.messages'.` - ); - assert.ok( - hasOwnProperty(rule.meta.messages, expectedSuggestion.messageId), - `${suggestionPrefix} Test has invalid messageId '${expectedSuggestion.messageId}', the rule under test allows only one of ${friendlyIDList}.` - ); - assert.strictEqual( - actualSuggestion.messageId, - expectedSuggestion.messageId, - `${suggestionPrefix} messageId should be '${expectedSuggestion.messageId}' but got '${actualSuggestion.messageId}' instead.` - ); - if (hasOwnProperty(expectedSuggestion, "data")) { - const unformattedMetaMessage = rule.meta.messages[expectedSuggestion.messageId]; - const rehydratedDesc = interpolate(unformattedMetaMessage, expectedSuggestion.data); + const actualSuggestion = message.suggestions[index]; + const suggestionPrefix = `Error Suggestion at index ${index}:`; + if (hasOwnProperty(expectedSuggestion, "desc")) { + assert.ok( + !hasOwnProperty(expectedSuggestion, "data"), + `${suggestionPrefix} Test should not specify both 'desc' and 'data'.` + ); + assert.ok( + !hasOwnProperty(expectedSuggestion, "messageId"), + `${suggestionPrefix} Test should not specify both 'desc' and 'messageId'.` + ); assert.strictEqual( actualSuggestion.desc, - rehydratedDesc, - `${suggestionPrefix} Hydrated test desc "${rehydratedDesc}" does not match received desc "${actualSuggestion.desc}".` + expectedSuggestion.desc, + `${suggestionPrefix} desc should be "${expectedSuggestion.desc}" but got "${actualSuggestion.desc}" instead.` + ); + } else if (hasOwnProperty(expectedSuggestion, "messageId")) { + assert.ok( + ruleHasMetaMessages, + `${suggestionPrefix} Test can not use 'messageId' if rule under test doesn't define 'meta.messages'.` + ); + assert.ok( + hasOwnProperty(rule.meta.messages, expectedSuggestion.messageId), + `${suggestionPrefix} Test has invalid messageId '${expectedSuggestion.messageId}', the rule under test allows only one of ${friendlyIDList}.` + ); + assert.strictEqual( + actualSuggestion.messageId, + expectedSuggestion.messageId, + `${suggestionPrefix} messageId should be '${expectedSuggestion.messageId}' but got '${actualSuggestion.messageId}' instead.` + ); + if (hasOwnProperty(expectedSuggestion, "data")) { + const unformattedMetaMessage = rule.meta.messages[expectedSuggestion.messageId]; + const rehydratedDesc = interpolate(unformattedMetaMessage, expectedSuggestion.data); + + assert.strictEqual( + actualSuggestion.desc, + rehydratedDesc, + `${suggestionPrefix} Hydrated test desc "${rehydratedDesc}" does not match received desc "${actualSuggestion.desc}".` + ); + } + } else if (hasOwnProperty(expectedSuggestion, "data")) { + assert.fail( + `${suggestionPrefix} Test must specify 'messageId' if 'data' is used.` + ); + } else { + assert.fail( + `${suggestionPrefix} Test must specify either 'messageId' or 'desc'.` ); } - } else { - assert.ok( - !hasOwnProperty(expectedSuggestion, "data"), - `${suggestionPrefix} Test must specify 'messageId' if 'data' is used.` - ); - } - if (hasOwnProperty(expectedSuggestion, "output")) { + assert.ok(hasOwnProperty(expectedSuggestion, "output"), `${suggestionPrefix} The "output" property is required.`); const codeWithAppliedSuggestion = SourceCodeFixer.applyFixes(item.code, [actualSuggestion]).output; // Verify if suggestion fix makes a syntax error or not. const errorMessageInSuggestion = - linter.verify(codeWithAppliedSuggestion, result.configs, result.filename).find(m => m.fatal); + linter.verify(codeWithAppliedSuggestion, result.configs, result.filename).find(m => m.fatal); assert(!errorMessageInSuggestion, [ "A fatal parsing error occurred in suggestion fix.", @@ -1119,8 +1131,11 @@ class RuleTester { ].join("\n")); assert.strictEqual(codeWithAppliedSuggestion, expectedSuggestion.output, `Expected the applied suggestion fix to match the test suggestion output for suggestion at index: ${index} on error with message: "${message.message}"`); - } - }); + assert.notStrictEqual(expectedSuggestion.output, item.code, `The output of a suggestion should differ from the original source code for suggestion at index: ${index} on error with message: "${message.message}"`); + }); + } else { + assert.fail("Test error object property 'suggestions' should be an array or a number"); + } } } } else { @@ -1140,6 +1155,7 @@ class RuleTester { ); } else { assert.strictEqual(result.output, item.output, "Output is incorrect."); + assert.notStrictEqual(item.code, item.output, "Test property 'output' matches 'code'. If no autofix is expected, then omit the 'output' property or set it to null."); } } else { assert.strictEqual( diff --git a/tests/fixtures/testers/rule-tester/suggestions.js b/tests/fixtures/testers/rule-tester/suggestions.js index 57cb84f1653..34f404d26d8 100644 --- a/tests/fixtures/testers/rule-tester/suggestions.js +++ b/tests/fixtures/testers/rule-tester/suggestions.js @@ -164,3 +164,37 @@ module.exports.withoutHasSuggestionsProperty = { }; } }; + +module.exports.withFixerWithoutChanges = { + meta: { hasSuggestions: true }, + create(context) { + return { + Identifier(node) { + if (node.name === "foo") { + context.report({ + node, + message: "Avoid using identifiers named 'foo'.", + suggest: [{ + desc: "Rename identifier 'foo' to 'bar'", + fix: fixer => fixer.replaceText(node, 'foo') + }] + }); + } + } + }; + } +}; + +module.exports.withFailingFixer = { + create(context) { + return { + Identifier(node) { + context.report({ + node, + message: "some message", + suggest: [{ desc: "some suggestion", fix: fixer => null }] + }); + } + }; + } +}; diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index 0de8f900267..15820284252 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -580,7 +580,7 @@ describe("RuleTester", () => { "bar = baz;" ], invalid: [ - { code: "var foo = bar; var baz = quux", errors: [{ type: "VariableDeclaration" }, null] } + { code: "var foo = bar; var baz = quux", errors: [{ message: "Bad var.", type: "VariableDeclaration" }, null] } ] }); }, /Error should be a string, object, or RegExp/u); @@ -636,6 +636,26 @@ describe("RuleTester", () => { }); }); + it("should not throw an error when the error is a string and the suggestion fixer is failing", () => { + ruleTester.run("no-var", require("../../fixtures/testers/rule-tester/suggestions").withFailingFixer, { + valid: [], + invalid: [ + { code: "foo", errors: ["some message"] } + ] + }); + }); + + it("throws an error when the error is a string and the suggestion fixer provides a fix", () => { + assert.throws(() => { + ruleTester.run("no-var", require("../../fixtures/testers/rule-tester/suggestions").basic, { + valid: [], + invalid: [ + { code: "foo", errors: ["Avoid using identifiers named 'foo'."] } + ] + }); + }, "Error at index 0 has suggestions. Please convert the test error into an object and specify 'suggestions' property on it to test suggestions."); + }); + it("should throw an error when the error is an object with an unknown property name", () => { assert.throws(() => { ruleTester.run("no-var", require("../../fixtures/testers/rule-tester/no-var"), { @@ -785,6 +805,17 @@ describe("RuleTester", () => { }, /Expected no autofixes to be suggested/u); }); + it("should throw an error when the expected output is not null and the output does not differ from the code", () => { + assert.throws(() => { + ruleTester.run("no-var", require("../../fixtures/testers/rule-tester/no-eval"), { + valid: [], + invalid: [ + { code: "eval('')", output: "eval('')", errors: 1 } + ] + }); + }, "Test property 'output' matches 'code'. If no autofix is expected, then omit the 'output' property or set it to null."); + }); + it("should throw an error when the expected output isn't specified and problems produce output", () => { assert.throws(() => { ruleTester.run("no-var", require("../../fixtures/testers/rule-tester/no-var"), { @@ -1020,14 +1051,28 @@ describe("RuleTester", () => { }, /fatal parsing error/iu); }); - it("should not throw an error if invalid code has at least an expected empty error object", () => { - ruleTester.run("no-eval", require("../../fixtures/testers/rule-tester/no-eval"), { - valid: ["Eval(foo)"], - invalid: [{ - code: "eval(foo)", - errors: [{}] - }] - }); + it("should throw an error if an error object has no properties", () => { + assert.throws(() => { + ruleTester.run("no-eval", require("../../fixtures/testers/rule-tester/no-eval"), { + valid: ["Eval(foo)"], + invalid: [{ + code: "eval(foo)", + errors: [{}] + }] + }); + }, "Test error must specify either a 'messageId' or 'message'."); + }); + + it("should throw an error if an error has a property besides message or messageId", () => { + assert.throws(() => { + ruleTester.run("no-eval", require("../../fixtures/testers/rule-tester/no-eval"), { + valid: ["Eval(foo)"], + invalid: [{ + code: "eval(foo)", + errors: [{ line: 1 }] + }] + }); + }, "Test error must specify either a 'messageId' or 'message'."); }); it("should pass-through the globals config of valid tests to the to rule", () => { @@ -1258,7 +1303,7 @@ describe("RuleTester", () => { languageOptions: { parser: esprima }, - errors: [{ line: 1 }] + errors: [{ message: "eval sucks.", line: 1 }] } ] }); @@ -1892,7 +1937,7 @@ describe("RuleTester", () => { valid: [], invalid: [{ code: "foo", errors: [{ data: "something" }] }] }); - }, "Error must specify 'messageId' if 'data' is used."); + }, "Test error must specify either a 'messageId' or 'message'."); }); // fixable rules with or without `meta` property @@ -1955,6 +2000,20 @@ describe("RuleTester", () => { }); describe("suggestions", () => { + it("should throw if suggestions are available but not specified", () => { + assert.throw(() => { + ruleTester.run("suggestions-basic", require("../../fixtures/testers/rule-tester/suggestions").basic, { + valid: [ + "var boo;" + ], + invalid: [{ + code: "var foo;", + errors: [{ message: "Avoid using identifiers named 'foo'." }] + }] + }); + }, "Error at index 0 has suggestions. Please specify 'suggestions' property on the test error object."); + }); + it("should pass with valid suggestions (tested using desc)", () => { ruleTester.run("suggestions-basic", require("../../fixtures/testers/rule-tester/suggestions").basic, { valid: [ @@ -1963,6 +2022,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [{ desc: "Rename identifier 'foo' to 'bar'", output: "var bar;" @@ -1979,11 +2039,13 @@ describe("RuleTester", () => { { code: "function foo() {\n var foo = 1;\n}", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [{ desc: "Rename identifier 'foo' to 'bar'", output: "function bar() {\n var foo = 1;\n}" }] }, { + message: "Avoid using identifiers named 'foo'.", suggestions: [{ desc: "Rename identifier 'foo' to 'bar'", output: "function foo() {\n var bar = 1;\n}" @@ -2000,6 +2062,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ messageId: "renameFoo", output: "var bar;" @@ -2018,6 +2081,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ messageId: "renameFoo", output: "var bar;" @@ -2030,24 +2094,27 @@ describe("RuleTester", () => { }); }); - it("should pass with valid suggestions (tested using both desc and messageIds for the same suggestion)", () => { - ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{ - desc: "Rename identifier 'foo' to 'bar'", - messageId: "renameFoo", - output: "var bar;" - }, { - desc: "Rename identifier 'foo' to 'baz'", - messageId: "renameFoo", - output: "var baz;" + it("should fail with valid suggestions when testing using both desc and messageIds for the same suggestion", () => { + assert.throw(() => { + ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + messageId: "avoidFoo", + suggestions: [{ + desc: "Rename identifier 'foo' to 'bar'", + messageId: "renameFoo", + output: "var bar;" + }, { + desc: "Rename identifier 'foo' to 'baz'", + messageId: "renameFoo", + output: "var baz;" + }] }] }] - }] - }); + }); + }, "Error Suggestion at index 0: Test should not specify both 'desc' and 'messageId'."); }); it("should pass with valid suggestions (tested using only desc on a rule that utilizes meta.messages)", () => { @@ -2056,6 +2123,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ desc: "Rename identifier 'foo' to 'bar'", output: "var bar;" @@ -2074,6 +2142,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ messageId: "renameFoo", data: { newName: "bar" }, @@ -2089,16 +2158,34 @@ describe("RuleTester", () => { }); - it("should pass when tested using empty suggestion test objects if the array length is correct", () => { - ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{}, {}] + it("should fail when tested using empty suggestion test objects even if the array length is correct", () => { + assert.throw(() => { + ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + messageId: "avoidFoo", + suggestions: [{}, {}] + }] }] - }] - }); + }); + }, "Error Suggestion at index 0: Test must specify either 'messageId' or 'desc'"); + }); + + it("should fail when tested using non-empty suggestion test objects without an output property", () => { + assert.throw(() => { + ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + messageId: "avoidFoo", + suggestions: [{ messageId: "renameFoo" }, {}] + }] + }] + }); + }, 'Error Suggestion at index 0: The "output" property is required.'); }); it("should support explicitly expecting no suggestions", () => { @@ -2108,6 +2195,7 @@ describe("RuleTester", () => { invalid: [{ code: "eval('var foo');", errors: [{ + message: "eval sucks.", suggestions }] }] @@ -2123,6 +2211,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions }] }] @@ -2138,13 +2227,27 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Bad var.", suggestions: [{ messageId: "this-does-not-exist" }] }] }] }); - }, "Error should have an array of suggestions. Instead received \"undefined\" on error with message: \"Bad var.\""); + }, 'Error should have suggestions on error with message: "Bad var."'); + }); + + it("should support specifying only the amount of suggestions", () => { + ruleTester.run("suggestions-basic", require("../../fixtures/testers/rule-tester/suggestions").basic, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + message: "Avoid using identifiers named 'foo'.", + suggestions: 1 + }] + }] + }); }); it("should fail when there are a different number of suggestions", () => { @@ -2154,6 +2257,22 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", + suggestions: 2 + }] + }] + }); + }, "Error should have 2 suggestions. Instead found 1 suggestions"); + }); + + it("should fail when there are a different number of suggestions for arrays", () => { + assert.throws(() => { + ruleTester.run("suggestions-basic", require("../../fixtures/testers/rule-tester/suggestions").basic, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [{ desc: "Rename identifier 'foo' to 'bar'", output: "var bar;" @@ -2167,6 +2286,21 @@ describe("RuleTester", () => { }, "Error should have 2 suggestions. Instead found 1 suggestions"); }); + it("should fail when the suggestion property is neither a number nor an array", () => { + assert.throws(() => { + ruleTester.run("suggestions-basic", require("../../fixtures/testers/rule-tester/suggestions").basic, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + message: "Avoid using identifiers named 'foo'.", + suggestions: "1" + }] + }] + }); + }, "Test error object property 'suggestions' should be an array or a number"); + }); + it("should throw if suggestion fix made a syntax error.", () => { assert.throw(() => { ruleTester.run( @@ -2197,6 +2331,7 @@ describe("RuleTester", () => { invalid: [{ code: "one()", errors: [{ + message: "make a syntax error", suggestions: [{ desc: "make a syntax error", output: "one two()" @@ -2215,6 +2350,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [{ desc: "not right", output: "var baz;" @@ -2222,29 +2358,27 @@ describe("RuleTester", () => { }] }] }); - }, "Error Suggestion at index 0 : desc should be \"not right\" but got \"Rename identifier 'foo' to 'bar'\" instead."); + }, "Error Suggestion at index 0: desc should be \"not right\" but got \"Rename identifier 'foo' to 'bar'\" instead."); }); - it("should throw if the suggestion description doesn't match (although messageIds match)", () => { - assert.throws(() => { - ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{ - desc: "Rename identifier 'foo' to 'bar'", - messageId: "renameFoo", - output: "var bar;" - }, { - desc: "Rename id 'foo' to 'baz'", - messageId: "renameFoo", - output: "var baz;" - }] + + it("should pass when different suggestion matchers use desc and messageId", () => { + ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + messageId: "avoidFoo", + suggestions: [{ + desc: "Rename identifier 'foo' to 'bar'", + output: "var bar;" + }, { + messageId: "renameFoo", + output: "var baz;" }] }] - }); - }, "Error Suggestion at index 1 : desc should be \"Rename id 'foo' to 'baz'\" but got \"Rename identifier 'foo' to 'baz'\" instead."); + }] + }); }); it("should throw if the suggestion messageId doesn't match", () => { @@ -2254,6 +2388,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ messageId: "unused", output: "var bar;" @@ -2264,29 +2399,7 @@ describe("RuleTester", () => { }] }] }); - }, "Error Suggestion at index 0 : messageId should be 'unused' but got 'renameFoo' instead."); - }); - - it("should throw if the suggestion messageId doesn't match (although descriptions match)", () => { - assert.throws(() => { - ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{ - desc: "Rename identifier 'foo' to 'bar'", - messageId: "renameFoo", - output: "var bar;" - }, { - desc: "Rename identifier 'foo' to 'baz'", - messageId: "avoidFoo", - output: "var baz;" - }] - }] - }] - }); - }, "Error Suggestion at index 1 : messageId should be 'avoidFoo' but got 'renameFoo' instead."); + }, "Error Suggestion at index 0: messageId should be 'unused' but got 'renameFoo' instead."); }); it("should throw if test specifies messageId for a rule that doesn't have meta.messages", () => { @@ -2296,6 +2409,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [{ messageId: "renameFoo", output: "var bar;" @@ -2303,7 +2417,7 @@ describe("RuleTester", () => { }] }] }); - }, "Error Suggestion at index 0 : Test can not use 'messageId' if rule under test doesn't define 'meta.messages'."); + }, "Error Suggestion at index 0: Test can not use 'messageId' if rule under test doesn't define 'meta.messages'."); }); it("should throw if test specifies messageId that doesn't exist in the rule's meta.messages", () => { @@ -2313,6 +2427,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ messageId: "renameFoo", output: "var bar;" @@ -2323,7 +2438,7 @@ describe("RuleTester", () => { }] }] }); - }, "Error Suggestion at index 1 : Test has invalid messageId 'removeFoo', the rule under test allows only one of ['avoidFoo', 'unused', 'renameFoo']."); + }, "Error Suggestion at index 1: Test has invalid messageId 'removeFoo', the rule under test allows only one of ['avoidFoo', 'unused', 'renameFoo']."); }); it("should throw if hydrated desc doesn't match (wrong data value)", () => { @@ -2333,6 +2448,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ messageId: "renameFoo", data: { newName: "car" }, @@ -2345,7 +2461,7 @@ describe("RuleTester", () => { }] }] }); - }, "Error Suggestion at index 0 : Hydrated test desc \"Rename identifier 'foo' to 'car'\" does not match received desc \"Rename identifier 'foo' to 'bar'\"."); + }, "Error Suggestion at index 0: Hydrated test desc \"Rename identifier 'foo' to 'car'\" does not match received desc \"Rename identifier 'foo' to 'bar'\"."); }); it("should throw if hydrated desc doesn't match (wrong data key)", () => { @@ -2355,6 +2471,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ messageId: "renameFoo", data: { newName: "bar" }, @@ -2367,7 +2484,7 @@ describe("RuleTester", () => { }] }] }); - }, "Error Suggestion at index 1 : Hydrated test desc \"Rename identifier 'foo' to '{{ newName }}'\" does not match received desc \"Rename identifier 'foo' to 'baz'\"."); + }, "Error Suggestion at index 1: Hydrated test desc \"Rename identifier 'foo' to '{{ newName }}'\" does not match received desc \"Rename identifier 'foo' to 'baz'\"."); }); it("should throw if test specifies both desc and data", () => { @@ -2377,6 +2494,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ desc: "Rename identifier 'foo' to 'bar'", messageId: "renameFoo", @@ -2390,7 +2508,7 @@ describe("RuleTester", () => { }] }] }); - }, "Error Suggestion at index 0 : Test should not specify both 'desc' and 'data'."); + }, "Error Suggestion at index 0: Test should not specify both 'desc' and 'data'."); }); it("should throw if test uses data but doesn't specify messageId", () => { @@ -2400,6 +2518,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ messageId: "renameFoo", data: { newName: "bar" }, @@ -2411,7 +2530,7 @@ describe("RuleTester", () => { }] }] }); - }, "Error Suggestion at index 1 : Test must specify 'messageId' if 'data' is used."); + }, "Error Suggestion at index 1: Test must specify 'messageId' if 'data' is used."); }); it("should throw if the resulting suggestion output doesn't match", () => { @@ -2421,6 +2540,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [{ desc: "Rename identifier 'foo' to 'bar'", output: "var baz;" @@ -2431,6 +2551,24 @@ describe("RuleTester", () => { }, "Expected the applied suggestion fix to match the test suggestion output"); }); + it("should throw if the resulting suggestion output is the same as the original source code", () => { + assert.throws(() => { + ruleTester.run("suggestions-basic", require("../../fixtures/testers/rule-tester/suggestions").withFixerWithoutChanges, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + message: "Avoid using identifiers named 'foo'.", + suggestions: [{ + desc: "Rename identifier 'foo' to 'bar'", + output: "var foo;" + }] + }] + }] + }); + }, "The output of a suggestion should differ from the original source code for suggestion at index: 0 on error with message: \"Avoid using identifiers named 'foo'.\""); + }); + it("should fail when specified suggestion isn't an object", () => { assert.throws(() => { ruleTester.run("suggestions-basic", require("../../fixtures/testers/rule-tester/suggestions").basic, { @@ -2438,6 +2576,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [null] }] }] @@ -2450,6 +2589,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [ { messageId: "renameFoo", @@ -2472,6 +2612,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [{ message: "Rename identifier 'foo' to 'bar'" }] @@ -2488,6 +2629,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ messageId: "renameFoo", output: "var bar;" @@ -2784,6 +2926,43 @@ describe("RuleTester", () => { }, /A fatal parsing error occurred in autofix.\nError: .+\nAutofix output:\n.+/u); }); + describe("type checking", () => { + it('should throw if "only" property is not a boolean', () => { + + // "only" has to be falsy as itOnly is not mocked for all test cases + assert.throws(() => { + ruleTester.run("foo", require("../../fixtures/testers/rule-tester/no-var"), { + valid: [{ code: "foo", only: "" }], + invalid: [] + }); + }, /Optional test case property 'only' must be a boolean/u); + + assert.throws(() => { + ruleTester.run("foo", require("../../fixtures/testers/rule-tester/no-var"), { + valid: [], + invalid: [{ code: "foo", only: 0, errors: 1 }] + }); + }, /Optional test case property 'only' must be a boolean/u); + }); + + it('should throw if "filename" property is not a string', () => { + assert.throws(() => { + ruleTester.run("foo", require("../../fixtures/testers/rule-tester/no-var"), { + valid: [{ code: "foo", filename: false }], + invalid: [] + + }); + }, /Optional test case property 'filename' must be a string/u); + + assert.throws(() => { + ruleTester.run("foo", require("../../fixtures/testers/rule-tester/no-var"), { + valid: ["foo"], + invalid: [{ code: "foo", errors: 1, filename: 0 }] + }); + }, /Optional test case property 'filename' must be a string/u); + }); + }); + describe("sanitize test cases", () => { let originalRuleTesterIt; let spyRuleTesterIt; diff --git a/tests/lib/rules/array-callback-return.js b/tests/lib/rules/array-callback-return.js index 18057c676b7..938f73f7a2d 100644 --- a/tests/lib/rules/array-callback-return.js +++ b/tests/lib/rules/array-callback-return.js @@ -204,7 +204,7 @@ ruleTester.run("array-callback-return", rule, { { code: "foo.every(cb || function() {})", options: allowImplicitOptions, errors: ["Array.prototype.every() expects a return value from function."] }, { code: "[\"foo\",\"bar\"].sort(function foo() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.sort" } }] }, { code: "[\"foo\",\"bar\"].toSorted(function foo() {})", options: allowImplicitOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.toSorted" } }] }, - { code: "foo.forEach(x => x)", options: allowImplicitCheckForEach, languageOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.forEach(x => x)", options: allowImplicitCheckForEach, languageOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, suggestions: [{ messageId: "wrapBraces", output: "foo.forEach(x => {x})" }] }] }, { code: "foo.forEach(function(x) { if (a == b) {return x;}})", options: allowImplicitCheckForEach, errors: [{ messageId: "expectedNoReturnValue", data: { name: "function", arrayMethodName: "Array.prototype.forEach" } }] }, { code: "foo.forEach(function bar(x) { return x;})", options: allowImplicitCheckForEach, errors: [{ messageId: "expectedNoReturnValue", data: { name: "function 'bar'", arrayMethodName: "Array.prototype.forEach" } }] }, @@ -282,8 +282,8 @@ ruleTester.run("array-callback-return", rule, { { code: "foo.filter(function foo() {})", options: checkForEachOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.filter" } }] }, { code: "foo.filter(function foo() { return; })", options: checkForEachOptions, errors: [{ messageId: "expectedReturnValue", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.filter" } }] }, { code: "foo.every(cb || function() {})", options: checkForEachOptions, errors: ["Array.prototype.every() expects a return value from function."] }, - { code: "foo.forEach((x) => void x)", options: checkForEachOptions, languageOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, - { code: "foo.forEach((x) => void bar(x))", options: checkForEachOptions, languageOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.forEach((x) => void x)", options: checkForEachOptions, languageOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, suggestions: [{ messageId: "wrapBraces", output: "foo.forEach((x) => {void x})" }] }] }, + { code: "foo.forEach((x) => void bar(x))", options: checkForEachOptions, languageOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, suggestions: [{ messageId: "wrapBraces", output: "foo.forEach((x) => {void bar(x)})" }] }] }, { code: "foo.forEach((x) => { return void bar(x); })", options: checkForEachOptions, languageOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, { code: "foo.forEach((x) => { if (a === b) { return void a; } bar(x) })", options: checkForEachOptions, languageOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, @@ -490,7 +490,8 @@ ruleTester.run("array-callback-return", rule, { line: 1, column: 17, endLine: 1, - endColumn: 19 + endColumn: 19, + suggestions: [{ messageId: "wrapBraces", output: "foo.forEach(bar => {bar})" }] }] }, { @@ -504,7 +505,8 @@ ruleTester.run("array-callback-return", rule, { line: 1, column: 41, endLine: 1, - endColumn: 43 + endColumn: 43, + suggestions: [{ messageId: "wrapBraces", output: "foo.forEach((function () { return (bar) => {bar}; })())" }] }] }, { @@ -518,7 +520,8 @@ ruleTester.run("array-callback-return", rule, { line: 2, column: 13, endLine: 2, - endColumn: 15 + endColumn: 15, + suggestions: [{ messageId: "wrapBraces", output: "foo.forEach((() => {\n return bar => {bar}; })())" }] }] }, { diff --git a/tests/lib/rules/max-params.js b/tests/lib/rules/max-params.js index 257869febbe..10ae32119e0 100644 --- a/tests/lib/rules/max-params.js +++ b/tests/lib/rules/max-params.js @@ -118,6 +118,7 @@ ruleTester.run("max-params", rule, { }`, options: [{ max: 2 }], errors: [{ + messageId: "exceed", line: 1, column: 1, endLine: 1, diff --git a/tests/lib/rules/no-array-constructor.js b/tests/lib/rules/no-array-constructor.js index c429ff1b450..04027d8b9cd 100644 --- a/tests/lib/rules/no-array-constructor.js +++ b/tests/lib/rules/no-array-constructor.js @@ -238,7 +238,6 @@ ruleTester.run("no-array-constructor", rule, { errors: [{ messageId: "preferLiteral", suggestions: [{ - desc: "Replace with an array literal, add preceding semicolon.", messageId: "useLiteralAfterSemicolon", output: props.code.replace(/(new )?Array\((?.*?)\)/su, ";[$]") }] @@ -405,7 +404,6 @@ ruleTester.run("no-array-constructor", rule, { errors: [{ messageId: "preferLiteral", suggestions: [{ - desc: "Replace with an array literal.", messageId: "useLiteral", output: props.code.replace(/(new )?Array\((?.*?)\)/su, "[$]") }] diff --git a/tests/lib/rules/no-object-constructor.js b/tests/lib/rules/no-object-constructor.js index f8ff2d76c27..5b94a12ffea 100644 --- a/tests/lib/rules/no-object-constructor.js +++ b/tests/lib/rules/no-object-constructor.js @@ -41,7 +41,6 @@ ruleTester.run("no-object-constructor", rule, { messageId: "preferLiteral", type: "NewExpression", suggestions: [{ - desc: "Replace with '({})'.", messageId: "useLiteral", output: "({})" }] @@ -53,7 +52,6 @@ ruleTester.run("no-object-constructor", rule, { messageId: "preferLiteral", type: "CallExpression", suggestions: [{ - desc: "Replace with '({})'.", messageId: "useLiteral", output: "({})" }] @@ -65,7 +63,6 @@ ruleTester.run("no-object-constructor", rule, { messageId: "preferLiteral", type: "CallExpression", suggestions: [{ - desc: "Replace with '({})'.", messageId: "useLiteral", output: "const fn = () => ({});" }] @@ -77,7 +74,6 @@ ruleTester.run("no-object-constructor", rule, { messageId: "preferLiteral", type: "CallExpression", suggestions: [{ - desc: "Replace with '({})'.", messageId: "useLiteral", output: "({}) instanceof Object;" }] @@ -89,7 +85,6 @@ ruleTester.run("no-object-constructor", rule, { messageId: "preferLiteral", type: "CallExpression", suggestions: [{ - desc: "Replace with '{}'.", messageId: "useLiteral", output: "const obj = {};" }] @@ -101,7 +96,6 @@ ruleTester.run("no-object-constructor", rule, { messageId: "preferLiteral", type: "NewExpression", suggestions: [{ - desc: "Replace with '{}'.", messageId: "useLiteral", output: "({} instanceof Object);" }] @@ -190,7 +184,6 @@ ruleTester.run("no-object-constructor", rule, { errors: [{ messageId: "preferLiteral", suggestions: [{ - desc: "Replace with '({})', add preceding semicolon.", messageId: "useLiteralAfterSemicolon", output: props.code.replace(/(new )?Object\(\)/u, ";({})") }] @@ -357,7 +350,6 @@ ruleTester.run("no-object-constructor", rule, { errors: [{ messageId: "preferLiteral", suggestions: [{ - desc: "Replace with '({})'.", messageId: "useLiteral", output: props.code.replace(/(new )?Object\(\)/u, "({})") }] diff --git a/tests/lib/rules/no-useless-return.js b/tests/lib/rules/no-useless-return.js index d599cfe3067..9dfd4a8879d 100644 --- a/tests/lib/rules/no-useless-return.js +++ b/tests/lib/rules/no-useless-return.js @@ -231,7 +231,7 @@ ruleTester.run("no-useless-return", rule, { }, { code: "function foo() { if (foo) return; }", - output: "function foo() { if (foo) return; }" + output: null }, { code: "function foo() { bar(); return/**/; }", diff --git a/tests/lib/rules/one-var-declaration-per-line.js b/tests/lib/rules/one-var-declaration-per-line.js index fb4f8e70c58..98969f78fb6 100644 --- a/tests/lib/rules/one-var-declaration-per-line.js +++ b/tests/lib/rules/one-var-declaration-per-line.js @@ -68,7 +68,7 @@ ruleTester.run("one-var-declaration-per-line", rule, { ], invalid: [ - { code: "var foo, bar;", output: "var foo, \nbar;", options: ["always"], errors: [{ line: 1, column: 10, endLine: 1, endColumn: 13 }] }, + { code: "var foo, bar;", output: "var foo, \nbar;", options: ["always"], errors: [{ messageId: "expectVarOnNewline", line: 1, column: 10, endLine: 1, endColumn: 13 }] }, { code: "var a, b;", output: "var a, \nb;", options: ["always"], errors: [errorAt(1, 8)] }, { code: "let a, b;", output: "let a, \nb;", options: ["always"], languageOptions: { ecmaVersion: 6 }, errors: [errorAt(1, 8)] }, { code: "var a, b = 0;", output: "var a, \nb = 0;", options: ["always"], errors: [errorAt(1, 8)] }, @@ -77,7 +77,7 @@ ruleTester.run("one-var-declaration-per-line", rule, { { code: "let a, b = 0;", output: "let a, \nb = 0;", options: ["always"], languageOptions: { ecmaVersion: 6 }, errors: [errorAt(1, 8)] }, { code: "const a = 0, b = 0;", output: "const a = 0, \nb = 0;", options: ["always"], languageOptions: { ecmaVersion: 6 }, errors: [errorAt(1, 14)] }, - { code: "var foo, bar, baz = 0;", output: "var foo, bar, \nbaz = 0;", options: ["initializations"], errors: [{ line: 1, column: 15, endLine: 1, endColumn: 22 }] }, + { code: "var foo, bar, baz = 0;", output: "var foo, bar, \nbaz = 0;", options: ["initializations"], errors: [{ messageId: "expectVarOnNewline", line: 1, column: 15, endLine: 1, endColumn: 22 }] }, { code: "var a, b, c = 0;", output: "var a, b, \nc = 0;", options: ["initializations"], errors: [errorAt(1, 11)] }, { code: "var a, b,\nc = 0, d;", output: "var a, b,\nc = 0, \nd;", options: ["initializations"], errors: [errorAt(2, 8)] }, { code: "var a, b,\nc = 0, d = 0;", output: "var a, b,\nc = 0, \nd = 0;", options: ["initializations"], errors: [errorAt(2, 8)] },