 ' for "test 1"';
      }
      expect(reporter._testsuites[1].testsuite[1].testcase[0]._attr.name).to.equal(failureMessage);
      expect(reporter._testsuites[1].testsuite[1].testcase[1].failure._attr.message).to.equal('error in before');
      expect(reporter._testsuites[2].testsuite[0]._attr.name).to.equal('good suite');
      expect(reporter._testsuites[2].testsuite[1].testcase).to.have.lengthOf(1);
      expect(reporter._testsuites[2].testsuite[1].testcase[0]._attr.name).to.equal('good suite test 2');
      done();
    });
  });

  it('properly diffs errors from Chai', function(done) {
    var reporter = createReporter();
    var rootSuite = reporter.runner.suite;
    var suite1 = Suite.create(rootSuite, 'failing with Chai');
    suite1.addTest(createTest('test 1', function () {
      expect({}).to.deep.equal({missingProperty: true});
    }));

    runRunner(reporter.runner, function() {
      if (reporter.runner.dispose) {
        reporter.runner.dispose();
      }

      expect(reporter._testsuites).to.have.lengthOf(2);
      expect(reporter._testsuites[1].testsuite[0]._attr.name).to.equal('failing with Chai');
      expect(reporter._testsuites[1].testsuite[1].testcase).to.have.lengthOf(2);
      expect(reporter._testsuites[1].testsuite[1].testcase[0]._attr.name).to.equal('failing with Chai test 1');
      expect(reporter._testsuites[1].testsuite[1].testcase[1].failure._attr.message).to.equal('expected {} to deeply equal { missingProperty: true }');
      expect(reporter._testsuites[1].testsuite[1].testcase[1].failure._cdata).to.match(/AssertionError: expected {} to deeply equal {\s*missingProperty:\s*true\s*}\n(?:\s* at .*?\n)*\n\s*\+ expected - actual\n+\s*-{}\n\s*\+{\n\s*\+\s*"missingProperty":\s*true\n\s*\+}[\s\S]*/);
      done();
    });
  });

  describe('when "useFullSuiteTitle" option is specified', function() {
    it('generates full suite title', function(done) {
      var reporter = createReporter({useFullSuiteTitle: true });
      runTests(reporter, function() {
        expect(suiteName(reporter._testsuites[0])).to.equal('');
        expect(suiteName(reporter._testsuites[1])).to.equal('Root Suite Foo Bar');
        expect(suiteName(reporter._testsuites[2])).to.equal('Root Suite Another suite!');
        done();
      });
    });

    it('generates full suite title separated by "suiteTitleSeparatedBy" option', function(done) {
      var reporter = createReporter({useFullSuiteTitle: true, suiteTitleSeparatedBy: '.'});
      runTests(reporter, function() {
        expect(suiteName(reporter._testsuites[0])).to.equal('');
        expect(suiteName(reporter._testsuites[1])).to.equal('Root Suite.Foo Bar');
        expect(suiteName(reporter._testsuites[2])).to.equal('Root Suite.Another suite!');
        done();
      });
    });

    function suiteName(suite) {
      return suite.testsuite[0]._attr.name;
    }
  });

  describe('when "outputs" option is specified', function() {
    it('adds output/error lines to xml report', function(done) {
      var reporter = createReporter({outputs: true});

      var test = createTest('has outputs');
      test.consoleOutputs = [ 'hello', 'world' ];
      test.consoleErrors = [ 'typical diagnostic info', 'all is OK' ];

      var suite = Suite.create(reporter.runner.suite, 'with console output and error');
      suite.addTest(test);

      runRunner(reporter.runner, function() {
        expect(reporter._testsuites[1].testsuite[0]._attr.name).to.equal(suite.title);
        expect(reporter._testsuites[1].testsuite[1].testcase).to.have.length(3);
        expect(reporter._testsuites[1].testsuite[1].testcase[0]._attr.name).to.equal(test.fullTitle());
        expect(reporter._testsuites[1].testsuite[1].testcase[1]).to.have.property('system-out', 'hello\nworld');
        expect(reporter._testsuites[1].testsuite[1].testcase[2]).to.have.property('system-err', 'typical diagnostic info\nall is OK');

        expect(reporter._xml).to.include('<system-out>hello\nworld</system-out>');
        expect(reporter._xml).to.include('<system-err>typical diagnostic info\nall is OK</system-err>');

        done();
      });
    });

    it('does not add system-out if no outputs/errors were passed', function(done) {
      var reporter = createReporter({outputs: true});
      var test = createTest('has outputs');
      var suite = Suite.create(reporter.runner.suite, 'with console output and error');
      suite.addTest(test);

      runRunner(reporter.runner, function() {
        expect(reporter._testsuites[1].testsuite[0]._attr.name).to.equal(suite.title);
        expect(reporter._testsuites[1].testsuite[1].testcase).to.have.length(1);
        expect(reporter._testsuites[1].testsuite[1].testcase[0]._attr.name).to.equal(test.fullTitle());

        expect(reporter._xml).not.to.include('<system-out>');
        expect(reporter._xml).not.to.include('<system-err>');

        done();
      });
    });

    it('does not add system-out if outputs/errors were empty', function(done) {
      var reporter = createReporter({outputs: true});
      var test = createTest('has outputs');
      test.consoleOutputs = [];
      test.consoleErrors = [];

      var suite = Suite.create(reporter.runner.suite, 'with console output and error');
      suite.addTest(test);

      runRunner(reporter.runner, function() {
        expect(reporter._testsuites[1].testsuite[0]._attr.name).to.equal(suite.title);
        expect(reporter._testsuites[1].testsuite[1].testcase).to.have.length(1);
        expect(reporter._testsuites[1].testsuite[1].testcase[0]._attr.name).to.equal(test.fullTitle());

        expect(reporter._xml).not.to.include('<system-out>');
        expect(reporter._xml).not.to.include('<system-err>');

        done();
      });
    });
  });

  describe('when "attachments" option is specified', function() {
    it('adds attachments to xml report', function(done) {
      var filePath = '/path/to/file';
      var reporter = createReporter({attachments: true});
      var test = createTest('has attachment');
      test.attachments = [filePath];

      var suite = Suite.create(reporter.runner.suite, 'with attachments');
      suite.addTest(test);

      runRunner(reporter.runner, function() {
        expect(reporter._testsuites[1].testsuite[0]._attr.name).to.equal(suite.title);
        expect(reporter._testsuites[1].testsuite[1].testcase).to.have.length(2);
        expect(reporter._testsuites[1].testsuite[1].testcase[0]._attr.name).to.equal(test.fullTitle());
        expect(reporter._testsuites[1].testsuite[1].testcase[1]).to.have.property('system-out', '[[ATTACHMENT|' + filePath + ']]');

        expect(reporter._xml).to.include('<system-out>[[ATTACHMENT|' + filePath + ']]</system-out>');

        done();
      });
    });

    it('does not add system-out if no attachments were passed', function(done) {
      var reporter = createReporter({attachments: true});
      var test = createTest('has attachment');

      var suite = Suite.create(reporter.runner.suite, 'with attachments');
      suite.addTest(test);

      runRunner(reporter.runner, function() {
        expect(reporter._testsuites[1].testsuite[0]._attr.name).to.equal(suite.title);
        expect(reporter._testsuites[1].testsuite[1].testcase).to.have.lengthOf(1);
        expect(reporter._testsuites[1].testsuite[1].testcase[0]._attr.name).to.equal(test.fullTitle());

        expect(reporter._xml).to.not.include('<system-out>');

        done();
      });
    });

    it('does not add system-out if attachments array is empty', function(done) {
      var reporter = createReporter({attachments: true});
      var test = createTest('has attachment');
      test.attachments = [];

      var suite = Suite.create(reporter.runner.suite, 'with attachments');
      suite.addTest(test);

      runRunner(reporter.runner, function() {
        expect(reporter._testsuites[1].testsuite[0]._attr.name).to.equal(suite.title);
        expect(reporter._testsuites[1].testsuite[1].testcase).to.have.lengthOf(1);
        expect(reporter._testsuites[1].testsuite[1].testcase[0]._attr.name).to.equal(test.fullTitle());

        expect(reporter._xml).to.not.include('<system-out>');

        done();
      });
    });

    it('includes both console outputs and attachments in XML', function(done) {
      var reporter = createReporter({attachments: true, outputs: true});
      var test = createTest('has attachment');
      var filePath = '/path/to/file';
      test.attachments = [filePath];
      test.consoleOutputs = [ 'first console line', 'second console line' ];

      var suite = Suite.create(reporter.runner.suite, 'with attachments and outputs');
      suite.addTest(test);

      runRunner(reporter.runner, function() {
        expect(reporter._testsuites[1].testsuite[0]._attr.name).to.equal(suite.title);
        expect(reporter._testsuites[1].testsuite[1].testcase).to.have.length(2);
        expect(reporter._testsuites[1].testsuite[1].testcase[0]._attr.name).to.equal(test.fullTitle());
        expect(reporter._testsuites[1].testsuite[1].testcase[1]).to.have.property('system-out', 'first console line\nsecond console line\n[[ATTACHMENT|' + filePath + ']]');

        expect(reporter._xml).to.include('<system-out>first console line\nsecond console line\n[[ATTACHMENT|' + filePath + ']]</system-out>');

        done();
      });
    });
  });

  describe('Output', function() {
    it('skips suites with empty title', function(done) {
      var reporter = createReporter();
      var suite = Suite.create(reporter.runner.suite, '');
      suite.root = false; // mocha treats suites with empty title as root, so not sure this is possible
      suite.addTest(createTest('test'));

      runRunner(reporter.runner, function() {
        expect(reporter._testsuites).to.have.lengthOf(1);
        expect(reporter._testsuites[0].testsuite[0]._attr.name).to.equal('Root Suite');
        done();
      });
    });

    it('skips suites without testcases and suites', function(done) {
      var reporter = createReporter();
      Suite.create(reporter.runner.suite, 'empty suite');

      // mocha won't emit the `suite` event if a suite has no tests in it, so we won't even output the root suite.
      // See https://github.com/mochajs/mocha/blob/c0137eb698add08f29035467ea1dc230904f82ba/lib/runner.js#L723.
      runRunner(reporter.runner, function() {
        expect(reporter._testsuites).to.have.lengthOf(0);
        done();
      });
    });

    it('skips suites without testcases even if they have nested suites', function(done) {
      var reporter = createReporter();
      var suite1 = Suite.create(reporter.runner.suite, 'suite');
      Suite.create(suite1, 'nested suite');

      runRunner(reporter.runner, function() {
        // even though we have nested suites, there are no tests so mocha won't emit the `suite` event
        expect(reporter._testsuites).to.have.lengthOf(0);
        done();
      });
    });

    it('does not skip suites with nested tests', function(done) {
      var reporter = createReporter();
      var suite = Suite.create(reporter.runner.suite, 'nested suite');
      suite.addTest(createTest('test'));

      runRunner(reporter.runner, function() {
        expect(reporter._testsuites).to.have.lengthOf(2);
        expect(reporter._testsuites[0].testsuite[0]._attr.name).to.equal('Root Suite');
        expect(reporter._testsuites[1].testsuite[1].testcase).to.have.lengthOf(1);
        expect(reporter._testsuites[1].testsuite[1].testcase[0]._attr.name).to.equal('nested suite test');
        done();
      });
    });

    it('does not skip root suite', function(done) {
      var reporter = createReporter();
      reporter.runner.suite.addTest(createTest('test'));

      runRunner(reporter.runner, function() {
        expect(reporter._testsuites).to.have.lengthOf(1);
        expect(reporter._testsuites[0].testsuite[0]._attr.name).to.equal('Root Suite');
        expect(reporter._testsuites[0].testsuite[1].testcase).to.have.lengthOf(1);

        var expectedName = 'test';
        if (['2', '3'].includes(mochaVersion)) {
          expectedName = ' ' + expectedName;
        }
        expect(reporter._testsuites[0].testsuite[1].testcase[0]._attr.name).to.equal(expectedName);
        done();
      });
    });

    it('respects the `rootSuiteTitle`', function(done) {
      var name = 'The Root Suite!';
      var reporter = createReporter({rootSuiteTitle: name});
      reporter.runner.suite.addTest(createTest('test'));

      runRunner(reporter.runner, function() {
        expect(reporter._testsuites).to.have.lengthOf(1);
        expect(reporter._testsuites[0].testsuite[0]._attr.name).to.equal(name);
        done();
      });
    });

    it('uses "Mocha Tests" by default', function(done) {
      var reporter = createReporter();
      reporter.runner.suite.addTest(createTest('test'));

      runRunner(reporter.runner, function() {
        expect(reporter._xml).to.include('testsuites name="Mocha Tests"');
        done();
      });
    });

    it('respects the `testsuitesTitle`', function(done) {
      var title = 'SuitesTitle';
      var reporter = createReporter({testsuitesTitle: title});
      reporter.runner.suite.addTest(createTest('test'));

      runRunner(reporter.runner, function() {
        expect(reporter._xml).to.include('testsuites name="SuitesTitle"');
        done();
      });
    });
  });

  describe('Feature "Configurable classname/name switch"', function() {
    var mockedTestCase = {
      title: "should behave like so",
      timestamp: 123,
      tests: "1",
      failures: "0",
      time: "0.004",
      fullTitle: function() {
        return 'Super Suite ' + this.title;
      }
    };

    it('should generate valid testCase for testCaseSwitchClassnameAndName default', function() {
      var reporter = createReporter();
      var testCase = reporter.getTestcaseData(mockedTestCase);
      expect(testCase.testcase[0]._attr.name).to.equal(mockedTestCase.fullTitle());
      expect(testCase.testcase[0]._attr.classname).to.equal(mockedTestCase.title);
    });

    it('should generate valid testCase for testCaseSwitchClassnameAndName=false', function() {
      var reporter = createReporter({testCaseSwitchClassnameAndName: false});
      var testCase = reporter.getTestcaseData(mockedTestCase);
      expect(testCase.testcase[0]._attr.name).to.equal(mockedTestCase.fullTitle());
      expect(testCase.testcase[0]._attr.classname).to.equal(mockedTestCase.title);
    });

    it('should generate valid testCase for testCaseSwitchClassnameAndName=true', function() {
      var reporter = createReporter({testCaseSwitchClassnameAndName: true});
      var testCase = reporter.getTestcaseData(mockedTestCase);
      expect(testCase.testcase[0]._attr.name).to.equal(mockedTestCase.title);
      expect(testCase.testcase[0]._attr.classname).to.equal(mockedTestCase.fullTitle());
    });
  });

  describe('XML format', function () {
    it('generates Jenkins compatible XML when in jenkinsMode', function(done) {
      this.timeout(10000); // xmllint is very slow

      var reporter = createReporter({jenkinsMode: true});
      var rootSuite = reporter.runner.suite;

      var suite1 = Suite.create(rootSuite, 'Inner Suite');
      suite1.addTest(createTest('test'));

      var suite2 = Suite.create(rootSuite, 'Another Suite');
      suite2.addTest(createTest('test', function(done) {
        done(new Error('failed test'));
      }));

      runRunner(reporter.runner, function() {
        var schema = fs.readFileSync(path.join(__dirname, 'resources', 'jenkins-junit.xsd'));
        var result = xmllint.validateXML({ xml: reporter._xml, schema: schema });
        expect(result.errors).to.equal(null, JSON.stringify(result.errors));

        done();
      });
    });

    it('generates Ant compatible XML when in antMode', function(done) {
      this.timeout(10000); // xmllint is very slow

      var reporter = createReporter({antMode: true});
      var rootSuite = reporter.runner.suite;

      var suite1 = Suite.create(rootSuite, 'Inner Suite');
      suite1.addTest(createTest('test'));

      var suite2 = Suite.create(rootSuite, 'Another Suite');
      suite2.addTest(createTest('test', function(done) {
        done(new Error('failed test'));
      }));

      runRunner(reporter.runner, function() {
        var schema = fs.readFileSync(path.join(__dirname, 'resources', 'JUnit.xsd'));
        var result = xmllint.validateXML({ xml: reporter._xml, schema: schema });
        expect(result.errors).to.equal(null, JSON.stringify(result.errors));

        done();
      });
    });

    describe('Jenkins format', function () {
      it('generates Jenkins compatible classnames and suite name', function(done) {
        var reporter = createReporter({jenkinsMode: true});
        var rootSuite = reporter.runner.suite;

        var suite1 = Suite.create(rootSuite, 'Inner Suite');
        suite1.addTest(createTest('test'));

        var suite2 = Suite.create(suite1, 'Another Suite');
        suite2.addTest(createTest('fail test', function(done) {
          done(new Error('failed test'));
        }));

        runRunner(reporter.runner, function() {
          expect(reporter._testsuites[0].testsuite[0]._attr.name).to.equal('');
          expect(reporter._testsuites[1].testsuite[1].testcase[0]._attr.name).to.equal('test');
          expect(reporter._testsuites[1].testsuite[1].testcase[0]._attr.classname).to.equal('Inner Suite');
          expect(reporter._testsuites[2].testsuite[0]._attr.name).to.equal('Root Suite.Inner Suite.Another Suite');
          expect(reporter._testsuites[2].testsuite[1].testcase[0]._attr.name).to.equal('fail test');
          expect(reporter._testsuites[2].testsuite[1].testcase[0]._attr.classname).to.equal('Inner Suite.Another Suite');

          done();
        });
      });
      it('prefix is added to a classname when jenkinsClassnamePrefix is specified', function(done) {
        var reporter = createReporter({jenkinsMode: true,  jenkinsClassnamePrefix: "Added Prefix"});
        var rootSuite = reporter.runner.suite;

        var suite1 = Suite.create(rootSuite, 'Inner Suite');
        suite1.addTest(createTest('test'));

        var suite2 = Suite.create(suite1, 'Another Suite');
        suite2.addTest(createTest('fail test', function(done) {
          done(new Error('failed test'));
        }));

        runRunner(reporter.runner, function() {
          expect(reporter._testsuites[0].testsuite[0]._attr.name).to.equal('');
          expect(reporter._testsuites[1].testsuite[1].testcase[0]._attr.name).to.equal('test');
          expect(reporter._testsuites[1].testsuite[1].testcase[0]._attr.classname).to.equal('Added Prefix.Inner Suite');
          expect(reporter._testsuites[2].testsuite[0]._attr.name).to.equal('Root Suite.Inner Suite.Another Suite');
          expect(reporter._testsuites[2].testsuite[1].testcase[0]._attr.name).to.equal('fail test');
          expect(reporter._testsuites[2].testsuite[1].testcase[0]._attr.classname).to.equal('Added Prefix.Inner Suite.Another Suite');

          done();
        });
      });
    });
  });
});
