diff --git a/package.json b/package.json index 053ddfc4..a6871f7c 100644 --- a/package.json +++ b/package.json @@ -129,6 +129,7 @@ "eslint": "^3.17.0", "eslint-config-airbnb-base": "^11.0.0", "eslint-plugin-import": "^2.2.0", + "jasmine-fix": "^1.0.1", "rimraf": "^2.5.4" }, "package-deps": [ diff --git a/spec/common.js b/spec/common.js deleted file mode 100644 index 6bf45be4..00000000 --- a/spec/common.js +++ /dev/null @@ -1,5 +0,0 @@ -const Path = require('path') - -module.exports.getFixturesPath = function (path) { - return Path.join(__dirname, 'fixtures', path) -} diff --git a/spec/linter-eslint-spec.js b/spec/linter-eslint-spec.js index 4923413e..2e604973 100644 --- a/spec/linter-eslint-spec.js +++ b/spec/linter-eslint-spec.js @@ -4,7 +4,9 @@ import * as path from 'path' import * as fs from 'fs' import { tmpdir } from 'os' import rimraf from 'rimraf' -import linter from '../lib/main' +import { beforeEach, it } from 'jasmine-fix' +// NOTE: If using fit you must add it to the list above! +import linterEslint from '../lib/main' const fixturesDir = path.join(__dirname, 'fixtures') @@ -29,137 +31,123 @@ function copyFileToTempDir(fileToCopyPath) { return new Promise((resolve) => { const tempFixtureDir = fs.mkdtempSync(tmpdir() + path.sep) const tempFixturePath = path.join(tempFixtureDir, path.basename(fileToCopyPath)) - const wr = fs.createWriteStream(tempFixturePath) - wr.on('close', () => + const ws = fs.createWriteStream(tempFixturePath) + ws.on('close', () => atom.workspace.open(tempFixturePath).then((openEditor) => { resolve({ openEditor, tempDir: tempFixtureDir }) }) ) - fs.createReadStream(fileToCopyPath).pipe(wr) + fs.createReadStream(fileToCopyPath).pipe(ws) + }) +} + +async function getNotification() { + return new Promise((resolve) => { + let notificationSub + const newNotification = (notification) => { + // Dispose of the notificaiton subscription + notificationSub.dispose() + resolve(notification) + } + // Subscribe to Atom's notifications + notificationSub = atom.notifications.onDidAddNotification(newNotification) }) } describe('The eslint provider for Linter', () => { - const { spawnWorker } = require('../lib/helpers') - - const worker = spawnWorker() - const lint = linter.provideLinter.call(worker).lint - const fix = (textEditor) => { - const ignoredRules = atom.config.get('linter-eslint.rulesToDisableWhileFixing') - .reduce((ids, id) => { - ids[id] = 0 // 0 is the severity to turn off a rule - return ids - }, {}) - return worker.worker.request('job', { - type: 'fix', - rules: ignoredRules, - config: atom.config.get('linter-eslint'), - filePath: textEditor.getPath() - }) - } + const linterProvider = linterEslint.provideLinter() + const lint = linterProvider.lint - beforeEach(() => { + beforeEach(async () => { atom.config.set('linter-eslint.disableFSCache', false) atom.config.set('linter-eslint.disableEslintIgnore', true) - waitsForPromise(() => - Promise.all([ - atom.packages.activatePackage('language-javascript'), - atom.packages.activatePackage('linter-eslint'), - ]).then(() => - atom.workspace.open(goodPath) - ) - ) + // Activate the JavaScript language so Atom knows what the files are + await atom.packages.activatePackage('language-javascript') + // Activate the provider + await atom.packages.activatePackage('linter-eslint') }) describe('checks bad.js and', () => { let editor = null - beforeEach(() => { - waitsForPromise(() => - atom.workspace.open(badPath).then((openEditor) => { - editor = openEditor - }) - ) + beforeEach(async () => { + editor = await atom.workspace.open(badPath) }) - it('finds at least one message', () => { - waitsForPromise(() => - lint(editor).then(messages => expect(messages.length).toBeGreaterThan(0)) - ) - }) - - it('verifies that message', () => { - waitsForPromise(() => - lint(editor).then((messages) => { - const expected = ''foo' is not defined. ' + - '(no-undef)' - expect(messages[0].type).toBe('Error') - expect(messages[0].text).not.toBeDefined() - expect(messages[0].html).toBe(expected) - expect(messages[0].filePath).toBe(badPath) - expect(messages[0].range).toEqual([[0, 0], [0, 3]]) - expect(Object.hasOwnProperty.call(messages[0], 'fix')).toBeFalsy() - }) - ) + it('verifies the messages', async () => { + const messages = await lint(editor) + expect(messages.length).toBe(2) + + const expected0 = ''foo' is not defined. ' + + '(no-undef)' + const expected1 = 'Extra semicolon. ' + + '(semi)' + + expect(messages[0].type).toBe('Error') + expect(messages[0].text).not.toBeDefined() + expect(messages[0].html).toBe(expected0) + expect(messages[0].filePath).toBe(badPath) + expect(messages[0].range).toEqual([[0, 0], [0, 3]]) + expect(messages[0].fix).not.toBeDefined() + + expect(messages[1].type).toBe('Error') + expect(messages[1].text).not.toBeDefined() + expect(messages[1].html).toBe(expected1) + expect(messages[1].filePath).toBe(badPath) + expect(messages[1].range).toEqual([[0, 8], [0, 9]]) + expect(messages[1].fix).toBeDefined() + expect(messages[1].fix.range).toEqual([[0, 6], [0, 9]]) + expect(messages[1].fix.newText).toBe('42') }) }) - it('finds nothing wrong with an empty file', () => { - waitsForPromise(() => - atom.workspace.open(emptyPath).then(editor => - lint(editor).then(messages => expect(messages.length).toBe(0)) - ) - ) + it('finds nothing wrong with an empty file', async () => { + const editor = await atom.workspace.open(emptyPath) + const messages = await lint(editor) + + expect(messages.length).toBe(0) }) - it('finds nothing wrong with a valid file', () => { - waitsForPromise(() => - atom.workspace.open(goodPath).then(editor => - lint(editor).then(messages => expect(messages.length).toBe(0)) - ) - ) + it('finds nothing wrong with a valid file', async () => { + const editor = await atom.workspace.open(goodPath) + const messages = await lint(editor) + + expect(messages.length).toBe(0) }) - it('reports the fixes for fixable errors', () => { - waitsForPromise(() => - atom.workspace.open(fixPath).then(editor => - lint(editor) - ).then((messages) => { - expect(messages[0].fix.range).toEqual([[0, 10], [1, 8]]) - expect(messages[0].fix.newText).toBe('6\nfunction') + it('reports the fixes for fixable errors', async () => { + const editor = await atom.workspace.open(fixPath) + const messages = await lint(editor) - expect(messages[1].fix.range).toEqual([[2, 0], [2, 1]]) - expect(messages[1].fix.newText).toBe(' ') - }) - ) + expect(messages[0].fix.range).toEqual([[0, 10], [1, 8]]) + expect(messages[0].fix.newText).toBe('6\nfunction') + + expect(messages[1].fix.range).toEqual([[2, 0], [2, 1]]) + expect(messages[1].fix.newText).toBe(' ') }) describe('when resolving import paths using eslint-plugin-import', () => { - it('correctly resolves imports from parent', () => { - waitsForPromise(() => - atom.workspace.open(importingpath).then(editor => - lint(editor).then(messages => expect(messages.length).toBe(0)) - ) - ) + it('correctly resolves imports from parent', async () => { + const editor = await atom.workspace.open(importingpath) + const messages = await lint(editor) + + expect(messages.length).toBe(0) }) - it('shows a message for an invalid import', () => { - waitsForPromise(() => - atom.workspace.open(badImportPath).then(editor => - lint(editor).then((messages) => { - const expected = 'Unable to resolve path to module '../nonexistent'. ' + - '(' + - 'import/no-unresolved)' - - expect(messages.length).toBeGreaterThan(0) - expect(messages[0].type).toBe('Error') - expect(messages[0].text).not.toBeDefined() - expect(messages[0].html).toBe(expected) - expect(messages[0].filePath).toBe(badImportPath) - expect(messages[0].range).toEqual([[0, 24], [0, 39]]) - expect(Object.hasOwnProperty.call(messages[0], 'fix')).toBeFalsy() - }) - ) - ) + + it('shows a message for an invalid import', async () => { + const editor = await atom.workspace.open(badImportPath) + const messages = await lint(editor) + const expected = 'Unable to resolve path to module '../nonexistent'. ' + + '(' + + 'import/no-unresolved)' + + expect(messages.length).toBe(1) + expect(messages[0].type).toBe('Error') + expect(messages[0].text).not.toBeDefined() + expect(messages[0].html).toBe(expected) + expect(messages[0].filePath).toBe(badImportPath) + expect(messages[0].range).toEqual([[0, 24], [0, 39]]) + expect(messages[0].fix).not.toBeDefined() }) }) @@ -167,318 +155,216 @@ describe('The eslint provider for Linter', () => { beforeEach(() => { atom.config.set('linter-eslint.disableEslintIgnore', false) }) - it('will not give warnings when linting the file', () => { - waitsForPromise(() => - atom.workspace.open(ignoredPath).then(editor => - lint(editor).then(messages => expect(messages.length).toBe(0)) - ) - ) + + it('will not give warnings when linting the file', async () => { + const editor = await atom.workspace.open(ignoredPath) + const messages = await lint(editor) + + expect(messages.length).toBe(0) }) - it('will not give warnings when autofixing the file', () => { - waitsForPromise(() => - atom.workspace.open(ignoredPath).then(editor => - fix(editor).then(result => expect(result).toBe('Linter-ESLint: Fix complete.')) - ) - ) + it('will not give warnings when autofixing the file', async () => { + const editor = await atom.workspace.open(ignoredPath) + atom.commands.dispatch(atom.views.getView(editor), 'linter-eslint:fix-file') + const notification = await getNotification() + + expect(notification.getMessage()).toBe('Linter-ESLint: Fix complete.') }) }) describe('fixes errors', () => { let editor - let doneCheckingFixes let tempFixtureDir - beforeEach(() => { - doneCheckingFixes = false - waitsForPromise(() => - copyFileToTempDir(fixPath) - .then(({ openEditor, tempDir }) => { - editor = openEditor - tempFixtureDir = tempDir - - return new Promise((resolve) => { - const configWritePath = path.join(tempFixtureDir, path.basename(configPath)) - const wr = fs.createWriteStream(configWritePath) - wr.on('close', () => { - resolve() - }) - fs.createReadStream(configPath).pipe(wr) - }) - }) - ) + beforeEach(async () => { + // Copy the file to a temporary folder + const { openEditor, tempDir } = await copyFileToTempDir(fixPath) + editor = openEditor + tempFixtureDir = tempDir + // Copy the config to the same temporary directory + return new Promise((resolve) => { + const configWritePath = path.join(tempDir, path.basename(configPath)) + const wr = fs.createWriteStream(configWritePath) + wr.on('close', () => resolve()) + fs.createReadStream(configPath).pipe(wr) + }) }) afterEach(() => { + // Remove the temporary directory rimraf.sync(tempFixtureDir) }) - it('should fix linting errors', () => { - function firstLint(textEditor) { - return lint(textEditor) - .then((messages) => { - // The original file has two errors - expect(messages.length).toBe(2) - return textEditor - }) - } - function makeFixes(textEditor) { - return fix(textEditor) - .then((messagesAfterSave) => { - // Linter reports a successful fix - expect(messagesAfterSave).toBe('Linter-ESLint: Fix complete.') - }) - } - // Create a subscription to watch when the editor changes (from the fix) - editor.onDidChange(() => { - lint(editor) - .then((messagesAfterFixing) => { - // Note: this fires several times, with only the final time resulting in - // a non-null messagesAfterFixing. This is the reason for the check here - // and for the `waitsFor` which makes sure the expectation is tested. - if (messagesAfterFixing) { - // After opening the file again, there are no linting errors - expect(messagesAfterFixing.length).toBe(0) - doneCheckingFixes = true - } - }) + async function firstLint(textEditor) { + const messages = await lint(textEditor) + // The original file has two errors + expect(messages.length).toBe(2) + } + + async function makeFixes(textEditor) { + return new Promise(async (resolve) => { + // Subscribe to the file reload event + const editorReloadSub = textEditor.getBuffer().onDidReload(async () => { + editorReloadSub.dispose() + // File has been reloaded in Atom, notification checking will happen + // async either way, but should already be finished at this point + resolve() + }) + + // Now that all the required subscriptions are active, send off a fix request + atom.commands.dispatch(atom.views.getView(textEditor), 'linter-eslint:fix-file') + const notification = await getNotification() + + expect(notification.getMessage()).toBe('Linter-ESLint: Fix complete.') + expect(notification.getType()).toBe('success') }) + } + + it('should fix linting errors', async () => { + await firstLint(editor) + await makeFixes(editor) + const messagesAfterFixing = await lint(editor) - waitsForPromise(() => - firstLint(editor) - .then(makeFixes) - ) - waitsFor( - () => doneCheckingFixes, - 'Messages should be checked after fixing' - ) + expect(messagesAfterFixing.length).toBe(0) }) - xit('should not fix linting errors for rules that are disabled with rulesToDisableWhileFixing', () => { + // NOTE: This actually works, but if both specs in this describe() are enabled + // a bug within Atom is somewhat reliably triggered, so this needs to stay + // disabled for now + xit('should not fix linting errors for rules that are disabled with rulesToDisableWhileFixing', async () => { atom.config.set('linter-eslint.rulesToDisableWhileFixing', ['semi']) - function firstLint(textEditor) { - return lint(textEditor) - .then((messages) => { - // The original file has two errors - expect(messages.length).toBe(2) - return textEditor - }) - } - function makeFixes(textEditor) { - return fix(textEditor) - .then((messagesAfterSave) => { - // Linter reports a successful fix - expect(messagesAfterSave).toBe('Linter-ESLint: Fix complete.') - }) - } - // Create a subscription to watch when the editor changes (from the fix) - editor.onDidChange(() => { - lint(editor) - .then((messagesAfterFixing) => { - // Note: this fires several times, with only the final time resulting in - // a non-null messagesAfterFixing. This is the reason for the check here - // and for the `waitsFor` which makes sure the expectation is tested. - if (messagesAfterFixing) { - // There is still one linting error, `semi` which was disabled during fixing - expect(messagesAfterFixing.length).toBe(1) - expect(messagesAfterFixing[0].html).toBe('Extra semicolon. (semi)') - doneCheckingFixes = true - } - }) - }) + await firstLint(editor) + await makeFixes(editor) + const messagesAfterFixing = await lint(editor) + const messageHTML = 'Extra semicolon. (semi)' - waitsForPromise(() => - firstLint(editor) - .then(makeFixes) - ) - waitsFor( - () => doneCheckingFixes, - 'Messages should be checked after fixing' - ) + expect(messagesAfterFixing.length).toBe(1) + expect(messagesAfterFixing[0].html).toBe(messageHTML) }) }) describe('Ignores specified rules when editing', () => { const expected = 'Trailing spaces not allowed. ' + '(no-trailing-spaces)' - it('does nothing on saved files', () => { + it('does nothing on saved files', async () => { atom.config.set('linter-eslint.rulesToSilenceWhileTyping', ['no-trailing-spaces']) - waitsForPromise(() => - atom.workspace.open(modifiedIgnoreSpacePath).then(editor => - lint(editor).then((messages) => { - expect(messages.length).toBe(1) - expect(messages[0].type).toBe('Error') - expect(messages[0].text).not.toBeDefined() - expect(messages[0].html).toBe(expected) - expect(messages[0].filePath).toBe(modifiedIgnoreSpacePath) - expect(messages[0].range).toEqual([[0, 9], [0, 10]]) - }) - ) - ) + const editor = await atom.workspace.open(modifiedIgnoreSpacePath) + const messages = await lint(editor) + + expect(messages.length).toBe(1) + expect(messages[0].type).toBe('Error') + expect(messages[0].text).not.toBeDefined() + expect(messages[0].html).toBe(expected) + expect(messages[0].filePath).toBe(modifiedIgnoreSpacePath) + expect(messages[0].range).toEqual([[0, 9], [0, 10]]) }) - it('works when the file is modified', () => { - let done - - // Set up an observer to check the editor once it is modified - waitsForPromise(() => - atom.workspace.open(modifiedIgnorePath).then((editor) => { - editor.onDidChange(() => { - lint(editor).then((messages) => { - if (messages) { - // Verify the space is showing an error - expect(messages.length).toBe(1) - expect(messages[0].type).toBe('Error') - expect(messages[0].text).not.toBeDefined() - expect(messages[0].html).toBe(expected) - expect(messages[0].filePath).toBe(modifiedIgnorePath) - expect(messages[0].range).toEqual([[0, 9], [0, 10]]) - - // Enable the option under test - atom.config.set('linter-eslint.rulesToSilenceWhileTyping', ['no-trailing-spaces']) - - // Check the lint results - lint(editor).then((newMessages) => { - expect(newMessages.length).toBe(0) - done = true - }) - } - }) - }) - - // Verify no error before - return lint(editor).then(messages => - expect(messages.length).toBe(0) - ) - - // Insert a space into the editor - .then(() => { - editor.getBuffer().insert([0, 9], ' ') - }) - }) - ) + it('works when the file is modified', async () => { + const editor = await atom.workspace.open(modifiedIgnorePath) + + // Verify no error before + const firstMessages = await lint(editor) + expect(firstMessages.length).toBe(0) + + // Insert a space into the editor + editor.getBuffer().insert([0, 9], ' ') - waitsFor( - () => done, - 'Messages should be checked after modifying the buffer' - ) + // Verify the space is showing an error + const messages = await lint(editor) + expect(messages.length).toBe(1) + expect(messages[0].type).toBe('Error') + expect(messages[0].text).not.toBeDefined() + expect(messages[0].html).toBe(expected) + expect(messages[0].filePath).toBe(modifiedIgnorePath) + expect(messages[0].range).toEqual([[0, 9], [0, 10]]) + + // Enable the option under test + atom.config.set('linter-eslint.rulesToSilenceWhileTyping', ['no-trailing-spaces']) + + // Check the lint results + const newMessages = await lint(editor) + expect(newMessages.length).toBe(0) }) }) describe('prints debugging information with the `debug` command', () => { let editor - beforeEach(() => { - waitsForPromise(() => - atom.workspace.open(goodPath).then((openEditor) => { - editor = openEditor - }) - ) + beforeEach(async () => { + editor = await atom.workspace.open(goodPath) }) - it('shows an info notification', () => { - let done - const checkNotificaton = (notification) => { - if (notification.getMessage() === 'linter-eslint debugging information') { - expect(notification.getType()).toEqual('info') - done = true - } - } - atom.notifications.onDidAddNotification(checkNotificaton) - + it('shows an info notification', async () => { atom.commands.dispatch(atom.views.getView(editor), 'linter-eslint:debug') + const notification = await getNotification() - waitsFor( - () => done, - 'Notification type should be checked', - 2000 - ) + expect(notification.getMessage()).toBe('linter-eslint debugging information') + expect(notification.getType()).toEqual('info') }) - it('includes debugging information in the details', () => { - let done - const checkNotificaton = (notification) => { - if (notification.getMessage() === 'linter-eslint debugging information') { - const detail = notification.getDetail() - expect(detail.includes(`Atom version: ${atom.getVersion()}`)).toBe(true) - expect(detail.includes('linter-eslint version:')).toBe(true) - expect(detail.includes(`Platform: ${process.platform}`)).toBe(true) - expect(detail.includes('linter-eslint configuration:')).toBe(true) - expect(detail.includes('Using local project ESLint')).toBe(true) - done = true - } - } - atom.notifications.onDidAddNotification(checkNotificaton) - + it('includes debugging information in the details', async () => { atom.commands.dispatch(atom.views.getView(editor), 'linter-eslint:debug') - - waitsFor( - () => done, - 'Notification details should be checked', - 2000 - ) + const notification = await getNotification() + const detail = notification.getDetail() + + expect(detail.includes(`Atom version: ${atom.getVersion()}`)).toBe(true) + expect(detail.includes('linter-eslint version:')).toBe(true) + expect(detail.includes(`Platform: ${process.platform}`)).toBe(true) + expect(detail.includes('linter-eslint configuration:')).toBe(true) + expect(detail.includes('Using local project ESLint')).toBe(true) }) }) - it('handles ranges in messages', () => - waitsForPromise(() => - atom.workspace.open(endRangePath).then(editor => - lint(editor).then((messages) => { - const expected = 'Unreachable code. ' + - '(no-unreachable)' - expect(messages[0].type).toBe('Error') - expect(messages[0].text).not.toBeDefined() - expect(messages[0].html).toBe(expected) - expect(messages[0].filePath).toBe(endRangePath) - expect(messages[0].range).toEqual([[5, 2], [6, 15]]) - }) - ) - ) - ) + it('handles ranges in messages', async () => { + const editor = await atom.workspace.open(endRangePath) + const messages = await lint(editor) + const expected = 'Unreachable code. ' + + '(no-unreachable)' + + expect(messages[0].type).toBe('Error') + expect(messages[0].text).not.toBeDefined() + expect(messages[0].html).toBe(expected) + expect(messages[0].filePath).toBe(endRangePath) + expect(messages[0].range).toEqual([[5, 2], [6, 15]]) + }) describe('when setting `disableWhenNoEslintConfig` is false', () => { let editor - let didError - let gotLintingErrors let tempFixtureDir - beforeEach(() => { + beforeEach(async () => { atom.config.set('linter-eslint.disableWhenNoEslintConfig', false) - waitsForPromise(() => - copyFileToTempDir(badInlinePath, tempFixtureDir) - .then(({ openEditor, tempDir }) => { - editor = openEditor - tempFixtureDir = tempDir - }) - ) + const { openEditor, tempDir } = await copyFileToTempDir(badInlinePath) + editor = openEditor + tempFixtureDir = tempDir }) afterEach(() => { rimraf.sync(tempFixtureDir) }) - it('errors when no config file is found', () => { - lint(editor) - .then((messages) => { - // Older versions of ESLint will report an error - // (or if current user running tests has a config in their home directory) - const expectedHtml = ''foo' is not defined. ' + - '(no-undef)' - expect(messages.length).toBe(1) - expect(messages[0].html).toBe(expectedHtml) - gotLintingErrors = true - }) - .catch((err) => { - // Newer versions of ESLint will throw an exception - expect(err.message).toBe('No ESLint configuration found.') - didError = true - }) + it('errors when no config file is found', async () => { + let didError + let gotLintingErrors + + try { + const messages = await lint(editor) + // Older versions of ESLint will report an error + // (or if current user running tests has a config in their home directory) + const expectedHtml = ''foo' is not defined. ' + + '(no-undef)' + + expect(messages.length).toBe(1) + expect(messages[0].html).toBe(expectedHtml) + gotLintingErrors = true + } catch (err) { + // Newer versions of ESLint will throw an exception + expect(err.message).toBe('No ESLint configuration found.') + didError = true + } - waitsFor( - () => didError || gotLintingErrors, - 'An error should have been thrown or linting performed' - ) + expect(didError || gotLintingErrors).toBe(true) }) }) @@ -486,29 +372,22 @@ describe('The eslint provider for Linter', () => { let editor let tempFixtureDir - beforeEach(() => { + beforeEach(async () => { atom.config.set('linter-eslint.disableWhenNoEslintConfig', true) - waitsForPromise(() => - copyFileToTempDir(badInlinePath) - .then(({ openEditor, tempDir }) => { - editor = openEditor - tempFixtureDir = tempDir - }) - ) + const { openEditor, tempDir } = await copyFileToTempDir(badInlinePath) + editor = openEditor + tempFixtureDir = tempDir }) afterEach(() => { rimraf.sync(tempFixtureDir) }) - it('does not report errors when no config file is found', () => - waitsForPromise(() => - lint(editor) - .then((messages) => { - expect(messages.length).toBe(0) - }) - ) - ) + it('does not report errors when no config file is found', async () => { + const messages = await lint(editor) + + expect(messages.length).toBe(0) + }) }) }) diff --git a/spec/worker-helpers-spec.js b/spec/worker-helpers-spec.js index 94a9f0d0..24e935a6 100644 --- a/spec/worker-helpers-spec.js +++ b/spec/worker-helpers-spec.js @@ -2,7 +2,8 @@ import * as Path from 'path' import * as Helpers from '../lib/worker-helpers' -import { getFixturesPath } from './common' + +const getFixturesPath = path => Path.join(__dirname, 'fixtures', path) describe('Worker Helpers', () => { describe('findESLintDirectory', () => {