From fd42091f8768cd624c8bcff4b7dc763dcf7e5cd0 Mon Sep 17 00:00:00 2001 From: Arik Fraimovich Date: Tue, 26 Mar 2019 16:40:26 +0200 Subject: [PATCH] Add Lint step to CircleCI (#3642) --- .circleci/config.yml | 20 ++++- .codeclimate.yml | 24 ++---- client/.eslintrc.js | 65 ++++++++-------- client/app/.eslintrc.js | 7 ++ client/cypress/.eslintrc.js | 12 +++ .../integration/dashboard/dashboard_spec.js | 75 ++++++++++++------- client/cypress/integration/user/login_spec.js | 4 +- client/cypress/support/commands.js | 2 +- package-lock.json | 38 +++++----- package.json | 1 + 10 files changed, 147 insertions(+), 101 deletions(-) create mode 100644 client/app/.eslintrc.js create mode 100644 client/cypress/.eslintrc.js diff --git a/.circleci/config.yml b/.circleci/config.yml index 5788c5b52..807580bdd 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -45,6 +45,16 @@ jobs: path: /tmp/test-results - store_artifacts: path: coverage.xml + frontend-lint: + docker: + - image: circleci/node:8 + steps: + - checkout + - run: mkdir -p /tmp/test-results/eslint + - run: npm install + - run: npm run lint:ci + - store_test_results: + path: /tmp/test-results frontend-unit-tests: docker: - image: circleci/node:8 @@ -54,6 +64,7 @@ jobs: - run: npm install - run: npm run bundle - run: npm test + - run: npm run lint frontend-e2e-tests: environment: COMPOSE_FILE: .circleci/docker-compose.cypress.yml @@ -105,8 +116,13 @@ workflows: - python-flake8-tests - legacy-python-flake8-tests - backend-unit-tests - - frontend-unit-tests - - frontend-e2e-tests + - frontend-lint + - frontend-unit-tests: + requires: + - frontend-lint + - frontend-e2e-tests: + requires: + - frontend-lint - build-tarball: requires: - backend-unit-tests diff --git a/.codeclimate.yml b/.codeclimate.yml index c7def082a..22f2e1f0c 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -21,20 +21,12 @@ plugins: pep8: enabled: true eslint: - enabled: true - channel: "eslint-5" - config: - config: client/.eslintrc.js - checks: - import/no-unresolved: - enabled: false - no-multiple-empty-lines: # TODO: Enable - enabled: false + enabled: false exclude_patterns: -- "tests/**/*.py" -- "migrations/**/*.py" -- "setup/**/*" -- "bin/**/*" -- "**/node_modules/" -- "client/dist/" -- "**/*.pyc" + - "tests/**/*.py" + - "migrations/**/*.py" + - "setup/**/*" + - "bin/**/*" + - "**/node_modules/" + - "client/dist/" + - "**/*.pyc" diff --git a/client/.eslintrc.js b/client/.eslintrc.js index ff41b8c6d..083d99390 100644 --- a/client/.eslintrc.js +++ b/client/.eslintrc.js @@ -1,23 +1,20 @@ module.exports = { root: true, - extends: ["airbnb", "plugin:jest/recommended"], - plugins: ["jest", "cypress", "chai-friendly"], + extends: ["airbnb"], settings: { "import/resolver": "webpack" }, parser: "babel-eslint", env: { - "jest/globals": true, - "cypress/globals": true, - "browser": true, - "node": true + browser: true, + node: true }, rules: { // allow debugger during development - 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, - 'no-param-reassign': 0, - 'no-mixed-operators': 0, - 'no-underscore-dangle': 0, + "no-debugger": process.env.NODE_ENV === "production" ? 2 : 0, + "no-param-reassign": 0, + "no-mixed-operators": 0, + "no-underscore-dangle": 0, "no-use-before-define": ["error", "nofunc"], "prefer-destructuring": "off", "prefer-template": "off", @@ -27,38 +24,42 @@ module.exports = { "no-lonely-if": "off", "consistent-return": "off", "no-control-regex": "off", - 'no-multiple-empty-lines': 'warn', + "no-multiple-empty-lines": "warn", "no-script-url": "off", // some tags should have href="javascript:void(0)" - 'operator-linebreak': 'off', - 'react/destructuring-assignment': 'off', + "operator-linebreak": "off", + "react/destructuring-assignment": "off", "react/jsx-filename-extension": "off", - 'react/jsx-one-expression-per-line': 'off', + "react/jsx-one-expression-per-line": "off", "react/jsx-uses-react": "error", "react/jsx-uses-vars": "error", - 'react/jsx-wrap-multilines': 'warn', - 'react/no-access-state-in-setstate': 'warn', + "react/jsx-wrap-multilines": "warn", + "react/no-access-state-in-setstate": "warn", "react/prefer-stateless-function": "warn", "react/forbid-prop-types": "warn", "react/prop-types": "warn", "jsx-a11y/anchor-is-valid": "off", "jsx-a11y/click-events-have-key-events": "off", - "jsx-a11y/label-has-associated-control": ["warn", { - "controlComponents": true - }], + "jsx-a11y/label-has-associated-control": [ + "warn", + { + controlComponents: true + } + ], "jsx-a11y/label-has-for": "off", "jsx-a11y/no-static-element-interactions": "off", - "max-len": ['error', 120, 2, { - ignoreUrls: true, - ignoreComments: false, - ignoreRegExpLiterals: true, - ignoreStrings: true, - ignoreTemplateLiterals: true, - }], - "no-else-return": ["error", {"allowElseIf": true}], - "object-curly-newline": ["error", {"consistent": true}], - // needed for cypress tests - "func-names": "off", - "no-unused-expressions": 0, - "chai-friendly/no-unused-expressions": 2 + "max-len": [ + "error", + 120, + 2, + { + ignoreUrls: true, + ignoreComments: false, + ignoreRegExpLiterals: true, + ignoreStrings: true, + ignoreTemplateLiterals: true + } + ], + "no-else-return": ["error", { allowElseIf: true }], + "object-curly-newline": ["error", { consistent: true }] } }; diff --git a/client/app/.eslintrc.js b/client/app/.eslintrc.js new file mode 100644 index 000000000..904b0635d --- /dev/null +++ b/client/app/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ["plugin:jest/recommended"], + plugins: ["jest"], + env: { + "jest/globals": true, + }, +}; \ No newline at end of file diff --git a/client/cypress/.eslintrc.js b/client/cypress/.eslintrc.js new file mode 100644 index 000000000..2049f8436 --- /dev/null +++ b/client/cypress/.eslintrc.js @@ -0,0 +1,12 @@ +module.exports = { + extends: ["plugin:cypress/recommended"], + plugins: ["cypress", "chai-friendly"], + env: { + "cypress/globals": true + }, + rules: { + "func-names": ["error", "never"], + "no-unused-expressions": 0, + "chai-friendly/no-unused-expressions": 2 + } +}; diff --git a/client/cypress/integration/dashboard/dashboard_spec.js b/client/cypress/integration/dashboard/dashboard_spec.js index cdb74061a..3ae55a78a 100644 --- a/client/cypress/integration/dashboard/dashboard_spec.js +++ b/client/cypress/integration/dashboard/dashboard_spec.js @@ -1,8 +1,7 @@ const DRAG_PLACEHOLDER_SELECTOR = '.grid-stack-placeholder'; function createNewDashboardByAPI(name) { - return cy.request('POST', 'api/dashboards', { name }) - .then(({ body }) => body); + return cy.request('POST', 'api/dashboards', { name }).then(({ body }) => body); } function editDashboard() { @@ -26,25 +25,28 @@ function addTextboxByAPI(text, dashId) { }, }; - return cy.request('POST', 'api/widgets', data) - .then(({ body }) => { - const id = Cypress._.get(body, 'id'); - assert.isDefined(id, 'Widget api call returns widget id'); - return `WidgetId${id}`; - }); + return cy.request('POST', 'api/widgets', data).then(({ body }) => { + const id = Cypress._.get(body, 'id'); + assert.isDefined(id, 'Widget api call returns widget id'); + return `WidgetId${id}`; + }); } function addQueryByAPI(data, shouldPublish = true) { - const merged = Object.assign({ - name: 'Test Query', - query: 'select 1', - data_source_id: 1, - options: { - parameters: [], + const merged = Object.assign( + { + name: 'Test Query', + query: 'select 1', + data_source_id: 1, + options: { + parameters: [], + }, + schedule: null, }, - schedule: null, - }, data); + data, + ); + // eslint-disable-next-line cypress/no-assigning-return-values const request = cy.request('POST', '/api/queries', merged); if (shouldPublish) { request.then(({ body }) => cy.request('POST', `/api/queries/${body.id}`, { is_draft: false })); @@ -109,7 +111,9 @@ describe('Dashboard', () => { it('creates new dashboard', () => { cy.visit('/dashboards'); cy.getByTestId('CreateButton').click(); - cy.get('li[role="menuitem"]').contains('Dashboard').click(); + cy.get('li[role="menuitem"]') + .contains('Dashboard') + .click(); cy.server(); cy.route('POST', 'api/dashboards').as('NewDashboard'); @@ -193,9 +197,13 @@ describe('Dashboard', () => { cy.visit(this.dashboardUrl); cy.getByTestId(elTestId) .within(() => { - cy.get('.widget-menu-regular').click({ force: true }).within(() => { - cy.get('li a').contains('Remove From Dashboard').click({ force: true }); - }); + cy.get('.widget-menu-regular') + .click({ force: true }) + .within(() => { + cy.get('li a') + .contains('Remove From Dashboard') + .click({ force: true }); + }); }) .should('not.exist'); }); @@ -235,18 +243,27 @@ describe('Dashboard', () => { it('edits textbox', function () { addTextboxByAPI('Hello World!', this.dashboardId).then((elTestId) => { cy.visit(this.dashboardUrl); - cy.getByTestId(elTestId).as('textboxEl') + cy.getByTestId(elTestId) + .as('textboxEl') .within(() => { - cy.get('.widget-menu-regular').click({ force: true }).within(() => { - cy.get('li a').contains('Edit').click({ force: true }); - }); + cy.get('.widget-menu-regular') + .click({ force: true }) + .within(() => { + cy.get('li a') + .contains('Edit') + .click({ force: true }); + }); }); const newContent = '[edited]'; - cy.get('edit-text-box').should('exist').within(() => { - cy.get('textarea').clear().type(newContent); - cy.contains('button', 'Save').click(); - }); + cy.get('edit-text-box') + .should('exist') + .within(() => { + cy.get('textarea') + .clear() + .type(newContent); + cy.contains('button', 'Save').click(); + }); cy.get('@textboxEl').should('contain', newContent); }); @@ -269,7 +286,7 @@ describe('Dashboard', () => { describe('Draggable', () => { describe('Grid snap', () => { - beforeEach(function () { + beforeEach(() => { editDashboard(); }); diff --git a/client/cypress/integration/user/login_spec.js b/client/cypress/integration/user/login_spec.js index 46cb45f67..3e24f3826 100644 --- a/client/cypress/integration/user/login_spec.js +++ b/client/cypress/integration/user/login_spec.js @@ -6,7 +6,7 @@ describe('Login', () => { it('greets the user and take a screenshot', () => { cy.contains('h3', 'Login to Redash'); - cy.wait(1000); + cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting cy.percySnapshot('Login'); }); @@ -24,7 +24,7 @@ describe('Login', () => { cy.title().should('eq', 'Redash'); cy.contains('Example Admin'); - cy.wait(1000); + cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting cy.percySnapshot('Homepage'); }); }); diff --git a/client/cypress/support/commands.js b/client/cypress/support/commands.js index e8b39361b..2ea4546b7 100644 --- a/client/cypress/support/commands.js +++ b/client/cypress/support/commands.js @@ -1,4 +1,4 @@ -import '@percy/cypress'; // eslint-disable-line import/no-extraneous-dependencies +import '@percy/cypress'; // eslint-disable-line import/no-extraneous-dependencies, import/no-unresolved Cypress.Commands.add('login', (email = 'admin@redash.io', password = 'password') => cy.request({ url: '/login', diff --git a/package-lock.json b/package-lock.json index 437983243..c2f8c2813 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2479,7 +2479,7 @@ }, "bl": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "resolved": "http://registry.npmjs.org/bl/-/bl-1.2.2.tgz", "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", "requires": { "readable-stream": "^2.3.5", @@ -2493,7 +2493,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -2705,7 +2705,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -6251,7 +6251,7 @@ }, "finalhandler": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", "dev": true, "requires": { @@ -8738,7 +8738,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -9080,7 +9080,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -9174,7 +9174,7 @@ }, "html-webpack-plugin": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", + "resolved": "http://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", "integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=", "dev": true, "requires": { @@ -9821,7 +9821,7 @@ }, "is-obj": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" }, "is-observable": { @@ -11374,7 +11374,7 @@ }, "magic-string": { "version": "0.22.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz", + "resolved": "http://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz", "integrity": "sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w==", "requires": { "vlq": "^0.2.2" @@ -11486,12 +11486,12 @@ }, "minimist": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -11681,7 +11681,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -11877,7 +11877,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -12370,7 +12370,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -12772,7 +12772,7 @@ "dependencies": { "minimist": { "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", "dev": true }, @@ -15920,7 +15920,7 @@ "dependencies": { "minimist": { "version": "0.0.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.5.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.5.tgz", "integrity": "sha1-16oye87PUY+RBqxrjwA/o7zqhWY=" } } @@ -16408,7 +16408,7 @@ }, "split": { "version": "0.2.10", - "resolved": "https://registry.npmjs.org/split/-/split-0.2.10.tgz", + "resolved": "http://registry.npmjs.org/split/-/split-0.2.10.tgz", "integrity": "sha1-Zwl8YB1pfOE2j0GPBs0gHPBSGlc=", "requires": { "through": "2" @@ -16766,7 +16766,7 @@ "dependencies": { "readable-stream": { "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "requires": { "core-util-is": "~1.0.0", @@ -16900,7 +16900,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { diff --git a/package.json b/package.json index 06332083b..9f45fb0af 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "analyze:build": "npm run clean && NODE_ENV=production BUNDLE_ANALYZER=on webpack", "lint": "npm run lint:base -- --ext .js --ext .jsx ./client", "lint:base": "eslint --config ./client/.eslintrc.js --ignore-path ./client/.eslintignore", + "lint:ci": "npm run lint -- --format junit --output-file /tmp/test-results/eslint/results.xml", "test": "TZ=Africa/Khartoum jest", "test:watch": "jest --watch", "cypress:install": "npm install --no-save cypress@^3.1.5 @percy/cypress@^0.2.3 atob@2.1.2",