diff --git a/FlatCalXP/demos/Classic/iflateng.htm b/FlatCalXP/demos/Classic/iflateng.htm
index 792437c..fbaa613 100644
--- a/FlatCalXP/demos/Classic/iflateng.htm
+++ b/FlatCalXP/demos/Classic/iflateng.htm
@@ -1,6 +1,6 @@
-FlatCalendarXP 10.0.0 - Copyright Idemfactor Solutions, Inc.
+FlatCalendarXP 10.0.1 - Copyright Idemfactor Solutions, Inc.
+
+
+
+
+
+
+
diff --git a/test/test.sh b/test/test.sh
new file mode 100755
index 0000000..6f985c3
--- /dev/null
+++ b/test/test.sh
@@ -0,0 +1,27 @@
+#! /bin/bash
+set -ex
+if [[ "$1" == "-c" ]] ; then
+ rm -rf work
+fi
+mkdir -p work
+cd work
+for m in mocha chai js-fixtures mocha-phantomjs ; do
+ test -d node_modules/"${m}" || npm install "${m}"
+done
+test -s "jquery-3.2.1.js" || wget "https://code.jquery.com/jquery-3.2.1.js"
+for js in ../test*.js ; do
+ base="${js##*/}"
+ name="${base#*-}"
+ name="${name%.js}"
+ mkdir -p "${name}"
+ test ../test.html -ot "${name}/test.html" || sed -e "s/\${FILENAME}/${base}/g" < ../test.html > "${name}/test.html"
+ test "${js}" -ot "${name}/test.js" || sed -e "s/\${FILENAME}/${base}/g" < "${js}" > "${name}/test.js"
+ d="../../${name//-/\/}"
+ test -d "${d}" || d="${d%/*}"
+ cp -a "${d}/"* "${name}/"
+ cd "${name}"
+ ../node_modules/mocha-phantomjs/bin/mocha-phantomjs test.html
+ cd ..
+done
+cd ..
+
diff --git a/test/test005-PopCalXP-themes-Normal.js b/test/test005-PopCalXP-themes-Normal.js
new file mode 100644
index 0000000..cec8d33
--- /dev/null
+++ b/test/test005-PopCalXP-themes-Normal.js
@@ -0,0 +1,76 @@
+
+mocha.ui('bdd');
+mocha.reporter('html');
+var expect = chai.expect;
+
+fixtures.path = '.';
+
+var $ = window.jQuery;
+
+describe("${FILENAME}", function () {
+
+ beforeEach( function(done) {
+ fixtures.clearCache()
+ fixtures.cleanUp()
+ fixtures.load('normalThemeTemplate.htm', done);
+ } );
+
+ var select = function(selector) {
+ return $(selector, fixtures.window().document);
+ };
+
+ var selectInput = function(name) {
+ return select('input[name="' + (name || "dc") + '"]');
+ }
+
+ var selectInCalendar = function(selector) {
+ return $(selector, select('iframe').contents());
+ };
+
+ var setText = function(text, name) {
+ var input = selectInput();
+ var e = $.Event("keypress");
+ e.which = e.keyCode = 13;
+ return input.val(text).trigger(e);
+ };
+
+ var clickCal = function(done) {
+ var img = select('a img[name="popcal"]');
+ img.click();
+ setTimeout(done, 200);
+ };
+
+ var clickCell = function(selector, done) {
+ var aSelector = 'a.CellAnchor' + selector;
+ var cell = selectInCalendar(aSelector);
+ expect(cell).to.have.lengthOf(1);
+ cell.click();
+ setTimeout(done, 200);
+ };
+
+ it('Typing in', function(done) {
+ setText('Blah');
+ expect(selectInput().val().length).to.be.equal(4);
+ done();
+ });
+
+ it('Popping up', function(done) {
+ clickCal(function() {
+ expect(selectInCalendar('#outerTable')).to.have.lengthOf(1);
+ done();
+ });
+ });
+
+ it('Choosing a date', function(done) {
+ clickCal(function() {
+ clickCell(':contains("15")', function(refreshedCell) {
+ expect(selectInput().val().split('/')[0]).to.equal('15');
+ done();
+ });
+ });
+ });
+});
+
+mocha.run();
+
+
diff --git a/test/test010-FlatCalXP-themes-Normal.js b/test/test010-FlatCalXP-themes-Normal.js
new file mode 100644
index 0000000..abc29c1
--- /dev/null
+++ b/test/test010-FlatCalXP-themes-Normal.js
@@ -0,0 +1,50 @@
+
+mocha.ui('bdd');
+mocha.reporter('html');
+var expect = chai.expect;
+
+fixtures.path = '.';
+
+var $ = window.jQuery;
+
+describe("${FILENAME}", function () {
+
+ beforeEach( function(done) {
+ fixtures.clearCache()
+ fixtures.cleanUp()
+ fixtures.load('normalThemeTemplate.htm', done);
+ } );
+
+ var select = function(selector) {
+ return $(selector, fixtures.window().document);
+ };
+
+ var selectInCalendar = function(selector) {
+ return $(selector, select('iframe').contents());
+ };
+
+ var clickCell = function(selector, done) {
+ var aSelector = 'a.CellAnchor' + selector;
+ var cell = selectInCalendar(aSelector);
+ expect(cell).to.have.lengthOf(1);
+ cell.click();
+ // cell seems to refer to a shadow copy of a DOM tree that will not
+ // receive a refresh, so obtain the new jQuery object from the
+ // refreshed DOM.
+ setTimeout(done(selectInCalendar(aSelector)), 200);
+ };
+
+ it('Choosing a date', function(done) {
+ clickCell(':contains("13")', function(refreshedCell) {
+ expect(refreshedCell.parent().parent().attr("bgcolor")).to.equal('#DB5141');
+ clickCell(':contains("15")', function(refreshedCell) {
+ expect(refreshedCell.parent().parent().attr("bgcolor")).to.equal('#DB5141');
+ expect(selectInCalendar('a.CellAnchor:contains("13")').parent().parent().attr("bgcolor")).not.to.equal('#DB5141');
+ done();
+ });
+ });
+ });
+});
+
+mocha.run();
+
diff --git a/test/test015-FlatCalXP-demos-MultiPickerDemo2.js b/test/test015-FlatCalXP-demos-MultiPickerDemo2.js
new file mode 100644
index 0000000..7031449
--- /dev/null
+++ b/test/test015-FlatCalXP-demos-MultiPickerDemo2.js
@@ -0,0 +1,143 @@
+
+mocha.ui('bdd');
+mocha.reporter('html');
+var expect = chai.expect;
+
+fixtures.path = '.';
+
+var $ = window.jQuery;
+
+describe('${FILENAME}', function () {
+
+ // this.timeout(5000);
+
+ beforeEach( function(done) {
+ fixtures.clearCache()
+ fixtures.cleanUp()
+ fixtures.load('MultiPickerDemo2.htm', function() {
+ // The body .. onload=".." handler gets parsed and executed by
+ // the bare javascript engine after fixtures.js loads HTML with
+ // doc.open(); doc.write(html); doc.close();
+ // Therefore the following load event dispatch seems redundant.
+ // $(fixtures.window().document.body).trigger('load');
+ setTimeout(done, 200);
+ });
+ } );
+
+ var select = function(selector) {
+ return $(selector, fixtures.window().document);
+ };
+
+ var selectInCalendar = function(prefix, selector) {
+ return $(selector, select('iframe[name^="' + prefix + '"]').contents());
+ };
+
+ var clickCell = function(prefix, text, done) {
+ var cellGetter = function() {
+ return selectInCalendar(prefix, 'a.CellAnchor').filter(function() {
+ return (this.style.color == 'black') && (this.textContent == text);
+ });
+ };
+ var cell = cellGetter();
+ expect(cell).to.have.lengthOf(1);
+ cell.mouseover();
+ setTimeout(function() {
+ var mousedownCell = cellGetter().mousedown();
+ setTimeout(function() {
+ var mouseupCell = cellGetter().mouseup();
+ setTimeout(function() {
+ if (mouseupCell === mousedownCell) {
+ cellGetter().click();
+ } else {
+ // console.log("Skipping the click event as browsers do when mousedown replaces the cell");
+ }
+ setTimeout(function() {
+ cellGetter().mouseout();
+ // cell seems to refer to a shadow copy of a DOM tree that will not
+ // receive a refresh, so obtain the new jQuery object from the
+ // refreshed DOM.
+ setTimeout(function() {
+ done(cellGetter);
+ }, 200);
+ }, 200);
+ }, 200);
+ }, 200);
+ }, 200);
+ };
+
+ var cellOp = function(prefix, text, op, done) {
+ var cellGetter = function() {
+ return selectInCalendar(prefix, 'a.CellAnchor').filter(function() {
+ return (this.style.color == 'black') && (this.textContent == text);
+ });
+ };
+ var cell = cellGetter();
+ expect(cell).to.have.lengthOf(1);
+ cell[op]();
+ setTimeout(function() {
+ done(cellGetter);
+ }, 200);
+ };
+
+ it('Choosing a date not marked by fInitAgenda in the first calendar', function(done) {
+ clickCell('gToday', '3', function(cellGetter) {
+ expect(cellGetter().parent().parent().attr("bgcolor")).to.equal('#fffacd');
+ done();
+ });
+ });
+
+ it('Choosing a date already marked by fInitAgenda in the first calendar', function(done) {
+ var pds = fixtures.window().gfFlat_1.fGetPDS();
+ expect(pds.length).to.equal(2);
+ expect(new Date(pds[0][0]).getUTCDate()).to.equal(5);
+ expect(new Date(pds[0][1]).getUTCDate()).to.equal(10);
+ expect(new Date(pds[1][0]).getUTCDate()).to.equal(15);
+ expect(new Date(pds[1][1]).getUTCDate()).to.equal(16);
+ clickCell('gToday', '16', function(cellGetter) {
+ // it's #e5e5e5 or white (if it's today) or skyblue (if it's a holiday)
+ // expect(cellGetter().parent().parent().attr("bgcolor")).to.not.equal('#fffacd');
+ expect(pds.length).to.equal(2);
+ expect(new Date(pds[0][0]).getUTCDate()).to.equal(5);
+ expect(new Date(pds[0][1]).getUTCDate()).to.equal(10);
+ expect(new Date(pds[1][0]).getUTCDate()).to.equal(15);
+ expect(new Date(pds[1][1]).getUTCDate()).to.equal(15);
+ done();
+ });
+ });
+
+ it('Choosing a date after clearing all dates', function(done) {
+ select('input[value="Clear All"]').click();
+ setTimeout(function() {
+ clickCell('gToday', '16', function(cellGetter) {
+ expect(cellGetter().parent().parent().attr("bgcolor")).to.equal('#fffacd');
+ var pds = fixtures.window().gfFlat_1.fGetPDS();
+ expect(pds.length).to.equal(1);
+ expect(new Date(pds[0][0]).getUTCDate()).to.equal(16);
+ expect(new Date(pds[0][1]).getUTCDate()).to.equal(16);
+ done();
+ });
+ }, 200);
+ });
+
+ it('Choosing a date range spanning two calendars', function(done) {
+ select('input[value="Clear All"]').click();
+ setTimeout(function() {
+ cellOp('gToday', '17', 'mousedown', function(cellGetter) {
+ expect(cellGetter().parent().parent().attr("bgcolor")).to.equal('#fffacd');
+ cellOp('[gToday[0],', '2', 'mouseover', function(cellGetter2) {
+ cellOp('[gToday[0],', '2', 'mouseup', function(cellGetter2) {
+ expect(cellGetter2().parent().parent().attr("bgcolor")).to.equal('#fffacd');
+ var pds = fixtures.window().gfFlat_1.fGetPDS();
+ expect(pds.length).to.equal(1);
+ expect(new Date(pds[0][0]).getUTCDate()).to.equal(17);
+ expect(new Date(pds[0][1]).getUTCDate()).to.equal(2);
+ done();
+ });
+ });
+ });
+ }, 200);
+ });
+});
+
+mocha.run();
+