Repository: yannickcr/eslint-plugin-react
Pull request #1519 (2017-11-07 06:47:00)

Proposal: jsx-child-element-spacing proposal
Failing tests to illustrate use cases for https://github.com/yannickcr/eslint-plugin-react/issues/1515


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index daecae18..70b8a0c2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,95 +3,25 @@ All notable changes to this project will be documented in this file.
 This project adheres to [Semantic Versioning](http://semver.org/).
 This change log adheres to standards from [Keep a CHANGELOG](http://keepachangelog.com).
 
-## [7.5.0] - 2017-11-18
-### Added
-* Add [`jsx-one-expression-per-line`][] rule ([#1497][] @TSMMark)
-* Add [`destructuring-assignment`][] rule ([#1462][] @DianaSuvorova)
-* Add [`no-access-state-in-setstate`][] rule ([#1374][] @jaaberg)
-* Add [`button-has-type`][] rule ([#1525][] @Hypnosphi)
-* Add warnings for `React.DOM` factories in [`no-deprecated`][] ([#1530][] @backjo)
-* Add `sortShapeProp` option to [`sort-prop-types`][] ([#1476][] @jomasti)
-* Add `parens-new-line` option to [`jsx-wrap-multilines`][] ([#1475][] @jomasti)
-* Add `checkContextTypes` and `checkChildContextTypes` options to [`forbid-prop-types`][] ([#1533][] @jomasti)
-* Add `forbidDefaultForRequired ` option to [`require-default-props`][] ([#1524][] @jomasti)
-* Add new nodes support to [`jsx-wrap-multilines`][] ([#1384][] @evgeny-petukhov)
-
-### Fixed
-* Fix [`jsx-curly-brace-presence`][] auto fix by bailing out when some chars exist ([#1479][] [#1449][] @jackyho112)
-* Fix [`boolean-prop-naming`][] crash with Object spread ([#1485][] @track0x1)
-* Fix [`no-unused-state`][] to correctly handle arrow function class method ([#1363][] @jackyho112)
-* Fix incompatibility with [`typescript-eslint-parser`](https://github.com/eslint/typescript-eslint-parser) ([#1496][] @timothykang)
-* Fix [`jsx-no-bind`][] to only warn for props and account for variable declaration ([#1444][] [#1395][] [#1417][] @jackyho112)
-* Fix [`no-props-unused-props`][] to handle props usage in custom prop validators ([#1518][] @petersendidit)
-* Fix [`prefer-stateless-function`][] to account for `contextTypes` and `defaultProps` ([#1521][] @jomasti)
-* Fix [`jsx-no-comment-textnodes`][] to not warn when using two slashes via html entities at the beginning of a literal ([#1517][] @jomasti)
-* Fix [`default-props-match-prop-types`][] crash ([#1499][] @jomasti)
-* Fix [`no-unused-prop-types`][] to handle props used in the `setState` update callback ([#1507][] @petersendidit)
-* Fix alignment bug in [`jsx-indent`][] ([#1246][] @jseminck)
-
-### Changed
-* Documentation improvements ([#1438][] @jseminck, [#1464][] @AlaaAttya, [#1494][] @piperchester, [#1467][] @felicio, [#1512][] @adam-golab)
-* Code refactoring ([#1423][] [#1398][] @jseminck, [#1500][] [#1514][] @Aladdin-ADD, [#1502][] @SimenB, [#1508][] [#1526][] @jomasti, @ljharb)
-* Update dependencies ([#1450][] @leebyron, @ljharb)
-
-[7.5.0]: https://github.com/yannickcr/eslint-plugin-react/compare/v7.4.0...v7.5.0
-[#1497]: https://github.com/yannickcr/eslint-plugin-react/pull/1497
-[#1462]: https://github.com/yannickcr/eslint-plugin-react/pull/1462
-[#1374]: https://github.com/yannickcr/eslint-plugin-react/pull/1374
-[#1525]: https://github.com/yannickcr/eslint-plugin-react/pull/1525
-[#1530]: https://github.com/yannickcr/eslint-plugin-react/pull/1530
-[#1476]: https://github.com/yannickcr/eslint-plugin-react/issues/1476
-[#1475]: https://github.com/yannickcr/eslint-plugin-react/pull/1475
-[#1533]: https://github.com/yannickcr/eslint-plugin-react/pull/1533
-[#1524]: https://github.com/yannickcr/eslint-plugin-react/issues/1524
-[#1384]: https://github.com/yannickcr/eslint-plugin-react/pull/1384
-[#1479]: https://github.com/yannickcr/eslint-plugin-react/issues/1479
-[#1449]: https://github.com/yannickcr/eslint-plugin-react/issues/1449
-[#1485]: https://github.com/yannickcr/eslint-plugin-react/pull/1485
-[#1363]: https://github.com/yannickcr/eslint-plugin-react/issues/1363
-[#1496]: https://github.com/yannickcr/eslint-plugin-react/pull/1496
-[#1444]: https://github.com/yannickcr/eslint-plugin-react/issues/1444
-[#1395]: https://github.com/yannickcr/eslint-plugin-react/issues/1395
-[#1417]: https://github.com/yannickcr/eslint-plugin-react/issues/1417
-[#1518]: https://github.com/yannickcr/eslint-plugin-react/pull/1518
-[#1521]: https://github.com/yannickcr/eslint-plugin-react/issues/1521
-[#1517]: https://github.com/yannickcr/eslint-plugin-react/issues/1517
-[#1499]: https://github.com/yannickcr/eslint-plugin-react/issues/1499
-[#1507]: https://github.com/yannickcr/eslint-plugin-react/pull/1507
-[#1246]: https://github.com/yannickcr/eslint-plugin-react/issues/1246
-[#1438]: https://github.com/yannickcr/eslint-plugin-react/pull/1438
-[#1464]: https://github.com/yannickcr/eslint-plugin-react/pull/1464
-[#1494]: https://github.com/yannickcr/eslint-plugin-react/pull/1494
-[#1467]: https://github.com/yannickcr/eslint-plugin-react/pull/1467
-[#1512]: https://github.com/yannickcr/eslint-plugin-react/pull/1512
-[#1423]: https://github.com/yannickcr/eslint-plugin-react/pull/1423
-[#1500]: https://github.com/yannickcr/eslint-plugin-react/pull/1500
-[#1514]: https://github.com/yannickcr/eslint-plugin-react/pull/1514
-[#1502]: https://github.com/yannickcr/eslint-plugin-react/pull/1502
-[#1508]: https://github.com/yannickcr/eslint-plugin-react/pull/1508
-[#1526]: https://github.com/yannickcr/eslint-plugin-react/pull/1526
-[#1398]: https://github.com/yannickcr/eslint-plugin-react/pull/1398
-[#1450]: https://github.com/yannickcr/eslint-plugin-react/pull/1450
-
 ## [7.4.0] - 2017-09-24
 ### Added
-* Add Flow 0.53 support ([#1376][] @jseminck)
-* Add [`jsx-curly-brace-presence`][] rule ([#1310][] @jackyho112)
-* Add support for Flow IntersectionTypeAnnotation to [`prop-types`][] and [`no-unused-prop-types`][] ([#1364][] [#1323][] @jseminck)
-* Add support for Flow TypedArgument to [`no-unused-prop-types`][] ([#1412][] @jseminck)
-* Add support for Flow ClassExpressions to [`prop-types`][] ([#1400][] @jseminck)
-* Add support for Flow read-only props to [`no-unused-prop-types`][] ([#1388][] @jseminck)
-* Add more tests for [`prop-types`][] and [`no-unused-prop-types`][] ([#1381][] @DianaSuvorova)
-* Add support for increment and decrement operations to [`no-direct-mutation-state`][] ([#1386][] @zpao)
+- Add Flow 0.53 support ([#1376][] @jseminck)
+- Add [`jsx-curly-brace-presence`][] rule ([#1310][] @jackyho112)
+- Add support for Flow IntersectionTypeAnnotation to [`prop-types`][] and [`no-unused-prop-types`][] ([#1364][] [#1323][] @jseminck)
+- Add support for Flow TypedArgument to [`no-unused-prop-types`][] ([#1412][] @jseminck)
+- Add support for Flow ClassExpressions to [`prop-types`][] ([#1400][] @jseminck)
+- Add support for Flow read-only props to [`no-unused-prop-types`][] ([#1388][] @jseminck)
+- Add more tests for [`prop-types`][] and [`no-unused-prop-types`][] ([#1381][] @DianaSuvorova)
+- Add support for increment and decrement operations to [`no-direct-mutation-state`][] ([#1386][] @zpao)
 
 ### Fixed
-* Fix [`no-unused-state`][] to ignore computed property keys ([#1361][] @jackyho112)
-* Fix [`no-typos`][] crash ([#1406][] @jseminck)
-* Fix [`boolean-prop-naming`][] crash ([#1409][] @EvHaus)
-* Fix [`prop-types`][] and [`no-unused-prop-types`][] crash with IntersectionTypeAnnotation ([#1413][] @jseminck)
+- Fix [`no-unused-state`][] to ignore computed property keys ([#1361][] @jackyho112)
+- Fix [`no-typos`][] crash ([#1406][] @jseminck)
+- Fix [`boolean-prop-naming`][] crash ([#1409][] @EvHaus)
+- Fix [`prop-types`][] and [`no-unused-prop-types`][] crash with IntersectionTypeAnnotation ([#1413][] @jseminck)
 
 ### Changed
-* Documentation improvements ([#1392][] @xcatliu, [#1403][] @piperchester, [#1432][] @jneuendorf)
+- Documentation improvements ([#1392][] @xcatliu, [#1403][] @piperchester, [#1432][] @jneuendorf)
 
 [7.4.0]: https://github.com/yannickcr/eslint-plugin-react/compare/v7.3.0...v7.4.0
 [#1376]: https://github.com/yannickcr/eslint-plugin-react/issues/1376
@@ -2028,7 +1958,3 @@ If you're still not using React 15 you can keep the old behavior by setting the
 [`no-comment-textnodes`]: docs/rules/jsx-no-comment-textnodes.md
 [`wrap-multilines`]: docs/rules/jsx-wrap-multilines.md
 [`jsx-curly-brace-presence`]: docs/rules/jsx-curly-brace-presence.md
-[`jsx-one-expression-per-line`]: docs/rules/jsx-one-expression-per-line.md
-[`destructuring-assignment`]: docs/rules/destructuring-assignment.md
-[`no-access-state-in-setstate`]: docs/rules/no-access-state-in-setstate.md
-[`button-has-type`]: docs/rules/button-has-type.md
diff --git a/index.js b/index.js
index 6c1a933a..765f681c 100644
--- a/index.js
+++ b/index.js
@@ -9,9 +9,10 @@ const allRules = {
   'display-name': require('./lib/rules/display-name'),
   'forbid-component-props': require('./lib/rules/forbid-component-props'),
   'forbid-elements': require('./lib/rules/forbid-elements'),
-  'forbid-prop-types': require('./lib/rules/forbid-prop-types'),
   'forbid-foreign-prop-types': require('./lib/rules/forbid-foreign-prop-types'),
+  'forbid-prop-types': require('./lib/rules/forbid-prop-types'),
   'jsx-boolean-value': require('./lib/rules/jsx-boolean-value'),
+  'jsx-child-element-spacing': require('./lib/rules/jsx-child-element-spacing'),
   'jsx-closing-bracket-location': require('./lib/rules/jsx-closing-bracket-location'),
   'jsx-closing-tag-location': require('./lib/rules/jsx-closing-tag-location'),
   'jsx-curly-spacing': require('./lib/rules/jsx-curly-spacing'),
@@ -23,12 +24,12 @@ const allRules = {
   'jsx-indent-props': require('./lib/rules/jsx-indent-props'),
   'jsx-key': require('./lib/rules/jsx-key'),
   'jsx-max-props-per-line': require('./lib/rules/jsx-max-props-per-line'),
-  'jsx-one-expression-per-line': require('./lib/rules/jsx-one-expression-per-line'),
   'jsx-no-bind': require('./lib/rules/jsx-no-bind'),
   'jsx-no-comment-textnodes': require('./lib/rules/jsx-no-comment-textnodes'),
   'jsx-no-duplicate-props': require('./lib/rules/jsx-no-duplicate-props'),
   'jsx-no-literals': require('./lib/rules/jsx-no-literals'),
   'jsx-no-target-blank': require('./lib/rules/jsx-no-target-blank'),
+  'jsx-one-expression-per-line': require('./lib/rules/jsx-one-expression-per-line'),
   'button-has-type': require('./lib/rules/button-has-type'),
   'jsx-no-undef': require('./lib/rules/jsx-no-undef'),
   'jsx-curly-brace-presence': require('./lib/rules/jsx-curly-brace-presence'),
diff --git a/lib/rules/jsx-child-element-spacing.js b/lib/rules/jsx-child-element-spacing.js
new file mode 100644
index 00000000..82d56917
--- /dev/null
+++ b/lib/rules/jsx-child-element-spacing.js
@@ -0,0 +1,103 @@
+'use strict';
+
+// This list is taken from https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements
+const INLINE_ELEMENTS = new Set([
+  'a',
+  'abbr',
+  'acronym',
+  'b',
+  'bdo',
+  'big',
+  'br',
+  'button',
+  'cite',
+  'code',
+  'dfn',
+  'em',
+  'i',
+  'img',
+  'input',
+  'kbd',
+  'label',
+  'map',
+  'object',
+  'q',
+  'samp',
+  'script',
+  'select',
+  'small',
+  'span',
+  'strong',
+  'sub',
+  'sup',
+  'textarea',
+  'tt',
+  'var'
+]);
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'Ensures inline tags are not rendered without spaces between them',
+      category: 'Stylistic Issues',
+      recommended: false
+    },
+    fixable: false,
+    schema: [
+      {
+        type: 'object',
+        properties: {},
+        default: {},
+        additionalProperties: false
+      }
+    ]
+  },
+  create: function (context) {
+    const elementName = node => (
+      node.openingElement &&
+      node.openingElement.name &&
+      node.openingElement.name.type === 'JSXIdentifier' &&
+      node.openingElement.name.name
+    );
+
+    const isInlineElement = node => (
+      node.type === 'JSXElement' &&
+      INLINE_ELEMENTS.has(elementName(node))
+    );
+
+    const TEXT_FOLLOWING_ELEMENT_PATTERN = /^\s*\n\s*\S/;
+    const TEXT_PRECEDING_ELEMENT_PATTERN = /\S\s*\n\s*$/;
+
+    return {
+      JSXElement: function(node) {
+        let lastChild = null;
+        let child = null;
+        (node.children.concat([null])).forEach(nextChild => {
+          if (
+            (lastChild || nextChild) &&
+            (!lastChild || isInlineElement(lastChild)) &&
+            (child && child.type === 'Literal') &&
+            (!nextChild || isInlineElement(nextChild)) &&
+            true
+          ) {
+            if (lastChild && child.value.match(TEXT_FOLLOWING_ELEMENT_PATTERN)) {
+              context.report({
+                node: child,
+                loc: child.loc,
+                message: `Ambiguous spacing after previous element ${elementName(lastChild)}`
+              });
+            } else if (nextChild && child.value.match(TEXT_PRECEDING_ELEMENT_PATTERN)) {
+              context.report({
+                node: child,
+                loc: child.loc,
+                message: `Ambiguous spacing before next element ${elementName(nextChild)}`
+              });
+            }
+          }
+          lastChild = child;
+          child = nextChild;
+        });
+      }
+    };
+  }
+};
diff --git a/lib/rules/jsx-closing-tag-location.js b/lib/rules/jsx-closing-tag-location.js
index 5b0e85da..93d9775c 100644
--- a/lib/rules/jsx-closing-tag-location.js
+++ b/lib/rules/jsx-closing-tag-location.js
@@ -4,8 +4,6 @@
  */
 'use strict';
 
-const astUtil = require('../util/ast');
-
 // ------------------------------------------------------------------------------
 // Rule Definition
 // ------------------------------------------------------------------------------
@@ -20,6 +18,31 @@ module.exports = {
   },
 
   create: function(context) {
+    const sourceCode = context.getSourceCode();
+
+    /**
+     * Checks if the node is the first in its line, excluding whitespace.
+     * @param {ASTNode} node The node to check
+     * @return {Boolean} true if its the first node in its line
+     */
+    function isNodeFirstInLine(node) {
+      let token = node;
+      let lines;
+      do {
+        token = sourceCode.getTokenBefore(token);
+        lines = token.type === 'JSXText'
+          ? token.value.split('\n')
+          : null;
+      } while (
+        token.type === 'JSXText' &&
+        /^\s*$/.test(lines[lines.length - 1])
+      );
+
+      const startLine = node.loc.start.line;
+      const endLine = token ? token.loc.end.line : -1;
+      return startLine !== endLine;
+    }
+
     return {
       JSXClosingElement: function(node) {
         if (!node.parent) {
@@ -36,7 +59,7 @@ module.exports = {
         }
 
         let message;
-        if (!astUtil.isNodeFirstInLine(context, node)) {
+        if (!isNodeFirstInLine(node)) {
           message = 'Closing tag of a multiline JSX expression must be on its own line.';
         } else {
           message = 'Expected closing tag to match indentation of opening.';
@@ -48,7 +71,7 @@ module.exports = {
           message,
           fix: function(fixer) {
             const indent = Array(opening.loc.start.column + 1).join(' ');
-            if (astUtil.isNodeFirstInLine(context, node)) {
+            if (isNodeFirstInLine(node)) {
               return fixer.replaceTextRange(
                 [node.range[0] - node.loc.start.column, node.range[0]],
                 indent
diff --git a/lib/rules/jsx-indent-props.js b/lib/rules/jsx-indent-props.js
index bfdaaed6..ec7007f2 100644
--- a/lib/rules/jsx-indent-props.js
+++ b/lib/rules/jsx-indent-props.js
@@ -29,8 +29,6 @@
  */
 'use strict';
 
-const astUtil = require('../util/ast');
-
 // ------------------------------------------------------------------------------
 // Rule Definition
 // ------------------------------------------------------------------------------
@@ -138,6 +136,20 @@ module.exports = {
       return indent ? indent[0].length : 0;
     }
 
+    /**
+     * Checks node is the first in its own start line. By default it looks by start line.
+     * @param {ASTNode} node The node to check
+     * @param {Boolean} [byEndLocation] Lookup based on start position or end
+     * @return {Boolean} true if its the first in the its start line
+     */
+    function isNodeFirstInLine(node, byEndLocation) {
+      const firstToken = byEndLocation === true ? sourceCode.getLastToken(node, 1) : sourceCode.getTokenBefore(node);
+      const startLine = byEndLocation === true ? node.loc.end.line : node.loc.start.line;
+      const endLine = firstToken ? firstToken.loc.end.line : -1;
+
+      return startLine !== endLine;
+    }
+
     /**
      * Check indent for nodes list
      * @param {ASTNode[]} nodes list of node objects
@@ -149,7 +161,7 @@ module.exports = {
         const nodeIndent = getNodeIndent(node, false, excludeCommas);
         if (
           node.type !== 'ArrayExpression' && node.type !== 'ObjectExpression' &&
-          nodeIndent !== indent && astUtil.isNodeFirstInLine(context, node)
+          nodeIndent !== indent && isNodeFirstInLine(node)
         ) {
           report(node, indent, nodeIndent);
         }
diff --git a/lib/rules/jsx-indent.js b/lib/rules/jsx-indent.js
index f668bd5e..0f711674 100644
--- a/lib/rules/jsx-indent.js
+++ b/lib/rules/jsx-indent.js
@@ -29,8 +29,6 @@
  */
 'use strict';
 
-const astUtil = require('../util/ast');
-
 // ------------------------------------------------------------------------------
 // Rule Definition
 // ------------------------------------------------------------------------------
@@ -154,6 +152,22 @@ module.exports = {
       return indent ? indent[0].length : 0;
     }
 
+    /**
+     * Checks node is the first in its own start line. By default it looks by start line.
+     * @param {ASTNode} node The node to check
+     * @return {Boolean} true if its the first in the its start line
+     */
+    function isNodeFirstInLine(node) {
+      let token = node;
+      do {
+        token = sourceCode.getTokenBefore(token);
+      } while (token.type === 'JSXText' && /^\s*$/.test(token.value));
+      const startLine = node.loc.start.line;
+      const endLine = token ? token.loc.end.line : -1;
+
+      return startLine !== endLine;
+    }
+
     /**
      * Check if the node is the right member of a logical expression
      * @param {ASTNode} node The node to check
@@ -195,7 +209,7 @@ module.exports = {
       const isCorrectAlternateInCondExp = isAlternateInConditionalExp(node) && (nodeIndent - indent) === 0;
       if (
         nodeIndent !== indent &&
-        astUtil.isNodeFirstInLine(context, node) &&
+        isNodeFirstInLine(node) &&
         !isCorrectRightInLogicalExp &&
         !isCorrectAlternateInCondExp
       ) {
diff --git a/lib/util/ast.js b/lib/util/ast.js
index 6161cb45..dc9ce770 100644
--- a/lib/util/ast.js
+++ b/lib/util/ast.js
@@ -3,30 +3,6 @@
  */
 'use strict';
 
-/**
- * Find a return statment in the current node
- *
- * @param {ASTNode} ASTnode The AST node being checked
- */
-function findReturnStatement(node) {
-  if (
-    (!node.value || !node.value.body || !node.value.body.body) &&
-    (!node.body || !node.body.body)
-  ) {
-    return false;
-  }
-
-  const bodyNodes = (node.value ? node.value.body.body : node.body.body);
-
-  let i = bodyNodes.length - 1;
-  for (; i >= 0; i--) {
-    if (bodyNodes[i].type === 'ReturnStatement') {
-      return bodyNodes[i];
-    }
-  }
-  return false;
-}
-
 /**
  * Get properties name
  * @param {Object} node - Property.
@@ -60,34 +36,31 @@ function getComponentProperties(node) {
 }
 
 /**
-     * Checks if the node is the first in its line, excluding whitespace.
-     * @param {Object} context The node to check
-     * @param {ASTNode} node The node to check
-     * @return {Boolean} true if its the first node in its line
-     */
-function isNodeFirstInLine(context, node) {
-  const sourceCode = context.getSourceCode();
-  let token = node;
-  let lines;
-  do {
-    token = sourceCode.getTokenBefore(token);
-    lines = token.type === 'JSXText'
-      ? token.value.split('\n')
-      : null;
-  } while (
-    token.type === 'JSXText' &&
-        /^\s*$/.test(lines[lines.length - 1])
-  );
+ * Find a return statment in the current node
+ *
+ * @param {ASTNode} ASTnode The AST node being checked
+ */
+function findReturnStatement(node) {
+  if (
+    (!node.value || !node.value.body || !node.value.body.body) &&
+    (!node.body || !node.body.body)
+  ) {
+    return false;
+  }
 
-  const startLine = node.loc.start.line;
-  const endLine = token ? token.loc.end.line : -1;
-  return startLine !== endLine;
-}
+  const bodyNodes = (node.value ? node.value.body.body : node.body.body);
 
+  let i = bodyNodes.length - 1;
+  for (; i >= 0; i--) {
+    if (bodyNodes[i].type === 'ReturnStatement') {
+      return bodyNodes[i];
+    }
+  }
+  return false;
+}
 
 module.exports = {
-  findReturnStatement: findReturnStatement,
   getPropertyName: getPropertyName,
   getComponentProperties: getComponentProperties,
-  isNodeFirstInLine: isNodeFirstInLine
+  findReturnStatement: findReturnStatement
 };
diff --git a/package.json b/package.json
index 23f55a8c..1a3b0f45 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "eslint-plugin-react",
-  "version": "7.5.0",
+  "version": "7.4.0",
   "author": "Yannick Croissant <yannick.croissant+npm@gmail.com>",
   "description": "React specific linting rules for ESLint",
   "main": "index.js",
@@ -30,11 +30,11 @@
     "prop-types": "^15.6.0"
   },
   "devDependencies": {
-    "babel-eslint": "^8.0.2",
-    "coveralls": "^3.0.0",
+    "babel-eslint": "^7.2.3",
+    "coveralls": "^2.13.1",
     "eslint": "^4.10.0",
     "istanbul": "0.4.5",
-    "mocha": "^4.0.1"
+    "mocha": "^3.5.0"
   },
   "peerDependencies": {
     "eslint": "^3.0.0 || ^4.0.0"


Test patch:
diff --git a/tests/lib/rules/jsx-child-element-spacing.js b/tests/lib/rules/jsx-child-element-spacing.js
new file mode 100644
index 00000000..51ebfcd6
--- /dev/null
+++ b/tests/lib/rules/jsx-child-element-spacing.js
@@ -0,0 +1,196 @@
+'use strict';
+
+const rule = require('../../../lib/rules/jsx-child-element-spacing');
+const RuleTester = require('eslint').RuleTester;
+const parserOptions = {
+  sourceType: 'module',
+  ecmaFeatures: {
+    jsx: true
+  }
+};
+
+const ruleTester = new RuleTester({parserOptions});
+ruleTester.run('jsx-child-element-spacing', rule, {
+  valid: [{
+    code: `
+      <App>
+        foo
+      </App>
+    `
+  }, {
+    code: `
+      <App>
+        <a>bar</a>
+      </App>
+    `
+  }, {
+    code: `
+      <App>
+        <a>
+          <b>nested</b>
+        </a>
+      </App>
+    `
+  }, {
+    code: `
+      <App>
+        foo
+        bar
+      </App>
+    `
+  }, {
+    code: `
+      <App>
+        foo<a>bar</a>baz
+      </App>
+    `
+  }, {
+    code: `
+      <App>
+        foo
+        {' '}
+        <a>bar</a>
+        {' '}
+        baz
+      </App>
+    `
+  }, {
+    code: `
+      <App>
+        foo
+        {' '}<a>bar</a>{' '}
+        baz
+      </App>
+    `
+  }, {
+    code: `
+      <App>
+        foo{' '}
+        <a>bar</a>
+        {' '}baz
+      </App>
+    `
+  }, {
+    code: `
+      <App>
+        foo{/*
+        */}<a>bar</a>{/*
+        */}baz
+      </App>
+    `
+  }, {
+    code: `
+      <App>
+        Please take a look at <a href="https://js.org">this link</a>.
+      </App>
+    `
+  }, {
+    code: `
+      <App>
+        Please take a look at
+        {' '}
+        <a href="https://js.org">this link</a>.
+      </App>
+    `
+  }, {
+    code: `
+      <App>
+        <p>A</p>
+        <p>B</p>
+      </App>
+    `
+  }, {
+    code: `
+      <App>
+        <p>A</p><p>B</p>
+      </App>
+    `
+  }, {
+    code: `
+      <App>
+        <a>foo</a>
+        <a>bar</a>
+      </App>
+    `
+  }, {
+    code: `
+      <App>
+        <a>
+          <b>nested1</b>
+          <b>nested2</b>
+        </a>
+      </App>
+    `
+  }, {
+    code: `
+      <App>
+        A
+        B
+      </App>
+    `
+  }],
+
+  invalid: [{
+    code: `
+      <App>
+        foo
+        <a>bar</a>
+      </App>
+    `,
+    errors: [
+      {message: 'Ambiguous spacing before next element a'}
+    ]
+  }, {
+    code: `
+      <App>
+        <a>bar</a>
+        baz
+      </App>
+    `,
+    errors: [
+      {message: 'Ambiguous spacing after previous element a'}
+    ]
+  }, {
+    code: `
+      <App>
+        {' '}<a>bar</a>
+        baz
+      </App>
+    `,
+    errors: [
+      {message: 'Ambiguous spacing after previous element a'}
+    ]
+  }, {
+    code: `
+      <App>
+        Please take a look at
+        <a href="https://js.org">this link</a>.
+      </App>
+    `,
+    errors: [
+      {message: 'Ambiguous spacing before next element a'}
+    ]
+  }, {
+    code: `
+      <App>
+        Some <code>loops</code> and some
+        <code>if</code> statements.
+      </App>
+    `,
+    errors: [
+      {message: 'Ambiguous spacing before next element code'}
+    ]
+  }, {
+    code: `
+      <App>
+        Here is
+        <a href="https://js.org">a link</a> and here is
+        <a href="https://js.org">another</a>
+      </App>
+    `,
+    errors: [
+      {message: 'Ambiguous spacing before next element a'},
+      {message: 'Ambiguous spacing before next element a'}
+    ]
+  }]
+});
diff --git a/tests/lib/rules/jsx-indent.js b/tests/lib/rules/jsx-indent.js
index 253f7e9c..7c7d40a7 100644
--- a/tests/lib/rules/jsx-indent.js
+++ b/tests/lib/rules/jsx-indent.js
@@ -963,16 +963,5 @@ ruleTester.run('jsx-indent', rule, {
     errors: [
       {message: 'Expected indentation of 4 space characters but found 0.'}
     ]
-  }, {
-    code: [
-      '<p>',
-      '    <div>',
-      '        <SelfClosingTag />Text',
-      '  </div>',
-      '</p>'
-    ].join('\n'),
-    errors: [
-      {message: 'Expected indentation of 4 space characters but found 2.'}
-    ]
   }]
 });