Skip to content

Commit

Permalink
(#39) Handling failures due to missing steps in the DOM - clear exit …
Browse files Browse the repository at this point in the history
…from the tour
  • Loading branch information
tnicola committed Mar 3, 2019
1 parent 592dabe commit e7ba0ee
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 32 deletions.
20 changes: 19 additions & 1 deletion src/lib/src/models/joyride-error.class.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,20 @@
export class JoyrideError extends Error {
}
constructor(message: string) {
super(message);
Object.setPrototypeOf(this, JoyrideError.prototype);
}
}

export class JoyrideStepDoesNotExist extends Error {
constructor(message: string) {
super(message);
Object.setPrototypeOf(this, JoyrideStepDoesNotExist.prototype);
}
}

export class JoyrideStepOutOfRange extends Error {
constructor(message: string) {
super(message);
Object.setPrototypeOf(this, JoyrideStepOutOfRange.prototype);
}
}
111 changes: 105 additions & 6 deletions src/lib/src/services/joyride-step.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import { EventEmitter } from '@angular/core';
import { Router } from '@angular/router';
import { RouterFake } from '../test/fake/router-fake.service';
import { JoyrideStepsContainerServiceFake } from '../test/fake/joyride-steps-container-fake.service';
import { LoggerService } from './logger.service';
import { LoggerFake } from '../test/fake/logger-fake.service';
import { JoyrideStepOutOfRange } from '../models/joyride-error.class';

describe('JoyrideStepService', () => {
let joyrideStepService: JoyrideStepService;
Expand All @@ -27,10 +30,13 @@ describe('JoyrideStepService', () => {
let stepsContainerService: JoyrideStepsContainerServiceFake;
let domRefService: DomRefServiceFake;
let stepDrawerService: StepDrawerServiceFake;
let logger: LoggerFake;
let router: RouterFake;

let FAKE_STEPS = <any>[];
let STEP0: any = new JoyrideStep();
let STEP1: any = new JoyrideStep();
let STEP2: any = new JoyrideStep();
let STEP0: JoyrideStep = new JoyrideStep();
let STEP1: JoyrideStep = new JoyrideStep();
let STEP2: JoyrideStep = new JoyrideStep();
let FAKE_WINDOW: { innerHeight: number; scrollTo: jasmine.Spy };
let FAKE_DOCUMENT: { body: { scrollHeight: number } };

Expand All @@ -45,7 +51,8 @@ describe('JoyrideStepService', () => {
{ provide: DocumentService, useClass: DocumentServiceFake },
{ provide: DomRefService, useClass: DomRefServiceFake },
{ provide: StepDrawerService, useClass: StepDrawerServiceFake },
{ provide: JoyrideOptionsService, useClass: JoyrideOptionsServiceFake }
{ provide: JoyrideOptionsService, useClass: JoyrideOptionsServiceFake },
{ provide: LoggerService, useClass: LoggerFake }
]
});
});
Expand All @@ -63,6 +70,8 @@ describe('JoyrideStepService', () => {
documentService = TestBed.get(DocumentService);
stepsContainerService = TestBed.get(JoyrideStepsContainerService);
stepDrawerService = TestBed.get(StepDrawerService);
logger = TestBed.get(LoggerService);
router = TestBed.get(Router);

STEP0 = createNewStep('nav');
STEP1 = createNewStep('credits');
Expand Down Expand Up @@ -91,10 +100,18 @@ describe('JoyrideStepService', () => {
});

describe('when eventListener.scrollEvent publish', () => {
it("should call backdropService.redraw with the right 'scroll' parameter", () => {
it("should call backdropService.redraw with the right 'scroll' parameter", fakeAsync(() => {
joyrideStepService.startTour();
tick(1);
eventListenerService.scrollEvent.next(240);

expect(backdropService.redraw).toHaveBeenCalledWith(jasmine.objectContaining(STEP0), 240);
}));

it('should NOT call backdropService.redraw if the tour is not started yet', () => {
eventListenerService.scrollEvent.next(240);

expect(backdropService.redraw).toHaveBeenCalledWith(undefined, 240);
expect(backdropService.redraw).not.toHaveBeenCalled();
});
});

Expand Down Expand Up @@ -143,6 +160,88 @@ describe('JoyrideStepService', () => {
it('should call stepsContainerService.get with StepActionType.NEXT', () => {
expect(stepsContainerService.get).toHaveBeenCalledWith(StepActionType.NEXT);
});

it('should navigate to the step route if the step has a route', fakeAsync(() => {
stepsContainerService.getStepRoute.and.returnValue('route1');
joyrideStepService.startTour();
tick(1);

expect(router.navigate).toHaveBeenCalledWith(['route1']);
}));

it('should NOT navigate to the step route if the step does not have a route', fakeAsync(() => {
stepsContainerService.getStepRoute.and.returnValue(null);
joyrideStepService.startTour();
tick(1);

expect(router.navigate).not.toHaveBeenCalled();
}));

describe('if stepsContainerService.get returns a null step', () => {
let tryShowSpy: jasmine.Spy;
beforeEach(() => {
tryShowSpy = spyOn(joyrideStepService, 'tryShowStep').and.callThrough();
});

it('should call tryShowStep twice if the first step is NOT null', fakeAsync(() => {
stepsContainerService.get.and.returnValues(STEP0);
joyrideStepService.startTour();
tick(3);

expect(tryShowSpy).toHaveBeenCalledTimes(1);
}));

it('should call tryShowStep twice if the first step is null', fakeAsync(() => {
stepsContainerService.get.and.returnValues(null, STEP0);
joyrideStepService.startTour();
tick(3);

expect(tryShowSpy).toHaveBeenCalledTimes(2);
}));
});

describe('if stepsContainerService.get returns an undefined step', () => {
let tryShowSpy: jasmine.Spy;
beforeEach(() => {
tryShowSpy = spyOn(joyrideStepService, 'tryShowStep').and.callThrough();
});

it('should call tryShowStep twice if the first step is NOT undefined', fakeAsync(() => {
stepsContainerService.get.and.returnValues(STEP0);
joyrideStepService.startTour();
tick(3);

expect(tryShowSpy).toHaveBeenCalledTimes(1);
}));

it('should call tryShowStep twice if the first step is null', fakeAsync(() => {
stepsContainerService.get.and.returnValues(undefined, STEP0);
joyrideStepService.startTour();
tick(3);

expect(tryShowSpy).toHaveBeenCalledTimes(2);
}));
});

describe('if stepsContainerService.get throw a JoyrideStepOutOfRange error', () => {
let closeSpy: jasmine.Spy;
beforeEach(fakeAsync(() => {
closeSpy = spyOn(joyrideStepService, 'close');
stepsContainerService.get.and.callFake(() => {
throw new JoyrideStepOutOfRange('fake error');
});
joyrideStepService.startTour();
tick(1);
}));

it('should log an error', () => {
expect(logger.error).toHaveBeenCalledWith('Forcing the tour closure: First or Last step not found in the DOM.');
});

it('should close the tour', () => {
expect(closeSpy).toHaveBeenCalled();
});
});
});

describe('next', () => {
Expand Down
58 changes: 37 additions & 21 deletions src/lib/src/services/joyride-step.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { JoyrideOptionsService } from './joyride-options.service';
import { Router } from '@angular/router';
import { ReplaySubject, Observable } from 'rxjs';
import { JoyrideStepInfo } from '../models/joyride-step-info.class';
import { JoyrideStepDoesNotExist, JoyrideStepOutOfRange } from '../models/joyride-error.class';
import { LoggerService } from './logger.service';

const SCROLLBAR_SIZE = 20;
export const DISTANCE_FROM_TARGET = 15;
Expand All @@ -26,7 +28,6 @@ export interface IJoyrideStepService {
@Injectable()
export class JoyrideStepService implements IJoyrideStepService {
private currentStep: JoyrideStep;

private winTopPosition: number = 0;
private winBottomPosition: number = 0;
private stepsObserver: ReplaySubject<JoyrideStepInfo> = new ReplaySubject<JoyrideStepInfo>();
Expand All @@ -39,7 +40,8 @@ export class JoyrideStepService implements IJoyrideStepService {
private readonly DOMService: DomRefService,
private readonly stepDrawerService: StepDrawerService,
private readonly optionsService: JoyrideOptionsService,
private readonly router: Router
private readonly router: Router,
private readonly logger: LoggerService
) {
this.initViewportPositions();
this.subscribeToScrollEvents();
Expand All @@ -56,13 +58,13 @@ export class JoyrideStepService implements IJoyrideStepService {
this.eventListener.scrollEvent.subscribe(scroll => {
this.winTopPosition = scroll.scrollY;
this.winBottomPosition = this.winTopPosition + this.DOMService.getNativeWindow().innerHeight - SCROLLBAR_SIZE;
this.backDropService.redraw(this.currentStep, scroll);
if (this.currentStep) this.backDropService.redraw(this.currentStep, scroll);
});
}

private subscribeToResizeEvents() {
this.eventListener.resizeEvent.subscribe(() => {
this.backDropService.redrawTarget(this.currentStep);
if (this.currentStep) this.backDropService.redrawTarget(this.currentStep);
});
}

Expand All @@ -76,8 +78,7 @@ export class JoyrideStepService implements IJoyrideStepService {
this.stepsContainerService.init();
this.documentService.setDocumentHeight();

this.navigateToStepPage(StepActionType.NEXT);
this.showStep(StepActionType.NEXT);
this.tryShowStep(StepActionType.NEXT);
this.eventListener.startListeningResizeEvents();
this.subscribeToStepsUpdates();
return this.stepsObserver.asObservable();
Expand All @@ -94,15 +95,13 @@ export class JoyrideStepService implements IJoyrideStepService {
prev() {
this.removeCurrentStep();
this.currentStep.prevCliked.emit();
this.navigateToStepPage(StepActionType.PREV);
this.showStep(StepActionType.PREV);
this.tryShowStep(StepActionType.PREV);
}

next() {
this.removeCurrentStep();
this.currentStep.nextClicked.emit();
this.navigateToStepPage(StepActionType.NEXT);
this.showStep(StepActionType.NEXT);
this.tryShowStep(StepActionType.NEXT);
}

private navigateToStepPage(action: StepActionType) {
Expand All @@ -114,24 +113,41 @@ export class JoyrideStepService implements IJoyrideStepService {

private subscribeToStepsUpdates() {
this.stepsContainerService.stepHasBeenModified.subscribe(updatedStep => {
if (this.currentStep.name === updatedStep.name) {
if (this.currentStep && this.currentStep.name === updatedStep.name) {
this.currentStep = updatedStep;
}
});
}

private showStep(actionType: StepActionType) {
private tryShowStep(actionType: StepActionType) {
this.navigateToStepPage(actionType);
setTimeout(() => {
this.currentStep = this.stepsContainerService.get(actionType);
// Scroll the element to get it visible if it's in a scrollable element
this.scrollIfElementBeyondOtherElements();
this.backDropService.draw(this.currentStep);
this.drawStep(this.currentStep);
this.scrollIfStepAndTargetAreNotVisible();
this.notifyStepClicked(actionType);
try {
this.showStep(actionType);
} catch (error) {
if (error instanceof JoyrideStepDoesNotExist) {
this.tryShowStep(actionType);
}
if (error instanceof JoyrideStepOutOfRange) {
this.logger.error('Forcing the tour closure: First or Last step not found in the DOM.');
this.close();
}
}
}, 1);
}

private showStep(actionType: StepActionType) {
this.currentStep = this.stepsContainerService.get(actionType /*, () => this.close()*/);

if (this.currentStep == null) throw new JoyrideStepDoesNotExist('');
// Scroll the element to get it visible if it's in a scrollable element
this.scrollIfElementBeyondOtherElements();
this.backDropService.draw(this.currentStep);
this.drawStep(this.currentStep);
this.scrollIfStepAndTargetAreNotVisible();
this.notifyStepClicked(actionType);
}

private notifyStepClicked(actionType: StepActionType) {
let stepInfo: JoyrideStepInfo = {
number: this.stepsContainerService.getStepNumber(this.currentStep.name),
Expand All @@ -143,11 +159,11 @@ export class JoyrideStepService implements IJoyrideStepService {
}

private notifyTourIsFinished() {
this.currentStep.tourDone.emit();
if (this.currentStep) this.currentStep.tourDone.emit();
this.stepsObserver.complete();
}
private removeCurrentStep() {
this.stepDrawerService.remove(this.currentStep);
if (this.currentStep) this.stepDrawerService.remove(this.currentStep);
}

private scrollIfStepAndTargetAreNotVisible() {
Expand Down
Loading

0 comments on commit e7ba0ee

Please sign in to comment.