Repository: oppia/oppia

Issue:
Transition to ESLint from JSCS for code linting
JSCS has been depreciated by the dev team as note in a blog post here: https://medium.com/@markelog/jscs-end-of-the-line-bc9bf0b3fdb2#.3lqbp3ilv Therefore, we will need to migrate to ESLint as they recommend in the blog post. The official comment by ESLint is here: http://eslint.org/blog/2016/04/welcoming-jscs-to-eslint#making-eslint-work-for-jscs-users
## Resources

Migration doc: https://github.com/eslint/eslint/blob/master/docs/user-guide/migrating-from-jscs.md

Edit: Add resources link



Discussion:
Currently, it is best **not** to transition to ESLint at the moment as ESLint does not have a 1-to-1 matching with the rules in JSCS. See the page [here](https://github.com/eslint/eslint/milestones/JSCS%20Compatibility) for their progress. Once those steps are complete, the process to migrate can begin. 

I think we can just go ahead and transition to ESLint, actually; I'm not sure we need to worry too much about full parity here. What do you think, @kevinlee12? 
Yup, looks like Google has released their [ESlint presets](https://github.com/google/eslint-config-google) (which was my main concern for transition in the first place). We can worry about the things we customized as go along. 
I don't think we even need to follow the google style here, tbh -- we can probably just enable all the checks on a best-effort basis.

I'm happy to make updates to the .eslintrc.json file if we can get a pipeline running. @kevinlee12 would you like to take this on?
Yes I can!
Great, thanks @kevinlee12! 

Resolving patch:
diff --git a/scripts/pre_commit_linter.py b/scripts/pre_commit_linter.py
--- a/scripts/pre_commit_linter.py
+++ b/scripts/pre_commit_linter.py
@@ -21,9 +21,7 @@
 it will lint all Python and JavaScript files in that directory; otherwise,
 it will only lint files that have been touched in this commit.
 
-This script ignores all filepaths contained within the excludeFiles
-argument in .jscsrc. Note that, as a side-effect, these filepaths will also
-prevent Python files in those paths from being linted.
+This script ignores all filepaths contained within .eslintignore.
 
 IMPORTANT NOTES:
 
@@ -53,7 +51,6 @@
 # pylint: disable=wrong-import-order
 import argparse
 import fnmatch
-import json
 import multiprocessing
 import os
 import re
@@ -100,21 +97,6 @@
         'excluded_dirs': ()}
 }
 
-BAD_PATTERNS_JS = {
-    ' == ': {
-        'message': 'Please replace == with === in this file.',
-        'excluded_files': (
-            'core/templates/dev/head/expressions/parserSpec.js',
-            'core/templates/dev/head/expressions/evaluatorSpec.js',
-            'core/templates/dev/head/expressions/typeParserSpec.js')},
-    ' != ': {
-        'message': 'Please replace != with !== in this file.',
-        'excluded_files': (
-            'core/templates/dev/head/expressions/parserSpec.js',
-            'core/templates/dev/head/expressions/evaluatorSpec.js',
-            'core/templates/dev/head/expressions/typeParserSpec.js')}
-}
-
 BAD_PATTERNS_JS_REGEXP = [
     {
         'regexp': r"\b(ddescribe|fdescribe)\(",
@@ -217,22 +199,19 @@ def _get_changed_filenames():
     return unstaged_files + staged_files
 
 
-def _get_glob_patterns_excluded_from_jscsrc(config_jscsrc):
-    """Collects excludeFiles from jscsrc file.
+def _get_glob_patterns_excluded_from_eslint(eslintignore_path):
+    """Collects excludeFiles from .eslintignore file.
 
     Args:
-        config_jscsrc: str. Path to .jscsrc file.
+        eslintignore_path: str. Path to .eslintignore file.
 
     Returns:
         a list of files in excludeFiles.
     """
-    with open(config_jscsrc) as f:
-        f.readline()  # First three lines are comments
-        f.readline()
-        f.readline()
-        json_data = json.loads(f.read())
-
-    return json_data['excludeFiles']
+    file_data = []
+    with open(eslintignore_path) as f:
+        file_data.extend(f.readlines())
+    return file_data
 
 
 def _get_all_files_in_directory(dir_path, excluded_glob_patterns):
@@ -258,16 +237,15 @@ def _get_all_files_in_directory(dir_path, excluded_glob_patterns):
     return files_in_directory
 
 
-def _lint_js_files(node_path, jscs_path, config_jscsrc, files_to_lint, stdout,
+def _lint_js_files(node_path, eslint_path, files_to_lint, stdout,
                    result):
     """Prints a list of lint errors in the given list of JavaScript files.
 
     Args:
         node_path: str. Path to the node binary.
-        jscs_path: str. Path to the JSCS binary.
-        config_jscsrc: str. Configuration args for the call to the JSCS binary.
+        eslint_path: str. Path to the ESLint binary.
         files_to_lint: list(str). A list of filepaths to lint.
-        stdout:  multiprocessing.Queue. A queue to store JSCS outputs.
+        stdout:  multiprocessing.Queue. A queue to store ESLint outputs.
         result: multiprocessing.Queue. A queue to put results of test.
 
     Returns:
@@ -283,10 +261,10 @@ def _lint_js_files(node_path, jscs_path, config_jscsrc, files_to_lint, stdout,
         return
 
     print 'Total js files: ', num_js_files
-    jscs_cmd_args = [node_path, jscs_path, config_jscsrc]
+    eslint_cmd_args = [node_path, eslint_path, '--quiet']
     for _, filename in enumerate(files_to_lint):
         print 'Linting: ', filename
-        proc_args = jscs_cmd_args + [filename]
+        proc_args = eslint_cmd_args + [filename]
         proc = subprocess.Popen(
             proc_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 
@@ -367,7 +345,7 @@ def _get_all_files():
     root directory and to return a list of all the files for linting and
     pattern checks.
     """
-    jscsrc_path = os.path.join(os.getcwd(), '.jscsrc')
+    eslintignore_path = os.path.join(os.getcwd(), '.eslintignore')
     parsed_args = _PARSER.parse_args()
     if parsed_args.path:
         input_path = os.path.join(os.getcwd(), parsed_args.path)
@@ -378,8 +356,8 @@ def _get_all_files():
         if os.path.isfile(input_path):
             all_files = [input_path]
         else:
-            excluded_glob_patterns = _get_glob_patterns_excluded_from_jscsrc(
-                jscsrc_path)
+            excluded_glob_patterns = _get_glob_patterns_excluded_from_eslint(
+                eslintignore_path)
             all_files = _get_all_files_in_directory(
                 input_path, excluded_glob_patterns)
     elif parsed_args.files:
@@ -397,31 +375,32 @@ def _get_all_files():
         all_files = valid_filepaths
     else:
         all_files = _get_changed_filenames()
+    all_files = [
+        filename for filename in all_files if not
+        any(fnmatch.fnmatch(filename, pattern) for pattern in EXCLUDED_PATHS)]
     return all_files
 
 
 def _pre_commit_linter(all_files):
-    """This function is used to check if node-jscs dependencies are installed
-    and pass JSCS binary path.
+    """This function is used to check if node-eslint dependencies are installed
+    and pass ESLint binary path.
     """
     print 'Starting linter...'
 
-    jscsrc_path = os.path.join(os.getcwd(), '.jscsrc')
     pylintrc_path = os.path.join(os.getcwd(), '.pylintrc')
 
-    config_jscsrc = '--config=%s' % jscsrc_path
     config_pylint = '--rcfile=%s' % pylintrc_path
 
     parent_dir = os.path.abspath(os.path.join(os.getcwd(), os.pardir))
 
     node_path = os.path.join(
         parent_dir, 'oppia_tools', 'node-6.9.1', 'bin', 'node')
-    jscs_path = os.path.join(
-        parent_dir, 'node_modules', 'jscs', 'bin', 'jscs')
+    eslint_path = os.path.join(
+        parent_dir, 'node_modules', 'eslint', 'bin', 'eslint.js')
 
-    if not os.path.exists(jscs_path):
+    if not os.path.exists(eslint_path):
         print ''
-        print 'ERROR    Please run start.sh first to install node-jscs '
+        print 'ERROR    Please run start.sh first to install node-eslint '
         print '         and its dependencies.'
         sys.exit(1)
 
@@ -434,8 +413,8 @@ def _pre_commit_linter(all_files):
     linting_processes = []
     js_stdout = multiprocessing.Queue()
     linting_processes.append(multiprocessing.Process(
-        target=_lint_js_files, args=(node_path, jscs_path, config_jscsrc,
-                                     js_files_to_lint, js_stdout, js_result)))
+        target=_lint_js_files, args=(node_path, eslint_path, js_files_to_lint,
+                                     js_stdout, js_result)))
 
     py_result = multiprocessing.Queue()
     linting_processes.append(multiprocessing.Process(
@@ -548,15 +527,6 @@ def _check_bad_patterns(all_files):
                         filename, BAD_PATTERNS[pattern]['message'])
                     total_error_count += 1
             if filename.endswith('.js'):
-                for pattern in BAD_PATTERNS_JS:
-                    if filename not in (
-                            BAD_PATTERNS_JS[pattern]['excluded_files']):
-                        if pattern in content:
-                            failed = True
-                            print '%s --> %s' % (
-                                filename,
-                                BAD_PATTERNS_JS[pattern]['message'])
-                            total_error_count += 1
                 for regexp in BAD_PATTERNS_JS_REGEXP:
                     regexp_pattern = regexp['regexp']
                     if filename not in regexp['excluded_files']:


Test patch:
diff --git a/core/templates/dev/head/pages/tests/FormBuilderTestPage.js b/core/templates/dev/head/pages/tests/FormBuilderTestPage.js
--- a/core/templates/dev/head/pages/tests/FormBuilderTestPage.js
+++ b/core/templates/dev/head/pages/tests/FormBuilderTestPage.js
@@ -44,222 +44,224 @@ oppia.directive('formOverlay', ['recursionHelper', function(recursionHelper) {
 }]);
 
 oppia.controller('FormBuilderTests', [
-    '$scope', 'parameterSpecsService', function($scope, parameterSpecsService) {
-  parameterSpecsService.addParamSpec('paramBool1', 'bool');
-  parameterSpecsService.addParamSpec('paramBool2', 'bool');
-  parameterSpecsService.addParamSpec('paramInt1', 'int');
-  parameterSpecsService.addParamSpec('paramFloat1', 'float');
-  parameterSpecsService.addParamSpec('paramFloat2', 'float');
-  parameterSpecsService.addParamSpec('paramUnicode1', 'unicode');
-  parameterSpecsService.addParamSpec('paramUnicode2', 'unicode');
-  parameterSpecsService.addParamSpec('paramUnicode3', 'unicode');
+  '$scope', 'parameterSpecsService',
+  function($scope, parameterSpecsService) {
+    parameterSpecsService.addParamSpec('paramBool1', 'bool');
+    parameterSpecsService.addParamSpec('paramBool2', 'bool');
+    parameterSpecsService.addParamSpec('paramInt1', 'int');
+    parameterSpecsService.addParamSpec('paramFloat1', 'float');
+    parameterSpecsService.addParamSpec('paramFloat2', 'float');
+    parameterSpecsService.addParamSpec('paramUnicode1', 'unicode');
+    parameterSpecsService.addParamSpec('paramUnicode2', 'unicode');
+    parameterSpecsService.addParamSpec('paramUnicode3', 'unicode');
 
-  $scope.testText = 'abc{{paramUnicode1}}';
+    $scope.testText = 'abc{{paramUnicode1}}';
 
-  $scope.unicodeForm = {
-    schema: {
-      type: 'unicode'
-    },
-    value: 'aab{{paramUnicode1}}',
-    allowExpressions: true
-  };
-
-  $scope.booleanForms = [{
-    name: 'Boolean form',
-    schema: {
-      type: 'bool'
-    },
-    value: true
-  }, {
-    name: 'Boolean form with expressions',
-    schema: {
-      type: 'bool'
-    },
-    value: 'paramBool1',
-    allowExpressions: true
-  }];
-
-  $scope.intForms = [{
-    name: 'Integer form (value must be greater than 2)',
-    schema: {
-      type: 'int',
-      validators: [{
-        id: 'is_at_least',
-        min_value: 2
-      }]
-    },
-    value: 3
-  }, {
-    // TODO(sll): Add test for bad initialization.
-    name: 'Integer form with expressions',
-    schema: {
-      type: 'int'
-    },
-    value: 'paramInt1',
-    allowExpressions: true
-  }];
+    $scope.unicodeForm = {
+      schema: {
+        type: 'unicode'
+      },
+      value: 'aab{{paramUnicode1}}',
+      allowExpressions: true
+    };
 
-  $scope.floatForms = [{
-    name: 'Float form (value must be between -3 and 6)',
-    schema: {
-      type: 'float',
-      validators: [{
-        id: 'is_at_least',
-        min_value: -3.0
-      }, {
-        id: 'is_at_most',
-        max_value: 6.0
-      }]
-    },
-    value: 3.14
-  }, {
-    name: 'Float form with expressions (value must be between -3 and 6)',
-    schema: {
-      type: 'float',
-      validators: [{
-        id: 'is_at_least',
-        min_value: -3.0
-      }, {
-        id: 'is_at_most',
-        max_value: 6.0
-      }]
-    },
-    allowExpressions: true,
-    value: 3.14
-  }];
+    $scope.booleanForms = [{
+      name: 'Boolean form',
+      schema: {
+        type: 'bool'
+      },
+      value: true
+    }, {
+      name: 'Boolean form with expressions',
+      schema: {
+        type: 'bool'
+      },
+      value: 'paramBool1',
+      allowExpressions: true
+    }];
 
-  $scope.unicodeForms = [{
-    name: 'Restricted unicode form; the value must be either a or b.',
-    schema: {
-      type: 'unicode',
-      choices: ['a', 'b']
-    },
-    value: 'a'
-  }];
+    $scope.intForms = [{
+      name: 'Integer form (value must be greater than 2)',
+      schema: {
+        type: 'int',
+        validators: [{
+          id: 'is_at_least',
+          min_value: 2
+        }]
+      },
+      value: 3
+    }, {
+      // TODO(sll): Add test for bad initialization.
+      name: 'Integer form with expressions',
+      schema: {
+        type: 'int'
+      },
+      value: 'paramInt1',
+      allowExpressions: true
+    }];
 
-  $scope.htmlForms = [{
-    name: 'HTML form',
-    schema: {
-      type: 'html'
-    },
-    value: 'Some <b>HTML</b>'
-  }];
+    $scope.floatForms = [{
+      name: 'Float form (value must be between -3 and 6)',
+      schema: {
+        type: 'float',
+        validators: [{
+          id: 'is_at_least',
+          min_value: -3.0
+        }, {
+          id: 'is_at_most',
+          max_value: 6.0
+        }]
+      },
+      value: 3.14
+    }, {
+      name: 'Float form with expressions (value must be between -3 and 6)',
+      schema: {
+        type: 'float',
+        validators: [{
+          id: 'is_at_least',
+          min_value: -3.0
+        }, {
+          id: 'is_at_most',
+          max_value: 6.0
+        }]
+      },
+      allowExpressions: true,
+      value: 3.14
+    }];
 
-  $scope.compositeForms = [{
-    name: (
-      'Dict with a bool, a unicode string and a list of ints. ' +
-      'The string must be either \'abc\' or \'def\'.'),
-    schema: {
-      type: 'dict',
-      properties: [{
-        name: 'a_unicode_string_appearing_first',
-        description: 'First field.',
-        schema: {
-          type: 'unicode',
-          choices: ['abc', 'def']
-        }
-      }, {
-        name: 'a_list_appearing_second',
-        description: 'Second field.',
-        schema: {
-          type: 'list',
-          items: {
-            type: 'int'
-          }
-        }
-      }, {
-        name: 'a_boolean_appearing_last',
-        description: 'Third field.',
-        schema: {
-          type: 'bool'
-        }
-      }]
-    },
-    value: {
-      a_boolean_appearing_last: false,
-      a_unicode_string_appearing_first: 'abc',
-      a_list_appearing_second: [2, 3]
-    }
-  }, {
-    name: 'List of code areas with custom \'add element\' text',
-    schema: {
-      type: 'list',
-      items: {
+    $scope.unicodeForms = [{
+      name: 'Restricted unicode form; the value must be either a or b.',
+      schema: {
         type: 'unicode',
-        ui_config: {
-          coding_mode: 'python'
-        }
+        choices: ['a', 'b']
       },
-      ui_config: {
-        add_element_text: '[Custom \'add element\' text]'
-      }
-    },
-    value: ['abc', 'def', 'ghi']
-  }, {
-    name: 'Fixed-length list of 2 multiple-choice floats',
-    schema: {
-      type: 'list',
-      items: {
-        type: 'float',
-        choices: [1.0, 0.0, -1.0, -2.0, -3.0]
+      value: 'a'
+    }];
+
+    $scope.htmlForms = [{
+      name: 'HTML form',
+      schema: {
+        type: 'html'
       },
-      len: 2
-    },
-    value: [1.0, -3.0]
-  }, {
-    name: 'List of complex items (no descriptions in the dicts)',
-    schema: {
-      type: 'list',
-      items: {
+      value: 'Some <b>HTML</b>'
+    }];
+
+    $scope.compositeForms = [{
+      name: (
+        'Dict with a bool, a unicode string and a list of ints. ' +
+        'The string must be either \'abc\' or \'def\'.'),
+      schema: {
         type: 'dict',
         properties: [{
-          name: 'intField',
+          name: 'a_unicode_string_appearing_first',
+          description: 'First field.',
           schema: {
-            type: 'int'
+            type: 'unicode',
+            choices: ['abc', 'def']
           }
         }, {
-          name: 'htmlField',
+          name: 'a_list_appearing_second',
+          description: 'Second field.',
           schema: {
-            type: 'html'
+            type: 'list',
+            items: {
+              type: 'int'
+            }
+          }
+        }, {
+          name: 'a_boolean_appearing_last',
+          description: 'Third field.',
+          schema: {
+            type: 'bool'
           }
         }]
+      },
+      value: {
+        a_boolean_appearing_last: false,
+        a_unicode_string_appearing_first: 'abc',
+        a_list_appearing_second: [2, 3]
       }
-    },
-    value: [{
-      intField: 5,
-      htmlField: '<span><b>d</b>ef</span>'
-    }]
-  }, {
-    name: 'Nested lists',
-    schema: {
-      type: 'list',
-      items: {
+    }, {
+      name: 'List of code areas with custom \'add element\' text',
+      schema: {
         type: 'list',
         items: {
-          type: 'unicode'
+          type: 'unicode',
+          ui_config: {
+            coding_mode: 'python'
+          }
+        },
+        ui_config: {
+          add_element_text: '[Custom \'add element\' text]'
         }
-      }
-    },
-    value: [['abc'], ['def', 'ghi']]
-  }];
+      },
+      value: ['abc', 'def', 'ghi']
+    }, {
+      name: 'Fixed-length list of 2 multiple-choice floats',
+      schema: {
+        type: 'list',
+        items: {
+          type: 'float',
+          choices: [1.0, 0.0, -1.0, -2.0, -3.0]
+        },
+        len: 2
+      },
+      value: [1.0, -3.0]
+    }, {
+      name: 'List of complex items (no descriptions in the dicts)',
+      schema: {
+        type: 'list',
+        items: {
+          type: 'dict',
+          properties: [{
+            name: 'intField',
+            schema: {
+              type: 'int'
+            }
+          }, {
+            name: 'htmlField',
+            schema: {
+              type: 'html'
+            }
+          }]
+        }
+      },
+      value: [{
+        intField: 5,
+        htmlField: '<span><b>d</b>ef</span>'
+      }]
+    }, {
+      name: 'Nested lists',
+      schema: {
+        type: 'list',
+        items: {
+          type: 'list',
+          items: {
+            type: 'unicode'
+          }
+        }
+      },
+      value: [['abc'], ['def', 'ghi']]
+    }];
 
-  $scope.formsets = [{
-    name: 'Boolean editors',
-    forms: $scope.booleanForms
-  }, {
-    name: 'Integer editors',
-    forms: $scope.intForms
-  }, {
-    name: 'Float editors',
-    forms: $scope.floatForms
-  }, {
-    name: 'Unicode editors',
-    forms: $scope.unicodeForms
-  }, {
-    name: 'HTML editors',
-    forms: $scope.htmlForms
-  }, {
-    name: 'Composite editors',
-    forms: $scope.compositeForms
-  }];
-}]);
+    $scope.formsets = [{
+      name: 'Boolean editors',
+      forms: $scope.booleanForms
+    }, {
+      name: 'Integer editors',
+      forms: $scope.intForms
+    }, {
+      name: 'Float editors',
+      forms: $scope.floatForms
+    }, {
+      name: 'Unicode editors',
+      forms: $scope.unicodeForms
+    }, {
+      name: 'HTML editors',
+      forms: $scope.htmlForms
+    }, {
+      name: 'Composite editors',
+      forms: $scope.compositeForms
+    }];
+  }
+]);
diff --git a/core/tests/karma.conf.js b/core/tests/karma.conf.js
--- a/core/tests/karma.conf.js
+++ b/core/tests/karma.conf.js
@@ -3,7 +3,7 @@ var isMinificationNeeded = (argv.minify === 'True');
 var generatedJs = 'third_party/generated/js/third_party.js';
 if (isMinificationNeeded) {
   generatedJs = 'third_party/generated/js/third_party.min.js';
-};
+}
 
 module.exports = function(config) {
   config.set({
diff --git a/core/tests/protractor/collections.js b/core/tests/protractor/collections.js
--- a/core/tests/protractor/collections.js
+++ b/core/tests/protractor/collections.js
@@ -30,10 +30,11 @@ describe('Collections', function() {
     general.acceptAlert();
     browser.waitForAngular();
     admin.editConfigProperty(
-        'Names of users allowed to use the collection editor',
-        'List', function(listEditor) {
-      listEditor.addItem('Unicode').setValue(USERNAME);
-    });
+      'Names of users allowed to use the collection editor',
+      'List', function(listEditor) {
+        listEditor.addItem('Unicode').setValue(USERNAME);
+      }
+    );
     users.logout();
   });
 
diff --git a/core/tests/protractor/editorAndPlayer.js b/core/tests/protractor/editorAndPlayer.js
--- a/core/tests/protractor/editorAndPlayer.js
+++ b/core/tests/protractor/editorAndPlayer.js
@@ -141,6 +141,7 @@ describe('Full exploration editor', function() {
         expectAvailableDestinationsToBe(['second', 'final card']);
       editor.addResponse('NumericInput', null, 'final card', false,
         'IsGreaterThan', 2);
+      // eslint-disable-next-line dot-notation
       editor.ResponseEditor(0).delete();
 
       // Setup a terminating state
diff --git a/core/tests/protractor/embedding.js b/core/tests/protractor/embedding.js
--- a/core/tests/protractor/embedding.js
+++ b/core/tests/protractor/embedding.js
@@ -187,7 +187,7 @@ describe('Embedding', function() {
       general.waitForSystem();
       browser.waitForAngular();
       expect(driver.findElement(by.css('.protractor-test-float-form-input'))
-          .getAttribute('placeholder')).toBe(expectedPlaceholder);
+        .getAttribute('placeholder')).toBe(expectedPlaceholder);
       browser.switchTo().defaultContent();
     };
 
diff --git a/core/tests/protractor/i18n.js b/core/tests/protractor/i18n.js
--- a/core/tests/protractor/i18n.js
+++ b/core/tests/protractor/i18n.js
@@ -71,24 +71,25 @@ describe('Site language', function() {
   });
 
   it('should save the language selected in the footer into the preferences.',
-      function() {
-    users.createUser('feanor@example.com', 'Feanor');
-    users.login('feanor@example.com');
-    browser.get('/about');
-    _selectLanguage('Español');
-    browser.get('/library');
-    expect(browser.getTitle()).toEqual('Biblioteca - Oppia');
+    function() {
+      users.createUser('feanor@example.com', 'Feanor');
+      users.login('feanor@example.com');
+      browser.get('/about');
+      _selectLanguage('Español');
+      browser.get('/library');
+      expect(browser.getTitle()).toEqual('Biblioteca - Oppia');
 
-    // The preference page shows the last selected language
-    browser.get('/preferences');
-    language = element(by.css('.protractor-test-system-language-selector'))
-      .element(by.css('.select2-chosen'));
-    expect(language.getText(), 'Español');
-    expect(browser.getTitle()).toEqual(
-      'Cambiar sus preferencias de perfil - Oppia');
-    general.ensurePageHasNoTranslationIds();
-    users.logout();
-  });
+      // The preference page shows the last selected language
+      browser.get('/preferences');
+      language = element(by.css('.protractor-test-system-language-selector'))
+        .element(by.css('.select2-chosen'));
+      expect(language.getText(), 'Español');
+      expect(browser.getTitle()).toEqual(
+        'Cambiar sus preferencias de perfil - Oppia');
+      general.ensurePageHasNoTranslationIds();
+      users.logout();
+    }
+  );
 
   it('should be used in titles of pages without controllers', function() {
     browser.get('/about');
diff --git a/core/tests/protractor/loginFlow.js b/core/tests/protractor/loginFlow.js
--- a/core/tests/protractor/loginFlow.js
+++ b/core/tests/protractor/loginFlow.js
@@ -28,11 +28,12 @@ describe('Login Flow', function() {
   it('visits the links in the dropdown', function() {
     var profileDropdown = element(by.css('.protractor-test-profile-dropdown'));
 
-    var classNames = ['.protractor-test-profile-link',
-                      '.protractor-test-dashboard-link',
-                      '.protractor-test-preferences-link',
-                      '.protractor-test-notifications-link'
-                     ];
+    var classNames = [
+      '.protractor-test-profile-link',
+      '.protractor-test-dashboard-link',
+      '.protractor-test-preferences-link',
+      '.protractor-test-notifications-link'
+    ];
     classNames.forEach(function(className) {
       browser.actions().mouseMove(profileDropdown).perform();
       profileDropdown.element(by.css(className)).click();
diff --git a/core/tests/protractor/stateEditor.js b/core/tests/protractor/stateEditor.js
--- a/core/tests/protractor/stateEditor.js
+++ b/core/tests/protractor/stateEditor.js
@@ -27,21 +27,22 @@ var player = require('../protractor_utils/player.js');
 
 describe('State editor', function() {
   it('should walk through the tutorial when user repeatedly clicks Next',
-      function() {
-    var NUM_TUTORIAL_STAGES = 7;
-    users.createUser(
-      'userTutorial@stateEditor.com', 'userTutorialStateEditor');
-    users.login('userTutorial@stateEditor.com');
-
-    workflow.createExplorationAndStartTutorial();
-    editor.startTutorial();
-    for (var i = 0; i < NUM_TUTORIAL_STAGES - 1; i++) {
-      editor.progressInTutorial();
-      general.waitForSystem();
+    function() {
+      var NUM_TUTORIAL_STAGES = 7;
+      users.createUser(
+        'userTutorial@stateEditor.com', 'userTutorialStateEditor');
+      users.login('userTutorial@stateEditor.com');
+
+      workflow.createExplorationAndStartTutorial();
+      editor.startTutorial();
+      for (var i = 0; i < NUM_TUTORIAL_STAGES - 1; i++) {
+        editor.progressInTutorial();
+        general.waitForSystem();
+      }
+      editor.finishTutorial();
+      users.logout();
     }
-    editor.finishTutorial();
-    users.logout();
-  });
+  );
 
   it('should display plain text content', function() {
     users.createUser('user1@stateEditor.com', 'user1StateEditor');
diff --git a/core/tests/protractor_utils/admin.js b/core/tests/protractor_utils/admin.js
--- a/core/tests/protractor_utils/admin.js
+++ b/core/tests/protractor_utils/admin.js
@@ -32,28 +32,31 @@ var editConfigProperty = function(
   general.waitForSystem();
   browser.get(general.ADMIN_URL_SUFFIX);
   element(by.css('.protractor-test-admin-config-tab')).click();
-  element.all(by.css('.protractor-test-config-property')).
-      map(function(configProperty) {
-    return configProperty.element(by.css('.protractor-test-config-title')).
-        getText().then(function(title) {
-      if (title.match(propertyName)) {
-        editingInstructions(forms.getEditor(objectType)(configProperty));
-        element(by.css('.protractor-test-save-all-configs')).click();
-        general.acceptAlert();
-        // Time is needed for the saving to complete.
-        browser.waitForAngular();
-        return true;
+  element.all(by.css('.protractor-test-config-property'))
+    .map(function(configProperty) {
+      return configProperty.element(by.css('.protractor-test-config-title'))
+        .getText()
+        .then(function(title) {
+          if (title.match(propertyName)) {
+            editingInstructions(forms.getEditor(objectType)(configProperty));
+            element(by.css('.protractor-test-save-all-configs')).click();
+            general.acceptAlert();
+            // Time is needed for the saving to complete.
+            browser.waitForAngular();
+            return true;
+          }
+        }
+      );
+    }).then(function(results) {
+      var success = false;
+      for (var i = 0; i < results.length; i++) {
+        success = success || results[i];
+      }
+      if (!success) {
+        throw Error('Could not find config property: ' + propertyName);
       }
-    });
-  }).then(function(results) {
-    var success = false;
-    for (var i = 0; i < results.length; i++) {
-      success = success || results[i];
-    }
-    if (!success) {
-      throw Error('Could not find config property: ' + propertyName);
     }
-  });
+  );
 };
 
 // The name should be as given in the admin page (including '.yaml' if
@@ -61,21 +64,23 @@ var editConfigProperty = function(
 var reloadExploration = function(name) {
   browser.get(general.ADMIN_URL_SUFFIX);
   element.all(by.css('.protractor-test-reload-exploration-row')).
-      map(function(explorationElement) {
-    explorationElement.element(
-        by.css('.protractor-test-reload-exploration-title')
-      ).getText().then(function(title) {
-      // We use match here in case there is whitespace around the name
-      if (title.match(name)) {
-        explorationElement.element(
-          by.css('.protractor-test-reload-exploration-button')
-        ).click();
-        general.acceptAlert();
-        // Time is needed for the reloading to complete.
-        browser.waitForAngular();
-      }
-    });
-  });
+    map(function(explorationElement) {
+      explorationElement.element(
+          by.css('.protractor-test-reload-exploration-title')
+        ).getText().then(function(title) {
+          // We use match here in case there is whitespace around the name
+          if (title.match(name)) {
+            explorationElement.element(
+              by.css('.protractor-test-reload-exploration-button')
+            ).click();
+            general.acceptAlert();
+            // Time is needed for the reloading to complete.
+            browser.waitForAngular();
+          }
+        }
+      );
+    }
+  );
 };
 
 exports.editConfigProperty = editConfigProperty;
diff --git a/core/tests/protractor_utils/editor.js b/core/tests/protractor_utils/editor.js
--- a/core/tests/protractor_utils/editor.js
+++ b/core/tests/protractor_utils/editor.js
@@ -783,6 +783,7 @@ var ResponseEditor = function(responseNum) {
       // Save the new rule.
       element(by.css('.protractor-test-save-answer')).click();
     },
+    // eslint-disable-next-line quote-props
     delete: function() {
       headerElem.element(by.css('.protractor-test-delete-response')).click();
       element(by.css('.protractor-test-confirm-delete-response')).click();
@@ -864,6 +865,7 @@ var FallbackEditor = function(fallbackNum) {
       // Save destination.
       element(by.css('.protractor-test-save-outcome-dest')).click();
     },
+    // eslint-disable-next-line quote-props
     delete: function() {
       headerElem.element(by.css('.protractor-test-delete-response')).click();
       element(by.css('.protractor-test-confirm-delete-fallback')).click();
@@ -1042,12 +1044,13 @@ var setLanguage = function(language) {
 
 var expectAvailableFirstStatesToBe = function(names) {
   runFromSettingsTab(function() {
-    element(by.css('.protractor-test-initial-state-select')).
-        all(by.tagName('option')).map(function(elem) {
-      return elem.getText();
-    }).then(function(options) {
-      expect(options.sort()).toEqual(names.sort());
-    });
+    element(by.css('.protractor-test-initial-state-select'))
+      .all(by.tagName('option')).map(function(elem) {
+        return elem.getText();
+      }).then(function(options) {
+        expect(options.sort()).toEqual(names.sort());
+      }
+    );
   });
 };
 
@@ -1139,44 +1142,49 @@ var _selectComparedVersions = function(v1, v2) {
   var v1Position = null;
   var v2Position = null;
   element.all(by.css('.protractor-test-history-checkbox-selector')).count()
-      .then(function(versionNumber) {
-    if (v1 < 0) {
-      throw Error('In editor._selectComparedVersions(' + v1 + ', ' + v2 + '),' +
-      'expected v1 to be >= 0');
-    }
-    if (v2 < 0) {
-      throw Error('In editor._selectComparedVersions(' + v1 + ', ' + v2 + '),' +
-      'expected v2 to be >= 0');
-    }
-    // Check to ensure no negative indices are queried
-    if (v1 > versionNumber) {
-      throw Error(
-        'In editor._selectComparedVersions(' + v1 + ', ' + v2 + '),' +
-        'expected v1 to be less than or equal to total number of saved ' +
-        'revisions');
-    }
-    if (v2 > versionNumber) {
-      throw Error('In editor._selectComparedVersions(' + v1 + ', ' + v2 + '),' +
-      'expected v2 be less than or equal to total number of saved revisions');
-    }
+    .then(function(versionNumber) {
+      if (v1 < 0) {
+        throw Error(
+          'In editor._selectComparedVersions(' + v1 + ', ' + v2 + '),' +
+          'expected v1 to be >= 0');
+      }
+      if (v2 < 0) {
+        throw Error(
+          'In editor._selectComparedVersions(' + v1 + ', ' + v2 + '),' +
+          'expected v2 to be >= 0');
+      }
+      // Check to ensure no negative indices are queried
+      if (v1 > versionNumber) {
+        throw Error(
+          'In editor._selectComparedVersions(' + v1 + ', ' + v2 + '),' +
+          'expected v1 to be less than or equal to total number of saved ' +
+          'revisions');
+      }
+      if (v2 > versionNumber) {
+        throw Error(
+          'In editor._selectComparedVersions(' + v1 + ', ' + v2 + '),' +
+        'expected v2 be less than or equal to total number of saved revisions');
+      }
 
-    v1Position = versionNumber - v1;
-    v2Position = versionNumber - v2;
+      v1Position = versionNumber - v1;
+      v2Position = versionNumber - v2;
 
-    element.all(by.css('.protractor-test-history-checkbox-selector'))
-      .get(v1Position).click();
-    element.all(by.css('.protractor-test-history-checkbox-selector'))
-      .get(v2Position).click();
-    browser.waitForAngular();
-  });
+      element.all(by.css('.protractor-test-history-checkbox-selector'))
+        .get(v1Position).click();
+      element.all(by.css('.protractor-test-history-checkbox-selector'))
+        .get(v2Position).click();
+      browser.waitForAngular();
+    }
+  );
 
   // Click button to show graph if necessary
   element(by.css('.protractor-test-show-history-graph')).isDisplayed()
-      .then(function(isDisplayed) {
-    if (isDisplayed) {
-      element(by.css('.protractor-test-show-history-graph')).click();
+    .then(function(isDisplayed) {
+      if (isDisplayed) {
+        element(by.css('.protractor-test-show-history-graph')).click();
+      }
     }
-  });
+  );
 };
 
 var expectGraphComparisonOf = function(v1, v2) {
@@ -1188,21 +1196,22 @@ var expectGraphComparisonOf = function(v1, v2) {
   //   - 'color': color of the node
   var _expectHistoryStatesToBe = function(expectedStates) {
     element(by.css('.protractor-test-history-graph'))
-        .all(by.css('.protractor-test-node')).map(function(stateNode) {
-      return {
-        label: stateNode.element(
-          by.css('.protractor-test-node-label')).getText(),
-        color: stateNode.element(
-          by.css('.protractor-test-node-background')).getCssValue('fill')
-      };
-    }).then(function(states) {
-      // Note: we need to compare this way because the state graph is sometimes
-      // generated with states in different configurations.
-      expect(states.length).toEqual(expectedStates.length);
-      for (var i = 0; i < states.length; i++) {
-        expect(expectedStates).toContain(states[i]);
+      .all(by.css('.protractor-test-node')).map(function(stateNode) {
+        return {
+          label: stateNode.element(
+            by.css('.protractor-test-node-label')).getText(),
+          color: stateNode.element(
+            by.css('.protractor-test-node-background')).getCssValue('fill')
+        };
+      }).then(function(states) {
+        // Note: we need to compare this way because the state graph is
+        // sometimes generated with states in different configurations.
+        expect(states.length).toEqual(expectedStates.length);
+        for (var i = 0; i < states.length; i++) {
+          expect(expectedStates).toContain(states[i]);
+        }
       }
-    });
+    );
   };
 
   // Checks that the history graph contains totalLinks links altogether,
@@ -1215,48 +1224,49 @@ var expectGraphComparisonOf = function(v1, v2) {
     var addedCount = 0;
     var deletedCount = 0;
     element(by.css('.protractor-test-history-graph'))
-        .all(by.css('.protractor-test-link')).map(function(link) {
-      return link.getCssValue('stroke').then(function(linkColor) {
-        if (linkColor === COLOR_ADDED) {
-          return 'added';
-        } else if (linkColor === COLOR_DELETED) {
-          return 'deleted';
-        } else {
-          return 'other';
-        }
-      });
-    }).then(function(linkTypes) {
-      var totalCount = 0;
-      var addedCount = 0;
-      var deletedCount = 0;
-      for (var i = 0; i < linkTypes.length; i++) {
-        totalCount++;
-        if (linkTypes[i] === 'added') {
-          addedCount++;
-        } else if (linkTypes[i] === 'deleted') {
-          deletedCount++;
+      .all(by.css('.protractor-test-link')).map(function(link) {
+        return link.getCssValue('stroke').then(function(linkColor) {
+          if (linkColor === COLOR_ADDED) {
+            return 'added';
+          } else if (linkColor === COLOR_DELETED) {
+            return 'deleted';
+          } else {
+            return 'other';
+          }
+        });
+      }).then(function(linkTypes) {
+        var totalCount = 0;
+        var addedCount = 0;
+        var deletedCount = 0;
+        for (var i = 0; i < linkTypes.length; i++) {
+          totalCount++;
+          if (linkTypes[i] === 'added') {
+            addedCount++;
+          } else if (linkTypes[i] === 'deleted') {
+            deletedCount++;
+          }
         }
-      }
 
-      if (totalCount !== totalLinks) {
-        throw Error(
-          'In editor.expectGraphComparisonOf(' + v1 + ', ' + v2 + '), ' +
-          'expected to find ' + totalLinks + ' links in total, ' +
-          'but found ' + totalCount);
-      }
-      if (addedCount !== addedLinks) {
-        throw Error(
-          'In editor.expectGraphComparisonOf(' + v1 + ', ' + v2 + '), ' +
-          'expected to find ' + addedLinks + ' added links, ' + 'but found ' +
-          addedCount);
-      }
-      if (deletedCount !== deletedLinks) {
-        throw Error(
-          'In editor.expectGraphComparisonOf(' + v1 + ', ' + v2 + '), ' +
-          'expected to find ' + deletedLinks + ' deleted links, ' +
-          'but found ' + deletedCount);
+        if (totalCount !== totalLinks) {
+          throw Error(
+            'In editor.expectGraphComparisonOf(' + v1 + ', ' + v2 + '), ' +
+            'expected to find ' + totalLinks + ' links in total, ' +
+            'but found ' + totalCount);
+        }
+        if (addedCount !== addedLinks) {
+          throw Error(
+            'In editor.expectGraphComparisonOf(' + v1 + ', ' + v2 + '), ' +
+            'expected to find ' + addedLinks + ' added links, ' + 'but found ' +
+            addedCount);
+        }
+        if (deletedCount !== deletedLinks) {
+          throw Error(
+            'In editor.expectGraphComparisonOf(' + v1 + ', ' + v2 + '), ' +
+            'expected to find ' + deletedLinks + ' deleted links, ' +
+            'but found ' + deletedCount);
+        }
       }
-    });
+    );
   };
 
   return {
diff --git a/core/tests/protractor_utils/forms.js b/core/tests/protractor_utils/forms.js
--- a/core/tests/protractor_utils/forms.js
+++ b/core/tests/protractor_utils/forms.js
@@ -37,10 +37,11 @@ var DictionaryEditor = function(elem) {
 var ListEditor = function(elem) {
   // NOTE: this returns a promise, not an integer.
   var _getLength = function() {
-    return elem.all(by.repeater('item in localValue track by $index')).
-        then(function(items) {
-      return items.length;
-    });
+    return elem.all(by.repeater('item in localValue track by $index'))
+      .then(function(items) {
+        return items.length;
+      }
+    );
   };
   // If objectType is specified this returns an editor for objects of that type
   // which can be used to make changes to the newly-added item (for example
@@ -217,16 +218,17 @@ var AutocompleteMultiDropdownEditor = function(elem) {
     setValues: function(texts) {
       // Clear all existing choices.
       elem.element(by.css('.select2-choices'))
-          .all(by.tagName('li')).map(function(choiceElem) {
-        return choiceElem.element(by.css('.select2-search-choice-close'));
-      }).then(function(deleteButtons) {
-        // We iterate in descending order, because clicking on a delete button
-        // removes the element from the DOM. We also omit the last element
-        // because it is the field for new input.
-        for (var i = deleteButtons.length - 2; i >= 0; i--) {
-          deleteButtons[i].click();
+        .all(by.tagName('li')).map(function(choiceElem) {
+          return choiceElem.element(by.css('.select2-search-choice-close'));
+        }).then(function(deleteButtons) {
+          // We iterate in descending order, because clicking on a delete button
+          // removes the element from the DOM. We also omit the last element
+          // because it is the field for new input.
+          for (var i = deleteButtons.length - 2; i >= 0; i--) {
+            deleteButtons[i].click();
+          }
         }
-      });
+      );
 
       for (var i = 0; i < texts.length; i++) {
         elem.element(by.css('.select2-container')).click();
@@ -235,14 +237,15 @@ var AutocompleteMultiDropdownEditor = function(elem) {
     },
     expectCurrentSelectionToBe: function(expectedCurrentSelection) {
       elem.element(by.css('.select2-choices'))
-          .all(by.tagName('li')).map(function(choiceElem) {
-        return choiceElem.getText();
-      }).then(function(actualSelection) {
-        // Remove the element corresponding to the last <li>, which actually
-        // corresponds to the field for new input.
-        actualSelection.pop();
-        expect(actualSelection).toEqual(expectedCurrentSelection);
-      });
+        .all(by.tagName('li')).map(function(choiceElem) {
+          return choiceElem.getText();
+        }).then(function(actualSelection) {
+          // Remove the element corresponding to the last <li>, which actually
+          // corresponds to the field for new input.
+          actualSelection.pop();
+          expect(actualSelection).toEqual(expectedCurrentSelection);
+        }
+      );
     }
   };
 };
@@ -298,14 +301,15 @@ var MultiSelectEditor = function(elem) {
 
       // Find the selected elements.
       elem.element(by.css('.dropdown-menu'))
-          .all(by.css('.protractor-test-selected')).map(function(selectedElem) {
-        return selectedElem.getText();
-      }).then(function(actualSelection) {
-        expect(actualSelection).toEqual(expectedCurrentSelection);
+        .all(by.css('.protractor-test-selected')).map(function(selectedElem) {
+          return selectedElem.getText();
+        }).then(function(actualSelection) {
+          expect(actualSelection).toEqual(expectedCurrentSelection);
 
-        // Close the dropdown menu at the end.
-        elem.element(by.css('.dropdown-toggle')).click();
-      });
+          // Close the dropdown menu at the end.
+          elem.element(by.css('.dropdown-toggle')).click();
+        }
+      );
     }
   };
 };
@@ -494,23 +498,25 @@ var CodeMirrorChecker = function(elem) {
     general.waitForSystem();
     elem.all(by.xpath('./div')).map(function(lineElement) {
       return lineElement.element(by.css('.CodeMirror-linenumber')).getText()
-          .then(function(lineNumber) {
-        // Note: the last line in codemirror will have an empty string for line
-        // number and for text. This is to skip that line.
-        if (lineNumber === '') {
+        .then(function(lineNumber) {
+          // Note: the last line in codemirror will have an empty string for
+          // line number and for text. This is to skip that line.
+          if (lineNumber === '') {
+            return lineNumber;
+          }
+          if (!compareDict.hasOwnProperty(lineNumber)) {
+            throw Error('Line ' + lineNumber + ' not found in CodeMirror');
+          }
+          expect(lineElement.element(by.xpath('./pre')).getText())
+            .toEqual(compareDict[lineNumber].text);
+          expect(
+            lineElement.element(
+              by.css('.CodeMirror-linebackground')).isPresent())
+            .toEqual(compareDict[lineNumber].highlighted);
+          compareDict[lineNumber].checked = true;
           return lineNumber;
         }
-        if (!compareDict.hasOwnProperty(lineNumber)) {
-          throw Error('Line ' + lineNumber + ' not found in CodeMirror');
-        }
-        expect(lineElement.element(by.xpath('./pre')).getText())
-          .toEqual(compareDict[lineNumber].text);
-        expect(
-          lineElement.element(by.css('.CodeMirror-linebackground')).isPresent())
-          .toEqual(compareDict[lineNumber].highlighted);
-        compareDict[lineNumber].checked = true;
-        return lineNumber;
-      });
+      );
     }).then(function(lineNumbers) {
       var largestLineNumber = lineNumbers[lineNumbers.length - 1];
       if (largestLineNumber !== currentLineNumber) {
diff --git a/core/tests/protractor_utils/library.js b/core/tests/protractor_utils/library.js
--- a/core/tests/protractor_utils/library.js
+++ b/core/tests/protractor_utils/library.js
@@ -58,12 +58,14 @@ var expectCurrentCategorySelectionToBe = function(expectedCategories) {
 // Returns a promise of all explorations with the given name.
 var _getExplorationElements = function(name) {
   return element.all(by.css('.protractor-test-exp-summary-tile')).filter(
-      function(tile) {
-    return tile.element(by.css('.protractor-test-exp-summary-tile-title')).
+    function(tile) {
+      return tile.element(by.css('.protractor-test-exp-summary-tile-title')).
         getText().then(function(tileTitle) {
-      return (tileTitle === name);
-    });
-  });
+          return (tileTitle === name);
+        }
+      );
+    }
+  );
 };
 
 var expectExplorationToBeVisible = function(name) {
diff --git a/core/tests/protractor_utils/users.js b/core/tests/protractor_utils/users.js
--- a/core/tests/protractor_utils/users.js
+++ b/core/tests/protractor_utils/users.js
@@ -66,9 +66,10 @@ var createModerator = function(email, username) {
   login(email, true);
   _completeSignup(username);
   admin.editConfigProperty(
-      'Usernames of moderators', 'List', function(listEditor) {
-    listEditor.addItem('Unicode').setValue(username);
-  });
+    'Usernames of moderators', 'List', function(listEditor) {
+      listEditor.addItem('Unicode').setValue(username);
+    }
+  );
   logout();
 };
 
@@ -81,9 +82,10 @@ var createAndLoginAdminUser = function(email, username) {
   login(email, true);
   _completeSignup(username);
   admin.editConfigProperty(
-      'Usernames of admins', 'List', function(listEditor) {
-    listEditor.addItem('Unicode').setValue(username);
-  });
+    'Usernames of admins', 'List', function(listEditor) {
+      listEditor.addItem('Unicode').setValue(username);
+    }
+  );
 };
 
 exports.login = login;