From 40de61bd4c1de967bb1a8366355f856a7c6a5092 Mon Sep 17 00:00:00 2001 From: Rustem Mustafin Date: Tue, 19 Nov 2013 16:45:16 +0400 Subject: [PATCH 1/6] Updated comments and before hook error behaviour --- lib/runner.js | 29 ++++++++++++++++++++++------- mocha.js | 33 +++++++++++++++++++++++---------- test/runner.js | 13 +++++++++++-- 3 files changed, 56 insertions(+), 19 deletions(-) diff --git a/lib/runner.js b/lib/runner.js index 6f7e28ca77..e1fbc5c2f0 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -201,10 +201,17 @@ Runner.prototype.fail = function(test, err){ /** * Fail the given `hook` with `err`. * - * Hook failures (currently) hard-end due - * to that fact that a failing hook will - * surely cause subsequent tests to fail, - * causing jumbled reporting. + * Hook failures work in the following pattern: + * - If bail, then exit + * - Failed `before` hook skips all tests in a suite + * and jumps to corresponding `after` hook + * - Failed `before each` hook skips remaining tests in a + * suite and jumps to corresponding `after each` hook, + * which is run only once + * - Failed `after` hook does not alter + * execution order + * - Failed `after each` hook skips remaining tests in a + * suite and jumps to corresponding `after` hook * * @param {Hook} hook * @param {Error} err @@ -213,7 +220,9 @@ Runner.prototype.fail = function(test, err){ Runner.prototype.failHook = function(hook, err){ this.fail(hook, err); - this.emit('end'); + if (this.suite.bail()) { + this.emit('end'); + } }; /** @@ -248,7 +257,12 @@ Runner.prototype.hook = function(name, fn){ hook.removeAllListeners('error'); var testError = hook.error(); if (testError) self.fail(self.test, testError); - if (err) return self.failHook(hook, err); + if (err) { + self.failHook(hook, err); + + // stop executing hooks, notify callee of hook err + return fn(err); + } self.emit('hook end', hook); delete hook.ctx.currentTest; next(++i); @@ -454,7 +468,8 @@ Runner.prototype.runSuite = function(suite, fn){ }); } - this.hook('beforeAll', function(){ + this.hook('beforeAll', function(err){ + if (err) return done(); self.runTests(suite, next); }); }; diff --git a/mocha.js b/mocha.js index 9b7c1fa56d..79452d9027 100644 --- a/mocha.js +++ b/mocha.js @@ -4422,9 +4422,7 @@ Runner.prototype.globalProps = function() { Runner.prototype.globals = function(arr){ if (0 == arguments.length) return this._globals; debug('globals %j', arr); - utils.forEach(arr, function(arr){ - this._globals.push(arr); - }, this); + this._globals = this._globals.concat(arr); return this; }; @@ -4480,10 +4478,17 @@ Runner.prototype.fail = function(test, err){ /** * Fail the given `hook` with `err`. * - * Hook failures (currently) hard-end due - * to that fact that a failing hook will - * surely cause subsequent tests to fail, - * causing jumbled reporting. + * Hook failures work in the following pattern: + * - If bail, then exit + * - Failed `before` hook skips all tests in a suite + * and jumps to corresponding `after` hook + * - Failed `before each` hook skips remaining tests in a + * suite and jumps to corresponding `after each` hook, + * which is run only once + * - Failed `after` hook does not alter + * execution order + * - Failed `after each` hook skips remaining tests in a + * suite and jumps to corresponding `after` hook * * @param {Hook} hook * @param {Error} err @@ -4492,7 +4497,9 @@ Runner.prototype.fail = function(test, err){ Runner.prototype.failHook = function(hook, err){ this.fail(hook, err); - this.emit('end'); + if (this.suite.bail()) { + this.emit('end'); + } }; /** @@ -4527,7 +4534,12 @@ Runner.prototype.hook = function(name, fn){ hook.removeAllListeners('error'); var testError = hook.error(); if (testError) self.fail(self.test, testError); - if (err) return self.failHook(hook, err); + if (err) { + self.failHook(hook, err); + + // stop executing hooks, notify callee of hook err + return fn(err); + } self.emit('hook end', hook); delete hook.ctx.currentTest; next(++i); @@ -4733,7 +4745,8 @@ Runner.prototype.runSuite = function(suite, fn){ }); } - this.hook('beforeAll', function(){ + this.hook('beforeAll', function(err){ + if (err) done(); self.runTests(suite, next); }); }; diff --git a/test/runner.js b/test/runner.js index 54d574c9de..98137399c8 100644 --- a/test/runner.js +++ b/test/runner.js @@ -160,7 +160,7 @@ describe('Runner', function(){ }) }) - describe('.failHook(hoot, err)', function(){ + describe('.failHook(hook, err)', function(){ it('should increment .failures', function(){ runner.failures.should.equal(0); runner.failHook({}, {}); @@ -179,10 +179,19 @@ describe('Runner', function(){ runner.failHook(hook, err); }) - it('should emit "end"', function(done){ + it('should emit "end" if suite bail is true', function(done){ var hook = {}, err = {}; + suite.bail(true); runner.on('end', done); runner.failHook(hook, err); }) + + it('should not emit "end" if suite bail is not true', function(done){ + var hook = {}, err = {}; + suite.bail(false); + runner.on('end', function() { throw new Error('"end" was emit, but the bail is false'); }); + runner.failHook(hook, err); + done(); + }) }) }) From ed9ce42af7ac13b7d5b390086a090557c4235fea Mon Sep 17 00:00:00 2001 From: Rustem Mustafin Date: Tue, 19 Nov 2013 18:41:07 +0400 Subject: [PATCH 2/6] Implement beforeEach hook fail handling --- lib/runner.js | 41 +++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/lib/runner.js b/lib/runner.js index e1fbc5c2f0..7dbdd3ee60 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -276,7 +276,7 @@ Runner.prototype.hook = function(name, fn){ /** * Run hook `name` for the given array of `suites` - * in order, and callback `fn(err)`. + * in order, and callback `fn(err, errSuite)`. * * @param {String} name * @param {Array} suites @@ -298,8 +298,9 @@ Runner.prototype.hooks = function(name, suites, fn){ self.hook(name, function(err){ if (err) { + var errSuite = self.suite; self.suite = orig; - return fn(err); + return fn(err, errSuite); } next(suites.pop()); @@ -411,7 +412,22 @@ Runner.prototype.runTests = function(suite, fn){ // execute test and hook(s) self.emit('test', self.test = test); - self.hookDown('beforeEach', function(){ + self.hookDown('beforeEach', function(err, errSuite){ + + if (err) { + // beforeEach hook for errSuite failed: + var orig = self.suite; + self.suite = errSuite; + + // call hookUp afterEach starting from errSuite + self.hookUp('afterEach', function() { + self.suite = orig; + + // report error suite + fn(errSuite); + }); + } + self.currentRunnable = self.test; self.runTest(function(err){ test = self.test; @@ -454,17 +470,30 @@ Runner.prototype.runSuite = function(suite, fn){ this.emit('suite', this.suite = suite); - function next() { + function next(errSuite) { + if (errSuite) { + // current suite failed on a hook from errSuite + if (errSuite == suite) { + // if errSuite is current suite + // continue to the next sibling suite + return done(); + } else { + // errSuite is among the parents of current suite + // stop execution of errSuite and all sub-suites + return done(errSuite); + } + } + var curr = suite.suites[i++]; if (!curr) return done(); self.runSuite(curr, next); } - function done() { + function done(errSuite) { self.suite = suite; self.hook('afterAll', function(){ self.emit('suite end', suite); - fn(); + fn(errSuite); }); } From 0c7f1271681deb6eb8ca378624bbda3cd718cfee Mon Sep 17 00:00:00 2001 From: Rustem Mustafin Date: Tue, 19 Nov 2013 19:49:20 +0400 Subject: [PATCH 3/6] And for afterEach hooks, too --- lib/runner.js | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/lib/runner.js b/lib/runner.js index 7dbdd3ee60..c7832b6dee 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -203,15 +203,16 @@ Runner.prototype.fail = function(test, err){ * * Hook failures work in the following pattern: * - If bail, then exit - * - Failed `before` hook skips all tests in a suite - * and jumps to corresponding `after` hook + * - Failed `before` hook skips all tests in a suite and subsuites, + * but jumps to corresponding `after` hook * - Failed `before each` hook skips remaining tests in a * suite and jumps to corresponding `after each` hook, * which is run only once * - Failed `after` hook does not alter * execution order * - Failed `after each` hook skips remaining tests in a - * suite and jumps to corresponding `after` hook + * suite and subsuites, but executes other `after each` + * hooks * * @param {Hook} hook * @param {Error} err @@ -388,10 +389,27 @@ Runner.prototype.runTests = function(suite, fn){ , tests = suite.tests.slice() , test; - function next(err) { + + function hookErr(err, errSuite) { + // before/after Each hook for errSuite failed: + var orig = self.suite; + self.suite = errSuite; + + // call hookUp afterEach starting from errSuite + self.hookUp('afterEach', function() { + self.suite = orig; + + // report error suite + fn(errSuite); + }); + } + + function next(err, errSuite) { // if we bail after first err if (self.failures && suite._bail) return fn(); + if (err) hookErr(err, errSuite); + // next test test = tests.shift(); @@ -414,19 +432,7 @@ Runner.prototype.runTests = function(suite, fn){ self.emit('test', self.test = test); self.hookDown('beforeEach', function(err, errSuite){ - if (err) { - // beforeEach hook for errSuite failed: - var orig = self.suite; - self.suite = errSuite; - - // call hookUp afterEach starting from errSuite - self.hookUp('afterEach', function() { - self.suite = orig; - - // report error suite - fn(errSuite); - }); - } + if (err) hookErr(err, errSuite); self.currentRunnable = self.test; self.runTest(function(err){ From 71aad70db5992809d03723963e93659ce1fe544c Mon Sep 17 00:00:00 2001 From: Rustem Mustafin Date: Tue, 19 Nov 2013 20:57:16 +0400 Subject: [PATCH 4/6] Fixed continuing to run tests, which should not be run, on hook fail --- lib/runner.js | 2 +- mocha.js | 57 +++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/lib/runner.js b/lib/runner.js index c7832b6dee..079f3f595d 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -432,7 +432,7 @@ Runner.prototype.runTests = function(suite, fn){ self.emit('test', self.test = test); self.hookDown('beforeEach', function(err, errSuite){ - if (err) hookErr(err, errSuite); + if (err) return hookErr(err, errSuite); self.currentRunnable = self.test; self.runTest(function(err){ diff --git a/mocha.js b/mocha.js index 79452d9027..09adb9e3af 100644 --- a/mocha.js +++ b/mocha.js @@ -4480,15 +4480,16 @@ Runner.prototype.fail = function(test, err){ * * Hook failures work in the following pattern: * - If bail, then exit - * - Failed `before` hook skips all tests in a suite - * and jumps to corresponding `after` hook + * - Failed `before` hook skips all tests in a suite and subsuites, + * but jumps to corresponding `after` hook * - Failed `before each` hook skips remaining tests in a * suite and jumps to corresponding `after each` hook, * which is run only once * - Failed `after` hook does not alter * execution order * - Failed `after each` hook skips remaining tests in a - * suite and jumps to corresponding `after` hook + * suite and subsuites, but executes other `after each` + * hooks * * @param {Hook} hook * @param {Error} err @@ -4553,7 +4554,7 @@ Runner.prototype.hook = function(name, fn){ /** * Run hook `name` for the given array of `suites` - * in order, and callback `fn(err)`. + * in order, and callback `fn(err, errSuite)`. * * @param {String} name * @param {Array} suites @@ -4575,8 +4576,9 @@ Runner.prototype.hooks = function(name, suites, fn){ self.hook(name, function(err){ if (err) { + var errSuite = self.suite; self.suite = orig; - return fn(err); + return fn(err, errSuite); } next(suites.pop()); @@ -4664,10 +4666,27 @@ Runner.prototype.runTests = function(suite, fn){ , tests = suite.tests.slice() , test; - function next(err) { + + function hookErr(err, errSuite) { + // before/after Each hook for errSuite failed: + var orig = self.suite; + self.suite = errSuite; + + // call hookUp afterEach starting from errSuite + self.hookUp('afterEach', function() { + self.suite = orig; + + // report error suite + fn(errSuite); + }); + } + + function next(err, errSuite) { // if we bail after first err if (self.failures && suite._bail) return fn(); + if (err) hookErr(err, errSuite); + // next test test = tests.shift(); @@ -4688,7 +4707,10 @@ Runner.prototype.runTests = function(suite, fn){ // execute test and hook(s) self.emit('test', self.test = test); - self.hookDown('beforeEach', function(){ + self.hookDown('beforeEach', function(err, errSuite){ + + if (err) return hookErr(err, errSuite); + self.currentRunnable = self.test; self.runTest(function(err){ test = self.test; @@ -4731,22 +4753,35 @@ Runner.prototype.runSuite = function(suite, fn){ this.emit('suite', this.suite = suite); - function next() { + function next(errSuite) { + if (errSuite) { + // current suite failed on a hook from errSuite + if (errSuite == suite) { + // if errSuite is current suite + // continue to the next sibling suite + return done(); + } else { + // errSuite is among the parents of current suite + // stop execution of errSuite and all sub-suites + return done(errSuite); + } + } + var curr = suite.suites[i++]; if (!curr) return done(); self.runSuite(curr, next); } - function done() { + function done(errSuite) { self.suite = suite; self.hook('afterAll', function(){ self.emit('suite end', suite); - fn(); + fn(errSuite); }); } this.hook('beforeAll', function(err){ - if (err) done(); + if (err) return done(); self.runTests(suite, next); }); }; From 3af2c462fc73a7b084522676b7fea26b31372b27 Mon Sep 17 00:00:00 2001 From: Rustem Mustafin Date: Wed, 20 Nov 2013 00:50:47 +0400 Subject: [PATCH 5/6] Multiple hooks fail scenario --- lib/runner.js | 28 +++++++++++++++++++--------- mocha.js | 28 +++++++++++++++++++--------- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/lib/runner.js b/lib/runner.js index 079f3f595d..f01fc6aeee 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -390,25 +390,35 @@ Runner.prototype.runTests = function(suite, fn){ , test; - function hookErr(err, errSuite) { + function hookErr(err, errSuite, after) { // before/after Each hook for errSuite failed: var orig = self.suite; - self.suite = errSuite; + + // for failed 'after each' hook start from errSuite parent, + // otherwise start from errSuite itself + self.suite = after ? errSuite.parent : errSuite; - // call hookUp afterEach starting from errSuite - self.hookUp('afterEach', function() { + if (self.suite) { + // call hookUp afterEach + self.hookUp('afterEach', function(err2, errSuite2) { + self.suite = orig; + // some hooks may fail even now + if (err2) return hookErr(err2, errSuite2, true); + // report error suite + fn(errSuite); + }); + } else { + // there is no need calling other 'after each' hooks self.suite = orig; - - // report error suite fn(errSuite); - }); + } } function next(err, errSuite) { // if we bail after first err if (self.failures && suite._bail) return fn(); - if (err) hookErr(err, errSuite); + if (err) return hookErr(err, errSuite, true); // next test test = tests.shift(); @@ -432,7 +442,7 @@ Runner.prototype.runTests = function(suite, fn){ self.emit('test', self.test = test); self.hookDown('beforeEach', function(err, errSuite){ - if (err) return hookErr(err, errSuite); + if (err) return hookErr(err, errSuite, false); self.currentRunnable = self.test; self.runTest(function(err){ diff --git a/mocha.js b/mocha.js index 09adb9e3af..16fbb8a305 100644 --- a/mocha.js +++ b/mocha.js @@ -4667,25 +4667,35 @@ Runner.prototype.runTests = function(suite, fn){ , test; - function hookErr(err, errSuite) { + function hookErr(err, errSuite, after) { // before/after Each hook for errSuite failed: var orig = self.suite; - self.suite = errSuite; + + // for failed 'after each' hook start from errSuite parent, + // otherwise start from errSuite itself + self.suite = after ? errSuite.parent : errSuite; - // call hookUp afterEach starting from errSuite - self.hookUp('afterEach', function() { + if (self.suite) { + // call hookUp afterEach + self.hookUp('afterEach', function(err2, errSuite2) { + self.suite = orig; + // some hooks may fail even now + if (err2) return hookErr(err2, errSuite2, true); + // report error suite + fn(errSuite); + }); + } else { + // there is no need calling other 'after each' hooks self.suite = orig; - - // report error suite fn(errSuite); - }); + } } function next(err, errSuite) { // if we bail after first err if (self.failures && suite._bail) return fn(); - if (err) hookErr(err, errSuite); + if (err) return hookErr(err, errSuite, true); // next test test = tests.shift(); @@ -4709,7 +4719,7 @@ Runner.prototype.runTests = function(suite, fn){ self.emit('test', self.test = test); self.hookDown('beforeEach', function(err, errSuite){ - if (err) return hookErr(err, errSuite); + if (err) return hookErr(err, errSuite, false); self.currentRunnable = self.test; self.runTest(function(err){ From 6e334642f2e5654b0ad46008c7e9db1c3ae65f39 Mon Sep 17 00:00:00 2001 From: Rustem Mustafin Date: Wed, 20 Nov 2013 01:43:29 +0400 Subject: [PATCH 6/6] Add tests for hook error handling --- test/hook.err.js | 296 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 296 insertions(+) create mode 100644 test/hook.err.js diff --git a/test/hook.err.js b/test/hook.err.js new file mode 100644 index 0000000000..f28ecdd501 --- /dev/null +++ b/test/hook.err.js @@ -0,0 +1,296 @@ +describe('hook error handling', function(){ + // Lines in this test should be uncommented to see actual behavior + // You will also see errors in hooks + describe('before hook error', function() { + var calls = []; + describe('spec 1', function () { + describe('spec 1 nested', function () { + it('should not be called, because hook error was in a parent suite', function() { + calls.push('test nested'); + }) + }) + before(function(){ + calls.push('before'); + // throw new Error('before hook error'); + }) + after(function(){ + calls.push('after'); + }) + it('should not be called because of error in before hook', function() { + calls.push('test'); + }) + }) + describe('spec 2', function () { + before(function(){ + calls.push('before 2'); + }) + after(function(){ + calls.push('after 2'); + }) + it('should be called, because hook error was in a sibling suite', function() { + calls.push('test 2'); + }) + }) + after(function () { + // calls.should.eql(['before', 'after', 'before 2', 'test 2', 'after 2']); + }) + }) + + describe('before each hook error', function() { + var calls = []; + describe('spec 1', function () { + describe('spec 1 nested', function () { + it('should not be called, because hook error was in a parent suite', function() { + calls.push('test nested'); + }) + }) + beforeEach(function(){ + calls.push('before'); + // throw new Error('before each hook error'); + }) + afterEach(function(){ + calls.push('after'); + }) + it('should not be called because of error in before each hook', function() { + calls.push('test'); + }) + }) + describe('spec 2', function () { + before(function(){ + calls.push('before 2'); + }) + after(function(){ + calls.push('after 2'); + }) + it('should be called, because hook error was in a sibling suite', function() { + calls.push('test 2'); + }) + }) + after(function () { + // This should be called ! + // calls.should.eql(['before', 'after', 'before 2', 'test 2', 'after 2']); + }) + }) + + describe('after hook error', function() { + var calls = []; + describe('spec 1', function () { + describe('spec 1 nested', function () { + it('should be called, because hook error will happen after parent suite', function() { + calls.push('test nested'); + }) + }) + before(function(){ + calls.push('before'); + }) + after(function(){ + calls.push('after'); + // throw new Error('after hook error'); + }) + it('should be called because error is in after hook', function() { + calls.push('test'); + }) + }) + describe('spec 2', function () { + before(function(){ + calls.push('before 2'); + }) + after(function(){ + calls.push('after 2'); + }) + it('should be called, because hook error was in a sibling suite', function() { + calls.push('test 2'); + }) + }) + after(function () { + // Even this should be called ! + // calls.should.eql(['before', 'test', 'test nested', 'after', 'before 2', 'test 2', 'after 2']); + }) + }) + + describe('after each hook error', function() { + var calls = []; + describe('spec 1', function () { + describe('spec 1 nested', function () { + it('should not be called, because hook error has already happened in parent suite', function() { + calls.push('test nested'); + }) + }) + beforeEach(function(){ + calls.push('before'); + }) + afterEach(function(){ + calls.push('after'); + // throw new Error('after each hook error'); + }) + it('should be called because error is in after each hook, and this is the first test', function() { + calls.push('test'); + }) + it('should not be called because error is in after each hook, and this is the second test', function() { + calls.push('another test'); + }) + }) + describe('spec 2', function () { + before(function(){ + calls.push('before 2'); + }) + after(function(){ + calls.push('after 2'); + }) + it('should be called, because hook error was in a sibling suite', function() { + calls.push('test 2'); + }) + }) + after(function () { + // This should be called ! + // calls.should.eql(['before', 'test', 'after', 'before 2', 'test 2', 'after 2']); + }) + }) + + describe('multiple hook errors', function() { + var calls = []; + before(function(){ + calls.push("root before"); + }); + beforeEach(function(){ + calls.push("root before each"); + }); + describe('1', function(){ + beforeEach(function() { + calls.push('1 before each') + }) + + describe('1.1', function(){ + before(function() { + calls.push('1.1 before'); + }); + beforeEach(function() { + calls.push('1.1 before each') + // throw new Error('1.1 before each hook failed') + }); + it('1.1 test 1', function () {calls.push('1.1 test 1')}); + it('1.1 test 2', function () {calls.push('1.1 test 2')}); + afterEach(function() { + calls.push("1.1 after each"); + }); + after(function(){ + calls.push("1.1 after"); + // throw new Error('1.1 after hook failed') + }); + }); + + describe('1.2', function(){ + before(function() { + calls.push('1.2 before'); + }); + beforeEach(function() { + calls.push('1.2 before each') + }); + it('1.2 test 1', function () {calls.push('1.2 test 1')}); + it('1.2 test 2', function () {calls.push('1.2 test 2')}); + afterEach(function() { + calls.push("1.2 after each"); + // throw new Error('1.2 after each hook failed') + }); + after(function(){ + calls.push("1.2 after"); + }); + }); + + afterEach(function() { + calls.push('1 after each') + }) + + after(function(){ + calls.push("1 after"); + }); + }) + + describe('2', function(){ + beforeEach(function() { + calls.push('2 before each') + // throw new Error('2 before each hook failed') + }) + + describe('2.1', function(){ + before(function() { + calls.push('2.1 before'); + }); + beforeEach(function() { + calls.push('2.1 before each') + }); + it('2.1 test 1', function () {calls.push('2.1 test 1')}); + it('2.1 test 2', function () {calls.push('2.1 test 2')}); + afterEach(function() { + calls.push("2.1 after each"); + }); + after(function(){ + calls.push("2.1 after"); + }); + }); + + describe('2.2', function(){ + before(function() { + calls.push('2.2 before'); + }); + beforeEach(function() { + calls.push('2.2 before each') + }); + it('2.2 test 1', function () {calls.push('2.2 test 1')}); + it('2.2 test 2', function () {calls.push('2.2 test 2')}); + afterEach(function() { + calls.push("2.2 after each"); + }); + after(function(){ + calls.push("2.2 after"); + }); + }); + + afterEach(function() { + calls.push('2 after each') + // throw new Error('2 after each hook failed') + }) + + after(function(){ + calls.push("2 after"); + }); + }) + + after(function(){ + calls.push("root after"); + /* calls.should.eql([ + "root before", + "1.1 before", + "root before each", + "1 before each", + "1.1 before each", + "1.1 after each", + "1 after each", + "root after each", + "1.1 after", + "1.2 before", + "root before each", + "1 before each", + "1.2 before each", + "1.2 test 1", + "1.2 after each", + "1 after each", + "root after each", + "1.2 after", + "1 after", + "2.1 before", + "root before each", + "2 before each", + "2 after each", + "root after each", + "2.1 after", + "2 after", + "root after" + ]); */ + }); + afterEach(function(){ + calls.push("root after each"); + }); + }) + +}) \ No newline at end of file