text: /*globals describe it beforeEach afterEach */
  "use strict";
  
  require("should");
  var path = require("path");
  var TestHelper = require("./helpers/TestHelper");
  var Watchpack = require("../lib/watchpack");
  
  var fixtures = path.join(__dirname, "fixtures");
  var testHelper = new TestHelper(fixtures);
  
  describe("Watchpack", function() {
  	this.timeout(10000);
  	beforeEach(testHelper.before);
  	afterEach(testHelper.after);
  
  	it("should watch a single file", function(done) {
  		var w = new Watchpack({
  			aggregateTimeout: 1000
  		});
  		var changeEvents = 0;
  		w.on("change", function(file) {
  			file.should.be.eql(path.join(fixtures, "a"));
  			changeEvents++;
  		});
  		w.on("aggregated", function(changes) {
  			Array.from(changes).should.be.eql([path.join(fixtures, "a")]);
  			changeEvents.should.be.greaterThan(0);
  			w.close();
  			done();
  		});
  		w.watch([path.join(fixtures, "a")], []);
  		testHelper.tick(function() {
  			testHelper.file("a");
  		});
  	});
  
  	it("should not watch a single ignored file (glob)", function(done) {
  		var w = new Watchpack({
  			aggregateTimeout: 300,
  			ignored: "**/a"
  		});
  		var changeEvents = 0;
  		var aggregatedEvents = 0;
  		w.on("change", () => {
  			changeEvents++;
  		});
  		w.on("aggregated", () => {
  			aggregatedEvents++;
  		});
  		w.watch([path.join(fixtures, "a")], []);
  		testHelper.tick(() => {
  			testHelper.file("a");
  			testHelper.tick(1000, () => {
  				changeEvents.should.be.eql(0);
  				aggregatedEvents.should.be.eql(0);
  				testHelper.getNumberOfWatchers().should.be.eql(0);
  				w.close();
  				done();
  			});
  		});
  	});
  
  	it("should not watch a single ignored file (regexp)", function(done) {
  		var w = new Watchpack({
  			aggregateTimeout: 300,
  			ignored: /\/a$/
  		});
  		var changeEvents = 0;
  		var aggregatedEvents = 0;
  		w.on("change", () => {
  			changeEvents++;
  		});
  		w.on("aggregated", () => {
  			aggregatedEvents++;
  		});
  		w.watch([path.join(fixtures, "a")], []);
  		testHelper.tick(() => {
  			testHelper.file("a");
  			testHelper.tick(1000, () => {
  				changeEvents.should.be.eql(0);
  				aggregatedEvents.should.be.eql(0);
  				testHelper.getNumberOfWatchers().should.be.eql(0);
  				w.close();
  				done();
  			});
  		});
  	});
  
  	it("should watch multiple files", function(done) {
  		var w = new Watchpack({
  			aggregateTimeout: 1000
  		});
  		var changeEvents = [];
  		w.on("change", function(file) {
  			if (changeEvents[changeEvents.length - 1] === file) return;
  			changeEvents.push(file);
  		});
  		w.on("aggregated", function(changes) {
  			Array.from(changes)
  				.sort()
  				.should.be.eql([path.join(fixtures, "a"), path.join(fixtures, "b")]);
  			changeEvents.should.be.eql([
  				path.join(fixtures, "a"),
  				path.join(fixtures, "b"),
  				path.join(fixtures, "a"),
  				path.join(fixtures, "b"),
  				path.join(fixtures, "a")
  			]);
  			Object.keys(w.getTimes())
  				.sort()
  				.should.be.eql([path.join(fixtures, "a"), path.join(fixtures, "b")]);
  			w.close();
  			done();
  		});
  		w.watch([path.join(fixtures, "a"), path.join(fixtures, "b")], []);
  		testHelper.tick(400, function() {
  			testHelper.file("a");
  			testHelper.tick(400, function() {
  				testHelper.file("b");
  				testHelper.tick(400, function() {
  					testHelper.file("a");
  					testHelper.tick(400, function() {
  						testHelper.file("b");
  						testHelper.tick(400, function() {
  							testHelper.file("a");
  						});
  					});
  				});
  			});
  		});
  	});
  
  	it("should watch a directory", function(done) {
  		var w = new Watchpack({
  			aggregateTimeout: 1000
  		});
  		var changeEvents = [];
  		w.on("change", function(file) {
  			if (changeEvents[changeEvents.length - 1] === file) return;
  			changeEvents.push(file);
  		});
  		w.on("aggregated", function(changes) {
  			Array.from(changes).should.be.eql([path.join(fixtures, "dir")]);
  			changeEvents.should.be.eql([path.join(fixtures, "dir", "a")]);
  			w.close();
  			done();
  		});
  		testHelper.dir("dir");
  		testHelper.tick(200, function() {
  			w.watch([], [path.join(fixtures, "dir")]);
  			testHelper.tick(200, function() {
  				testHelper.file(path.join("dir", "a"));
  			});
  		});
  	});
  
  	it("should not watch an ignored directory", function(done) {
  		var w = new Watchpack({
  			aggregateTimeout: 300,
  			ignored: ["**/dir"]
  		});
  		var changeEvents = 0;
  		var aggregatedEvents = 0;
  		w.on("change", () => {
  			changeEvents++;
  		});
  		w.on("aggregated", () => {
  			aggregatedEvents++;
  		});
  		testHelper.dir("dir");
  		testHelper.tick(200, function() {
  			w.watch([], [path.join(fixtures, "dir")]);
  			testHelper.tick(200, function() {
  				testHelper.file(path.join("dir", "a"));
  				testHelper.tick(1000, function() {
  					changeEvents.should.be.eql(0);
  					aggregatedEvents.should.be.eql(0);
  					testHelper.getNumberOfWatchers().should.be.eql(0);
  					w.close();
  					done();
  				});
  			});
  		});
  	});
  
  	it("should not watch an ignored file in a directory", function(done) {
  		var w = new Watchpack({
  			aggregateTimeout: 300,
  			ignored: ["**/a"]
  		});
  		var changeEvents = 0;
  		var aggregatedEvents = 0;
  		w.on("change", () => {
  			changeEvents++;
  		});
  		w.on("aggregated", () => {
  			aggregatedEvents++;
  		});
  		testHelper.dir("dir");
  		testHelper.tick(200, function() {
  			w.watch([], [path.join(fixtures, "dir")]);
  			testHelper.tick(200, function() {
  				testHelper.file(path.join("dir", "a"));
  				testHelper.tick(1000, function() {
  					changeEvents.should.be.eql(0);
  					aggregatedEvents.should.be.eql(0);
  					w.close();
  					done();
  				});
  			});
  		});
  	});
  
  	it("should watch a file then a directory", function(done) {
  		var w = new Watchpack({
  			aggregateTimeout: 1000
  		});
  		var changeEvents = [];
  		w.on("change", function(file) {
  			if (changeEvents[changeEvents.length - 1] === file) return;
  			changeEvents.push(file);
  		});
  		w.on("aggregated", function(changes, removals) {
  			Array.from(changes).should.be.eql([path.join(fixtures, "dir")]);
  			Array.from(removals).should.be.eql([]);
  			changeEvents.should.be.eql([path.join(fixtures, "dir", "a")]);
  			w.close();
  			done();
  		});
  		testHelper.dir("dir");
  		testHelper.dir(path.join("dir", "subdir"));
  		testHelper.file(path.join("dir", "a"));
  		testHelper.tick(400, function() {
  			w.watch([path.join(fixtures, "dir", "a")], []);
  			testHelper.tick(function() {
  				w.watch([], [path.join(fixtures, "dir")]);
  				testHelper.tick(function() {
  					testHelper.file(path.join("dir", "a"));
  				});
  			});
  		});
  	});
  
  	it("should watch a directory (delete file)", function(done) {
  		var w = new Watchpack({
  			aggregateTimeout: 1000
  		});
  		var changeEvents = [];
  		w.on("change", function(file) {
  			if (changeEvents[changeEvents.length - 1] === file) return;
  			changeEvents.push(file);
  		});
  		w.on("aggregated", function(changes) {
  			Array.from(changes).should.be.eql([path.join(fixtures, "dir")]);
  			changeEvents.should.be.eql([path.join(fixtures, "dir", "a")]);
  			w.close();
  			done();
  		});
  		testHelper.dir("dir");
  		testHelper.file(path.join("dir", "a"));
  		testHelper.tick(function() {
  			w.watch([], [path.join(fixtures, "dir")]);
  			testHelper.tick(function() {
  				testHelper.remove(path.join("dir", "a"));
  			});
  		});
  	});
  
  	it("should watch a directory (delete and recreate file)", function(done) {
  		var w = new Watchpack({
  			aggregateTimeout: 1000
  		});
  		var changeEvents = [];
  		w.on("change", function(file) {
  			if (changeEvents[changeEvents.length - 1] === file) return;
  			changeEvents.push(file);
  		});
  		w.on("aggregated", function(changes) {
  			Array.from(changes).should.be.eql([path.join(fixtures, "dir")]);
  			changeEvents.should.be.eql([
  				path.join(fixtures, "dir", "a"),
  				path.join(fixtures, "dir", "b"),
  				path.join(fixtures, "dir", "a")
  			]);
  			w.close();
  			done();
  		});
  		testHelper.dir("dir");
  		testHelper.file(path.join("dir", "a"));
  		testHelper.tick(function() {
  			w.watch([], [path.join(fixtures, "dir")]);
  			testHelper.tick(function() {
  				testHelper.remove(path.join("dir", "a"));
  				testHelper.tick(function() {
  					testHelper.file(path.join("dir", "b"));
  					testHelper.tick(500, function() {
  						testHelper.file(path.join("dir", "a"));
  					});
  				});
  			});
  		});
  	});
  
  	it("should watch a missing directory", function(done) {
  		var w = new Watchpack({
  			aggregateTimeout: 1000
  		});
  		var changeEvents = [];
  		w.on("change", function(file) {
  			if (changeEvents[changeEvents.length - 1] === file) return;
  			changeEvents.push(file);
  		});
  		w.on("aggregated", function(changes) {
  			Array.from(changes).should.be.eql([path.join(fixtures, "dir", "sub")]);
  			changeEvents.should.be.eql([path.join(fixtures, "dir", "sub")]);
  			w.close();
  			done();
  		});
  		testHelper.dir("dir");
  		testHelper.tick(function() {
  			w.watch({ missing: [path.join(fixtures, "dir", "sub")] });
  			testHelper.tick(function() {
  				testHelper.dir(path.join("dir", "sub"));
  			});
  		});
  	});
  
  	it("should watch a directory (add directory)", function(done) {
  		var w = new Watchpack({
  			aggregateTimeout: 1000
  		});
  		var changeEvents = [];
  		w.on("change", function(file) {
  			if (changeEvents[changeEvents.length - 1] === file) return;
  			changeEvents.push(file);
  		});
  		w.on("aggregated", function(changes) {
  			Array.from(changes).should.be.eql([path.join(fixtures, "dir")]);
  			changeEvents.should.be.eql([path.join(fixtures, "dir", "sub")]);
  			w.close();
  			done();
  		});
  		testHelper.dir("dir");
  		testHelper.tick(function() {
  			w.watch([], [path.join(fixtures, "dir")]);
  			testHelper.tick(function() {
  				testHelper.dir(path.join("dir", "sub"));
  			});
  		});
  	});
  
  	it("should watch a directory (delete directory)", function(done) {
  		var w = new Watchpack({
  			aggregateTimeout: 1000
  		});
  		var changeEvents = [];
  		w.on("change", function(file) {
  			if (changeEvents[changeEvents.length - 1] === file) return;
  			changeEvents.push(file);
  		});
  		w.on("aggregated", function(changes) {
  			Array.from(changes).should.be.eql([path.join(fixtures, "dir")]);
  			w.close();
  			done();
  		});
  		testHelper.dir("dir");
  		testHelper.dir(path.join("dir", "sub"));
  		testHelper.file(path.join("dir", "sub", "a"));
  		testHelper.tick(function() {
  			w.watch([], [path.join(fixtures, "dir")]);
  			testHelper.tick(function() {
  				testHelper.remove(path.join("dir", "sub"));
  			});
  		});
  	});
  
  	it("should watch a directory (delete, restore and change directory)", function(done) {
  		var w = new Watchpack({
  			aggregateTimeout: 1000
  		});
  		var changeEvents = [];
  		w.on("change", function(file) {
  			if (changeEvents[changeEvents.length - 1] === file) return;
  			changeEvents.push(file);
  		});
  		w.on("aggregated", function(changes, removals) {
  			Array.from(changes).should.be.eql([
  				path.join(fixtures, "dir", "sub", "a")
  			]);
  			Array.from(removals).should.be.eql([]);
  			w.close();
  			done();
  		});
  		testHelper.dir("dir");
  		testHelper.dir(path.join("dir", "sub"));
  		testHelper.file(path.join("dir", "sub", "a"));
  		testHelper.tick(function() {
  			w.watch([path.join(fixtures, "dir", "sub", "a")], []);
  			testHelper.tick(function() {
  				testHelper.remove(path.join("dir", "sub"));
  				testHelper.tick(function() {
  					testHelper.dir(path.join("dir", "sub"));
  					testHelper.file(path.join("dir", "sub", "a"));
  				});
  			});
  		});
  	});
  
  	it("should watch a directory (delete directory2)", function(done) {
  		var w = new Watchpack({
  			aggregateTimeout: 1000
  		});
  		var changeEvents = [];
  		w.on("change", function(file) {
  			if (changeEvents[changeEvents.length - 1] === file) return;
  			changeEvents.push(file);
  		});
  		w.on("aggregated", function(changes) {
  			Array.from(changes).should.be.eql([path.join(fixtures, "dir")]);
  			changeEvents.should.be.eql([path.join(fixtures, "dir", "sub")]);
  			w.close();
  			done();
  		});
  		testHelper.dir("dir");
  		testHelper.dir(path.join("dir", "sub"));
  		testHelper.tick(function() {
  			w.watch([], [path.join(fixtures, "dir")]);
  			testHelper.tick(function() {
  				testHelper.remove(path.join("dir", "sub"));
  			});
  		});
  	});
  
  	it("should watch already watched directory", function(done) {
  		var w = new Watchpack({
  			aggregateTimeout: 1000
  		});
  		var changeEvents = [];
  		w.on("change", function(file) {
  			if (changeEvents[changeEvents.length - 1] === file) return;
  			changeEvents.push(file);
  		});
  		w.on("aggregated", function(changes) {
  			Array.from(changes).should.be.eql([path.join(fixtures, "dir")]);
  			changeEvents.should.be.eql([path.join(fixtures, "dir", "a")]);
  			w.close();
  			done();
  		});
  		testHelper.dir("dir");
  		testHelper.file(path.join("dir", "a"));
  		testHelper.tick(400, function() {
  			w.watch([path.join(fixtures, "dir", "a")], []);
  			testHelper.tick(1000, function() {
  				w.watch([], [path.join(fixtures, "dir")]);
  				testHelper.tick(400, function() {
  					testHelper.remove(path.join("dir", "a"));
  				});
  			});
  		});
  	});
  
  	it("should watch file in a sub directory", function(done) {
  		var w = new Watchpack({
  			aggregateTimeout: 1000
  		});
  		var changeEvents = [];
  		w.on("change", function(file) {
  			if (changeEvents[changeEvents.length - 1] === file) return;
  			changeEvents.push(file);
  		});
  		w.on("aggregated", function(changes) {
  			Array.from(changes).should.be.eql([path.join(fixtures, "dir")]);
  			changeEvents.should.be.eql([path.join(fixtures, "dir", "sub", "a")]);
  			var times = w.getTimeInfoEntries();
  			const dir = times.get(path.join(fixtures, "dir"));
  			const sub = times.get(path.join(fixtures, "dir", "sub"));
  			const a = times.get(path.join(fixtures, "dir", "sub", "a"));
  			dir.should.be.type("object");
  			dir.should.have.property("safeTime");
  			sub.safeTime.should.be.aboveOrEqual(a.safeTime);
  			dir.safeTime.should.be.aboveOrEqual(sub.safeTime);
  			w.close();
  			done();
  		});
  		testHelper.dir("dir");
  		testHelper.dir(path.join("dir", "sub"));
  		testHelper.tick(function() {
  			w.watch([], [path.join(fixtures, "dir")]);
  			testHelper.tick(function() {
  				testHelper.file(path.join("dir", "sub", "a"));
  			});
  		});
  	});
  
  	it("should watch 2 files in a not-existing directory", function(done) {
  		var w = new Watchpack({
  			aggregateTimeout: 1000
  		});
  		w.on("aggregated", function(changes) {
  			Array.from(changes)
  				.sort()
  				.should.be.eql([
  					path.join(fixtures, "dir", "sub", "a"),
  					path.join(fixtures, "dir", "sub", "b")
  				]);
  			w.close();
  			done();
  		});
  		testHelper.dir("dir");
  		w.watch(
  			[
  				path.join(fixtures, "dir", "sub", "a"),
  				path.join(fixtures, "dir", "sub", "b")
  			],
  			[]
  		);
  		testHelper.tick(function() {
  			testHelper.dir(path.join("dir", "sub"));
  			testHelper.file(path.join("dir", "sub", "a"));
  			testHelper.file(path.join("dir", "sub", "b"));
  		});
  	});
  
  	it("should watch file in a sub sub directory", function(done) {
  		var w = new Watchpack({
  			aggregateTimeout: 1000
  		});
  		var changeEvents = [];
  		w.on("change", function(file) {
  			if (changeEvents[changeEvents.length - 1] === file) return;
  			changeEvents.push(file);
  		});
  		w.on("aggregated", function(changes) {
  			Array.from(changes).should.be.eql([path.join(fixtures, "dir")]);
  			changeEvents.should.be.eql([
  				path.join(fixtures, "dir", "sub", "sub", "a")
  			]);
  			Object.keys(w.getTimes())
  				.sort()
  				.should.be.eql([
  					path.join(fixtures, "dir"),
  					path.join(fixtures, "dir", "sub"),
  					path.join(fixtures, "dir", "sub", "sub"),
  					path.join(fixtures, "dir", "sub", "sub", "a")
  				]);
  			w.close();
  			done();
  		});
  		testHelper.dir("dir");
  		testHelper.dir(path.join("dir", "sub"));
  		testHelper.dir(path.join("dir", "sub", "sub"));
  		testHelper.tick(2000, function() {
  			w.watch([], [path.join(fixtures, "dir")], Date.now());
  			testHelper.tick(function() {
  				testHelper.file(path.join("dir", "sub", "sub", "a"));
  			});
  		});
  	});
  
  	it("should watch file in a directory that contains special glob characters", function(done) {
  		var w = new Watchpack({
  			aggregateTimeout: 1000
  		});
  		var changeEvents = [];
  		w.on("change", function(file) {
  			if (changeEvents[changeEvents.length - 1] === file) return;
  			changeEvents.push(file);
  		});
  		w.on("aggregated", function(changes) {
  			Array.from(changes).should.be.eql([path.join(fixtures, "dir")]);
  			changeEvents.should.be.eql([path.join(fixtures, "dir", "sub()", "a")]);
  			var times = w.getTimeInfoEntries();
  			const dir = times.get(path.join(fixtures, "dir"));
  			const sub = times.get(path.join(fixtures, "dir", "sub()"));
  			const a = times.get(path.join(fixtures, "dir", "sub()", "a"));
  			sub.safeTime.should.be.aboveOrEqual(a.safeTime);
  			dir.safeTime.should.be.aboveOrEqual(sub.safeTime);
  			w.close();
  			done();
  		});
  		testHelper.dir("dir");
  		testHelper.dir(path.join("dir", "sub()"));
  		testHelper.tick(2000, function() {
  			w.watch([], [path.join(fixtures, "dir")]);
  			testHelper.tick(function() {
  				testHelper.file(path.join("dir", "sub()", "a"));
  			});
  		});
  	});
  
  	it("should detect a single change to future timestamps", function(done) {
  		const options = {
  			aggregateTimeout: 1000
  		};
  		var w = new Watchpack(options);
  		var w2 = new Watchpack(options);
  		w.on("change", function() {
  			throw new Error("should not report change event");
  		});
  		w.on("aggregated", function() {
  			throw new Error("should not report aggregated event");
  		});
  		testHelper.file("a");
  		testHelper.tick(400, function() {
  			w2.watch([path.join(fixtures, "a")], [], Date.now());
  			testHelper.tick(1000, function() {
  				// wait for initial scan
  				testHelper.mtime("a", Date.now() + 1000000);
  				testHelper.tick(400, function() {
  					w.watch([path.join(fixtures, "a")], [], Date.now());
  					testHelper.tick(1000, function() {
  						w2.close();
  						w.close();
  						done();
  					});
  				});
  			});
  		});
  	});
  
  	it("should create different watchers for different options", function(done) {
  		var w = new Watchpack({
  			aggregateTimeout: 1000
  		});
  		var w2 = new Watchpack({
  			aggregateTimeout: 1000
  		});
  		testHelper.file("a");
  		testHelper.tick(400, function() {
  			w.watch([path.join(fixtures, "a")], [], Date.now());
  			w2.watch([path.join(fixtures, "a")], [], Date.now());
  			testHelper.tick(1000, function() {
  				testHelper.file("a");
  				testHelper.tick(400, function() {
  					testHelper.file("a");
  					testHelper.tick(1000, function() {
  						w2.close();
  						w.close();
  						done();
  					});
  				});
  			});
  		});
  	});
  
  	it("should detect a past change to a file (timestamp)", function(done) {
  		var w = new Watchpack({
  			aggregateTimeout: 1000
  		});
  		var changeEvents = 0;
  		w.on("change", function(file) {
  			file.should.be.eql(path.join(fixtures, "a"));
  			changeEvents++;
  		});
  		w.on("aggregated", function(changes) {
  			Array.from(changes).should.be.eql([path.join(fixtures, "a")]);
  			changeEvents.should.be.greaterThan(0);
  			w.close();
  			done();
  		});
  		var startTime = Date.now();
  		testHelper.tick(function() {
  			testHelper.file("a");
  			testHelper.tick(function() {
  				w.watch([path.join(fixtures, "a")], [], startTime);
  			});
  		});
  	});
  
  	it("should not detect a past change to a file (watched)", function(done) {
  		var w2 = new Watchpack();
  		var w = new Watchpack();
  		w.on("change", function() {
  			throw new Error("Should not be detected");
  		});
  		testHelper.tick(function() {
  			testHelper.file("b");
  			w2.watch([path.join(fixtures, "b")], []);
  			testHelper.tick(1000, function() {
  				// wait for stable state
  				testHelper.file("a");
  				testHelper.tick(1000, function() {
  					var startTime = Date.now();
  					testHelper.tick(400, function() {
  						w.watch([path.join(fixtures, "a")], [], startTime);
  						testHelper.tick(1000, function() {
  							w.close();
  							w2.close();
  							done();
  						});
  					});
  				});
  			});
  		});
  	});
  
  	it("should detect a past change to a file (watched)", function(done) {
  		var w2 = new Watchpack();
  		var w = new Watchpack();
  		var changeEvents = 0;
  		w.on("change", function(file) {
  			file.should.be.eql(path.join(fixtures, "a"));
  			changeEvents++;
  		});
  		w.on("aggregated", function(changes) {
  			Array.from(changes).should.be.eql([path.join(fixtures, "a")]);
  			changeEvents.should.be.eql(1);
  			w.close();
  			w2.close();
  			done();
  		});
  		testHelper.tick(function() {
  			testHelper.file("b");
  			w2.watch([path.join(fixtures, "b")], []);
  			testHelper.tick(function() {
  				var startTime = Date.now();
  				testHelper.tick(function() {
  					testHelper.file("a");
  					testHelper.tick(400, function() {
  						w.watch([path.join(fixtures, "a")], [], startTime);
  					});
  				});
  			});
  		});
  	});
  
  	it("should watch a single file removal", function(done) {
  		testHelper.file("a");
  		var w = new Watchpack({
  			aggregateTimeout: 1000
  		});
  		var removeEvents = 0;
  		w.on("remove", function(file) {
  			file.should.be.eql(path.join(fixtures, "a"));
  			removeEvents++;
  		});
  		w.on("aggregated", function(changes, removals) {
  			Array.from(removals).should.be.eql([path.join(fixtures, "a")]);
  			removeEvents.should.be.eql(1);
  			w.close();
  			done();
  		});
  		testHelper.tick(400, function() {
  			w.watch([path.join(fixtures, "a")], []);
  			testHelper.tick(function() {
  				testHelper.remove("a");
  			});
  		});
  	});
  
  	it("should watch multiple file removals", function(done) {
  		var step = 0;
  		testHelper.file("a");
  		testHelper.file("b");
  		var w = new Watchpack({
  			aggregateTimeout: 3000
  		});
  		var removeEvents = [];
  		w.on("remove", function(file) {
  			if (removeEvents[removeEvents.length - 1] === file) return;
  			removeEvents.push(file);
  		});
  		w.on("aggregated", function(changes, removals) {
  			step.should.be.eql(6);
  			Array.from(removals)
  				.sort()
  				.should.be.eql([path.join(fixtures, "a"), path.join(fixtures, "b")]);
  			if (!+process.env.WATCHPACK_POLLING) {
  				removeEvents.should.be.eql([
  					path.join(fixtures, "a"),
  					path.join(fixtures, "b"),
  					path.join(fixtures, "a"),
  					path.join(fixtures, "b")
  				]);
  			}
  			Object.keys(w.getTimes())
  				.sort()
  				.should.be.eql([path.join(fixtures, "a"), path.join(fixtures, "b")]);
  			w.close();
  			done();
  		});
  		testHelper.tick(400, function() {
  			w.watch([path.join(fixtures, "a"), path.join(fixtures, "b")], []);
  			step = 1;
  			testHelper.tick(1000, function() {
  				testHelper.remove("a");
  				step = 2;
  				testHelper.tick(function() {
  					testHelper.remove("b");
  					step = 3;
  					testHelper.tick(1000, function() {
  						testHelper.file("a");
  						testHelper.file("b");
  						step = 4;
  						testHelper.tick(1000, function() {
  							testHelper.remove("a");
  							step = 5;
  							testHelper.tick(function() {
  								testHelper.remove("b");
  								step = 6;
  							});
  						});
  					});
  				});
  			});
  		});
  	});
  
  	it("should not report changes in initial scan when no start time is provided", function(done) {
  		var w = new Watchpack({
  			aggregateTimeout: 100
  		});
  		w.on("aggregated", () => {
  			done(new Error("should not fire"));
  		});
  		testHelper.dir("dir");
  		testHelper.dir("dir/b");
  		testHelper.dir("dir/b/sub");
  		testHelper.file("dir/b/sub/file");
  		testHelper.dir("dir/b/sub/sub");
  		testHelper.file("dir/b/sub/sub/file");
  		testHelper.file("dir/b/file");
  		testHelper.file("dir/a");
  		testHelper.tick(1000, () => {
  			w.watch({
  				directories: [path.join(fixtures, "dir", "b", "sub")]
  			});
  			testHelper.tick(2000, () => {
  				// no event fired
  				w.watch({
  					files: [path.join(fixtures, "dir", "a")],
  					directories: [
  						path.join(fixtures, "dir", "b", "sub"),
  						path.join(fixtures, "dir", "b")
  					],
  					missing: [path.join(fixtures, "dir", "c")]
  				});
  				testHelper.tick(2000, () => {
  					// no event fired
  					w.close();
  					done();
  				});
  			});
  		});
  	});
  
  	it("should not report changes in initial scan when start time is provided", function(done) {
  		var w = new Watchpack({
  			aggregateTimeout: 100
  		});
  		w.on("aggregated", () => {
  			done(new Error("should not fire"));
  		});
  		testHelper.dir("dir");
  		testHelper.dir("dir/b");
  		testHelper.dir("dir/b/sub");
  		testHelper.file("dir/b/sub/file");
  		testHelper.dir("dir/b/sub/sub");
  		testHelper.file("dir/b/sub/sub/file");
  		testHelper.file("dir/b/file");
  		testHelper.file("dir/a");
  		testHelper.tick(1000, () => {
  			w.watch({
  				directories: [path.join(fixtures, "dir", "b", "sub")],
  				startTime: Date.now()
  			});
  			testHelper.tick(2000, () => {
  				// no event fired
  				w.watch({
  					files: [path.join(fixtures, "dir", "a")],
  					directories: [
  						path.join(fixtures, "dir", "b", "sub"),
  						path.join(fixtures, "dir", "b")
  					],
  					missing: [path.join(fixtures, "dir", "c")],
  					startTime: Date.now()
  				});
  				testHelper.tick(2000, () => {
  					// no event fired
  					w.close();
  					done();
  				});
  			});
  		});
  	});
  
  	it("should not report changes to a folder watched as file when items are added", function(done) {
  		var w = new Watchpack({
  			aggregateTimeout: 100
  		});
  		w.on("aggregated", () => {
  			done(new Error("should not fire"));
  		});
  		testHelper.dir("dir");
  		testHelper.file("dir/a");
  		testHelper.tick(1000, () => {
  			testHelper.file("dir/b");
  			w.watch({
  				files: [path.join(fixtures, "dir")],
  				startTime: Date.now()
  			});
  			testHelper.tick(1000, () => {
  				testHelper.file("dir/c");
  				testHelper.tick(1000, () => {
  					// no event fired
  					w.close();
  					done();
  				});
  			});
  		});
  	});
  
  	it("should report removal of file and directory if it is missing in initial scan", function(done) {
  		var w = new Watchpack({
  			aggregateTimeout: 1000
  		});
  		w.on("aggregated", function(changes, removals) {
  			Array.from(changes).should.be.eql([]);
  			Array.from(removals)
  				.sort()
  				.should.be.eql([
  					path.join(fixtures, "dir", "a"),
  					path.join(fixtures, "dir", "b")
  				]);
  			w.close();
  			done();
  		});
  		testHelper.dir("dir");
  		testHelper.tick(() => {
  			w.watch({
  				files: [path.join(fixtures, "dir", "a")],
  				directories: [path.join(fixtures, "dir", "b")],
  				missing: [path.join(fixtures, "dir", "c")]
  			});
  		});
  	});
  
  	it("should report removal of file and directory if parent directory is missing in initial scan", function(done) {
  		var w = new Watchpack({
  			aggregateTimeout: 1000
  		});
  		w.on("aggregated", function(changes, removals) {
  			Array.from(changes).should.be.eql([]);
  			Array.from(removals)
  				.sort()
  				.should.be.eql([
  					path.join(fixtures, "dir", "a"),
  					path.join(fixtures, "dir", "b")
  				]);
  			w.close();
  			done();
  		});
  		testHelper.tick(() => {
  			w.watch({
  				files: [path.join(fixtures, "dir", "a")],
  				directories: [path.join(fixtures, "dir", "b")],
  				missing: [path.join(fixtures, "dir", "c")]
  			});
  		});
  	});
  
  	it("should not detect file reading as change, but atomic file writes", done => {
  		var w = new Watchpack({
  			aggregateTimeout: 1000
  		});
  		w.on("aggregated", function(changes, removals) {
  			Array.from(changes)
  				.sort()
  				.should.be.eql([
  					path.join(fixtures, "dir", "b"),
  					path.join(fixtures, "dir", "c")
  				]);
  			Array.from(removals).should.be.eql([]);
  			w.close();
  			done();
  		});
  		testHelper.dir("dir");
  		testHelper.file(path.join("dir", "a"));
  		testHelper.file(path.join("dir", "b"));
  		testHelper.file(path.join("dir", "c"));
  		testHelper.tick(1000, () => {
  			w.watch({
  				files: [
  					path.join(fixtures, "dir", "a"),
  					path.join(fixtures, "dir", "b"),
  					path.join(fixtures, "dir", "c")
  				]
  			});
  			testHelper.tick(1000, () => {
  				testHelper.accessFile(path.join("dir", "a"));
  				testHelper.fileAtomic(path.join("dir", "b"));
  				testHelper.file(path.join("dir", "c"));
  			});
  		});
  	});
  
  	it("should allow to reuse watchers when watch is called again", function(done) {
  		var w = new Watchpack({
  			aggregateTimeout: 1000
  		});
  		w.on("aggregated", () => {
  			done(new Error("should not fire"));
  		});
  		testHelper.dir("dir");
  		testHelper.dir("dir/b");
  		testHelper.dir("dir/b/sub");
  		testHelper.file("dir/b/sub/file");
  		testHelper.file("dir/b/file");
  		testHelper.dir("dir/b1");
  		testHelper.dir("dir/b1/sub");
  		testHelper.file("dir/b1/sub/file");
  		testHelper.file("dir/b1/file");
  		testHelper.dir("dir/b2");
  		testHelper.dir("dir/b2/sub");
  		testHelper.file("dir/b2/sub/file");
  		testHelper.file("dir/b2/file");
  		testHelper.file("dir/a");
  		testHelper.file("dir/a1");
  		testHelper.file("dir/a2");
  		testHelper.tick(() => {
  			w.watch({
  				files: [
  					path.join(fixtures, "dir", "a"),
  					path.join(fixtures, "dir", "a1")
  				],
  				directories: [
  					path.join(fixtures, "dir", "b"),
  					path.join(fixtures, "dir", "b1")
  				],
  				missing: [
  					path.join(fixtures, "dir", "c"),
  					path.join(fixtures, "dir", "c1")
  				]
  			});
  			testHelper.tick(() => {
  				w.watch({
  					files: [
  						path.join(fixtures, "dir", "a"),
  						path.join(fixtures, "dir", "a2")
  					],
  					directories: [
  						path.join(fixtures, "dir", "b"),
  						path.join(fixtures, "dir", "b2")
  					],
  					missing: [
  						path.join(fixtures, "dir", "c"),
  						path.join(fixtures, "dir", "c2")
  					]
  				});
  				testHelper.file("dir/b1/sub/file");
  				testHelper.file("dir/a1");
  				testHelper.file("dir/c1");
  				testHelper.tick(2000, () => {
  					// no event fired
  					w.close();
  					done();
  				});
  			});
  		});
  	});
  
  	let symlinksSupported = false;
  	try {
  		const fs = require("fs");
  		fs.symlinkSync("helpers", path.join(__dirname, "fixtures"), "dir");
  		fs.unlinkSync(path.join(__dirname, "fixtures"));
  		symlinksSupported = true;
  	} catch (e) {
  		// ignore
  	}
  
  	if (symlinksSupported) {
  		describe("symlinks", () => {
  			beforeEach(done => {
  				testHelper.dir("a");
  				testHelper.dir(path.join("a", "b"));
  				testHelper.file(path.join("a", "b", "c"));
  				testHelper.file(path.join("a", "b", "d"));
  				testHelper.symlinkDir("link", "a");
  				testHelper.symlinkDir(path.join("a", "link"), "b");
  				testHelper.symlinkFile(path.join("a", "b", "link"), "c");
  				testHelper.symlinkFile(path.join("a", "b", "link2"), "link");
  				testHelper.symlinkFile("link2", "link/link/link2");
  
  				testHelper.tick(1000, done);
  			});
  
  			function expectWatchEvent(files, dirs, callback, ready) {
  				var w = new Watchpack({
  					aggregateTimeout: 500,
  					followSymlinks: true
  				});
  
  				w.watch([].concat(files), [].concat(dirs), Date.now());
  
  				let active = false;
  				let closed = false;
  
  				w.on("aggregated", changes => {
  					w.close();
  					closed = true;
  					if (!active) throw new Error("Events are not expected yet");
  					callback(changes);
  				});
  
  				testHelper.tick(1000, () => {
  					if (closed) return;
  					active = true;
  					ready();
  				});
  			}
  
  			it("should detect a change to the original file", function(done) {
  				expectWatchEvent(
  					path.join(fixtures, "link2"),
  					[],
  					changes => {
  						Array.from(changes).should.be.eql([path.join(fixtures, "link2")]);
  						done();
  					},
  					() => {
  						testHelper.file(path.join("a", "b", "c"));
  					}
  				);
  			});
  
  			it("should detect a change to the direct symlink", function(done) {
  				expectWatchEvent(
  					path.join(fixtures, "link2"),
  					[],
  					changes => {
  						Array.from(changes).should.be.eql([path.join(fixtures, "link2")]);
  						done();
  					},
  					() => {
  						testHelper.unlink("link2");
  						testHelper.symlinkFile("link2", path.join("a", "b", "d"));
  					}
  				);
  			});
  
  			it("should detect a change to the double symlink", function(done) {
  				expectWatchEvent(
  					path.join(fixtures, "link2"),
  					[],
  					changes => {
  						Array.from(changes).should.be.eql([path.join(fixtures, "link2")]);
  						done();
  					},
  					() => {
  						testHelper.unlink(path.join("a", "b", "link2"));
  						testHelper.symlinkFile(path.join("a", "b", "link2"), "d");
  					}
  				);
  			});
  
  			it("should detect a change to the directory symlink", function(done) {
  				expectWatchEvent(
  					path.join(fixtures, "link2"),
  					[],
  					changes => {
  						Array.from(changes).should.be.eql([path.join(fixtures, "link2")]);
  						done();
  					},
  					() => {
  						testHelper.unlink(path.join("a", "link"));
  						testHelper.symlinkDir(path.join("a", "link"), path.join("b", "d"));
  					}
  				);
  			});
  
  			it("should detect a file change in a watched symlinked directory", function(done) {
  				expectWatchEvent(
  					[],
  					path.join(fixtures, "link"),
  					changes => {
  						Array.from(changes).should.be.eql([path.join(fixtures, "link")]);
  						done();
  					},
  					() => {
  						testHelper.file(path.join("a", "b", "c"));
  					}
  				);
  			});
  
  			it("should detect a symlink file change in a watched symlinked directory", function(done) {
  				expectWatchEvent(
  					[],
  					path.join(fixtures, "link"),
  					changes => {
  						Array.from(changes).should.be.eql([path.join(fixtures, "link")]);
  						done();
  					},
  					() => {
  						testHelper.unlink(path.join("a", "b", "link2"));
  						testHelper.symlinkFile(path.join("a", "b", "link2"), "d");
  					}
  				);
  			});
  
  			it("should detect a symlink dir change in a watched symlinked directory", function(done) {
  				expectWatchEvent(
  					[],
  					path.join(fixtures, "link"),
  					changes => {
  						Array.from(changes).should.be.eql([path.join(fixtures, "link")]);
  						done();
  					},
  					() => {
  						testHelper.unlink(path.join("a", "link"));
  						testHelper.symlinkDir(path.join("a", "link"), path.join("b", "d"));
  					}
  				);
  			});
  		});
  	} else {
  		it("symlinks");
  	}
  });
  