From 9dcd158347560cbacdcb5442aefeb307f5be9751 Mon Sep 17 00:00:00 2001 From: Robert Wagner Date: Wed, 22 Aug 2018 22:04:34 -0400 Subject: [PATCH 1/2] Add some tour tests --- src/js/tour.js | 80 ++++++++++++++++++++-------- test/test.tour.js | 132 +++++++++++++++++++++++++++++++++++----------- 2 files changed, 159 insertions(+), 53 deletions(-) diff --git a/src/js/tour.js b/src/js/tour.js index 0c624ad02..73708414b 100644 --- a/src/js/tour.js +++ b/src/js/tour.js @@ -38,17 +38,27 @@ export class Tour extends Evented { }); } - addStep(name, step) { - if (_.isUndefined(step)) { - step = name; + /** + * Adds a new step to the tour + * @param {Object|Number|Step|String} arg1 + * When arg2 is defined, arg1 can either be a string or number, to use for the `id` for the step + * When arg2 is undefined, arg1 is either an object containing step options or a Step instance + * @param {Object|Step} arg2 An object containing step options or a Step instance + * @returns {Step} The newly added step + */ + addStep(arg1, arg2) { + let name, step; + + // If we just have one argument, we can assume it is an object of step options, with an id + if (_.isUndefined(arg2)) { + step = arg1; + } else { + name = arg1; + step = arg2; } if (!(step instanceof Step)) { - if (typeof name === 'string' || typeof name === 'number') { - step.id = name.toString(); - } - step = Object.assign({}, this.options.defaults, step); - step = new Step(this, step); + step = this.setupStep(step, name); } else { step.tour = this; } @@ -83,6 +93,22 @@ export class Tour extends Evented { } } + /** + * Setup a new step object + * @param {Object} stepOptions The object describing the options for the step + * @param {String|Number} name The string or number to use as the `id` for the step + * @returns {Step} + */ + setupStep(stepOptions, name) { + if (_.isString(name) || _.isNumber(name)) { + stepOptions.id = name.toString(); + } + + stepOptions = Object.assign({}, this.options.defaults, stepOptions); + + return new Step(this, stepOptions); + } + getById(id) { for (let i = 0; i < this.steps.length; ++i) { const step = this.steps[i]; @@ -96,16 +122,9 @@ export class Tour extends Evented { return this.currentStep; } - next() { - const index = this.steps.indexOf(this.currentStep); - - if (index === this.steps.length - 1) { - this.complete(); - } else { - this.show(index + 1, true); - } - } - + /** + * Go to the previous step in the tour + */ back() { const index = this.steps.indexOf(this.currentStep); this.show(index - 1, false); @@ -136,15 +155,31 @@ export class Tour extends Evented { this.trigger(event); - Shepherd.activeTour.steps.forEach((step) => { - step.destroy(); - }); + if (Shepherd.activeTour) { + Shepherd.activeTour.steps.forEach((step) => { + step.destroy(); + }); + } Shepherd.activeTour = null; document.body.classList.remove('shepherd-active'); this.trigger('inactive', { tour: this }); } + /** + * Go to the next step in the tour + * If we are at the end, call `complete` + */ + next() { + const index = this.steps.indexOf(this.currentStep); + + if (index === this.steps.length - 1) { + this.complete(); + } else { + this.show(index + 1, true); + } + } + show(key = 0, forward = true) { if (this.currentStep) { this.currentStep.hide(); @@ -176,6 +211,9 @@ export class Tour extends Evented { } } + /** + * Start the tour + */ start() { this.trigger('start'); diff --git a/test/test.tour.js b/test/test.tour.js index ad42edc55..3947109df 100644 --- a/test/test.tour.js +++ b/test/test.tour.js @@ -1,58 +1,95 @@ /* global window,require,describe,it */ +import _ from 'lodash'; import { assert } from 'chai'; import Shepherd from '../src/js/shepherd'; +import { Step } from '../src/js/step'; // since importing non UMD, needs assignment window.Shepherd = Shepherd; describe('Tour', function() { + let instance; const defaults = { classes: 'shepherd-theme-arrows', scrollTo: true }; - after(function() { - instance.cancel(); - }); + beforeEach(() => { + instance = new Shepherd.Tour({ + defaults + }); - const instance = new Shepherd.Tour({ - defaults, + instance.addStep('test', { + classes: 'foo', + id: 'test', + title: 'This is a test step for our tour' + }); + + instance.addStep('test2', { + id: 'test2', + title: 'Another Step' + }); + + instance.addStep('test3', { + id: 'test3', + title: 'Yet, another test step' + }); }); - it('creates a new tour instance', function() { - assert.isOk(instance instanceof Shepherd.Tour); + afterEach(() => { + instance.cancel(); }); - it('returns the default options on the instance', function() { - assert.isOk(instance.options); + describe('constructor', function() { + it('creates a new tour instance', function() { + assert.isOk(instance instanceof Shepherd.Tour); + }); + + it('returns the default options on the instance', function() { + assert.deepEqual(instance.options.defaults, { + classes: 'shepherd-theme-arrows', + scrollTo: true + }); + }); + + it('sets the correct bindings', function() { + const bindings = Object.keys(instance.bindings); + const tourEvents = ['complete', 'cancel', 'start', 'show', 'active', 'inactive']; + // Check that all bindings are included + const difference = _.difference(tourEvents, bindings); + assert.equal(difference.length, 0, 'all tour events bound'); + }); }); describe('.addStep()', function() { it('adds tour steps', function() { - instance.addStep('test', { - id: 'test', - title: 'This is a test step for our tour' + assert.equal(instance.steps.length, 3); + assert.equal(instance.getById('test').options.classes, 'foo', 'classes passed to step options'); + }); + + it('adds steps with only one arg', function() { + const step = instance.addStep({ + id: 'one-arg' }); - assert.equal(instance.steps.length, 1); + assert.equal(instance.steps.length, 4); + assert.equal(step.id, 'one-arg', 'id applied to step with just one arg'); }); - // this is not working as documented - it('returns the step options', function() { - assert.equal(instance.options.defaults, defaults); + it('adds steps that are already Step instances', function() { + const step = instance.addStep(new Step(instance, { + id: 'already-a-step' + })); + + assert.equal(instance.steps.length, 4); + assert.equal(step.id, 'already-a-step', 'id applied to step instance'); + assert.equal(step.tour, instance, 'tour is set to `this`'); }); + }); + describe('.getById()', function() { it('returns the step by ID with the right title', function() { - instance.addStep('test2', { - id: 'test2', - title: 'Another Step' - }); - - instance.addStep('test3', { - id: 'test3', - title: 'Yet, another test step' - }); assert.equal(instance.steps.length, 3); - assert.equal(instance.getById('test').options.title, 'This is a test step for our tour'); + assert.equal(instance.getById('test3').options.title, 'Yet, another test step'); }); }); @@ -67,21 +104,52 @@ describe('Tour', function() { describe('.getCurrentStep()', function() { it('returns the currently shown step', function() { + instance.start(); assert.equal(instance.getCurrentStep().id, 'test'); }); }); - describe('.next()', function() { - it('goes to the next step after next() is invoked', function() { + describe('.next()/.back()', function() { + it('goes to the next/previous steps', function() { + instance.start(); instance.next(); assert.equal(instance.getCurrentStep().id, 'test2'); + instance.back(); + assert.equal(instance.getCurrentStep().id, 'test'); }); }); - describe('.back()', function() { - it('goes to the previous step after back() is invoked', function() { - instance.back(); - assert.equal(instance.getCurrentStep().id, 'test'); + describe('.complete()', function() { + it('tears down tour on complete', function() { + let inactiveFired = false; + instance.on('inactive', () => { + inactiveFired = true; + }); + instance.start(); + assert.equal(instance, Shepherd.activeTour, 'activeTour is set to our tour'); + instance.complete(); + assert.isNotOk(Shepherd.activeTour, 'activeTour is torn down'); + assert.isOk(inactiveFired, 'inactive event fired'); + }); + + it('triggers complete event when complete function is called', function() { + let completeFired = false; + instance.on('complete', () => { + completeFired = true; + }); + + instance.start(); + instance.complete(); + assert.isOk(completeFired, 'complete event fired'); + }); + }); + + describe('.removeStep()', function() { + it('removes the step when passed the id', function() { + instance.start(); + assert.equal(instance.steps.length, 3); + instance.removeStep('test2'); + assert.equal(instance.steps.length, 2); }); }); }); From 5587d304ca3c5040350fd9e03b769d5205fbbb72 Mon Sep 17 00:00:00 2001 From: Robert Wagner Date: Wed, 22 Aug 2018 23:04:34 -0400 Subject: [PATCH 2/2] Add more tests --- src/js/tour.js | 21 ++++++++++++--------- test/test.tour.js | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/src/js/tour.js b/src/js/tour.js index 73708414b..05d2cea63 100644 --- a/src/js/tour.js +++ b/src/js/tour.js @@ -67,29 +67,32 @@ export class Tour extends Evented { return step; } + /** + * Removes the step from the tour + * @param {String} name The id for the step to remove + */ removeStep(name) { const current = this.getCurrentStep(); - for (let i = 0; i < this.steps.length; ++i) { - const step = this.steps[i]; + // Find the step, destroy it and remove it from this.steps + this.steps.some((step, i) => { if (step.id === name) { if (step.isOpen()) { step.hide(); } + step.destroy(); this.steps.splice(i, 1); - break; + + return true; } - } + }); if (current && current.id === name) { this.currentStep = undefined; - if (this.steps.length) { - this.show(0); - } else { - this.cancel(); - } + // If we have steps left, show the first one, otherwise just cancel the tour + this.steps.length ? this.show(0) : this.cancel(); } } diff --git a/test/test.tour.js b/test/test.tour.js index 3947109df..aba25461e 100644 --- a/test/test.tour.js +++ b/test/test.tour.js @@ -117,6 +117,19 @@ describe('Tour', function() { instance.back(); assert.equal(instance.getCurrentStep().id, 'test'); }); + + it('next completes tour when on last step', function() { + let completeFired = false; + instance.on('complete', () => { + completeFired = true; + }); + + instance.start(); + instance.show('test3'); + assert.equal(instance.getCurrentStep().id, 'test3'); + instance.next(); + assert.isOk(completeFired, 'complete is called when next is clicked on last step'); + }); }); describe('.complete()', function() { @@ -151,5 +164,30 @@ describe('Tour', function() { instance.removeStep('test2'); assert.equal(instance.steps.length, 2); }); + + it('hides the step before removing', function() { + let hideFired = false; + instance.start(); + assert.equal(instance.steps.length, 3); + const step = instance.getById('test'); + step.on('hide', () => { + hideFired = true; + }); + instance.removeStep('test'); + assert.equal(instance.steps.length, 2); + assert.isOk(hideFired, 'hide is fired before step is destroyed'); + }); + }); + + describe('.show()', function() { + it('show short circuits if next is not found', function() { + let showFired = false; + instance.start(); + instance.on('show', () => { + showFired = true; + }); + instance.show('not-a-real-key'); + assert.isNotOk(showFired, 'showFired is false because show short circuits'); + }); }); });