From 2f98d34b1d1bc63adcd6aa6129925ba2fda64e6e Mon Sep 17 00:00:00 2001 From: vanilla-wave Date: Thu, 27 Feb 2025 17:23:10 +0100 Subject: [PATCH] feat: add close hint reason --- src/controller.ts | 24 ++++--- src/hints/hintStore.ts | 7 +- src/tests/event-hooks.test.ts | 35 +++++++++- src/tests/steps.test.ts | 117 ++++++++++++++++++++-------------- src/types.ts | 16 +++-- 5 files changed, 133 insertions(+), 66 deletions(-) diff --git a/src/controller.ts b/src/controller.ts index d189e6a..87564a8 100644 --- a/src/controller.ts +++ b/src/controller.ts @@ -11,6 +11,7 @@ import type { PresetStatus, ResolvedOptions, UserPreset, + HintCloseSource, } from './types'; import {HintStore} from './hints/hintStore'; import {createLogger, Logger} from './logger'; @@ -203,7 +204,7 @@ export class Controller { + closeHintByUser = (stepSlug?: Steps, eventSource: HintCloseSource = 'closedByUser') => { const currentHintStep = this.hintStore.state.hint?.step.slug; this.logger.debug('Close hint(internal)', currentHintStep); if (stepSlug && stepSlug !== currentHintStep) { @@ -332,15 +333,18 @@ export class Controller { + closeHint = (stepSlug?: Steps, eventSource: HintCloseSource = 'externalEvent') => { const currentHintStep = this.hintStore.state.hint?.step.slug; this.logger.debug('Close hint(internal)', currentHintStep); if (stepSlug && stepSlug !== currentHintStep) { @@ -623,10 +627,10 @@ export class Controller { diff --git a/src/hints/hintStore.ts b/src/hints/hintStore.ts index dc755c5..b4647c9 100644 --- a/src/hints/hintStore.ts +++ b/src/hints/hintStore.ts @@ -1,7 +1,6 @@ import type {MutableRefObject} from 'react'; -import type {ShowHintParams} from '../types'; +import type {HintCloseSource, ShowHintParams, EventListener, EventsMap, EventTypes} from '../types'; import {EventEmitter} from '../event-emitter'; -import {EventListener, EventsMap, EventTypes} from '../types'; export type HintState = { anchorRef: MutableRefObject; @@ -45,9 +44,9 @@ export class HintStore this.emitter.emit('hintDataChanged', {state: this.state}); }; - closeHint = () => { + closeHint = (eventSource: HintCloseSource = 'externalEvent') => { if (this.state.hint) { - this.emitter.emit('closeHint', {hint: this.state.hint}); + this.emitter.emit('closeHint', {hint: this.state.hint, eventSource}); } this.state = { diff --git a/src/tests/event-hooks.test.ts b/src/tests/event-hooks.test.ts index 96957fe..e4fa846 100644 --- a/src/tests/event-hooks.test.ts +++ b/src/tests/event-hooks.test.ts @@ -328,6 +328,7 @@ describe('event subscriptions', function () { description: '', }, }, + eventSource: 'stepPassed', }, controller, ); @@ -355,6 +356,7 @@ describe('event subscriptions', function () { description: '', }, }, + eventSource: 'elementHidden', }, controller, ); @@ -370,7 +372,35 @@ describe('event subscriptions', function () { stepSlug: 'createSprint', element: getAnchorElement(), }); - controller.closeHint('createSprint'); + controller.closeHintByUser('createSprint'); + + expect(mock).toHaveBeenCalledWith( + { + hint: { + preset: 'createProject', + step: { + slug: 'createSprint', + name: '', + description: '', + }, + }, + eventSource: 'closedByUser', + }, + controller, + ); + }); + + it('call closeHint -> trigger with eventSource=externalEvent', async function () { + const controller = new Controller(getOptions()); + + const mock = jest.fn(); + controller.events.subscribe('closeHint', mock); + + await controller.stepElementReached({ + stepSlug: 'createSprint', + element: getAnchorElement(), + }); + controller.closeHint(); expect(mock).toHaveBeenCalledWith( { @@ -382,6 +412,7 @@ describe('event subscriptions', function () { description: '', }, }, + eventSource: 'externalEvent', }, controller, ); @@ -437,6 +468,7 @@ describe('event subscriptions', function () { description: '', }, }, + eventSource: 'stepPassed', }, controller, ); @@ -479,6 +511,7 @@ describe('event subscriptions', function () { description: '', }, }, + eventSource: 'closedByUser', }, controller, ); diff --git a/src/tests/steps.test.ts b/src/tests/steps.test.ts index 372dbda..7b780d1 100644 --- a/src/tests/steps.test.ts +++ b/src/tests/steps.test.ts @@ -221,67 +221,90 @@ describe('pass step', function () { }); }); - it('user close hint -> call onCloseHintByUser and onCloseHint', async function () { - const options = getOptions(); - const onCloseHintByUserMock = jest.fn(); - const onCloseHintMock = jest.fn(); + describe('closeHint hooks', () => { + it('user close hint -> call onCloseHintByUser and onCloseHint', async function () { + const options = getOptions(); + const onCloseHintByUserMock = jest.fn(); + const onCloseHintMock = jest.fn(); - options.config.presets.createProject.steps[1].hooks = { - onCloseHintByUser: onCloseHintByUserMock, - onCloseHint: onCloseHintMock, - }; + options.config.presets.createProject.steps[1].hooks = { + onCloseHintByUser: onCloseHintByUserMock, + onCloseHint: onCloseHintMock, + }; - const controller = new Controller(options); - await controller.stepElementReached({ - stepSlug: 'createSprint', - element: getAnchorElement(), + const controller = new Controller(options); + await controller.stepElementReached({ + stepSlug: 'createSprint', + element: getAnchorElement(), + }); + await controller.closeHintByUser(); + + expect(onCloseHintByUserMock).toHaveBeenCalledWith({eventSource: 'closedByUser'}); + expect(onCloseHintMock).toHaveBeenCalledWith({eventSource: 'closedByUser'}); }); - await controller.closeHintByUser(); - expect(onCloseHintByUserMock).toHaveBeenCalled(); - expect(onCloseHintMock).toHaveBeenCalled(); - }); + it('element disappear -> call only onCloseHint', async function () { + const options = getOptions(); + const onCloseHintByUserMock = jest.fn(); + const onCloseHintMock = jest.fn(); - it('element disappear -> call only onCloseHint', async function () { - const options = getOptions(); - const onCloseHintByUserMock = jest.fn(); - const onCloseHintMock = jest.fn(); + options.config.presets.createProject.steps[1].hooks = { + onCloseHintByUser: onCloseHintByUserMock, + onCloseHint: onCloseHintMock, + }; - options.config.presets.createProject.steps[1].hooks = { - onCloseHintByUser: onCloseHintByUserMock, - onCloseHint: onCloseHintMock, - }; + const controller = new Controller(options); + await controller.stepElementReached({ + stepSlug: 'createSprint', + element: getAnchorElement(), + }); + await controller.stepElementDisappeared('createSprint'); - const controller = new Controller(options); - await controller.stepElementReached({ - stepSlug: 'createSprint', - element: getAnchorElement(), + expect(onCloseHintByUserMock).not.toHaveBeenCalled(); + expect(onCloseHintMock).toHaveBeenCalledWith({eventSource: 'elementHidden'}); }); - await controller.stepElementDisappeared('createSprint'); - expect(onCloseHintByUserMock).not.toHaveBeenCalled(); - expect(onCloseHintMock).toHaveBeenCalled(); - }); + it('pass step -> call onCloseHint + onCloseByUser', async function () { + const options = getOptions(); + const onCloseHintByUserMock = jest.fn(); + const onCloseHintMock = jest.fn(); - it('pass step -> call onCloseHint + onCloseByUser', async function () { - const options = getOptions(); - const onCloseHintByUserMock = jest.fn(); - const onCloseHintMock = jest.fn(); + options.config.presets.createProject.steps[1].hooks = { + onCloseHintByUser: onCloseHintByUserMock, + onCloseHint: onCloseHintMock, + }; - options.config.presets.createProject.steps[1].hooks = { - onCloseHintByUser: onCloseHintByUserMock, - onCloseHint: onCloseHintMock, - }; + const controller = new Controller(options); + await controller.stepElementReached({ + stepSlug: 'createSprint', + element: getAnchorElement(), + }); + await controller.passStep('createSprint'); - const controller = new Controller(options); - await controller.stepElementReached({ - stepSlug: 'createSprint', - element: getAnchorElement(), + expect(onCloseHintByUserMock).toHaveBeenCalledWith({eventSource: 'stepPassed'}); + expect(onCloseHintMock).toHaveBeenCalledWith({eventSource: 'stepPassed'}); }); - await controller.passStep('createSprint'); - expect(onCloseHintByUserMock).toHaveBeenCalled(); - expect(onCloseHintMock).toHaveBeenCalled(); + it('call closeHint -> call onCloseHint with eventSource=externalEvent', async function () { + const options = getOptions(); + const onCloseHintByUserMock = jest.fn(); + const onCloseHintMock = jest.fn(); + + options.config.presets.createProject.steps[1].hooks = { + onCloseHintByUser: onCloseHintByUserMock, + onCloseHint: onCloseHintMock, + }; + + const controller = new Controller(options); + await controller.stepElementReached({ + stepSlug: 'createSprint', + element: getAnchorElement(), + }); + await controller.closeHint(); + + expect(onCloseHintByUserMock).not.toHaveBeenCalled(); + expect(onCloseHintMock).toHaveBeenCalledWith({eventSource: 'externalEvent'}); + }); }); }); }); diff --git a/src/types.ts b/src/types.ts index 46c2f3e..e46ed9e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -22,6 +22,8 @@ type HintPlacement = | 'left-start' | 'left-end'; +export type HintCloseSource = 'stepPassed' | 'closedByUser' | 'elementHidden' | 'externalEvent'; + export type PresetStatus = 'unPassed' | 'inProgress' | 'finished'; export type PresetStep = { @@ -35,8 +37,8 @@ export type PresetStep = { passRestriction?: 'afterPrevious'; hooks?: { onStepPass?: () => void; - onCloseHint?: () => void; - onCloseHintByUser?: () => void; + onCloseHint?: (params: {eventSource: HintCloseSource}) => void; + onCloseHintByUser?: (params: {eventSource: HintCloseSource}) => void; }; }; @@ -182,8 +184,14 @@ export type EventsMap< beforeShowHint: {stepData: ReachElementParams}; stateChange: {state: Controller['state']}; hintDataChanged: {state: HintState}; - closeHint: {hint: Pick, 'preset' | 'step'>}; - closeHintByUser: {hint: Pick, 'preset' | 'step'>}; + closeHint: { + hint: Pick, 'preset' | 'step'>; + eventSource: HintCloseSource; + }; + closeHintByUser: { + hint: Pick, 'preset' | 'step'>; + eventSource: HintCloseSource; + }; init: {}; wizardStateChanged: {wizardState: BaseState['wizardState']}; };