/** * @fileoverview Tests for ConfigArray class. * @author Toru Nagashima */ "use strict"; const path = require("path"); const { assert } = require("chai"); const { ConfigArray, OverrideTester, getUsedExtractedConfigs } = require("../../../../lib/cli-engine/config-array"); describe("ConfigArray", () => { it("should be a sub class of Array.", () => { assert(new ConfigArray() instanceof Array); }); describe("'constructor(...elements)' should adopt the elements as array elements.", () => { const patterns = [ { elements: [] }, { elements: [{ value: 1 }] }, { elements: [{ value: 2 }, { value: 3 }] }, { elements: [{ value: 4 }, { value: 5 }, { value: 6 }] } ]; for (const { elements } of patterns) { describe(`if it gave ${JSON.stringify(elements)} then`, () => { let configArray; beforeEach(() => { configArray = new ConfigArray(...elements); }); it(`should have ${elements.length} as the length.`, () => { assert.strictEqual(configArray.length, elements.length); }); for (let i = 0; i < elements.length; ++i) { it(`should have ${JSON.stringify(elements[i])} at configArray[${i}].`, () => { // eslint-disable-line no-loop-func assert.strictEqual(configArray[i], elements[i]); }); } }); } }); describe("'isRoot()' method should be the value of the last element which has 'root' property.", () => { const patterns = [ { elements: [], expected: false }, { elements: [{}], expected: false }, { elements: [{}, {}], expected: false }, { elements: [{ root: false }], expected: false }, { elements: [{ root: true }], expected: true }, { elements: [{ root: true }, { root: false }], expected: false }, { elements: [{ root: false }, { root: true }], expected: true }, { elements: [{ root: false }, { root: true }, { rules: {} }], expected: true }, // ignore undefined. { elements: [{ root: true }, { root: 1 }], expected: true } // ignore non-boolean value ]; for (const { elements, expected } of patterns) { it(`should be ${expected} if the elements are ${JSON.stringify(elements)}.`, () => { assert.strictEqual(new ConfigArray(...elements).isRoot(), expected); }); } }); describe("'pluginEnvironments' property should be the environments of all plugins.", () => { const env = { "aaa/xxx": {}, "bbb/xxx": {} }; let configArray; beforeEach(() => { configArray = new ConfigArray( { plugins: { aaa: { definition: { environments: { xxx: env["aaa/xxx"] } } } } }, { criteria: OverrideTester.create(["*.ts"], [], process.cwd()), plugins: { bbb: { definition: { environments: { xxx: env["bbb/xxx"] } } } } } ); }); it("should return null for built-in env", () => { assert.strictEqual(configArray.pluginEnvironments.get("node"), void 0); }); it("should return 'aaa/xxx' if it exists.", () => { assert.strictEqual(configArray.pluginEnvironments.get("aaa/xxx"), env["aaa/xxx"]); }); it("should return 'bbb/xxx' if it exists.", () => { assert.strictEqual(configArray.pluginEnvironments.get("bbb/xxx"), env["bbb/xxx"]); }); it("should throw an error if it tried to mutate.", () => { assert.throws(() => { configArray.pluginEnvironments.set("ccc/xxx", {}); }); }); }); describe("'pluginProcessors' property should be the processors of all plugins.", () => { const processors = { "aaa/.xxx": {}, "bbb/.xxx": {} }; let configArray; beforeEach(() => { configArray = new ConfigArray( { plugins: { aaa: { definition: { processors: { ".xxx": processors["aaa/.xxx"] } } } } }, { criteria: OverrideTester.create(["*.ts"], [], process.cwd()), plugins: { bbb: { definition: { processors: { ".xxx": processors["bbb/.xxx"] } } } } } ); }); it("should return 'aaa/.xxx' if it exists.", () => { assert.strictEqual(configArray.pluginProcessors.get("aaa/.xxx"), processors["aaa/.xxx"]); }); it("should return 'bbb/.xxx' if it exists.", () => { assert.strictEqual(configArray.pluginProcessors.get("bbb/.xxx"), processors["bbb/.xxx"]); }); it("should throw an error if it tried to mutate.", () => { assert.throws(() => { configArray.pluginProcessors.set("ccc/.xxx", {}); }); }); }); describe("'pluginRules' property should be the rules of all plugins.", () => { const rules = { "aaa/xxx": {}, "bbb/xxx": {} }; let configArray; beforeEach(() => { configArray = new ConfigArray( { plugins: { aaa: { definition: { rules: { xxx: rules["aaa/xxx"] } } } } }, { criteria: OverrideTester.create(["*.ts"], [], process.cwd()), plugins: { bbb: { definition: { rules: { xxx: rules["bbb/xxx"] } } } } } ); }); it("should return null for built-in rules", () => { assert.strictEqual(configArray.pluginRules.get("eqeqeq"), void 0); }); it("should return 'aaa/xxx' if it exists.", () => { assert.strictEqual(configArray.pluginRules.get("aaa/xxx"), rules["aaa/xxx"]); }); it("should return 'bbb/xxx' if it exists.", () => { assert.strictEqual(configArray.pluginRules.get("bbb/xxx"), rules["bbb/xxx"]); }); it("should throw an error if it tried to mutate.", () => { assert.throws(() => { configArray.pluginRules.set("ccc/xxx", {}); }); }); }); describe("'extractConfig(filePath)' method should retrieve the merged config for a given file.", () => { it("should throw an error if a 'parser' has the loading error.", () => { assert.throws(() => { new ConfigArray( { parser: { error: new Error("Failed to load a parser.") } } ).extractConfig(__filename); }, "Failed to load a parser."); }); it("should not throw if the errored 'parser' was not used; overwriten", () => { const parser = { id: "a parser" }; const config = new ConfigArray( { parser: { error: new Error("Failed to load a parser.") } }, { parser } ).extractConfig(__filename); assert.strictEqual(config.parser, parser); }); it("should not throw if the errored 'parser' was not used; not matched", () => { const config = new ConfigArray( { criteria: OverrideTester.create(["*.ts"], [], process.cwd()), parser: { error: new Error("Failed to load a parser.") } } ).extractConfig(__filename); assert.strictEqual(config.parser, null); }); it("should throw an error if a 'plugins' value has the loading error.", () => { assert.throws(() => { new ConfigArray( { plugins: { foo: { error: new Error("Failed to load a plugin.") } } } ).extractConfig(__filename); }, "Failed to load a plugin."); }); it("should not throw if the errored 'plugins' value was not used; not matched", () => { const config = new ConfigArray( { criteria: OverrideTester.create(["*.ts"], [], process.cwd()), plugins: { foo: { error: new Error("Failed to load a plugin.") } } } ).extractConfig(__filename); assert.deepStrictEqual(config.plugins, {}); }); it("should not merge the elements which were not matched.", () => { const config = new ConfigArray( { rules: { "no-redeclare": "error" } }, { criteria: OverrideTester.create(["*.js"], [], process.cwd()), rules: { "no-undef": "error" } }, { criteria: OverrideTester.create(["*.js"], [path.basename(__filename)], process.cwd()), rules: { "no-use-before-define": "error" } }, { criteria: OverrideTester.create(["*.ts"], [], process.cwd()), rules: { "no-unused-vars": "error" } } ).extractConfig(__filename); assert.deepStrictEqual(config.rules, { "no-redeclare": ["error"], "no-undef": ["error"] }); }); it("should return the same instance for every the same matching.", () => { const configArray = new ConfigArray( { rules: { "no-redeclare": "error" } }, { criteria: OverrideTester.create(["*.js"], [], process.cwd()), rules: { "no-undef": "error" } }, { criteria: OverrideTester.create(["*.js"], [path.basename(__filename)], process.cwd()), rules: { "no-use-before-define": "error" } }, { criteria: OverrideTester.create(["*.ts"], [], process.cwd()), rules: { "no-unused-vars": "error" } } ); assert.strictEqual( configArray.extractConfig(path.join(__dirname, "a.js")), configArray.extractConfig(path.join(__dirname, "b.js")) ); }); /** * Merge two config data. * * The test cases which depend on this function were moved from * 'tests/lib/config/config-ops.js' when refactoring to keep the * cumulated test cases. * * Previously, the merging logic of multiple config data had been * implemented in `ConfigOps.merge()` function. But currently, it's * implemented in `ConfigArray#extractConfig()` method. * @param {Object} target A config data. * @param {Object} source Another config data. * @returns {Object} The merged config data. */ function merge(target, source) { return new ConfigArray(target, source).extractConfig(__filename); } it("should combine two objects when passed two objects with different top-level properties", () => { const config = [ { env: { browser: true } }, { globals: { foo: "bar" } } ]; const result = merge(config[0], config[1]); assert.strictEqual(result.globals.foo, "bar"); assert.isTrue(result.env.browser); }); it("should combine without blowing up on null values", () => { const config = [ { env: { browser: true } }, { env: { node: null } } ]; const result = merge(config[0], config[1]); assert.strictEqual(result.env.node, null); assert.isTrue(result.env.browser); }); it("should combine two objects with parser when passed two objects with different top-level properties", () => { const config = [ { env: { browser: true }, parser: "espree" }, { globals: { foo: "bar" } } ]; const result = merge(config[0], config[1]); assert.strictEqual(result.parser, "espree"); }); it("should combine configs and override rules when passed configs with the same rules", () => { const config = [ { rules: { "no-mixed-requires": [0, false] } }, { rules: { "no-mixed-requires": [1, true] } } ]; const result = merge(config[0], config[1]); assert.isArray(result.rules["no-mixed-requires"]); assert.strictEqual(result.rules["no-mixed-requires"][0], 1); assert.strictEqual(result.rules["no-mixed-requires"][1], true); }); it("should combine configs when passed configs with parserOptions", () => { const config = [ { parserOptions: { ecmaFeatures: { jsx: true } } }, { parserOptions: { ecmaFeatures: { globalReturn: true } } } ]; const result = merge(config[0], config[1]); assert.deepStrictEqual(result, { configNameOfNoInlineConfig: "", env: {}, globals: {}, ignores: void 0, noInlineConfig: void 0, parser: null, parserOptions: { ecmaFeatures: { jsx: true, globalReturn: true } }, plugins: {}, processor: null, reportUnusedDisableDirectives: void 0, rules: {}, settings: {} }); // double-check that originals were not changed assert.deepStrictEqual(config[0], { parserOptions: { ecmaFeatures: { jsx: true } } }); assert.deepStrictEqual(config[1], { parserOptions: { ecmaFeatures: { globalReturn: true } } }); }); it("should override configs when passed configs with the same ecmaFeatures", () => { const config = [ { parserOptions: { ecmaFeatures: { globalReturn: false } } }, { parserOptions: { ecmaFeatures: { globalReturn: true } } } ]; const result = merge(config[0], config[1]); assert.deepStrictEqual(result, { configNameOfNoInlineConfig: "", env: {}, globals: {}, ignores: void 0, noInlineConfig: void 0, parser: null, parserOptions: { ecmaFeatures: { globalReturn: true } }, plugins: {}, processor: null, reportUnusedDisableDirectives: void 0, rules: {}, settings: {} }); }); it("should combine configs and override rules when merging two configs with arrays and int", () => { const config = [ { rules: { "no-mixed-requires": [0, false] } }, { rules: { "no-mixed-requires": 1 } } ]; const result = merge(config[0], config[1]); assert.isArray(result.rules["no-mixed-requires"]); assert.strictEqual(result.rules["no-mixed-requires"][0], 1); assert.strictEqual(result.rules["no-mixed-requires"][1], false); assert.deepStrictEqual(config[0], { rules: { "no-mixed-requires": [0, false] } }); assert.deepStrictEqual(config[1], { rules: { "no-mixed-requires": 1 } }); }); it("should combine configs and override rules options completely", () => { const config = [ { rules: { "no-mixed-requires1": [1, { event: ["evt", "e"] }] } }, { rules: { "no-mixed-requires1": [1, { err: ["error", "e"] }] } } ]; const result = merge(config[0], config[1]); assert.isArray(result.rules["no-mixed-requires1"]); assert.deepStrictEqual(result.rules["no-mixed-requires1"][1], { err: ["error", "e"] }); assert.deepStrictEqual(config[0], { rules: { "no-mixed-requires1": [1, { event: ["evt", "e"] }] } }); assert.deepStrictEqual(config[1], { rules: { "no-mixed-requires1": [1, { err: ["error", "e"] }] } }); }); it("should combine configs and override rules options without array or object", () => { const config = [ { rules: { "no-mixed-requires1": ["warn", "nconf", "underscore"] } }, { rules: { "no-mixed-requires1": [2, "requirejs"] } } ]; const result = merge(config[0], config[1]); assert.strictEqual(result.rules["no-mixed-requires1"][0], 2); assert.strictEqual(result.rules["no-mixed-requires1"][1], "requirejs"); assert.isUndefined(result.rules["no-mixed-requires1"][2]); assert.deepStrictEqual(config[0], { rules: { "no-mixed-requires1": ["warn", "nconf", "underscore"] } }); assert.deepStrictEqual(config[1], { rules: { "no-mixed-requires1": [2, "requirejs"] } }); }); it("should combine configs and override rules options without array or object but special case", () => { const config = [ { rules: { "no-mixed-requires1": [1, "nconf", "underscore"] } }, { rules: { "no-mixed-requires1": "error" } } ]; const result = merge(config[0], config[1]); assert.strictEqual(result.rules["no-mixed-requires1"][0], "error"); assert.strictEqual(result.rules["no-mixed-requires1"][1], "nconf"); assert.strictEqual(result.rules["no-mixed-requires1"][2], "underscore"); assert.deepStrictEqual(config[0], { rules: { "no-mixed-requires1": [1, "nconf", "underscore"] } }); assert.deepStrictEqual(config[1], { rules: { "no-mixed-requires1": "error" } }); }); it("should combine configs correctly", () => { const config = [ { rules: { "no-mixed-requires1": [1, { event: ["evt", "e"] }], "valid-jsdoc": 1, semi: 1, quotes1: [2, { exception: ["hi"] }], smile: [1, ["hi", "bye"]] }, parserOptions: { ecmaFeatures: { jsx: true } }, env: { browser: true }, globals: { foo: false } }, { rules: { "no-mixed-requires1": [1, { err: ["error", "e"] }], "valid-jsdoc": 2, test: 1, smile: [1, ["xxx", "yyy"]] }, parserOptions: { ecmaFeatures: { globalReturn: true } }, env: { browser: false }, globals: { foo: true } } ]; const result = merge(config[0], config[1]); assert.deepStrictEqual(result, { configNameOfNoInlineConfig: "", parser: null, parserOptions: { ecmaFeatures: { jsx: true, globalReturn: true } }, plugins: {}, env: { browser: false }, globals: { foo: true }, rules: { "no-mixed-requires1": [1, { err: [ "error", "e" ] } ], quotes1: [2, { exception: [ "hi" ] } ], semi: [1], smile: [1, ["xxx", "yyy"]], test: [1], "valid-jsdoc": [2] }, settings: {}, processor: null, noInlineConfig: void 0, reportUnusedDisableDirectives: void 0, ignores: void 0 }); assert.deepStrictEqual(config[0], { rules: { "no-mixed-requires1": [1, { event: ["evt", "e"] }], "valid-jsdoc": 1, semi: 1, quotes1: [2, { exception: ["hi"] }], smile: [1, ["hi", "bye"]] }, parserOptions: { ecmaFeatures: { jsx: true } }, env: { browser: true }, globals: { foo: false } }); assert.deepStrictEqual(config[1], { rules: { "no-mixed-requires1": [1, { err: ["error", "e"] }], "valid-jsdoc": 2, test: 1, smile: [1, ["xxx", "yyy"]] }, parserOptions: { ecmaFeatures: { globalReturn: true } }, env: { browser: false }, globals: { foo: true } }); }); it("should copy deeply if there is not the destination's property", () => { const a = {}; const b = { settings: { bar: 1 } }; const result = merge(a, b); assert(a.settings === void 0); assert(b.settings.bar === 1); assert(result.settings.bar === 1); result.settings.bar = 2; assert(b.settings.bar === 1); assert(result.settings.bar === 2); }); }); describe("'getUsedExtractedConfigs(instance)' function should retrieve used extracted configs from the instance's internal cache.", () => { let configArray; beforeEach(() => { configArray = new ConfigArray( { rules: { "no-redeclare": "error" } }, { criteria: OverrideTester.create(["*.js"], [], process.cwd()), rules: { "no-undef": "error" } }, { criteria: OverrideTester.create(["*.js"], [path.basename(__filename)], process.cwd()), rules: { "no-use-before-define": "error" } }, { criteria: OverrideTester.create(["*.ts"], [], process.cwd()), rules: { "no-unused-vars": "error" } } ); }); it("should return empty array before it called 'extractConfig(filePath)'.", () => { assert.deepStrictEqual(getUsedExtractedConfigs(configArray), []); }); for (const { filePaths } of [ { filePaths: [__filename] }, { filePaths: [__filename, `${__filename}.ts`] }, { filePaths: [__filename, `${__filename}.ts`, path.join(__dirname, "foo.js")] } ]) { describe(`after it called 'extractConfig(filePath)' ${filePaths.length} time(s) with ${JSON.stringify(filePaths, null, 4)}, the returned array`, () => { // eslint-disable-line no-loop-func let configs; let usedConfigs; beforeEach(() => { configs = filePaths.map(filePath => configArray.extractConfig(filePath)); usedConfigs = getUsedExtractedConfigs(configArray); }); it(`should have ${filePaths.length} as the length.`, () => { assert.strictEqual(usedConfigs.length, configs.length); }); for (let i = 0; i < filePaths.length; ++i) { it(`should contain 'configs[${i}]'.`, () => { // eslint-disable-line no-loop-func assert(usedConfigs.includes(configs[i])); }); } }); } it("should not contain duplicate values.", () => { // Call some times, including with the same arguments. configArray.extractConfig(__filename); configArray.extractConfig(`${__filename}.ts`); configArray.extractConfig(path.join(__dirname, "foo.js")); configArray.extractConfig(__filename); configArray.extractConfig(path.join(__dirname, "foo.js")); configArray.extractConfig(path.join(__dirname, "bar.js")); configArray.extractConfig(path.join(__dirname, "baz.js")); const usedConfigs = getUsedExtractedConfigs(configArray); assert.strictEqual(new Set(usedConfigs).size, usedConfigs.length); }); }); });